feat: 落地页改造 + 集章弹窗全状态品牌/奖品说明
- 落地页:顶部改为活动海报,底部替换为「活动规则」5 条编号列表 - 集章收集弹窗 (StampPopup):新增奖品规则卡片展示 Prize 信息 - 集章册 (AlbumPage / StampGrid):所有状态图章均可点击查看详情 - 兑换弹窗 (RedeemModal):新增 uncollected 分支,统一承载未收集/ 已集齐/已兑换三种状态;新增可选品牌说明区 - 后端 /api/stamps/:id 补充返回 prize 字段 - 管理后台字段标签改名:备注 → 品牌说明;奖品描述 → 奖品说明 - 新增一次性脚本 update-brand-rules,批量写入 16 条品牌权益文案 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
BIN
packages/web/public/poster.jpg
Normal file
BIN
packages/web/public/poster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
@@ -177,12 +177,12 @@ export default function StampForm({ open, id, onClose, onSaved }: Props) {
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="备注">
|
||||
<Field label="品牌说明" hint="选填,展示在收集弹窗与集章册详情中">
|
||||
<textarea
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
rows={2}
|
||||
placeholder="选填"
|
||||
placeholder="例:品牌定位、特色亮点一句话"
|
||||
className={fieldCls + " resize-none"}
|
||||
/>
|
||||
</Field>
|
||||
@@ -234,12 +234,12 @@ export default function StampForm({ open, id, onClose, onSaved }: Props) {
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="奖品描述">
|
||||
<Field label="奖品说明" hint="展示在收集弹窗与兑换页的规则文案">
|
||||
<textarea
|
||||
value={prizeDescription}
|
||||
onChange={(e) => setPrizeDescription(e.target.value)}
|
||||
rows={2}
|
||||
placeholder="选填,展示在用户兑换页"
|
||||
rows={3}
|
||||
placeholder="例:进店消费 19.9 元,可赠送人气爆款谷物锅巴 1 份。"
|
||||
className={fieldCls + " resize-none"}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -9,9 +9,10 @@ type RedeemModalProps = {
|
||||
|
||||
const CONFIRM_COUNTDOWN = 5;
|
||||
|
||||
type Mode = "redeemed" | "sold-out" | "unavailable" | "ready";
|
||||
type Mode = "uncollected" | "redeemed" | "sold-out" | "unavailable" | "ready";
|
||||
|
||||
function resolveMode(stamp: StampWithStatus): Mode {
|
||||
if (!stamp.collected) return "uncollected";
|
||||
if (stamp.redeemed) return "redeemed";
|
||||
if (!stamp.prize || !stamp.prize.enabled) return "unavailable";
|
||||
if (stamp.prize.stock <= 0) return "sold-out";
|
||||
@@ -70,6 +71,8 @@ export default function RedeemModal({ stamp, onRedeem, onClose }: RedeemModalPro
|
||||
|
||||
const buttonCopy = () => {
|
||||
switch (mode) {
|
||||
case "uncollected":
|
||||
return "前往点位收集";
|
||||
case "redeemed":
|
||||
return "已兑换";
|
||||
case "sold-out":
|
||||
@@ -96,7 +99,9 @@ export default function RedeemModal({ stamp, onRedeem, onClose }: RedeemModalPro
|
||||
>
|
||||
<div className="w-full max-w-lg bg-[var(--bg-cream)] rounded-t-2xl p-6 pb-10 animate-slide-up safe-bottom relative overflow-hidden">
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">兑换奖品</h3>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{mode === "uncollected" ? "品牌权益" : "兑换奖品"}
|
||||
</h3>
|
||||
<button onClick={onClose} className="text-[var(--text-muted)] p-1">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
@@ -108,21 +113,40 @@ export default function RedeemModal({ stamp, onRedeem, onClose }: RedeemModalPro
|
||||
<div className="flex items-center gap-4 mb-5">
|
||||
<div
|
||||
className="w-16 h-16 rounded-full bg-white overflow-hidden flex items-center justify-center border border-[var(--border-muted)] shrink-0"
|
||||
style={{ boxShadow: "0 2px 6px rgba(212,165,116,0.25), inset 0 0 0 1px rgba(212,165,116,0.15)" }}
|
||||
style={{
|
||||
boxShadow: stamp.collected
|
||||
? "0 2px 6px rgba(212,165,116,0.25), inset 0 0 0 1px rgba(212,165,116,0.15)"
|
||||
: "0 1px 3px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
<img src={stamp.imageColor} alt={stamp.name} className="w-[92%] h-[92%] object-contain" />
|
||||
<img
|
||||
src={stamp.collected ? stamp.imageColor : stamp.imageGrey}
|
||||
alt={stamp.name}
|
||||
className="w-[92%] h-[92%] object-contain"
|
||||
style={{ opacity: stamp.collected ? 1 : 0.6 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] tracking-[0.25em] uppercase text-[var(--gold)] mb-0.5">Stamp</p>
|
||||
<p className="text-base font-semibold text-[var(--text-primary)] truncate">{stamp.name}</p>
|
||||
{stamp.collectedAt && (
|
||||
{mode === "uncollected" ? (
|
||||
<p className="text-xs text-[var(--text-muted)] mt-0.5">尚未收集</p>
|
||||
) : stamp.collectedAt ? (
|
||||
<p className="text-xs text-[var(--text-muted)] mt-0.5">
|
||||
收集于 {new Date(stamp.collectedAt).toLocaleDateString("zh-CN")}
|
||||
</p>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Brand description (optional) */}
|
||||
{stamp.note && (
|
||||
<div className="rounded-xl border border-[var(--border-muted)] bg-white p-3.5 mb-3">
|
||||
<p className="text-[10px] tracking-[0.2em] text-[var(--gold)] uppercase mb-1">Brand</p>
|
||||
<p className="text-xs text-[var(--text-secondary)] leading-relaxed">{stamp.note}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prize card */}
|
||||
{prize ? (
|
||||
<div className="rounded-xl border border-[var(--jade)] bg-[rgba(45,106,79,0.05)] p-4 mb-4">
|
||||
@@ -148,6 +172,11 @@ export default function RedeemModal({ stamp, onRedeem, onClose }: RedeemModalPro
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === "uncollected" && (
|
||||
<p className="text-xs text-[var(--text-muted)] text-center mb-4 leading-relaxed">
|
||||
请前往线下点位触碰 NFC 收集该图章后再来兑换
|
||||
</p>
|
||||
)}
|
||||
{mode === "redeemed" && (
|
||||
<p className="text-xs text-[var(--text-muted)] text-center mb-4">你已经兑换过这枚图章对应的奖品</p>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function StampGrid({ stamps, onStampClick }: StampGridProps) {
|
||||
imageGrey={stamp.imageGrey}
|
||||
collected={stamp.collected}
|
||||
redeemed={stamp.redeemed}
|
||||
onClick={stamp.collected ? () => onStampClick?.(stamp) : undefined}
|
||||
onClick={() => onStampClick?.(stamp)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { PrizeInfo } from "@stamp/shared";
|
||||
|
||||
type StampPopupProps = {
|
||||
name: string;
|
||||
imageColor: string;
|
||||
note?: string | null;
|
||||
prize?: PrizeInfo | null;
|
||||
status: "preview" | "collected" | "already";
|
||||
onCollect?: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function StampPopup({ name, imageColor, note, status, onCollect, onClose }: StampPopupProps) {
|
||||
export default function StampPopup({ name, imageColor, note, prize, status, onCollect, onClose }: StampPopupProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center animate-overlay-fade"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center animate-overlay-fade px-5"
|
||||
style={{ backgroundColor: "var(--overlay)" }}
|
||||
onClick={(e) => e.target === e.currentTarget && onClose()}
|
||||
>
|
||||
<div className="w-72 bg-[var(--bg-cream)] rounded-2xl p-6 text-center shadow-[var(--shadow-lg)]">
|
||||
<div className="w-full max-w-xs bg-[var(--bg-cream)] rounded-2xl p-6 text-center shadow-[var(--shadow-lg)] max-h-[90vh] overflow-y-auto">
|
||||
{/* Stamp image */}
|
||||
<div className="w-40 h-40 mx-auto mb-4">
|
||||
<div className="w-36 h-36 mx-auto mb-4">
|
||||
<div
|
||||
className="w-full h-full rounded-full bg-white overflow-hidden flex items-center justify-center border border-[var(--border-muted)] animate-stamp-press"
|
||||
style={{
|
||||
@@ -44,13 +46,24 @@ export default function StampPopup({ name, imageColor, note, status, onCollect,
|
||||
|
||||
{/* Stamp name */}
|
||||
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-1">{name}</h3>
|
||||
{note && <p className="text-xs text-[var(--text-muted)] mb-4">{note}</p>}
|
||||
{note && <p className="text-xs text-[var(--text-muted)] mb-3 leading-relaxed">{note}</p>}
|
||||
|
||||
{/* Prize rule (preview only) */}
|
||||
{status === "preview" && prize && (
|
||||
<div className="mt-3 mb-1 rounded-xl border border-[var(--jade)] bg-[rgba(45,106,79,0.05)] p-3.5 text-left">
|
||||
<p className="text-[10px] tracking-[0.2em] text-[var(--jade)] uppercase mb-1">Reward</p>
|
||||
<p className="text-sm font-semibold text-[var(--text-primary)]">{prize.name}</p>
|
||||
{prize.description && (
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1 leading-relaxed">{prize.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status message & action */}
|
||||
{status === "preview" && (
|
||||
<button
|
||||
onClick={onCollect}
|
||||
className="w-full py-3 rounded-xl text-sm font-medium text-white mt-2"
|
||||
className="w-full py-3 rounded-xl text-sm font-medium text-white mt-4"
|
||||
style={{ backgroundColor: "var(--terracotta)" }}
|
||||
>
|
||||
立即获取
|
||||
|
||||
@@ -55,7 +55,6 @@ export default function AlbumPage() {
|
||||
setShowRegister(true);
|
||||
return;
|
||||
}
|
||||
if (!stamp.collected) return;
|
||||
setSelectedStampId(stamp.id);
|
||||
};
|
||||
|
||||
@@ -107,11 +106,9 @@ export default function AlbumPage() {
|
||||
style={{ width: stamps.length ? `${(collectedCount / stamps.length) * 100}%` : "0%" }}
|
||||
/>
|
||||
</div>
|
||||
{collectedCount > 0 && (
|
||||
<p className="mt-3 text-[11px] text-[var(--text-muted)] leading-relaxed">
|
||||
点击已点亮的图章,即可兑换对应奖品
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-3 text-[11px] text-[var(--text-muted)] leading-relaxed">
|
||||
点击任意图章查看品牌权益,已点亮的图章可直接兑换
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stamp Grid */}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
import type { PrizeInfo } from "@stamp/shared";
|
||||
import { apiFetch } from "../lib/api";
|
||||
import { useAuth } from "../lib/auth";
|
||||
import FloatingButton from "../components/FloatingButton";
|
||||
@@ -14,14 +16,49 @@ type StampDetail = {
|
||||
note: string | null;
|
||||
imageColor: string;
|
||||
imageGrey: string;
|
||||
prize: PrizeInfo | null;
|
||||
};
|
||||
|
||||
type CollectState = "idle" | "loading" | "show_stamp" | "needs_register" | "collecting" | "collected" | "already_collected";
|
||||
|
||||
const STEPS = [
|
||||
{ num: "01", title: "前往点位", desc: "跟随路线,探索散落在城市中的文化地标" },
|
||||
{ num: "02", title: "触碰集章", desc: "手机轻触点位 NFC 芯片,图章即刻收入囊中" },
|
||||
{ num: "03", title: "兑换好礼", desc: "集满图章,兑换城市限定纪念品" },
|
||||
const RULES: { num: string; title: string; desc: ReactNode }[] = [
|
||||
{
|
||||
num: "01",
|
||||
title: "去朝天宫读城",
|
||||
desc: "活动期间,用户可在朝天宫街道辖区范围内自由探索,拍摄美食美景,记录你眼中的城南烟火气——红墙下的光影、打钉巷里热腾腾的锅贴、南台巷排队的咖啡店,街角一只晒太阳的猫……",
|
||||
},
|
||||
{
|
||||
num: "02",
|
||||
title: "线上打卡",
|
||||
desc: (
|
||||
<>
|
||||
将您的美图在小红书或者微博带下列两个话题{" "}
|
||||
<span className="text-[var(--terracotta)] font-medium">#我在朝天宫读城</span>
|
||||
{" "}
|
||||
<span className="text-[var(--terracotta)] font-medium">#跟着巷主去读城</span>
|
||||
{" "}发布笔记或微博即可完成线上打卡,发布时别忘了附带定位信息。
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
num: "03",
|
||||
title: "线下打卡",
|
||||
desc: '前往任意一家门口贴有"读城窗口"标志的小店,进店后找到活动立牌,触碰 NFC 热点,在对应网页中完成线下打卡。',
|
||||
},
|
||||
{
|
||||
num: "04",
|
||||
title: "解锁权益",
|
||||
desc: "用户完成线上打卡、线下打卡后,即可在小店解锁活动权益。活动期间每个账号在同一小店仅享受 1 次权益,权益现场核销,不重复享受,名额有限,先到先得。",
|
||||
},
|
||||
{
|
||||
num: "05",
|
||||
title: "活动时间",
|
||||
desc: (
|
||||
<span className="text-[var(--terracotta)] font-medium">
|
||||
2026 年 4 月 21 日 — 2026 年 5 月 21 日
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function LandingPage() {
|
||||
@@ -100,149 +137,66 @@ export default function LandingPage() {
|
||||
const showRegister = collectState === "needs_register";
|
||||
|
||||
return (
|
||||
<div className="grain-overlay">
|
||||
{/* ═══════════ HERO ═══════════ */}
|
||||
<section className="relative min-h-svh flex flex-col items-center justify-center overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[var(--bg-dark-deep)]" />
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
radial-gradient(ellipse 60% 50% at 50% 40%, rgba(212, 165, 116, 0.08) 0%, transparent 100%),
|
||||
radial-gradient(circle at 15% 80%, rgba(199, 91, 57, 0.06) 0%, transparent 50%),
|
||||
radial-gradient(circle at 85% 20%, rgba(45, 106, 79, 0.04) 0%, transparent 40%)
|
||||
`,
|
||||
}}
|
||||
<div className="min-h-svh bg-[var(--bg-cream)]">
|
||||
{/* ═══════════ POSTER ═══════════ */}
|
||||
<section className="relative w-full">
|
||||
<img
|
||||
src="/poster.jpg"
|
||||
alt="读城·行走朝天宫"
|
||||
className="block w-full h-auto select-none"
|
||||
draggable={false}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(212, 165, 116, 0.5) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(212, 165, 116, 0.5) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: "60px 60px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 text-center px-8 flex flex-col items-center">
|
||||
<div className="animate-fade-in mb-8" style={{ animationDelay: "0.2s" }}>
|
||||
<div className="inline-flex items-center gap-3">
|
||||
<span className="block w-8 h-px bg-[var(--gold)]/40" />
|
||||
<span className="text-[var(--gold)] text-[11px] tracking-[0.4em] uppercase"
|
||||
style={{ fontFamily: "'Playfair Display', serif" }}>
|
||||
CityWalk
|
||||
</span>
|
||||
<span className="block w-8 h-px bg-[var(--gold)]/40" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="animate-fade-in-up text-[var(--text-inverted)] leading-none mb-6"
|
||||
style={{
|
||||
animationDelay: "0.4s",
|
||||
fontSize: "clamp(3rem, 12vw, 4.5rem)",
|
||||
fontFamily: "'Playfair Display', serif",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.02em",
|
||||
}}>
|
||||
城市漫步
|
||||
</h1>
|
||||
|
||||
<p className="animate-fade-in-up text-[var(--gold-light)]/70 text-sm leading-relaxed max-w-[260px]"
|
||||
style={{ animationDelay: "0.6s", letterSpacing: "0.08em" }}>
|
||||
走过每一条街巷<br />收集属于你的城市记忆
|
||||
</p>
|
||||
|
||||
<div className="animate-scale-in mt-14" style={{ animationDelay: "0.9s" }}>
|
||||
<div className="stamp-seal w-[100px] h-[100px] animate-float">
|
||||
<div className="w-[100px] h-[100px] rounded-full flex items-center justify-center"
|
||||
style={{
|
||||
background: "radial-gradient(circle, rgba(212, 165, 116, 0.12) 0%, rgba(212, 165, 116, 0.02) 70%)",
|
||||
border: "1.5px solid rgba(212, 165, 116, 0.2)",
|
||||
}}>
|
||||
<div className="text-center">
|
||||
<div className="text-[var(--gold)] text-[10px] tracking-[0.2em] uppercase opacity-60">Stamp</div>
|
||||
<div className="text-[var(--gold)] text-2xl mt-0.5 opacity-80"
|
||||
style={{ fontFamily: "'Playfair Display', serif" }}>9</div>
|
||||
<div className="text-[var(--gold)] text-[9px] tracking-[0.15em] uppercase opacity-50">Collect</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="animate-fade-in mt-16" style={{ animationDelay: "1.4s" }}>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<span className="text-[var(--gold)]/30 text-[10px] tracking-[0.3em] uppercase">探索</span>
|
||||
<div className="w-px h-8 bg-gradient-to-b from-[var(--gold)]/30 to-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ═══════════ ABOUT ═══════════ */}
|
||||
<section className="relative bg-[var(--bg-dark)] py-20 px-6 overflow-hidden">
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-[var(--gold)]/20 to-transparent" />
|
||||
<div className="max-w-sm mx-auto">
|
||||
<div className="flex items-center gap-3 mb-8 animate-fade-in-up">
|
||||
<span className="block w-6 h-px bg-[var(--gold)]/40" />
|
||||
<span className="text-[var(--gold)]/50 text-[10px] tracking-[0.3em] uppercase">About</span>
|
||||
</div>
|
||||
<h2 className="text-[var(--text-inverted)] text-2xl leading-snug mb-6 animate-fade-in-up"
|
||||
style={{ fontFamily: "'Playfair Display', serif", animationDelay: "0.1s" }}>
|
||||
一场属于你的<br /><span className="text-[var(--gold)]">城市寻宝之旅</span>
|
||||
</h2>
|
||||
<p className="text-[var(--text-inverted)]/50 text-sm leading-[1.9] animate-fade-in-up"
|
||||
style={{ animationDelay: "0.2s" }}>
|
||||
穿行于古桥与老街之间,在园林深处驻足片刻,于茶馆中听一段旧时光。每一个城市坐标都藏着一枚专属图章,等你亲手揭开。
|
||||
</p>
|
||||
<div className="ornament-line mt-10" />
|
||||
<div className="mt-10 grid grid-cols-3 gap-4 stagger-children">
|
||||
{[
|
||||
{ num: "9", label: "城市坐标" },
|
||||
{ num: "4", label: "限定好礼" },
|
||||
{ num: "∞", label: "重复挑战" },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="text-center">
|
||||
<div className="text-[var(--gold)] text-3xl mb-1.5"
|
||||
style={{ fontFamily: "'Playfair Display', serif" }}>{item.num}</div>
|
||||
<div className="text-[var(--text-inverted)]/35 text-[11px] tracking-wider">{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ═══════════ HOW IT WORKS ═══════════ */}
|
||||
<section className="relative paper-texture py-20 px-6 pb-32">
|
||||
<div className="relative z-10 max-w-sm mx-auto pt-4">
|
||||
{/* ═══════════ RULES ═══════════ */}
|
||||
<section className="relative paper-texture px-6 py-14 pb-32">
|
||||
<div className="relative z-10 max-w-sm mx-auto">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="block w-6 h-px bg-[var(--text-primary)]/20" />
|
||||
<span className="text-[var(--text-muted)] text-[10px] tracking-[0.3em] uppercase">How it works</span>
|
||||
<span className="text-[var(--text-muted)] text-[10px] tracking-[0.3em] uppercase">
|
||||
Rules
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-[var(--text-primary)] text-2xl leading-snug mb-12"
|
||||
style={{ fontFamily: "'Playfair Display', serif" }}>
|
||||
三步开启旅程
|
||||
<h2
|
||||
className="text-[var(--text-primary)] text-2xl leading-snug mb-10"
|
||||
style={{ fontFamily: "'Playfair Display', serif" }}
|
||||
>
|
||||
活动规则
|
||||
</h2>
|
||||
<div className="space-y-0 stagger-children">
|
||||
{STEPS.map((step, i) => (
|
||||
<div key={step.num} className="relative flex gap-5">
|
||||
|
||||
<ol className="space-y-0 stagger-children">
|
||||
{RULES.map((rule, i) => (
|
||||
<li key={rule.num} className="relative flex gap-5">
|
||||
<div className="flex flex-col items-center shrink-0">
|
||||
<div className="w-11 h-11 rounded-full border-2 flex items-center justify-center"
|
||||
style={{ borderColor: "var(--gold)", background: "rgba(212, 165, 116, 0.06)" }}>
|
||||
<span className="text-[var(--gold)] text-xs"
|
||||
style={{ fontFamily: "'Playfair Display', serif", fontWeight: 600 }}>
|
||||
{step.num}
|
||||
<div
|
||||
className="w-11 h-11 rounded-full border-2 flex items-center justify-center"
|
||||
style={{
|
||||
borderColor: "var(--gold)",
|
||||
background: "rgba(212, 165, 116, 0.06)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="text-[var(--gold)] text-xs"
|
||||
style={{ fontFamily: "'Playfair Display', serif", fontWeight: 600 }}
|
||||
>
|
||||
{rule.num}
|
||||
</span>
|
||||
</div>
|
||||
{i < STEPS.length - 1 && <div className="w-px flex-1 min-h-[40px] bg-[var(--gold)]/20" />}
|
||||
{i < RULES.length - 1 && (
|
||||
<div className="w-px flex-1 min-h-[40px] bg-[var(--gold)]/20" />
|
||||
)}
|
||||
</div>
|
||||
<div className="pb-10 pt-1.5">
|
||||
<h3 className="text-base font-semibold text-[var(--text-primary)] mb-1">{step.title}</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)] leading-relaxed">{step.desc}</p>
|
||||
<div className="pb-8 pt-1.5">
|
||||
<h3 className="text-base font-semibold text-[var(--text-primary)] mb-1.5">
|
||||
{rule.title}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)] leading-[1.9]">
|
||||
{rule.desc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -254,6 +208,7 @@ export default function LandingPage() {
|
||||
name={stamp.name}
|
||||
imageColor={stamp.imageColor}
|
||||
note={stamp.note}
|
||||
prize={stamp.prize}
|
||||
status="preview"
|
||||
onCollect={handleCollect}
|
||||
onClose={handleClose}
|
||||
@@ -264,6 +219,7 @@ export default function LandingPage() {
|
||||
name={stamp.name}
|
||||
imageColor={stamp.imageColor}
|
||||
note={stamp.note}
|
||||
prize={stamp.prize}
|
||||
status="collected"
|
||||
onClose={handleClose}
|
||||
/>
|
||||
@@ -273,6 +229,7 @@ export default function LandingPage() {
|
||||
name={stamp.name}
|
||||
imageColor={stamp.imageColor}
|
||||
note={stamp.note}
|
||||
prize={stamp.prize}
|
||||
status="already"
|
||||
onClose={handleClose}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user