투표 제출 CTA 단일 카드 통합 + Red Wave 2026 트랙 추가

develop
jwkim 2026-06-18 10:27:13 +09:00
parent 7a68d27693
commit 2988a59749
7 changed files with 42 additions and 27 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ yarn-error.log*
# secrets (구 firebase 잔재)
serviceAccount.json
.firebaserc
# 로컬 전용 댓글 관리 도구
tp-comment-admin/

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

View File

@ -13,7 +13,7 @@ import { ApiError, getDeviceId, getRecentEmails, rememberEmail, submitPrediction
import Countdown from "./Countdown";
import TeamFlag from "./TeamFlag";
type Step = "pick" | "form" | "done";
type Step = "form" | "done";
const MODEL_ICON: Record<ModelName, string> = {
GPT: "/icons/gpt.png",
@ -42,7 +42,7 @@ export default function Arena({
const [outcome, setOutcome] = useState<Outcome | null>("DRAW");
const [scoreA, setScoreA] = useState(0);
const [scoreB, setScoreB] = useState(0);
const [step, setStep] = useState<Step>("pick");
const [step, setStep] = useState<Step>("form");
const [recentEmails, setRecentEmails] = useState<string[]>(getRecentEmails);
const [email, setEmail] = useState(""); // 칸은 비우고, 입력/포커스 시 datalist 드롭다운으로만 자동완성
const [notify, setNotify] = useState(true);
@ -79,10 +79,7 @@ export default function Arena({
.map((p) => ({ model: p.model, exact: p.scoreA === scoreA && p.scoreB === scoreB }));
}, [outcome, scoreA, scoreB, predictions]);
const submitPick = () => {
if (!outcome) return;
setStep("form");
};
const emailValid = EMAIL_RE.test(email.trim());
const confirmSubmit = async () => {
if (!EMAIL_RE.test(email.trim()) || !outcome) return;
@ -285,20 +282,22 @@ export default function Arena({
</section>
)}
{/* ===== CTA 민트 #85FCA8 (진행 중일 때) ===== */}
{!finished && step === "pick" && (
{/* ===== 투표 마감/오픈 전: 상태 버튼만 ===== */}
{!finished && disabled && (
<button
onClick={submitPick}
disabled={!outcome || disabled}
className="btn-mint mt-5 w-full rounded-2xl py-5 text-[20px] font-extrabold transition active:scale-[0.99] disabled:opacity-40 disabled:shadow-none"
disabled
className="btn-mint mt-5 w-full rounded-2xl py-5 text-[20px] font-extrabold opacity-60"
>
{ctaLabel}
</button>
)}
{/* ===== 폼: 이메일만 (D5, 닉네임 제거) ===== */}
{!finished && step === "form" && (
<div className="mt-5 space-y-2.5 rounded-2xl border border-[var(--line-d)] bg-[var(--bg2)] p-4">
{/* ===== 투표 제출 (D5): 이메일 + 단일 CTA를 한 카드로 통합 ===== */}
{!finished && !disabled && step === "form" && (
<div className="mt-5 rounded-2xl border-2 border-[var(--green)]/60 bg-[var(--bg2)] p-4 shadow-[0_0_24px_rgba(74,255,160,0.08)]">
<div className="mb-2.5 text-center text-[15px] font-extrabold text-[var(--green)]">
{t.emailGate}
</div>
<input
type="email"
inputMode="email"
@ -307,7 +306,7 @@ export default function Arena({
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t.emailPh}
className="w-full rounded-lg border border-[var(--line-d)] bg-[#0f1217] px-3 py-3 text-[15px] text-white outline-none focus:border-[var(--green)]"
className="w-full rounded-2xl border border-[var(--share)] bg-[#0f1217] px-4 py-3 text-[16px] text-white shadow-[0_0_0_2px_rgba(166,94,255,0.12)] outline-none focus:shadow-[0_0_0_2px_rgba(166,94,255,0.3)]"
/>
{recentEmails.length > 0 && (
<datalist id="tp-email-suggestions">
@ -316,24 +315,24 @@ export default function Arena({
))}
</datalist>
)}
<label className="flex items-start gap-2 text-[13px] text-[var(--ink-muted)]">
<label className="mt-2.5 flex items-start gap-2 text-[13px] leading-snug text-[var(--ink-muted)]">
<input
type="checkbox"
checked={notify}
onChange={(e) => setNotify(e.target.checked)}
className="mt-0.5 accent-[var(--mint)]"
className="mt-0.5 accent-[var(--share)]"
/>
{t.notify}
</label>
{error && (
<p className="text-[13px] font-bold text-[#ff6b6b]">{error}</p>
<p className="mt-2 text-[13px] font-bold text-[#ff6b6b]">{error}</p>
)}
<button
onClick={confirmSubmit}
disabled={!EMAIL_RE.test(email.trim()) || submitting}
className="btn-mint w-full rounded-xl py-3.5 text-[17px] font-extrabold transition active:scale-[0.99] disabled:opacity-40 disabled:shadow-none"
disabled={!emailValid || submitting}
className="btn-share mt-3.5 flex w-full items-center justify-center gap-1.5 rounded-2xl py-3 text-[15px] font-extrabold transition active:scale-[0.99] disabled:opacity-[0.72]"
>
{submitting ? "…" : t.submit}
{submitting ? "…" : <>{t.submit} <span aria-hidden></span></>}
</button>
</div>
)}

View File

@ -130,6 +130,15 @@ body {
0 10px 28px rgba(133, 252, 168, 0.28);
}
/* 사용자 액션(투표 제출) — 브랜드 포인트 퍼플 */
.btn-share {
background: var(--share);
color: #fff;
box-shadow:
0 0 0 1px rgba(166, 94, 255, 0.5),
0 12px 30px rgba(166, 94, 255, 0.35);
}
.gold-card {
background:
linear-gradient(180deg, rgba(94, 240, 180, 0.08), rgba(21, 169, 155, 0.03)),

View File

@ -63,6 +63,7 @@ type Dict = {
ctaLocked: string;
ctaLive: string;
ctaOpens: (d: string) => string;
emailGate: string;
emailPh: string;
notify: string;
submit: string;
@ -150,9 +151,10 @@ export const DICT: Record<Lang, Dict> = {
ctaLocked: "투표 종료",
ctaLive: "경기 진행 중",
ctaOpens: (d) => `투표 오픈 ${d}`,
emailPh: "이메일 (결과·다음 경기 알림)",
notify: "경기 결과가 나오면 알려주세요. 다음 경기·100만원 챌린지 소식도 받겠습니다.",
submit: "제출하고 AI와 겨루기",
emailGate: "투표를 제출하려면 이메일을 입력하세요",
emailPh: "이메일 입력 → 투표 결과·다음 경기 알림",
notify: "결과 알림 + 다음 경기 소식 + ₩100만 챌린지 기회까지 무료로 받기",
submit: "투표 제출하고 AI와 겨루기",
myPick: "내 예측",
sameAI: (m, exact) =>
`당신은 ${m}와 같은 선택입니다.${exact ? " 스코어까지 일치!" : ""} 나머지 AI는 생각이 다릅니다.`,
@ -237,9 +239,10 @@ export const DICT: Record<Lang, Dict> = {
ctaLocked: "Voting closed",
ctaLive: "Match in progress",
ctaOpens: (d) => `Opens ${d}`,
emailPh: "Email (result & next-match alerts)",
notify: "Notify me when the result is out. I'll also get next-match & ₩1,000,000 challenge news.",
submit: "Submit & face the AI",
emailGate: "Enter your email to submit your vote",
emailPh: "Your email → result & next-match alerts",
notify: "Result alerts + next-match news + a shot at the ₩1,000,000 challenge — free",
submit: "Submit your vote & face the AI",
myPick: "My Pick",
sameAI: (m, exact) =>
`You matched ${m}.${exact ? " Exact score too!" : ""} The other AIs disagree.`,

View File

@ -13,6 +13,7 @@ export type Track = {
// VISIBLE_PATHS는 이 맵의 키에서 자동 생성됩니다.
export const PATH_PLAYLISTS: Record<string, Track[]> = {
"/match/A_MEX_KOR_20260619": [
{ title: "Red Wave 2026", artist: "aio2o", src: "/audio/Red Wave 2026.mp3", cover: "/thumbnail/Red Wave 2026.webp" },
{ title: "붉은 물결", artist: "aio2o", src: "/audio/붉은 물결.mp3", cover: "/thumbnail/붉은 물결.webp" },
{ title: "Triple Pick, We Believe", artist: "aio2o", src: "/audio/Triple Pick, We Believe.mp3", cover: "/thumbnail/Triple Pick, We Believe.webp" },
],