투표 제출 CTA 단일 카드 통합 + Red Wave 2026 트랙 추가
parent
7a68d27693
commit
2988a59749
|
|
@ -42,3 +42,6 @@ yarn-error.log*
|
||||||
# secrets (구 firebase 잔재)
|
# secrets (구 firebase 잔재)
|
||||||
serviceAccount.json
|
serviceAccount.json
|
||||||
.firebaserc
|
.firebaserc
|
||||||
|
|
||||||
|
# 로컬 전용 댓글 관리 도구
|
||||||
|
tp-comment-admin/
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 268 KiB |
|
|
@ -13,7 +13,7 @@ import { ApiError, getDeviceId, getRecentEmails, rememberEmail, submitPrediction
|
||||||
import Countdown from "./Countdown";
|
import Countdown from "./Countdown";
|
||||||
import TeamFlag from "./TeamFlag";
|
import TeamFlag from "./TeamFlag";
|
||||||
|
|
||||||
type Step = "pick" | "form" | "done";
|
type Step = "form" | "done";
|
||||||
|
|
||||||
const MODEL_ICON: Record<ModelName, string> = {
|
const MODEL_ICON: Record<ModelName, string> = {
|
||||||
GPT: "/icons/gpt.png",
|
GPT: "/icons/gpt.png",
|
||||||
|
|
@ -42,7 +42,7 @@ export default function Arena({
|
||||||
const [outcome, setOutcome] = useState<Outcome | null>("DRAW");
|
const [outcome, setOutcome] = useState<Outcome | null>("DRAW");
|
||||||
const [scoreA, setScoreA] = useState(0);
|
const [scoreA, setScoreA] = useState(0);
|
||||||
const [scoreB, setScoreB] = 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 [recentEmails, setRecentEmails] = useState<string[]>(getRecentEmails);
|
||||||
const [email, setEmail] = useState(""); // 칸은 비우고, 입력/포커스 시 datalist 드롭다운으로만 자동완성
|
const [email, setEmail] = useState(""); // 칸은 비우고, 입력/포커스 시 datalist 드롭다운으로만 자동완성
|
||||||
const [notify, setNotify] = useState(true);
|
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 }));
|
.map((p) => ({ model: p.model, exact: p.scoreA === scoreA && p.scoreB === scoreB }));
|
||||||
}, [outcome, scoreA, scoreB, predictions]);
|
}, [outcome, scoreA, scoreB, predictions]);
|
||||||
|
|
||||||
const submitPick = () => {
|
const emailValid = EMAIL_RE.test(email.trim());
|
||||||
if (!outcome) return;
|
|
||||||
setStep("form");
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmSubmit = async () => {
|
const confirmSubmit = async () => {
|
||||||
if (!EMAIL_RE.test(email.trim()) || !outcome) return;
|
if (!EMAIL_RE.test(email.trim()) || !outcome) return;
|
||||||
|
|
@ -285,20 +282,22 @@ export default function Arena({
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ===== CTA 민트 #85FCA8 (진행 중일 때) ===== */}
|
{/* ===== 투표 마감/오픈 전: 상태 버튼만 ===== */}
|
||||||
{!finished && step === "pick" && (
|
{!finished && disabled && (
|
||||||
<button
|
<button
|
||||||
onClick={submitPick}
|
disabled
|
||||||
disabled={!outcome || disabled}
|
className="btn-mint mt-5 w-full rounded-2xl py-5 text-[20px] font-extrabold opacity-60"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{ctaLabel}
|
{ctaLabel}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ===== 폼: 이메일만 (D5, 닉네임 제거) ===== */}
|
{/* ===== 투표 제출 (D5): 이메일 + 단일 CTA를 한 카드로 통합 ===== */}
|
||||||
{!finished && step === "form" && (
|
{!finished && !disabled && step === "form" && (
|
||||||
<div className="mt-5 space-y-2.5 rounded-2xl border border-[var(--line-d)] bg-[var(--bg2)] p-4">
|
<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
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
inputMode="email"
|
inputMode="email"
|
||||||
|
|
@ -307,7 +306,7 @@ export default function Arena({
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder={t.emailPh}
|
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 && (
|
{recentEmails.length > 0 && (
|
||||||
<datalist id="tp-email-suggestions">
|
<datalist id="tp-email-suggestions">
|
||||||
|
|
@ -316,24 +315,24 @@ export default function Arena({
|
||||||
))}
|
))}
|
||||||
</datalist>
|
</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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={notify}
|
checked={notify}
|
||||||
onChange={(e) => setNotify(e.target.checked)}
|
onChange={(e) => setNotify(e.target.checked)}
|
||||||
className="mt-0.5 accent-[var(--mint)]"
|
className="mt-0.5 accent-[var(--share)]"
|
||||||
/>
|
/>
|
||||||
{t.notify}
|
{t.notify}
|
||||||
</label>
|
</label>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-[13px] font-bold text-[#ff6b6b]">{error}</p>
|
<p className="mt-2 text-[13px] font-bold text-[#ff6b6b]">{error}</p>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={confirmSubmit}
|
onClick={confirmSubmit}
|
||||||
disabled={!EMAIL_RE.test(email.trim()) || submitting}
|
disabled={!emailValid || 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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,15 @@ body {
|
||||||
0 10px 28px rgba(133, 252, 168, 0.28);
|
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 {
|
.gold-card {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(94, 240, 180, 0.08), rgba(21, 169, 155, 0.03)),
|
linear-gradient(180deg, rgba(94, 240, 180, 0.08), rgba(21, 169, 155, 0.03)),
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ type Dict = {
|
||||||
ctaLocked: string;
|
ctaLocked: string;
|
||||||
ctaLive: string;
|
ctaLive: string;
|
||||||
ctaOpens: (d: string) => string;
|
ctaOpens: (d: string) => string;
|
||||||
|
emailGate: string;
|
||||||
emailPh: string;
|
emailPh: string;
|
||||||
notify: string;
|
notify: string;
|
||||||
submit: string;
|
submit: string;
|
||||||
|
|
@ -150,9 +151,10 @@ export const DICT: Record<Lang, Dict> = {
|
||||||
ctaLocked: "투표 종료",
|
ctaLocked: "투표 종료",
|
||||||
ctaLive: "경기 진행 중",
|
ctaLive: "경기 진행 중",
|
||||||
ctaOpens: (d) => `투표 오픈 ${d}`,
|
ctaOpens: (d) => `투표 오픈 ${d}`,
|
||||||
emailPh: "이메일 (결과·다음 경기 알림)",
|
emailGate: "투표를 제출하려면 이메일을 입력하세요",
|
||||||
notify: "경기 결과가 나오면 알려주세요. 다음 경기·100만원 챌린지 소식도 받겠습니다.",
|
emailPh: "이메일 입력 → 투표 결과·다음 경기 알림",
|
||||||
submit: "제출하고 AI와 겨루기",
|
notify: "결과 알림 + 다음 경기 소식 + ₩100만 챌린지 기회까지 무료로 받기",
|
||||||
|
submit: "투표 제출하고 AI와 겨루기",
|
||||||
myPick: "내 예측",
|
myPick: "내 예측",
|
||||||
sameAI: (m, exact) =>
|
sameAI: (m, exact) =>
|
||||||
`당신은 ${m}와 같은 선택입니다.${exact ? " 스코어까지 일치!" : ""} 나머지 AI는 생각이 다릅니다.`,
|
`당신은 ${m}와 같은 선택입니다.${exact ? " 스코어까지 일치!" : ""} 나머지 AI는 생각이 다릅니다.`,
|
||||||
|
|
@ -237,9 +239,10 @@ export const DICT: Record<Lang, Dict> = {
|
||||||
ctaLocked: "Voting closed",
|
ctaLocked: "Voting closed",
|
||||||
ctaLive: "Match in progress",
|
ctaLive: "Match in progress",
|
||||||
ctaOpens: (d) => `Opens ${d}`,
|
ctaOpens: (d) => `Opens ${d}`,
|
||||||
emailPh: "Email (result & next-match alerts)",
|
emailGate: "Enter your email to submit your vote",
|
||||||
notify: "Notify me when the result is out. I'll also get next-match & ₩1,000,000 challenge news.",
|
emailPh: "Your email → result & next-match alerts",
|
||||||
submit: "Submit & face the AI",
|
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",
|
myPick: "My Pick",
|
||||||
sameAI: (m, exact) =>
|
sameAI: (m, exact) =>
|
||||||
`You matched ${m}.${exact ? " Exact score too!" : ""} The other AIs disagree.`,
|
`You matched ${m}.${exact ? " Exact score too!" : ""} The other AIs disagree.`,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export type Track = {
|
||||||
// VISIBLE_PATHS는 이 맵의 키에서 자동 생성됩니다.
|
// VISIBLE_PATHS는 이 맵의 키에서 자동 생성됩니다.
|
||||||
export const PATH_PLAYLISTS: Record<string, Track[]> = {
|
export const PATH_PLAYLISTS: Record<string, Track[]> = {
|
||||||
"/match/A_MEX_KOR_20260619": [
|
"/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: "붉은 물결", 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" },
|
{ title: "Triple Pick, We Believe", artist: "aio2o", src: "/audio/Triple Pick, We Believe.mp3", cover: "/thumbnail/Triple Pick, We Believe.webp" },
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue