fix(call-to-play): explain unavailable game calls
Calls for a game missing from the local catalog still contributed to the badge but were discarded by both the ticker and overlay. Opening the badge could therefore show a blank modal with no explanation. Render those calls with an unavailable-game label, the sender and game ID, the normal roster and chat, and safe coordination actions. The creator can mark the match started, but automatic launch remains disabled until catalog data exists. Test Plan: - `just fmt` -- passed - `just frontend-test` -- passed, 21 tests - `just build` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
@@ -81,8 +81,7 @@ export const CallToPlayOverlay = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sorted.map(nomination => {
|
{sorted.map(nomination => {
|
||||||
const game = gameById.get(nomination.gameId);
|
const game = gameById.get(nomination.gameId) ?? null;
|
||||||
if (!game) return null;
|
|
||||||
return (
|
return (
|
||||||
<NominationCard
|
<NominationCard
|
||||||
key={nomination.id}
|
key={nomination.id}
|
||||||
@@ -91,7 +90,7 @@ export const CallToPlayOverlay = ({
|
|||||||
actorId={actorId}
|
actorId={actorId}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
focused={nomination.id === focusId}
|
focused={nomination.id === focusId}
|
||||||
thumbnailUrl={getThumbnail(game.id)}
|
thumbnailUrl={game ? getThumbnail(game.id) : null}
|
||||||
totalPeerCount={totalPeerCount}
|
totalPeerCount={totalPeerCount}
|
||||||
onLaunch={onLaunch}
|
onLaunch={onLaunch}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export const CallToPlayTicker = ({ nominations, games, accent, onOpen }: Props)
|
|||||||
<div className="ctp-ticker-stack">
|
<div className="ctp-ticker-stack">
|
||||||
{active.map(nomination => {
|
{active.map(nomination => {
|
||||||
const game = gameById.get(nomination.gameId);
|
const game = gameById.get(nomination.gameId);
|
||||||
if (!game) return null;
|
|
||||||
const status = statusOf(nomination, now);
|
const status = statusOf(nomination, now);
|
||||||
if (status === 'started') return null;
|
if (status === 'started') return null;
|
||||||
const ready = readyCountOf(nomination, now);
|
const ready = readyCountOf(nomination, now);
|
||||||
@@ -102,7 +101,10 @@ export const CallToPlayTicker = ({ nominations, games, accent, onOpen }: Props)
|
|||||||
>
|
>
|
||||||
<span className="ctp-ticker-dot" data-status={status} />
|
<span className="ctp-ticker-dot" data-status={status} />
|
||||||
<span className="ctp-ticker-label" data-status={status}>{LABEL[status]}</span>
|
<span className="ctp-ticker-label" data-status={status}>{LABEL[status]}</span>
|
||||||
<span className="ctp-ticker-game">{game.name}</span>
|
<span
|
||||||
|
className="ctp-ticker-game"
|
||||||
|
title={game ? undefined : `Game ID: ${nomination.gameId}`}
|
||||||
|
>{game?.name ?? 'Game unavailable here'}</span>
|
||||||
<span className="ctp-ticker-by">by {nomination.creator}</span>
|
<span className="ctp-ticker-by">by {nomination.creator}</span>
|
||||||
<span className="ctp-ticker-ready">{count}</span>
|
<span className="ctp-ticker-ready">{count}</span>
|
||||||
<span className="ctp-ticker-time">{time}</span>
|
<span className="ctp-ticker-time">{time}</span>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { CallToPlayParticipant, Game, Nomination } from '../../lib/types';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
nomination: Nomination;
|
nomination: Nomination;
|
||||||
game: Game;
|
game: Game | null;
|
||||||
actorId: string | null;
|
actorId: string | null;
|
||||||
actions: CallToPlayActions;
|
actions: CallToPlayActions;
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
@@ -94,7 +94,9 @@ export const NominationCard = ({
|
|||||||
Math.min(100, (remaining / Math.max(1, nomination.deadline - windowStart)) * 100),
|
Math.min(100, (remaining / Math.max(1, nomination.deadline - windowStart)) * 100),
|
||||||
);
|
);
|
||||||
const urgency = percentage < 20 ? 'high' : percentage < 50 ? 'mid' : 'low';
|
const urgency = percentage < 20 ? 'high' : percentage < 50 ? 'mid' : 'low';
|
||||||
const installedCount = (game.installed_peer_count ?? game.peer_count) + (game.installed ? 1 : 0);
|
const installedCount = game === null
|
||||||
|
? 0
|
||||||
|
: (game.installed_peer_count ?? game.peer_count) + (game.installed ? 1 : 0);
|
||||||
const lanCount = Math.max(totalPeerCount + 1, installedCount);
|
const lanCount = Math.max(totalPeerCount + 1, installedCount);
|
||||||
|
|
||||||
const timer = isStarted
|
const timer = isStarted
|
||||||
@@ -130,15 +132,19 @@ export const NominationCard = ({
|
|||||||
>
|
>
|
||||||
<div className="ctp-card-top">
|
<div className="ctp-card-top">
|
||||||
<div className="ctp-card-cover">
|
<div className="ctp-card-cover">
|
||||||
<GameCover game={game} aspect="square" thumbnailUrl={thumbnailUrl} />
|
{game
|
||||||
|
? <GameCover game={game} aspect="square" thumbnailUrl={thumbnailUrl} />
|
||||||
|
: <div className="ctp-card-cover-missing"><Icon.flag /></div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="ctp-card-info">
|
<div className="ctp-card-info">
|
||||||
<div className="ctp-card-title">{game.name}</div>
|
<div className="ctp-card-title">{game?.name ?? 'Game unavailable here'}</div>
|
||||||
<div className="ctp-card-sub">
|
<div className="ctp-card-sub">
|
||||||
{nomination.scheduledFor === null ? 'Called' : 'Scheduled'} by{' '}
|
{nomination.scheduledFor === null ? 'Called' : 'Scheduled'} by{' '}
|
||||||
<strong>{nomination.creator}</strong>
|
<strong>{nomination.creator}</strong>
|
||||||
{nomination.scheduledFor !== null && ` · starts at ${formatClock(nomination.scheduledFor)}`}
|
{nomination.scheduledFor !== null && ` · starts at ${formatClock(nomination.scheduledFor)}`}
|
||||||
{` · ${installedCount}/${lanCount} peers have it installed`}
|
{game
|
||||||
|
? ` · ${installedCount}/${lanCount} peers have it installed`
|
||||||
|
: ` · this game is not in your current library (ID: ${nomination.gameId})`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{timer}
|
{timer}
|
||||||
@@ -229,7 +235,7 @@ export const NominationCard = ({
|
|||||||
|
|
||||||
interface CardActionsProps {
|
interface CardActionsProps {
|
||||||
nomination: Nomination;
|
nomination: Nomination;
|
||||||
game: Game;
|
game: Game | null;
|
||||||
actorId: string | null;
|
actorId: string | null;
|
||||||
actions: CallToPlayActions;
|
actions: CallToPlayActions;
|
||||||
onLaunch: (game: Game) => void;
|
onLaunch: (game: Game) => void;
|
||||||
@@ -274,7 +280,11 @@ const CardActions = ({
|
|||||||
const readyCount = readyCountOf(nomination, now);
|
const readyCount = readyCountOf(nomination, now);
|
||||||
|
|
||||||
if (isStarted) {
|
if (isStarted) {
|
||||||
return <div className="ctp-note ctp-note-launch">Launching {game.name}…</div>;
|
return (
|
||||||
|
<div className="ctp-note ctp-note-launch">
|
||||||
|
{game ? `Launching ${game.name}…` : 'The match is starting.'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (scheduled) {
|
if (scheduled) {
|
||||||
if (isCreator) {
|
if (isCreator) {
|
||||||
@@ -310,9 +320,9 @@ const CardActions = ({
|
|||||||
<button
|
<button
|
||||||
className="act-btn act-play"
|
className="act-btn act-play"
|
||||||
onClick={() => void actions.startNow(nomination.id).then(accepted => {
|
onClick={() => void actions.startNow(nomination.id).then(accepted => {
|
||||||
if (accepted) onLaunch(game);
|
if (accepted && game) onLaunch(game);
|
||||||
})}
|
})}
|
||||||
><Icon.play /><span>Start now</span></button>
|
><Icon.play /><span>{game ? 'Start now' : 'Mark as started'}</span></button>
|
||||||
<button className="ghost-btn" onClick={() => actions.addTime(nomination.id)}>
|
<button className="ghost-btn" onClick={() => actions.addTime(nomination.id)}>
|
||||||
Add 5 more minutes
|
Add 5 more minutes
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1933,6 +1933,14 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.ctp-card-cover-missing {
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
display: grid; place-items: center;
|
||||||
|
color: var(--t-3);
|
||||||
|
background: color-mix(in srgb, var(--bd-2) 55%, transparent);
|
||||||
|
border: 1px dashed var(--bd-2);
|
||||||
|
}
|
||||||
|
.ctp-card-cover-missing svg { width: 22px; height: 22px; }
|
||||||
.ctp-card-info { flex: 1; min-width: 0; }
|
.ctp-card-info { flex: 1; min-width: 0; }
|
||||||
.ctp-card-title { font-size: 14.5px; font-weight: 700; color: var(--t-1); }
|
.ctp-card-title { font-size: 14.5px; font-weight: 700; color: var(--t-1); }
|
||||||
.ctp-card-sub { margin-top: 3px; font-size: 11.5px; color: var(--t-3); }
|
.ctp-card-sub { margin-top: 3px; font-size: 11.5px; color: var(--t-3); }
|
||||||
|
|||||||
Reference in New Issue
Block a user