feat: 盲盒房间体系重构 — 强制登录、独立房间、用户归属
- 新增 BlindBoxRoom/BlindBoxMember 模型,BlindBoxIdea 增加 userId/drawnById - 新增房间 API(创建/加入/列表/详情),所有盲盒 API 增加认证和成员校验 - 新建盲盒大厅页面(三层引导式设计:未登录氛围页/首次创建引导/房间列表) - 新建盲盒房间页面(成员校验/邀请分享/用户归属展示/自动聚焦) - 首页删除契约画廊和 localStorage 盲盒逻辑,周末契约跳转到 /blindbox - 清理旧路由 /room/[id]/blindbox - 提取共享工具 src/lib/blindbox.ts(错误响应/房间号生成/成员校验) - AuthModal 支持 defaultTab 参数 - 更新项目规范:新项目原则、代码优雅和复用优先
This commit is contained in:
+54
-119
@@ -1,115 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { motion } from "framer-motion";
|
||||
import { Zap, Gift, Clock, Trophy } from "lucide-react";
|
||||
import { Zap, Gift, Clock, ChevronRight } from "lucide-react";
|
||||
import BrandLogo from "@/components/BrandLogo";
|
||||
|
||||
function generateRoomCode() {
|
||||
return Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
}
|
||||
|
||||
interface DrawnIdea {
|
||||
id: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function LandingPage() {
|
||||
const router = useRouter();
|
||||
const [drawnHistory, setDrawnHistory] = useState<DrawnIdea[]>([]);
|
||||
const [blindboxRoom, setBlindboxRoom] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem("nw_blindbox_room");
|
||||
if (saved) {
|
||||
setBlindboxRoom(saved);
|
||||
fetch(`/api/blindbox?roomId=${saved}`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (data.drawn) setDrawnHistory(data.drawn);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePanicMode = () => {
|
||||
router.push("/panic");
|
||||
};
|
||||
|
||||
const handleAdventureMode = () => {
|
||||
let room = blindboxRoom;
|
||||
if (!room) {
|
||||
room = generateRoomCode();
|
||||
localStorage.setItem("nw_blindbox_room", room);
|
||||
setBlindboxRoom(room);
|
||||
}
|
||||
router.push(`/room/${room}/blindbox`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-dvh flex-col items-center bg-background px-5 py-8 overflow-y-auto scrollbar-none">
|
||||
<div className="relative flex min-h-dvh flex-col items-center bg-background px-5 py-10 overflow-y-auto scrollbar-none">
|
||||
{/* Ambient glow */}
|
||||
<div className="pointer-events-none fixed left-1/2 top-0 -translate-x-1/2 -translate-y-1/3 h-[420px] w-[420px] rounded-full bg-orange-500/8 blur-3xl" />
|
||||
<div className="pointer-events-none fixed left-1/4 top-1/2 h-[300px] w-[300px] rounded-full bg-purple-500/5 blur-3xl" />
|
||||
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
className="flex items-center gap-3"
|
||||
className="flex flex-col items-center gap-4"
|
||||
initial={{ y: -30, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
>
|
||||
<BrandLogo size={40} />
|
||||
<div>
|
||||
<h1 className="text-xl font-black tracking-tight text-white">
|
||||
<BrandLogo size={48} />
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-black tracking-tight text-white">
|
||||
NoWhatever
|
||||
</h1>
|
||||
<p className="text-[10px] font-medium tracking-[0.2em] text-muted">
|
||||
<p className="mt-1 text-[11px] font-medium tracking-[0.2em] text-gray-500">
|
||||
别说随便 · 亲密关系决策引擎
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.p
|
||||
className="mt-3 max-w-xs text-center text-xs leading-relaxed text-muted"
|
||||
className="mt-4 max-w-68 text-center text-sm leading-relaxed text-gray-400"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
别再说"随便"了。两个模式,覆盖你们所有的选择困难症。
|
||||
别再说"随便"了。
|
||||
<br />
|
||||
两个模式,覆盖你们所有的选择困难症。
|
||||
</motion.p>
|
||||
|
||||
{/* Dual Cards */}
|
||||
<div className="mt-8 flex w-full max-w-sm flex-col gap-5">
|
||||
<div className="mt-9 flex w-full max-w-sm flex-col gap-4">
|
||||
{/* Card A: Panic Mode */}
|
||||
<motion.button
|
||||
onClick={handlePanicMode}
|
||||
className="group relative w-full overflow-hidden rounded-2xl bg-linear-to-br from-yellow-400 to-orange-500 p-6 text-left shadow-lg shadow-orange-500/20 transition-shadow hover:shadow-xl hover:shadow-orange-500/30"
|
||||
onClick={() => router.push("/panic")}
|
||||
className="group relative w-full overflow-hidden rounded-2xl bg-linear-to-br from-yellow-400 to-orange-500 p-5 pb-4 text-left shadow-xl shadow-orange-500/25 ring-1 ring-white/15 transition-shadow hover:shadow-2xl hover:shadow-orange-500/35"
|
||||
initial={{ x: -40, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
whileHover={{ scale: 1.02, rotate: -0.5 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
>
|
||||
<div className="absolute -right-4 -top-4 h-24 w-24 rounded-full bg-white/10 blur-2xl" />
|
||||
<div className="absolute -bottom-6 -left-6 h-20 w-20 rounded-full bg-white/10 blur-xl" />
|
||||
<div className="absolute -right-4 -top-4 h-28 w-28 rounded-full bg-white/15 blur-2xl" />
|
||||
<div className="absolute -bottom-6 -left-6 h-20 w-20 rounded-full bg-yellow-300/20 blur-xl" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-black/10">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-black/15 ring-1 ring-white/10">
|
||||
<Zap size={22} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-lg font-black text-white">⚡️ 极速救场</h2>
|
||||
<p className="text-[10px] font-semibold tracking-wider text-white/70">
|
||||
<p className="text-[10px] font-semibold tracking-wider text-white/60">
|
||||
PANIC MODE
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 text-sm font-medium leading-relaxed text-white/90">
|
||||
<p className="mt-3.5 text-sm font-medium leading-relaxed text-white/90">
|
||||
10秒内出结果,立刻闭嘴,听天由命
|
||||
</p>
|
||||
<div className="mt-3 flex items-center gap-1.5 text-xs font-bold text-white/60">
|
||||
<Clock size={12} />
|
||||
<span>即时决策 · 转盘匹配</span>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5 text-xs font-bold text-white/50">
|
||||
<Clock size={12} />
|
||||
<span>即时决策 · 转盘匹配</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 text-xs font-semibold text-white/40 transition-colors group-hover:text-white/70">
|
||||
进入
|
||||
<ChevronRight size={14} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -124,93 +96,56 @@ export default function LandingPage() {
|
||||
|
||||
{/* Card B: Adventure Roulette */}
|
||||
<motion.button
|
||||
onClick={handleAdventureMode}
|
||||
className="group relative w-full overflow-hidden rounded-2xl bg-linear-to-br from-indigo-900 to-purple-800 p-6 text-left shadow-lg shadow-purple-900/30 transition-shadow hover:shadow-xl hover:shadow-purple-500/30"
|
||||
onClick={() => router.push("/blindbox")}
|
||||
className="group relative w-full overflow-hidden rounded-2xl bg-linear-to-br from-indigo-900 to-purple-800 p-5 pb-4 text-left shadow-xl shadow-purple-600/20 ring-1 ring-purple-400/15 transition-shadow hover:shadow-2xl hover:shadow-purple-500/30"
|
||||
initial={{ x: 40, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.4 }}
|
||||
whileHover={{ scale: 1.02, rotate: 0.5 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
>
|
||||
<div className="absolute -right-6 -top-6 h-28 w-28 rounded-full bg-purple-500/20 blur-2xl transition-all group-hover:bg-purple-400/30 group-hover:blur-3xl" />
|
||||
<div className="absolute -bottom-4 -left-4 h-20 w-20 rounded-full bg-indigo-400/15 blur-xl transition-all group-hover:bg-indigo-300/25" />
|
||||
<div className="absolute -right-6 -top-6 h-32 w-32 rounded-full bg-purple-500/20 blur-2xl transition-all group-hover:bg-purple-400/30 group-hover:blur-3xl" />
|
||||
<div className="absolute -bottom-4 -left-4 h-24 w-24 rounded-full bg-indigo-400/15 blur-xl transition-all group-hover:bg-indigo-300/25" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-white/10">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-white/10 ring-1 ring-purple-300/15">
|
||||
<Gift size={22} className="text-purple-200" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-lg font-black text-white drop-shadow-[0_0_12px_rgba(192,132,252,0.5)]">
|
||||
🎁 周末契约
|
||||
</h2>
|
||||
<p className="text-[10px] font-semibold tracking-wider text-purple-300/70">
|
||||
<p className="text-[10px] font-semibold tracking-wider text-purple-300/60">
|
||||
ADVENTURE ROULETTE
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 text-sm font-medium leading-relaxed text-purple-100/90">
|
||||
<p className="mt-3.5 text-sm font-medium leading-relaxed text-purple-100/90">
|
||||
丢入疯狂想法,周末盲盒开奖,绝不反悔
|
||||
</p>
|
||||
<div className="mt-3 flex items-center gap-1.5 text-xs font-bold text-purple-300/60">
|
||||
<Gift size={12} />
|
||||
<span>盲盒蓄水 · 仪式开奖</span>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5 text-xs font-bold text-purple-300/50">
|
||||
<Gift size={12} />
|
||||
<span>盲盒蓄水 · 仪式开奖</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 text-xs font-semibold text-purple-200/40 transition-colors group-hover:text-purple-200/70">
|
||||
进入
|
||||
<ChevronRight size={14} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{/* Trophy Wall */}
|
||||
{drawnHistory.length > 0 && (
|
||||
<motion.div
|
||||
className="mt-10 w-full max-w-sm"
|
||||
initial={{ y: 30, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Trophy size={14} className="text-amber-400" />
|
||||
<h3 className="text-xs font-bold tracking-wider text-muted">
|
||||
契约画廊
|
||||
</h3>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{drawnHistory.map((item, i) => (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
className="flex items-start gap-3 rounded-xl bg-surface/80 px-4 py-3 ring-1 ring-border"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.7 + i * 0.08 }}
|
||||
>
|
||||
<span className="mt-0.5 text-base">🏆</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{item.content}
|
||||
</p>
|
||||
<p className="mt-0.5 text-[10px] text-dim">
|
||||
{new Date(item.createdAt).toLocaleDateString("zh-CN", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
weekday: "short",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<motion.p
|
||||
className="mt-auto pt-8 text-center text-[10px] text-dim"
|
||||
className="mt-auto pt-10 text-center text-[10px] font-medium tracking-widest text-muted"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
>
|
||||
NoWhatever — 拒绝"随便",从今天开始
|
||||
NOWHATEVER — 拒绝随便,从今天开始
|
||||
</motion.p>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user