"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import { motion, AnimatePresence } from "framer-motion"; import { Plus, LogIn, Loader2, MapPin, Navigation, X, Users, Heart, Sparkles, ChevronRight, Flame, User } from "lucide-react"; import { getUserId, getCachedProfile, getCachedPreferences } from "@/lib/userId"; import { getAvatarBg } from "@/lib/avatars"; import AuthModal from "@/components/AuthModal"; import BrandLogo from "@/components/BrandLogo"; import { SCENES, getSceneConfig } from "@/lib/sceneConfig"; import type { UserProfile, SceneType } from "@/types"; interface LocationSuggestion { id: string; name: string; district: string; address: string; lat: number; lng: number; } type GpsStatus = "idle" | "locating" | "success" | "failed" | "denied"; const DISTANCE_OPTIONS = [ { label: "1km", value: 1000 }, { label: "3km", value: 3000 }, { label: "5km", value: 5000 }, ] as const; type GpsResult = | { ok: true; lat: number; lng: number } | { ok: false; reason: "unsupported" | "denied" | "timeout" | "unknown" }; function requestGps(): Promise { return new Promise((resolve) => { if (!navigator.geolocation) { resolve({ ok: false, reason: "unsupported" }); return; } navigator.geolocation.getCurrentPosition( (pos) => resolve({ ok: true, lat: pos.coords.latitude, lng: pos.coords.longitude }), (err) => { const reason = err.code === err.PERMISSION_DENIED ? "denied" : err.code === err.TIMEOUT ? "timeout" : "unknown"; resolve({ ok: false, reason }); }, { timeout: 8000, enableHighAccuracy: false }, ); }); } async function reverseGeocode(lat: number, lng: number): Promise { try { const res = await fetch(`/api/location/regeo?lat=${lat}&lng=${lng}`); const data = await res.json(); return data.name || data.formatted || null; } catch { return null; } } export default function LandingPage() { const router = useRouter(); const [roomCode, setRoomCode] = useState(""); const [loading, setLoading] = useState(false); const [loadingText, setLoadingText] = useState(""); const [error, setError] = useState(""); const [locationQuery, setLocationQuery] = useState(""); const [suggestions, setSuggestions] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [selectedLocation, setSelectedLocation] = useState(null); const [fetchingSuggestions, setFetchingSuggestions] = useState(false); const [radius, setRadius] = useState(3000); const [priceRange, setPriceRange] = useState("any"); const [cuisine, setCuisine] = useState(""); const suggestRef = useRef(null); const debounceRef = useRef>(null); const [gpsStatus, setGpsStatus] = useState("idle"); const [gpsCoords, setGpsCoords] = useState<{ lat: number; lng: number } | null>(null); const [gpsLocationName, setGpsLocationName] = useState(null); const [scene, setScene] = useState("eat"); const sceneConfig = getSceneConfig(scene); const [profile, setProfile] = useState(null); const [authModalOpen, setAuthModalOpen] = useState(false); useEffect(() => { const cached = getCachedProfile(); if (cached) setProfile(cached); const prefs = getCachedPreferences(); if (prefs.cuisine) setCuisine(prefs.cuisine); if (prefs.priceRange) setPriceRange(prefs.priceRange); if (prefs.radius) setRadius(prefs.radius); }, []); const handleSceneChange = useCallback((s: SceneType) => { setScene(s); setCuisine(""); setPriceRange("any"); }, []); const doGpsLocate = useCallback(async () => { setGpsStatus("locating"); const result = await requestGps(); if (result.ok) { setGpsCoords({ lat: result.lat, lng: result.lng }); setGpsStatus("success"); const name = await reverseGeocode(result.lat, result.lng); if (name) setGpsLocationName(name); } else { setGpsCoords(null); setGpsLocationName(null); setGpsStatus(result.reason === "denied" ? "denied" : "failed"); } }, []); useEffect(() => { doGpsLocate(); }, [doGpsLocate]); const fetchSuggestions = useCallback(async (query: string) => { if (query.length < 1) { setSuggestions([]); setShowSuggestions(false); return; } setFetchingSuggestions(true); try { const res = await fetch(`/api/location/suggest?keywords=${encodeURIComponent(query)}`); const data: LocationSuggestion[] = await res.json(); setSuggestions(data); setShowSuggestions(data.length > 0); } catch { setSuggestions([]); } finally { setFetchingSuggestions(false); } }, []); const handleLocationInput = (val: string) => { setLocationQuery(val); setSelectedLocation(null); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => fetchSuggestions(val), 300); }; const handleSelectLocation = (loc: LocationSuggestion) => { setSelectedLocation(loc); setLocationQuery(loc.name); setShowSuggestions(false); setSuggestions([]); }; const clearLocation = () => { setSelectedLocation(null); setLocationQuery(""); setSuggestions([]); setShowSuggestions(false); }; useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (suggestRef.current && !suggestRef.current.contains(e.target as Node)) { setShowSuggestions(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const joinRoom = async (roomId: string) => { const userId = getUserId(); const res = await fetch(`/api/room/${roomId}/join`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId }), }); if (!res.ok) throw new Error("房间不存在"); return roomId; }; const handleCreate = async () => { setError(""); let coords: { lat: number; lng: number }; if (selectedLocation) { coords = { lat: selectedLocation.lat, lng: selectedLocation.lng }; } else if (gpsCoords) { coords = gpsCoords; } else if (gpsStatus === "locating") { setError("正在定位中,请稍候..."); return; } else { setError("无法获取位置,请在上方搜索并选择一个地点"); return; } setLoading(true); try { setLoadingText(sceneConfig.loadingText); const res = await fetch("/api/room/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...coords, radius, priceRange, cuisine, userId: getUserId(), scene }), }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || "创建房间失败"); } if (!data.roomId) { throw new Error("创建房间失败"); } setLoadingText("正在进入房间..."); await joinRoom(data.roomId); router.push(`/room/${data.roomId}`); } catch (e) { setError(e instanceof Error ? e.message : "创建失败,请重试"); setLoading(false); setLoadingText(""); } }; const handleJoin = async (e: React.FormEvent) => { e.preventDefault(); if (roomCode.length !== 4) { setError("请输入 4 位房间号"); return; } setLoading(true); setError(""); try { await joinRoom(roomCode); router.push(`/room/${roomCode}`); } catch { setError("房间不存在,请检查房间号"); setLoading(false); } }; return (
{/* Profile / Auth button */}
{profile ? ( ) : ( )}

NoWhatever

别说随便

{sceneConfig.subtitle}

创建房间 邀请朋友加入
各自滑卡 右滑喜欢的店
匹配结果 滑中同一家就去
{SCENES.map((s) => { const cfg = getSceneConfig(s); const active = scene === s; return ( ); })}
handleLocationInput(e.target.value)} onFocus={() => suggestions.length > 0 && setShowSuggestions(true)} disabled={loading} className="h-10 w-full rounded-xl border border-zinc-200 bg-white pl-9 pr-9 text-sm text-zinc-700 outline-none transition-colors placeholder:text-zinc-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-100 disabled:opacity-50" /> {(selectedLocation || locationQuery) && !loading && ( )} {fetchingSuggestions && ( )}
{selectedLocation && (
{selectedLocation.district} {selectedLocation.address || selectedLocation.name}
)} {!selectedLocation && !locationQuery && gpsStatus === "locating" && (
正在获取当前位置...
)} {!selectedLocation && !locationQuery && gpsStatus === "success" && (
当前位置:{gpsLocationName || "已定位"}
)} {!selectedLocation && !locationQuery && (gpsStatus === "failed" || gpsStatus === "denied") && (
{gpsStatus === "denied" ? "定位权限被拒绝" : "定位失败"},请搜索选择位置
)} {!selectedLocation && !locationQuery && gpsStatus === "idle" && (
将使用当前定位
)} {showSuggestions && ( {suggestions.map((s) => (
  • ))}
    )}
    {sceneConfig.tagLabel}
    setCuisine(e.target.value)} disabled={loading} className="h-7 w-full rounded-full border-none bg-white pl-3 pr-7 text-xs text-zinc-700 outline-none ring-1 ring-zinc-200 transition-colors placeholder:text-zinc-300 focus:ring-2 focus:ring-emerald-300 disabled:opacity-50" /> {cuisine && !loading && ( )}
    {sceneConfig.hotTags.map((tag) => ( ))}
    距离
    {DISTANCE_OPTIONS.map((opt) => ( ))}
    人均
    {sceneConfig.priceOptions.map((opt) => ( ))}
    或加入已有房间
    { setRoomCode(e.target.value.replace(/\D/g, "").slice(0, 4)); setError(""); }} disabled={loading} className="h-12 flex-1 rounded-xl border border-zinc-200 bg-white px-4 text-center text-lg font-semibold tracking-[0.3em] text-zinc-900 outline-none transition-colors placeholder:text-sm placeholder:tracking-normal placeholder:text-zinc-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-100 disabled:opacity-50" />
    {error && ( {error} )} setAuthModalOpen(false)} onAuth={(p) => setProfile(p)} />
    ); }