567 lines
21 KiB
TypeScript
567 lines
21 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useMemo, useState } from "react";
|
||
import { DEMO_FORCE_OPEN, MODEL_VERSIONS } from "@/lib/mockData";
|
||
import { outcomeLabel, pct, kickoffDisplay, winProb } from "@/lib/format";
|
||
import { type Lang, dict, teamShort } from "@/lib/i18n";
|
||
import type {
|
||
Outcome,
|
||
CrowdStats,
|
||
ModelName,
|
||
Match,
|
||
AIPrediction,
|
||
Team,
|
||
} from "@/lib/types";
|
||
import Countdown from "./Countdown";
|
||
import TeamFlag from "./TeamFlag";
|
||
import {
|
||
fetchMyPredictions,
|
||
saveMyPrediction,
|
||
rememberEmail,
|
||
recallEmail,
|
||
type MyPrediction,
|
||
} from "@/lib/myPredictions";
|
||
|
||
type Step = "form" | "done";
|
||
|
||
const MODEL_ICON: Record<ModelName, string> = {
|
||
GPT: "/icons/gpt.png",
|
||
Claude: "/icons/claude.jpg",
|
||
Gemini: "/icons/gemini.jpeg",
|
||
};
|
||
|
||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
|
||
// 제출 시각 ISO → "6.18" (내 예측 목록 우측 라벨)
|
||
function fmtSubmitted(iso: string): string {
|
||
const m = iso.match(/\d{4}-(\d{2})-(\d{2})/);
|
||
return m ? `${+m[1]}.${+m[2]}` : "";
|
||
}
|
||
|
||
export default function Arena({
|
||
match,
|
||
predictions,
|
||
crowd: initialCrowd,
|
||
shareUrl,
|
||
lang = "ko",
|
||
}: {
|
||
match: Match;
|
||
predictions: AIPrediction[];
|
||
crowd: CrowdStats;
|
||
shareUrl: string;
|
||
lang?: Lang;
|
||
}) {
|
||
const t = dict(lang);
|
||
const aShort = teamShort(match.teamA, lang);
|
||
const bShort = teamShort(match.teamB, lang);
|
||
const genDate = (predictions[0]?.generatedAt ?? "").replace(/-/g, ".");
|
||
const modelVersions = predictions.map((p) => MODEL_VERSIONS[p.model]).join(" · ");
|
||
const [outcome, setOutcome] = useState<Outcome | null>(null);
|
||
const [scoreA, setScoreA] = useState(2);
|
||
const [scoreB, setScoreB] = useState(1);
|
||
const [step, setStep] = useState<Step>("form");
|
||
const [email, setEmail] = useState("");
|
||
const [notify, setNotify] = useState(true);
|
||
const [crowd, setCrowd] = useState<CrowdStats>(initialCrowd);
|
||
const [copied, setCopied] = useState(false);
|
||
const [myPreds, setMyPreds] = useState<MyPrediction[]>([]);
|
||
const [votesExpanded, setVotesExpanded] = useState(false);
|
||
|
||
// 재방문 시: 기억된 이메일 복원 → 내 지난 예측 로드 (로그인 대체)
|
||
useEffect(() => {
|
||
const saved = recallEmail();
|
||
if (!saved) return;
|
||
setEmail(saved);
|
||
fetchMyPredictions(saved).then(setMyPreds);
|
||
}, []);
|
||
|
||
// 이 경기에 대한 내 기존 픽 (있으면 스코어 프리필 + 강조)
|
||
const myThisMatch = useMemo(
|
||
() => myPreds.find((p) => p.matchId === match.matchId),
|
||
[myPreds, match.matchId],
|
||
);
|
||
useEffect(() => {
|
||
if (myThisMatch) {
|
||
setScoreA(myThisMatch.scoreA);
|
||
setScoreB(myThisMatch.scoreB);
|
||
}
|
||
}, [myThisMatch]);
|
||
|
||
// 이메일 입력이 유효해지면 그 즉시 내 기록 조회(다른 기기/세션 흔적 표시)
|
||
const onEmailChange = (v: string) => {
|
||
setEmail(v);
|
||
if (EMAIL_RE.test(v.trim())) fetchMyPredictions(v).then(setMyPreds);
|
||
};
|
||
|
||
// 점수 선택 시 승/무/패 자동 선택 (스코어가 결과의 소스)
|
||
useEffect(() => {
|
||
setOutcome(
|
||
scoreA > scoreB ? "TEAM_A_WIN" : scoreA < scoreB ? "TEAM_B_WIN" : "DRAW",
|
||
);
|
||
}, [scoreA, scoreB]);
|
||
|
||
// 투표 창: 오픈(D-2) ≤ now < 마감(킥오프 정각). 종료 = 결과 존재.
|
||
const now = Date.now();
|
||
const finished = !!match.result;
|
||
const notOpen = !DEMO_FORCE_OPEN && now < new Date(match.opensAt).getTime();
|
||
const locked = now >= new Date(match.lockAt).getTime();
|
||
const disabled = finished || locked || notOpen;
|
||
const ctaLabel = finished
|
||
? t.ctaResult
|
||
: notOpen
|
||
? t.ctaOpens(kickoffDisplay(match.opensAt, lang))
|
||
: locked
|
||
? t.ctaLocked
|
||
: t.ctaBeat;
|
||
|
||
const matched = useMemo(() => {
|
||
if (!outcome) return [];
|
||
return predictions
|
||
.filter((p) => p.outcome === outcome)
|
||
.map((p) => ({ model: p.model, exact: p.scoreA === scoreA && p.scoreB === scoreB }));
|
||
}, [outcome, scoreA, scoreB, predictions]);
|
||
|
||
const emailValid = EMAIL_RE.test(email.trim());
|
||
const confirmSubmit = async () => {
|
||
if (!emailValid || !outcome) return;
|
||
// 군중 분포 증분은 새 투표일 때만 (이미 투표한 경기 수정 시 중복 카운트 방지)
|
||
if (!myThisMatch) {
|
||
setCrowd((c) => ({
|
||
...c,
|
||
total: c.total + 1,
|
||
teamAWin: c.teamAWin + (outcome === "TEAM_A_WIN" ? 1 : 0),
|
||
draw: c.draw + (outcome === "DRAW" ? 1 : 0),
|
||
teamBWin: c.teamBWin + (outcome === "TEAM_B_WIN" ? 1 : 0),
|
||
}));
|
||
}
|
||
// 내 예측 저장(경기당 1건 upsert) + 이메일 기억 → 재방문 자동 복원
|
||
await saveMyPrediction(email, {
|
||
matchId: match.matchId,
|
||
teamAShort: aShort,
|
||
teamBShort: bShort,
|
||
kickoffKst: match.kickoffKst,
|
||
outcome,
|
||
scoreA,
|
||
scoreB,
|
||
submittedAt: new Date().toISOString(),
|
||
});
|
||
rememberEmail(email);
|
||
setMyPreds(await fetchMyPredictions(email));
|
||
setStep("done");
|
||
};
|
||
|
||
const shareText = useMemo(() => {
|
||
if (!outcome) return "";
|
||
const me = `${aShort} ${scoreA}-${scoreB} ${bShort}`;
|
||
const sameAI = matched.map((m) => m.model).join("·");
|
||
if (lang === "en") {
|
||
return sameAI
|
||
? `I picked ${me}, same as ${sameAI}! You? — TriplePick`
|
||
: `I picked ${me} — different from all 3 AIs! You? — TriplePick`;
|
||
}
|
||
return sameAI
|
||
? `나는 ${me}. ${sameAI}와 같은 선택! 당신은? — TriplePick`
|
||
: `나는 ${me}. AI 셋과 다 다른 선택! 당신은? — TriplePick`;
|
||
}, [outcome, scoreA, scoreB, matched, aShort, bShort, lang]);
|
||
|
||
const copyLink = async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(`${shareText}\n${shareUrl}`);
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 1800);
|
||
} catch {
|
||
setCopied(false);
|
||
}
|
||
};
|
||
const nativeShare = async () => {
|
||
if (navigator.share) {
|
||
try {
|
||
await navigator.share({
|
||
title: `${aShort} vs ${bShort} — TriplePick`,
|
||
text: shareText,
|
||
url: shareUrl,
|
||
});
|
||
} catch {
|
||
/* cancelled */
|
||
}
|
||
} else copyLink();
|
||
};
|
||
|
||
return (
|
||
<>
|
||
{/* ===== 카운트다운 (D7) — 투표 진행 중일 때만 ===== */}
|
||
{!finished && !notOpen && (
|
||
<div className="mt-4">
|
||
<Countdown to={match.lockAt} lang={lang} />
|
||
</div>
|
||
)}
|
||
|
||
{/* ===== 레이어드 화이트 시트 (002) ===== */}
|
||
<section className="sheet mt-4 p-5 text-[var(--ink)]">
|
||
<div className="mb-3 flex items-end justify-between gap-2">
|
||
<h2 className="shrink-0 text-[22px] font-extrabold">{t.aiBattle}</h2>
|
||
<div className="flex-1 pb-1 text-center text-[11px] leading-snug">
|
||
<div className="font-bold text-[var(--ink)]">
|
||
{t.genLabel} · {genDate} 00:00 KST
|
||
</div>
|
||
<div className="text-[var(--ink-muted)]">{modelVersions}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-3">
|
||
{predictions.map((p) => {
|
||
const wp = winProb(match, p);
|
||
const wpLabel = wp.team
|
||
? t.winProb(teamShort(wp.team, lang))
|
||
: t.drawOdds;
|
||
return (
|
||
<div
|
||
key={p.model}
|
||
className="rounded-2xl border border-[var(--line-l)] bg-[var(--sheet-card)] p-4"
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<img
|
||
src={MODEL_ICON[p.model]}
|
||
alt={p.model}
|
||
className="h-12 w-12 shrink-0 rounded-full object-cover"
|
||
/>
|
||
<div className="min-w-0">
|
||
<div className="text-[20px] font-extrabold leading-none">
|
||
{p.model}
|
||
</div>
|
||
{/* D3: 승리 예측 팀의 승리 확률 + 승리팀 국기 */}
|
||
<div className="mt-1.5 flex items-center gap-1.5">
|
||
{wp.team && (
|
||
<TeamFlag team={wp.team} className="h-3.5 w-5 shrink-0" />
|
||
)}
|
||
<span className="text-[12px] font-bold text-[var(--ink-muted)]">
|
||
{wpLabel} <span className="text-[var(--gpt)]">{wp.pct}%</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="ml-auto shrink-0 whitespace-nowrap font-mono text-[28px] font-extrabold tabular-nums">
|
||
{p.scoreA} - {p.scoreB}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 메인 리즌 강조 (D3: 확률은 텍스트, 바 제거) */}
|
||
<p className="mt-3 text-[15px] font-bold leading-snug text-[var(--ink)]">
|
||
“{p.reasonShort}”
|
||
</p>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* 당신의 선택 */}
|
||
<div className="mt-5 flex items-center justify-between">
|
||
<span className="text-[13px] font-bold text-[var(--ink-muted)]">
|
||
{t.yourPickLabel}
|
||
</span>
|
||
<span className="text-[20px] font-extrabold">{t.yourChoice}</span>
|
||
</div>
|
||
|
||
<div className="mt-3 grid grid-cols-3 gap-2">
|
||
{(
|
||
[
|
||
["TEAM_A_WIN", `${aShort} ${t.win}`],
|
||
["DRAW", t.drawLabel],
|
||
["TEAM_B_WIN", `${bShort} ${t.win}`],
|
||
] as [Outcome, string][]
|
||
).map(([val, label]) => (
|
||
<button
|
||
key={val}
|
||
disabled={disabled}
|
||
onClick={() => setOutcome(val)}
|
||
className={`rounded-xl border py-3.5 text-[16px] font-extrabold transition disabled:opacity-50 ${
|
||
outcome === val
|
||
? "border-transparent bg-[var(--mint)] text-[var(--mint-ink)]"
|
||
: "border-[var(--line-l)] bg-white text-[var(--ink-muted)]"
|
||
}`}
|
||
>
|
||
{label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="mt-3 flex items-center justify-center gap-5 rounded-xl border border-[var(--line-l)] bg-white py-3">
|
||
<Stepper label={aShort} value={scoreA} onChange={setScoreA} disabled={disabled} />
|
||
<span className="text-[26px] font-extrabold text-[var(--ink-muted)]">:</span>
|
||
<Stepper label={bShort} value={scoreB} onChange={setScoreB} disabled={disabled} />
|
||
</div>
|
||
</section>
|
||
|
||
{/* ===== 종료된 경기: 결과 보기 ===== */}
|
||
{finished && match.result && (
|
||
<section className="mt-5 rounded-2xl border border-[var(--green)]/50 bg-[var(--bg2)] p-5">
|
||
<div className="text-[13px] text-[var(--ink-muted)]">{t.finalResult}</div>
|
||
<div className="mt-1 text-[26px] font-extrabold">
|
||
{aShort} {match.result.scoreA}-{match.result.scoreB} {bShort}
|
||
<span className="ml-2 text-[16px] font-bold text-[var(--green)]">
|
||
{outcomeLabel(match, match.result.outcome, lang)}
|
||
</span>
|
||
</div>
|
||
<div className="mt-3 flex flex-col gap-1.5">
|
||
{predictions.map((p) => {
|
||
const hit = p.outcome === match.result!.outcome;
|
||
return (
|
||
<div key={p.model} className="flex items-center justify-between text-[13px]">
|
||
<span className="font-bold text-white/85">{p.model}</span>
|
||
<span className={hit ? "font-bold text-[var(--green)]" : "text-white/45"}>
|
||
{hit ? t.hit : t.miss}
|
||
</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* ===== 투표 마감/오픈 전: 상태 버튼만 ===== */}
|
||
{!finished && disabled && (
|
||
<button
|
||
disabled
|
||
className="btn-mint mt-5 w-full rounded-2xl py-5 text-[20px] font-extrabold opacity-60"
|
||
>
|
||
{ctaLabel}
|
||
</button>
|
||
)}
|
||
|
||
{/* ===== 투표 제출 (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"
|
||
value={email}
|
||
onChange={(e) => onEmailChange(e.target.value)}
|
||
placeholder={t.emailPh}
|
||
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)]"
|
||
/>
|
||
<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(--share)]"
|
||
/>
|
||
{t.notify}
|
||
</label>
|
||
<button
|
||
onClick={confirmSubmit}
|
||
disabled={!emailValid}
|
||
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]"
|
||
>
|
||
{t.submit} <span aria-hidden>→</span>
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* ===== 제출 완료: 같은 AI + 경기별 공유 (D8) ===== */}
|
||
{!finished && step === "done" && outcome && (
|
||
<div className="mt-5 rounded-2xl border border-[var(--green)]/50 bg-[var(--bg2)] p-5">
|
||
<div className="text-[13px] text-[var(--ink-muted)]">{t.myPick}</div>
|
||
<div className="mt-1 text-[26px] font-extrabold">
|
||
{aShort} {scoreA}-{scoreB} {bShort}
|
||
<span className="ml-2 text-[16px] font-bold text-[var(--green)]">
|
||
{outcomeLabel(match, outcome, lang)}
|
||
</span>
|
||
</div>
|
||
<p className="mt-2 text-[14px] leading-relaxed text-white/80">
|
||
{matched.length > 0
|
||
? t.sameAI(
|
||
matched.map((m) => m.model).join("·"),
|
||
matched.some((m) => m.exact),
|
||
)
|
||
: t.soloPick}
|
||
</p>
|
||
<div className="mt-4 grid grid-cols-2 gap-2.5">
|
||
<button onClick={nativeShare} className="btn-mint rounded-xl py-3 text-[15px] font-bold active:scale-[0.99]">
|
||
{t.shareThis}
|
||
</button>
|
||
<button onClick={copyLink} className="rounded-xl border border-[var(--line-d)] py-3 text-[15px] font-bold active:scale-[0.99]">
|
||
{copied ? t.copied : t.copyLink}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ===== 내 지난 예측 (이메일 기준, 로그인 없음) ===== */}
|
||
{myPreds.length > 0 && (
|
||
<section className="mt-5 rounded-2xl border border-[var(--line-d)] bg-[var(--bg2)] p-4">
|
||
<div className="mb-3 flex items-baseline justify-between gap-2">
|
||
<h3 className="text-[16px] font-extrabold">{t.myVotesTitle}</h3>
|
||
<span className="shrink-0 text-[12px] text-[var(--ink-muted)]">
|
||
{t.myVotesSub}
|
||
</span>
|
||
</div>
|
||
<ul className="flex flex-col gap-2">
|
||
{(votesExpanded ? myPreds : myPreds.slice(0, 1)).map((p) => {
|
||
const isThis = p.matchId === match.matchId;
|
||
const pickTxt =
|
||
p.outcome === "TEAM_A_WIN"
|
||
? `${p.teamAShort} ${t.win}`
|
||
: p.outcome === "TEAM_B_WIN"
|
||
? `${p.teamBShort} ${t.win}`
|
||
: t.drawLabel;
|
||
return (
|
||
<li
|
||
key={p.matchId}
|
||
className={`flex items-center justify-between gap-3 rounded-xl border px-3 py-2.5 ${
|
||
isThis
|
||
? "border-[var(--share)] bg-[rgba(166,94,255,0.1)]"
|
||
: "border-[var(--line-d)]"
|
||
}`}
|
||
>
|
||
<div className="min-w-0">
|
||
<div className="text-[15px] font-bold text-white">
|
||
{p.teamAShort} {p.scoreA}-{p.scoreB} {p.teamBShort}
|
||
</div>
|
||
<div className="mt-0.5 text-[12px] text-[var(--ink-muted)]">
|
||
{pickTxt}
|
||
{isThis && (
|
||
<span className="text-[var(--share)]"> · {t.thisMatch}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
{p.result ? (
|
||
<span
|
||
className={`shrink-0 text-[13px] font-bold ${
|
||
p.result.hitOutcome
|
||
? "text-[var(--share)]"
|
||
: "text-[var(--ink-muted)]"
|
||
}`}
|
||
>
|
||
{p.result.hitOutcome ? t.hit : t.miss}
|
||
</span>
|
||
) : (
|
||
<span className="shrink-0 text-[11px] text-[var(--ink-muted)]">
|
||
{fmtSubmitted(p.submittedAt)}
|
||
</span>
|
||
)}
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
{myPreds.length > 1 && (
|
||
<button
|
||
onClick={() => setVotesExpanded((v) => !v)}
|
||
className="mt-3 flex w-full items-center justify-center gap-1 rounded-xl border border-[var(--line-d)] py-2.5 text-[13px] font-bold text-[var(--ink-muted)] transition active:scale-[0.99]"
|
||
>
|
||
{votesExpanded ? t.showLess : t.showMore(myPreds.length - 1)}
|
||
<span aria-hidden>{votesExpanded ? "▲" : "▼"}</span>
|
||
</button>
|
||
)}
|
||
</section>
|
||
)}
|
||
|
||
{/* ===== 골드 상금 (003) ===== */}
|
||
<section className="gold-card mt-5 rounded-2xl p-5">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<div className="min-w-0">
|
||
<div className="flex items-center gap-1.5 text-[18px] font-extrabold text-[var(--gold-border)]">
|
||
<span>₩1,000,000 Final Challenge</span>
|
||
<span>★</span>
|
||
</div>
|
||
<p className="mt-1.5 text-[13px] leading-snug text-white/75">
|
||
{t.goldDesc}
|
||
</p>
|
||
</div>
|
||
<button className="gold-btn shrink-0 whitespace-pre-line rounded-xl px-4 py-3.5 text-[14px] font-extrabold leading-tight active:scale-[0.98]">
|
||
{t.goldBtn}
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
{/* ===== Crowd Pick (003) — 보팅 비율(≠ AI 승리 확률) ===== */}
|
||
<section className="mt-6">
|
||
<div className="mb-2.5 text-[20px] font-extrabold">
|
||
Crowd Pick{" "}
|
||
<span className="text-[14px] text-[var(--ink-muted)]">{t.crowdSub}</span>
|
||
</div>
|
||
<div className="flex h-14 overflow-hidden rounded-2xl border border-[var(--line-d)]">
|
||
<CrowdSeg team={match.teamA} value={pct(crowd.teamAWin, crowd.total)} tone="a" />
|
||
<CrowdSeg value={pct(crowd.draw, crowd.total)} tone="draw" />
|
||
<CrowdSeg team={match.teamB} value={pct(crowd.teamBWin, crowd.total)} tone="b" />
|
||
</div>
|
||
<div className="mt-1.5 text-right text-[11px] text-[var(--ink-muted)]">
|
||
{t.joined(crowd.total.toLocaleString())}
|
||
</div>
|
||
</section>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function Stepper({
|
||
label,
|
||
value,
|
||
onChange,
|
||
disabled,
|
||
}: {
|
||
label: string;
|
||
value: number;
|
||
onChange: (n: number) => void;
|
||
disabled?: boolean;
|
||
}) {
|
||
return (
|
||
<div className="flex items-center gap-3">
|
||
<StepBtn disabled={disabled || value <= 0} onClick={() => onChange(Math.max(0, value - 1))}>
|
||
−
|
||
</StepBtn>
|
||
<div className="flex w-12 flex-col items-center">
|
||
<span className="font-mono text-[32px] font-extrabold leading-none tabular-nums">
|
||
{value}
|
||
</span>
|
||
<span className="mt-1 text-[11px] text-[var(--ink-muted)]">{label}</span>
|
||
</div>
|
||
<StepBtn disabled={disabled || value >= 9} onClick={() => onChange(Math.min(9, value + 1))}>
|
||
+
|
||
</StepBtn>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function StepBtn({
|
||
children,
|
||
onClick,
|
||
disabled,
|
||
}: {
|
||
children: React.ReactNode;
|
||
onClick: () => void;
|
||
disabled?: boolean;
|
||
}) {
|
||
return (
|
||
<button
|
||
onClick={onClick}
|
||
disabled={disabled}
|
||
className="grid h-11 w-11 place-items-center rounded-xl border border-[var(--line-l)] bg-white text-[22px] font-bold leading-none text-[var(--ink)] active:scale-95 disabled:opacity-30"
|
||
>
|
||
{children}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
function CrowdSeg({
|
||
team,
|
||
value,
|
||
tone,
|
||
}: {
|
||
team?: Team;
|
||
value: number;
|
||
tone: "a" | "draw" | "b";
|
||
}) {
|
||
const bg = tone === "draw" ? "#2b313c" : tone === "a" ? "rgba(74,255,160,0.22)" : "#222831";
|
||
return (
|
||
<div
|
||
className="flex items-center justify-center gap-1.5 border-r border-[var(--line-d)] text-[16px] font-extrabold last:border-r-0"
|
||
style={{ width: `${value}%`, background: bg, minWidth: 56 }}
|
||
>
|
||
{team && <TeamFlag team={team} className="h-4 w-6" />}
|
||
<span>{value}%</span>
|
||
</div>
|
||
);
|
||
}
|