Files
no-whatever/src/app/page.tsx
T
kurihada 7d51f5200d feat: 新增周末契约盲盒功能,首页重构为双模式入口
- 新增 BlindBoxIdea 数据模型及 migration
- 新增盲盒 API (提交想法/查询/抽取)
- 新增周末契约盲盒页面 (动效震动+彩带开奖)
- 原首页功能拆分至 /panic 路由
- 首页重构为极速救场 + 周末契约双卡片入口
2026-02-26 11:27:10 +08:00

218 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 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">
{/* Header */}
<motion.div
className="flex items-center gap-3"
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">
NoWhatever
</h1>
<p className="text-[10px] font-medium tracking-[0.2em] text-muted">
便 ·
</p>
</div>
</motion.div>
<motion.p
className="mt-3 max-w-xs text-center text-xs leading-relaxed text-muted"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
>
"随便"
</motion.p>
{/* Dual Cards */}
<div className="mt-8 flex w-full max-w-sm flex-col gap-5">
{/* 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"
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 }}
>
<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="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">
<Zap size={22} className="text-white" />
</div>
<div>
<h2 className="text-lg font-black text-white"> </h2>
<p className="text-[10px] font-semibold tracking-wider text-white/70">
PANIC MODE
</p>
</div>
</div>
<p className="mt-4 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>
</div>
<motion.div
className="absolute inset-0 rounded-2xl"
whileHover={{
x: [0, -2, 2, -2, 2, 0],
transition: { duration: 0.4, repeat: Infinity },
}}
/>
</motion.button>
{/* 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"
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 }}
>
<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="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">
<Gift size={22} className="text-purple-200" />
</div>
<div>
<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">
ADVENTURE ROULETTE
</p>
</div>
</div>
<p className="mt-4 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>
</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"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
>
NoWhatever "随便"
</motion.p>
</div>
);
}