남아공전 투표 페이지에 마스코트 추가
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -21,9 +21,50 @@ import {
|
|||
import Countdown from "./Countdown";
|
||||
import RankingBoard from "./RankingBoard";
|
||||
import TeamFlag from "./TeamFlag";
|
||||
import TalkingMascot from "./TalkingMascot";
|
||||
|
||||
type Step = "form" | "done";
|
||||
|
||||
// 투표하기 버튼 양옆 캐릭터 말풍선 — 투표 유도(코믹).
|
||||
// 왼쪽 시바견(👉 버튼 가리킴) / 오른쪽 호랑이(👈 버튼 가리킴).
|
||||
const DOG_LINES = [
|
||||
"여기 눌러서 투표! 👉",
|
||||
"AI 이길 자신 있어? 😏",
|
||||
"고민은 짧게, 픽은 빠르게",
|
||||
"스코어 예측 가즈아 ⚽",
|
||||
"투표 안 하면 후회함 ㅋㅋ",
|
||||
"직감 믿고 ㄱㄱ!",
|
||||
"딱 한 번이면 돼, 찍어봐",
|
||||
"AI 셋 다 발라버리자 🔥",
|
||||
"1분이면 충분해, ㄱㄱ",
|
||||
"네 예측이 궁금해 👀",
|
||||
];
|
||||
const TIGER_LINES = [
|
||||
"👈 여기서 투표하는 거야",
|
||||
"한국 몇 대 몇? 찍어봐! 🇰🇷",
|
||||
"투표하고 AI랑 붙자 🔥",
|
||||
"네 픽 보여줘 봐!",
|
||||
"정확히 맞히면 네가 레전드 🏆",
|
||||
"망설이면 마감된다 ⏰",
|
||||
"감으로라도 일단 ㄱㄱ",
|
||||
"예측 한 방에 인생역전? ㅋㅋ",
|
||||
"한국 이긴다에 한 표! 🇰🇷",
|
||||
"스코어까지 맞혀봐 🎯",
|
||||
];
|
||||
// AI 박스 사자 — AI 예측에 대한 멘트.
|
||||
const LION_LINES = [
|
||||
"AI 셋이 다 다르게 봤네 🤔",
|
||||
"GPT·클로드·제미나이… 누가 맞을까?",
|
||||
"내 직감은 AI보다 정확함 😼",
|
||||
"데이터로 보면 말이지…",
|
||||
"예측은 예측일 뿐, 공은 둥글다 ⚽",
|
||||
"AI도 가끔 틀린다구 ㅋㅋ",
|
||||
"이 확률, 믿어? 말아?",
|
||||
"분석 결과는… 일단 비밀 🤫",
|
||||
"통계는 통계일 뿐이지 📊",
|
||||
"이변은 항상 있더라고 👀",
|
||||
];
|
||||
|
||||
// 제출 시각 ISO → "6.18" (내 예측 목록 우측 라벨)
|
||||
function fmtSubmitted(iso: string): string {
|
||||
const m = iso.match(/\d{4}-(\d{2})-(\d{2})/);
|
||||
|
|
@ -58,6 +99,10 @@ export default function Arena({
|
|||
const flip = match.teamB.code === "KOR" && match.teamA.code !== "KOR";
|
||||
const leftShort = flip ? bShort : aShort;
|
||||
const rightShort = flip ? aShort : bShort;
|
||||
// 마스코트(사자·버튼 양옆 캐릭터)는 한국-남아공 경기에서만 노출.
|
||||
const isKorRsa =
|
||||
[match.teamA.code, match.teamB.code].includes("KOR") &&
|
||||
[match.teamA.code, match.teamB.code].includes("RSA");
|
||||
const [outcome, setOutcome] = useState<Outcome | null>("DRAW");
|
||||
const [scoreA, setScoreA] = useState(0);
|
||||
const [scoreB, setScoreB] = useState(0);
|
||||
|
|
@ -184,7 +229,18 @@ export default function Arena({
|
|||
)}
|
||||
|
||||
{/* ===== 레이어드 화이트 시트 (002) ===== */}
|
||||
<section className="sheet mt-4 p-5 text-[var(--ink)]">
|
||||
<section className="sheet relative mt-4 p-5 text-[var(--ink)]">
|
||||
{/* AI 박스 오른쪽 위 사자 — 한국-남아공전 전용 */}
|
||||
{isKorRsa && (
|
||||
<div className="absolute right-1 -top-4 z-10 h-20 sm:h-24">
|
||||
<TalkingMascot
|
||||
src="/assets/mascots/lion_point.webp"
|
||||
lines={LION_LINES}
|
||||
className="h-20 w-auto sm:h-24"
|
||||
draggable
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3">
|
||||
<h2 className="text-[22px] font-extrabold">{t.aiBattle}</h2>
|
||||
</div>
|
||||
|
|
@ -366,13 +422,36 @@ export default function Arena({
|
|||
{error && (
|
||||
<p className="mt-2 text-[13px] font-bold text-[#ff6b6b]">{error}</p>
|
||||
)}
|
||||
{/* 투표하기 버튼 — 한국-남아공전에서만 양옆에 한국·일본 캐릭터(탭 시 투표 말풍선) */}
|
||||
<div className="mt-3.5 flex items-end justify-center gap-1">
|
||||
{isKorRsa && (
|
||||
<TalkingMascot
|
||||
src="/assets/mascots/dog_point.webp"
|
||||
lines={DOG_LINES}
|
||||
className="h-16 w-auto select-none sm:h-20"
|
||||
auto
|
||||
autoDelayMs={1200}
|
||||
autoIntervalMs={6000}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={confirmSubmit}
|
||||
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]"
|
||||
className="btn-share flex flex-1 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} <span aria-hidden>→</span></>}
|
||||
</button>
|
||||
{isKorRsa && (
|
||||
<TalkingMascot
|
||||
src="/assets/mascots/tiger_point.webp"
|
||||
lines={TIGER_LINES}
|
||||
className="h-16 w-auto select-none sm:h-20"
|
||||
auto
|
||||
autoDelayMs={4200}
|
||||
autoIntervalMs={6000}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { kickoffDisplay } from "@/lib/format";
|
|||
import { type Lang, dict, teamShort } from "@/lib/i18n";
|
||||
import BallIcon from "./BallIcon";
|
||||
import TeamFlag from "./TeamFlag";
|
||||
import WaveStrip from "./WaveStrip";
|
||||
|
||||
export default function MatchupHUD({ match, lang = "ko" }: { match: Match; lang?: Lang }) {
|
||||
const t = dict(lang);
|
||||
|
|
@ -14,13 +15,23 @@ export default function MatchupHUD({ match, lang = "ko" }: { match: Match; lang?
|
|||
const right = flip ? match.teamA : match.teamB;
|
||||
const leftScore = flip ? match.result?.scoreB : match.result?.scoreA;
|
||||
const rightScore = flip ? match.result?.scoreA : match.result?.scoreB;
|
||||
// 마스코트(파도타기 등)는 한국-남아공 경기에서만 노출.
|
||||
const codes = [match.teamA.code, match.teamB.code];
|
||||
const isKorRsa = codes.includes("KOR") && codes.includes("RSA");
|
||||
return (
|
||||
<section className="mt-6">
|
||||
{/* 대결 카드 (녹색 글로우 보더) */}
|
||||
<div
|
||||
className="rounded-3xl border-2 border-[var(--green)] bg-[#171b21] p-5"
|
||||
className="relative rounded-3xl border-2 border-[var(--green)] bg-[#171b21] p-5"
|
||||
style={{ boxShadow: "0 0 28px rgba(74,255,160,0.35), inset 0 0 24px rgba(74,255,160,0.06)" }}
|
||||
>
|
||||
{/* 박스 오른쪽 상단 파도타기 (응원 5마리) — 한국-남아공전 전용 */}
|
||||
{isKorRsa && (
|
||||
<div className="absolute right-2 top-2 z-10">
|
||||
<WaveStrip />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 헤더: 브랜드 아이콘 + 팀명 + 일시 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<BallIcon className="h-14 w-14 shrink-0" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
// 탭하면 랜덤 말풍선이 뜨는 마스코트. draggable 이면 끌어서 옮길 수도 있다.
|
||||
const BUBBLE_MS = 2600;
|
||||
const TAP_THRESHOLD = 6;
|
||||
|
||||
function Bubble({ text, below }: { text: string; below?: boolean }) {
|
||||
const box = (
|
||||
<div className="rounded-2xl bg-white px-3 py-1.5 text-[12px] font-bold text-[#1a1d22] shadow-[0_4px_14px_rgba(0,0,0,0.35)]">
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
const tail = <div className="mx-auto h-2.5 w-2.5 rotate-45 bg-white" />;
|
||||
return below ? (
|
||||
<div className="pointer-events-none absolute top-full left-1/2 mt-1 -translate-x-1/2 whitespace-nowrap">
|
||||
<div className="-mb-1">{tail}</div>
|
||||
{box}
|
||||
</div>
|
||||
) : (
|
||||
<div className="pointer-events-none absolute bottom-full left-1/2 mb-1 -translate-x-1/2 whitespace-nowrap">
|
||||
{box}
|
||||
<div className="-mt-1">{tail}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TalkingMascot({
|
||||
src,
|
||||
lines,
|
||||
className = "",
|
||||
bubbleBelow = false,
|
||||
draggable = false,
|
||||
auto = false,
|
||||
autoDelayMs = 0,
|
||||
autoIntervalMs = 6000,
|
||||
}: {
|
||||
src: string;
|
||||
lines: string[];
|
||||
className?: string;
|
||||
bubbleBelow?: boolean;
|
||||
draggable?: boolean;
|
||||
auto?: boolean;
|
||||
autoDelayMs?: number;
|
||||
autoIntervalMs?: number;
|
||||
}) {
|
||||
const [bubble, setBubble] = useState<string | null>(null);
|
||||
const [off, setOff] = useState({ x: 0, y: 0 });
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const lastLine = useRef<string>("");
|
||||
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const start = useRef({ px: 0, py: 0, ox: 0, oy: 0, moved: false });
|
||||
|
||||
useEffect(() => () => clearTimeout(timer.current), []);
|
||||
|
||||
const speak = useCallback(() => {
|
||||
let line = lines[Math.floor(Math.random() * lines.length)];
|
||||
if (lines.length > 1)
|
||||
while (line === lastLine.current)
|
||||
line = lines[Math.floor(Math.random() * lines.length)];
|
||||
lastLine.current = line;
|
||||
setBubble(line);
|
||||
clearTimeout(timer.current);
|
||||
timer.current = setTimeout(() => setBubble(null), BUBBLE_MS);
|
||||
}, [lines]);
|
||||
|
||||
// 자동 모드: 클릭 없이 일정 간격으로 말풍선 (두 캐릭터에 offset 주면 번갈아 나옴)
|
||||
useEffect(() => {
|
||||
if (!auto) return;
|
||||
let interval: ReturnType<typeof setInterval> | undefined;
|
||||
const start = setTimeout(() => {
|
||||
speak();
|
||||
interval = setInterval(speak, autoIntervalMs);
|
||||
}, autoDelayMs);
|
||||
return () => {
|
||||
clearTimeout(start);
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [auto, autoDelayMs, autoIntervalMs, speak]);
|
||||
|
||||
// --- 드래그 모드 ---
|
||||
const onPointerDown = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!draggable) return;
|
||||
e.preventDefault();
|
||||
start.current = { px: e.clientX, py: e.clientY, ox: off.x, oy: off.y, moved: false };
|
||||
setDragging(true);
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
},
|
||||
[draggable, off],
|
||||
);
|
||||
const onPointerMove = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!draggable || !dragging) return;
|
||||
const dx = e.clientX - start.current.px;
|
||||
const dy = e.clientY - start.current.py;
|
||||
if (Math.abs(dx) > TAP_THRESHOLD || Math.abs(dy) > TAP_THRESHOLD)
|
||||
start.current.moved = true;
|
||||
setOff({ x: start.current.ox + dx, y: start.current.oy + dy });
|
||||
},
|
||||
[draggable, dragging],
|
||||
);
|
||||
const onPointerUp = useCallback(() => {
|
||||
if (!draggable) return;
|
||||
setDragging(false);
|
||||
if (!start.current.moved) speak();
|
||||
}, [draggable, speak]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
style={
|
||||
draggable
|
||||
? {
|
||||
transform: `translate(${off.x}px, ${off.y}px) scale(${dragging ? 1.06 : 1})`,
|
||||
transition: dragging ? "none" : "transform 0.15s ease",
|
||||
zIndex: dragging || bubble ? 50 : 1,
|
||||
}
|
||||
: { zIndex: bubble ? 50 : 1 }
|
||||
}
|
||||
>
|
||||
{bubble && <Bubble text={bubble} below={bubbleBelow} />}
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
draggable={false}
|
||||
onClick={draggable || auto ? undefined : speak}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={() => setDragging(false)}
|
||||
className={className}
|
||||
style={{
|
||||
cursor: draggable ? (dragging ? "grabbing" : "grab") : auto ? "default" : "pointer",
|
||||
touchAction: draggable ? "none" : "manipulation",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
// 페이지 맨 위 파도타기 — 5마리가 순차로 만세(팔 올림)했다 내린다.
|
||||
// 5마리는 한 그룹으로 묶여 함께 끌어 옮길 수 있고, 각 캐릭터를 탭하면
|
||||
// 남아공전 응원 말풍선이 뜬다.
|
||||
type Animal = { name: string; lines: string[] };
|
||||
|
||||
const WAVE: Animal[] = [
|
||||
{
|
||||
name: "bull",
|
||||
lines: [
|
||||
"한국, 남아공 잡고 16강 가자! 🇰🇷",
|
||||
"남아공 수비를 뚫어라! 🐂",
|
||||
"오늘 이기면 16강이 보인다",
|
||||
"대~한민국! 짝짝짝 👏",
|
||||
"투우사도 한국 응원한다 ⚽",
|
||||
"스페인도 한국 편이다 🇪🇸",
|
||||
"남아공 골키퍼 긴장해라!",
|
||||
"한 골이 16강을 가른다",
|
||||
"투지로 밀어붙여! 💪",
|
||||
"끝까지 간다, 대한민국!",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "eagle",
|
||||
lines: [
|
||||
"남아공전 무조건 이긴다 🦅",
|
||||
"한국 16강 가즈아 🇰🇷",
|
||||
"남아공 골문을 노려라!",
|
||||
"이 경기 잡으면 토너먼트행 ✈️",
|
||||
"골! 골! 대한민국! ⚽",
|
||||
"아르헨도 한국 편 🇦🇷",
|
||||
"역습 한 방이면 끝난다",
|
||||
"오늘 클린시트 가자 🧤",
|
||||
"남아공 수비 흔들어!",
|
||||
"16강에서 또 보자 ✈️",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "rooster",
|
||||
lines: [
|
||||
"꼬끼오~ 한국 남아공 이겨라! 🐓",
|
||||
"16강 길목, 오늘이 중요해",
|
||||
"남아공 막고 골 넣자!",
|
||||
"대한민국 파이팅 🇰🇷",
|
||||
"프랑스도 한국 편이다 🙌",
|
||||
"전반부터 몰아쳐라 🐓",
|
||||
"남아공 김빠지게 골 넣자",
|
||||
"한국 미드필드 최고지",
|
||||
"오늘 분위기 우리 거야",
|
||||
"꼬끼오! 승리의 함성 📣",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "parrot",
|
||||
lines: [
|
||||
"삼바 응원! 한국 남아공 격파 🥁",
|
||||
"골 넣고 16강 가자 💃",
|
||||
"남아공 수비 흔들어! 🔥",
|
||||
"대한민국 가즈아 🇰🇷",
|
||||
"오늘 승점 3점 챙기자 ⚽",
|
||||
"리듬 타고 골 가자 🎶",
|
||||
"남아공? 삼바로 제압! 💃",
|
||||
"한 골 한 골 쌓아가자 ⚽",
|
||||
"응원석도 파도타기 🌊",
|
||||
"오늘 MVP는 대한민국 🏆",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "horse",
|
||||
lines: [
|
||||
"달려라 대한민국, 남아공 잡자! 🏇",
|
||||
"16강 가는 길, 오늘이 분수령",
|
||||
"남아공 골문 열어라 ⚽",
|
||||
"끝까지 응원! 한국 화이팅 📣",
|
||||
"호날두도 한국 응원한다 🐴",
|
||||
"측면 돌파로 무너뜨려!",
|
||||
"스피드로 남아공 제쳐라 🏇",
|
||||
"한 점 차도 이기면 돼",
|
||||
"16강 티켓 가져오자 🎫",
|
||||
"포르투갈도 함께 외친다 🇵🇹",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const STEP_MS = 150;
|
||||
const REST = 1;
|
||||
const BUBBLE_MS = 2600;
|
||||
const TAP_THRESHOLD = 6;
|
||||
|
||||
export default function WaveStrip() {
|
||||
const [active, setActive] = useState(-1);
|
||||
const [bubble, setBubble] = useState<{ i: number; text: string } | null>(null);
|
||||
const [off, setOff] = useState({ x: 0, y: 0 });
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const lastLine = useRef<string>("");
|
||||
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const drag = useRef({ px: 0, py: 0, ox: 0, oy: 0, moved: false, idx: -1 });
|
||||
|
||||
// 파도타기 루프
|
||||
useEffect(() => {
|
||||
let tick = 0;
|
||||
const id = setInterval(() => {
|
||||
tick = (tick + 1) % (WAVE.length + REST);
|
||||
setActive(tick < WAVE.length ? tick : -1);
|
||||
}, STEP_MS);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
useEffect(() => () => clearTimeout(timer.current), []);
|
||||
|
||||
const speak = useCallback((i: number) => {
|
||||
const lines = WAVE[i].lines;
|
||||
let line = lines[Math.floor(Math.random() * lines.length)];
|
||||
if (lines.length > 1)
|
||||
while (line === lastLine.current)
|
||||
line = lines[Math.floor(Math.random() * lines.length)];
|
||||
lastLine.current = line;
|
||||
setBubble({ i, text: line });
|
||||
clearTimeout(timer.current);
|
||||
timer.current = setTimeout(() => setBubble(null), BUBBLE_MS);
|
||||
}, []);
|
||||
|
||||
// 그룹 전체 드래그 + 개별 탭 구분
|
||||
const onPointerDown = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
const el = (e.target as HTMLElement).closest("[data-idx]");
|
||||
const idx = el ? Number((el as HTMLElement).dataset.idx) : -1;
|
||||
drag.current = { px: e.clientX, py: e.clientY, ox: off.x, oy: off.y, moved: false, idx };
|
||||
setDragging(true);
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
},
|
||||
[off],
|
||||
);
|
||||
const onPointerMove = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!dragging) return;
|
||||
const dx = e.clientX - drag.current.px;
|
||||
const dy = e.clientY - drag.current.py;
|
||||
if (Math.abs(dx) > TAP_THRESHOLD || Math.abs(dy) > TAP_THRESHOLD)
|
||||
drag.current.moved = true;
|
||||
setOff({ x: drag.current.ox + dx, y: drag.current.oy + dy });
|
||||
},
|
||||
[dragging],
|
||||
);
|
||||
const onPointerUp = useCallback(() => {
|
||||
setDragging(false);
|
||||
if (!drag.current.moved && drag.current.idx >= 0) speak(drag.current.idx);
|
||||
}, [speak]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={() => setDragging(false)}
|
||||
className="flex items-end justify-end gap-0"
|
||||
style={{
|
||||
transform: `translate(${off.x}px, ${off.y}px) scale(${dragging ? 1.04 : 1})`,
|
||||
transition: dragging ? "none" : "transform 0.15s ease",
|
||||
cursor: dragging ? "grabbing" : "grab",
|
||||
touchAction: "none",
|
||||
position: "relative",
|
||||
zIndex: dragging || bubble ? 50 : 1,
|
||||
}}
|
||||
>
|
||||
{WAVE.map((a, i) => {
|
||||
const up = active === i;
|
||||
return (
|
||||
<div
|
||||
key={a.name}
|
||||
className="relative"
|
||||
style={{ zIndex: bubble?.i === i ? 50 : up ? 20 : 1, marginLeft: i ? -8 : 0 }}
|
||||
>
|
||||
{bubble?.i === i && (
|
||||
<div className="pointer-events-none absolute bottom-full left-1/2 mb-1 -translate-x-1/2 whitespace-nowrap">
|
||||
<div className="rounded-2xl bg-white px-3 py-1.5 text-[12px] font-bold text-[#1a1d22] shadow-[0_4px_14px_rgba(0,0,0,0.35)]">
|
||||
{bubble.text}
|
||||
</div>
|
||||
<div className="mx-auto -mt-1 h-2.5 w-2.5 rotate-45 bg-white" />
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={`/assets/mascots/wave/${a.name}_${up ? "up" : "down"}.webp`}
|
||||
alt=""
|
||||
data-idx={i}
|
||||
draggable={false}
|
||||
className="h-9 w-auto select-none sm:h-11"
|
||||
style={{
|
||||
transform: up ? "translateY(-6px)" : "translateY(0)",
|
||||
transition: "transform 0.12s ease",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||