방장 + 멈춘 게임에서 막대 버튼이 7개가 되어 320px 휴대폰에서 넘쳤다. 막대는 최대 6개로 두고, 끝내기 버튼은 "오류로 게임이 멈췄어요" 안내 바로 아래에 둔다. 도움말의 "메뉴에서" 문구도 고침. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
6.5 KiB
TypeScript
158 lines
6.5 KiB
TypeScript
/**
|
|
* 초보자 도움 (docs/07 §5): "30초 규칙 요약" cards (first game of a kind on this device) and the
|
|
* in-game [?] sheet ("지금 할 수 있는 것" + full rules + summary again). Content lives in each game's chunk.
|
|
*/
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import type { PublicUser, RoomView } from '@bg/shared/lite';
|
|
import { Sheet } from './ui';
|
|
import { useRoom } from '../net/socket';
|
|
import type { GameUI, QuickCard } from '../games/types';
|
|
|
|
const seenKey = (gameId: string) => `bg:qs:${gameId}`;
|
|
|
|
export function quickStartSeen(gameId: string): boolean {
|
|
try {
|
|
return localStorage.getItem(seenKey(gameId)) === '1';
|
|
} catch {
|
|
return true; // storage blocked: don't nag every game
|
|
}
|
|
}
|
|
|
|
function markQuickStartSeen(gameId: string) {
|
|
try {
|
|
localStorage.setItem(seenKey(gameId), '1');
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
/** The 3 summary cards with [다음] / [건너뛰기] / [시작하기]. */
|
|
export function QuickStartCards({ title, cards, onClose, finish = '시작하기' }: { title: string; cards: QuickCard[]; onClose: () => void; finish?: string }) {
|
|
const [i, setI] = useState(0);
|
|
const card = cards[i]!;
|
|
const last = i === cards.length - 1;
|
|
return (
|
|
<Sheet onClose={onClose} label={`${title} 30초 규칙 요약`}>
|
|
<p className="muted small" style={{ margin: 0, textAlign: 'center' }}>
|
|
{title} 30초 규칙 요약 · {i + 1}/{cards.length}
|
|
</p>
|
|
<div
|
|
role="img"
|
|
aria-label={card.alt}
|
|
style={{ minHeight: 170, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, flexWrap: 'wrap', padding: 12, borderRadius: 16, background: 'var(--bg)' }}
|
|
>
|
|
{card.pic}
|
|
</div>
|
|
<h2 style={{ margin: 0, textAlign: 'center' }} aria-live="polite">
|
|
{card.title}
|
|
</h2>
|
|
<p style={{ margin: 0, fontSize: '1.15rem', lineHeight: 1.6, textAlign: 'center' }}>{card.text}</p>
|
|
<div className="row" style={{ justifyContent: 'center', gap: 6 }} aria-hidden>
|
|
{cards.map((_, k) => (
|
|
<span key={k} className="dot" style={{ width: 10, height: 10, background: k === i ? 'var(--primary)' : 'var(--border)' }} />
|
|
))}
|
|
</div>
|
|
{last ? (
|
|
<button className="btn btn-primary btn-block" style={{ minHeight: 60 }} onClick={onClose} autoFocus>
|
|
{finish}
|
|
</button>
|
|
) : (
|
|
<div className="row" style={{ flexWrap: 'nowrap' }}>
|
|
<button className="btn btn-secondary" style={{ flex: 1 }} onClick={onClose}>
|
|
건너뛰기
|
|
</button>
|
|
<button className="btn btn-primary" style={{ flex: 2, minHeight: 60 }} onClick={() => setI(i + 1)}>
|
|
다음 →
|
|
</button>
|
|
</div>
|
|
)}
|
|
</Sheet>
|
|
);
|
|
}
|
|
|
|
/** "30초 요약 보기" button for places outside a game (rules page). */
|
|
export function QuickStartButton({ title, cards }: { title: string; cards: QuickCard[] | undefined }) {
|
|
const [open, setOpen] = useState(false);
|
|
const close = useCallback(() => setOpen(false), []);
|
|
if (!cards?.length) return null;
|
|
return (
|
|
<>
|
|
<button className="btn btn-secondary btn-block" onClick={() => setOpen(true)}>
|
|
🃏 30초 요약 보기
|
|
</button>
|
|
{open && <QuickStartCards title={title} cards={cards} finish="닫기" onClose={close} />}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* [?] button next to the turn status while a game is on screen. Also opens the summary cards by itself
|
|
* the first time this device sees a running game of this kind.
|
|
*/
|
|
export function GameHelp({ room, ui, title, me, players }: { room: RoomView; ui: GameUI; title: string; me: string; players: Record<string, PublicUser> }) {
|
|
const view = useRoom((s) => s.game?.view);
|
|
const [open, setOpen] = useState(false);
|
|
const [rules, setRules] = useState(false);
|
|
const [cards, setCards] = useState(false);
|
|
const playing = room.status === 'playing';
|
|
const gameId = room.gameId;
|
|
|
|
useEffect(() => {
|
|
if (playing && ui.quickStart?.length && !quickStartSeen(gameId)) setCards(true);
|
|
}, [playing, gameId, ui]);
|
|
|
|
// Stable callbacks: Sheet re-runs its focus effect when onClose changes (every game update otherwise).
|
|
const closeCards = useCallback(() => {
|
|
markQuickStartSeen(gameId);
|
|
setCards(false);
|
|
}, [gameId]);
|
|
const closeHelp = useCallback(() => setOpen(false), []);
|
|
const seated = room.seats.some((s) => s?.user.id === me);
|
|
const now =
|
|
room.status === 'paused'
|
|
? '오류로 게임이 멈췄어요. 방장이 [게임 끝내기] 버튼으로 끝낼 수 있어요.'
|
|
: view === undefined
|
|
? ''
|
|
: (ui.helpNow ?? ui.statusText)(view, me, players);
|
|
|
|
return (
|
|
<>
|
|
<button className="btn btn-secondary" style={{ flexShrink: 0, padding: '0 12px', flexDirection: 'column', gap: 0, lineHeight: 1.1 }} onClick={() => (setRules(false), setOpen(true))} aria-label="도움말: 지금 할 수 있는 것" aria-haspopup="dialog">
|
|
<span aria-hidden style={{ fontSize: '1.3rem' }}>?</span>
|
|
<span className="small">도움</span>
|
|
</button>
|
|
{open &&
|
|
createPortal(
|
|
<Sheet onClose={closeHelp} label="지금 할 수 있는 것">
|
|
<h2 style={{ margin: 0 }}>지금 할 수 있는 것</h2>
|
|
{!seated && <p className="muted" style={{ margin: 0 }}>지금은 구경하고 있어요. 게임에는 손댈 수 없어요.</p>}
|
|
<p className="help-now" style={{ margin: 0, fontSize: '1.2rem', lineHeight: 1.6, whiteSpace: 'pre-line', fontWeight: 600 }}>
|
|
{now}
|
|
</p>
|
|
<div className="stack" style={{ gap: 8 }}>
|
|
<button className="btn btn-secondary btn-block" aria-expanded={rules} onClick={() => setRules(!rules)}>
|
|
📖 {rules ? '규칙 접기' : '전체 규칙 보기'}
|
|
</button>
|
|
{rules && (
|
|
<section className="card stack" aria-label={`${title} 규칙`}>
|
|
<ui.Rules options={room.options} />
|
|
</section>
|
|
)}
|
|
{!!ui.quickStart?.length && (
|
|
<button className="btn btn-secondary btn-block" onClick={() => (setOpen(false), setCards(true))}>
|
|
🃏 30초 요약 다시 보기
|
|
</button>
|
|
)}
|
|
<button className="btn btn-primary btn-block" onClick={() => setOpen(false)}>
|
|
닫기
|
|
</button>
|
|
</div>
|
|
</Sheet>,
|
|
document.body,
|
|
)}
|
|
{cards && ui.quickStart && createPortal(<QuickStartCards title={title} cards={ui.quickStart} onClose={closeCards} />, document.body)}
|
|
</>
|
|
);
|
|
}
|