init: init prok

This commit is contained in:
2026-04-16 15:34:47 +08:00
commit db74381f13
56 changed files with 5850 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
import { useState } from "react";
import type { RedemptionRuleInfo } from "@stamp/shared";
type RedeemModalProps = {
rules: RedemptionRuleInfo[];
collectedCount: number;
onRedeem: (ruleId: string) => Promise<void>;
onClose: () => void;
};
export default function RedeemModal({ rules, collectedCount, onRedeem, onClose }: RedeemModalProps) {
const [redeeming, setRedeeming] = useState<string | null>(null);
const [error, setError] = useState("");
const handleRedeem = async (ruleId: string) => {
if (!confirm("兑换后所有已收集的图章将被清空,确定兑换吗?")) return;
setRedeeming(ruleId);
setError("");
try {
await onRedeem(ruleId);
onClose();
} catch (e) {
setError(e instanceof Error ? e.message : "兑换失败");
} finally {
setRedeeming(null);
}
};
return (
<div className="fixed inset-0 z-50 flex items-end justify-center animate-overlay-fade"
style={{ backgroundColor: "var(--overlay)" }}
onClick={(e) => e.target === e.currentTarget && onClose()}
>
<div className="w-full max-w-lg bg-[var(--bg-cream)] rounded-t-2xl p-6 pb-10 animate-slide-up safe-bottom">
<div className="flex items-center justify-between mb-5">
<h3 className="text-lg font-semibold text-[var(--text-primary)]"></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" />
</svg>
</button>
</div>
<p className="text-sm text-[var(--text-secondary)] mb-4">
<span className="font-semibold text-[var(--jade)]">{collectedCount}</span>
</p>
{error && (
<p className="text-sm text-[var(--terracotta)] mb-3">{error}</p>
)}
<div className="space-y-3">
{rules.map((rule) => {
const canRedeem = collectedCount >= rule.threshold;
return (
<div
key={rule.id}
className="flex items-center justify-between p-4 rounded-xl border"
style={{
borderColor: canRedeem ? "var(--jade)" : "var(--border-muted)",
backgroundColor: canRedeem ? "rgba(45, 106, 79, 0.05)" : "white",
}}
>
<div className="flex-1 min-w-0 mr-3">
<p className="text-sm font-medium text-[var(--text-primary)] truncate">
{rule.name}
</p>
{rule.description && (
<p className="text-xs text-[var(--text-muted)] mt-0.5 truncate">{rule.description}</p>
)}
<p className="text-xs text-[var(--text-muted)] mt-1">
{rule.threshold}
</p>
</div>
<button
onClick={() => handleRedeem(rule.id)}
disabled={!canRedeem || !!redeeming}
className="shrink-0 px-4 py-2 rounded-lg text-sm font-medium transition-colors"
style={{
backgroundColor: canRedeem ? "var(--jade)" : "var(--border-muted)",
color: canRedeem ? "white" : "var(--text-muted)",
opacity: redeeming === rule.id ? 0.6 : 1,
}}
>
{redeeming === rule.id ? "兑换中..." : "兑换"}
</button>
</div>
);
})}
</div>
</div>
</div>
);
}