Score reporting UI pretty complete ver

This commit is contained in:
Kalle (Sendou)
2022-01-03 08:55:26 +02:00
parent 7d7785d6b7
commit 5536f8c1cb
9 changed files with 167 additions and 39 deletions

10
app/components/Label.tsx Normal file
View File

@@ -0,0 +1,10 @@
import classNames from "classnames";
export interface LabelProps
extends React.LabelHTMLAttributes<HTMLLabelElement> {
className?: string;
}
export function Label({ className, ...rest }: LabelProps) {
return <label className={classNames(className, "label")} {...rest} />;
}

View File

@@ -1,3 +1,4 @@
import classNames from "classnames";
import { MyCSSProperties } from "~/utils";
export function ActionSectionWrapper({
@@ -7,6 +8,7 @@ export function ActionSectionWrapper({
}: {
children: React.ReactNode;
icon?: "warning" | "info" | "success" | "error";
"justify-center"?: boolean;
"data-cy"?: string;
}) {
// todo: flex-dir: column on mobile
@@ -21,7 +23,13 @@ export function ActionSectionWrapper({
style={style}
data-cy={rest["data-cy"]}
>
<div className="tournament__action-section__content">{children}</div>
<div
className={classNames("tournament__action-section__content", {
"justify-center": rest["justify-center"],
})}
>
{children}
</div>
</section>
);
}

View File

@@ -9,6 +9,7 @@ import type {
} from "~/services/tournament";
import { modeToImageUrl, Unpacked } from "~/utils";
import { Button } from "../Button";
import { Label } from "../Label";
import { ActionSectionWrapper } from "./ActionSectionWrapper";
import { DuringMatchActionsRosters } from "./DuringMatchActionsRosters";
@@ -65,15 +66,19 @@ export function DuringMatchActions({
/>
{modesShortToLong[stage.mode]} on {stage.name}
</h4>
<h4>Stage 1</h4>
<h4>
Stage {currentMatch.score?.reduce((acc, cur) => acc + cur, 1)}
</h4>
</div>
</div>
<ActionSectionWrapper>
<ActionSectionWrapper justify-center>
<div className="tournament-bracket__infos">
<div className="tournament-bracket__infos__columns">
{roundInfo.map(([title, value]) => (
<React.Fragment key={title}>
<label>{title}</label>
<Label className="tournament-bracket__infos__label">
{title}
</Label>
<div>{value}</div>
</React.Fragment>
))}

View File

@@ -1,9 +1,10 @@
import clone from "just-clone";
import { useState } from "react";
import * as React from "react";
import { TOURNAMENT_TEAM_ROSTER_MIN_SIZE } from "~/constants";
import type { FindTournamentByNameForUrlI } from "~/services/tournament";
import { Unpacked } from "~/utils";
import { Button } from "../Button";
import { Label } from "../Label";
export function DuringMatchActionsRosters({
ownTeam,
@@ -12,15 +13,28 @@ export function DuringMatchActionsRosters({
ownTeam: Unpacked<FindTournamentByNameForUrlI["teams"]>;
opponentTeam: Unpacked<FindTournamentByNameForUrlI["teams"]>;
}) {
const [checkedPlayers, setCheckedPlayers] = useState<[string[], string[]]>(
checkedPlayersInitialState([ownTeam, opponentTeam])
);
const [checkedPlayers, setCheckedPlayers] = React.useState<
[string[], string[]]
>(checkedPlayersInitialState([ownTeam, opponentTeam]));
const [winnerId, setWinnerId] = React.useState<string | null>(null);
return (
<div>
<div className="width-full">
<div className="tournament-bracket__during-match-actions__rosters">
{[ownTeam, opponentTeam].map((team, teamI) => (
<div key={team.id}>
<h4>{team.name}</h4>
<div className="flex items-center justify-center my-1-5">
<input
type="radio"
id={team.id}
name="winner"
value={team.id}
onChange={(e) => setWinnerId(e.currentTarget.value)}
/>
<Label className="mb-0 ml-2" htmlFor={team.id}>
Winner
</Label>
</div>
<div className="tournament-bracket__during-match-actions__team-players">
{team.members.map(({ member }) => (
<div
@@ -50,23 +64,34 @@ export function DuringMatchActionsRosters({
})
}
/>{" "}
<label
<Label
className="plain tournament-bracket__during-match-actions__player-name"
htmlFor={member.id}
>
{member.discordName}
</label>
</Label>
</div>
))}
</div>
</div>
))}
</div>
<div>
<ReportScoreButtons checkedPlayers={checkedPlayers} />
<div className="tournament-bracket__during-match-actions__actions">
<ReportScoreButtons
checkedPlayers={checkedPlayers}
winnerName={winningTeam()}
/>
</div>
</div>
);
function winningTeam() {
if (!winnerId) return;
if (ownTeam.id === winnerId) return ownTeam.name;
if (opponentTeam.id === winnerId) return opponentTeam.name;
throw new Error("No winning team matching the id");
}
}
function checkedPlayersInitialState([teamOne, teamTwo]: [
@@ -88,8 +113,10 @@ function checkedPlayersInitialState([teamOne, teamTwo]: [
function ReportScoreButtons({
checkedPlayers,
winnerName,
}: {
checkedPlayers: string[][];
winnerName?: string;
}) {
if (
!checkedPlayers.every(
@@ -97,12 +124,24 @@ function ReportScoreButtons({
)
) {
return (
<p>
<p className="tournament-bracket__during-match-actions__amount-warning-paragraph">
Please choose exactly {TOURNAMENT_TEAM_ROSTER_MIN_SIZE}+
{TOURNAMENT_TEAM_ROSTER_MIN_SIZE} players to report score
</p>
);
}
return <Button>Report score</Button>;
if (!winnerName) {
return (
<p className="tournament-bracket__during-match-actions__amount-warning-paragraph">
Please select the winning team
</p>
);
}
return (
<Button type="submit" variant="minimal">
Report {winnerName} win
</Button>
);
}

View File

@@ -15,6 +15,7 @@ import { Alert } from "~/components/Alert";
import { Catcher } from "~/components/Catcher";
import { FormErrorMessage } from "~/components/FormErrorMessage";
import { FormInfoText } from "~/components/FormInfoText";
import { Label } from "~/components/Label";
import { SubmitButton } from "~/components/SubmitButton";
import { TeamRoster } from "~/components/tournament/TeamRoster";
import { TOURNAMENT_TEAM_ROSTER_MAX_SIZE } from "~/constants";
@@ -189,9 +190,9 @@ export default function ManageRosterPage() {
<input type="hidden" name="teamId" value={ownTeam.id} />
<fieldset>
<legend>Edit team info</legend>
<label htmlFor="friendCode">
<Label htmlFor="friendCode">
Friend code for your opponents to add
</label>
</Label>
<input
name="friendCode"
id="friendCode"
@@ -200,9 +201,9 @@ export default function ManageRosterPage() {
pattern={friendCodeRegExpString}
/>
<label className="mt-3" htmlFor="roomPass">
<Label className="mt-3" htmlFor="roomPass">
Room password
</label>
</Label>
<input
name="roomPass"
id="roomPass"
@@ -214,9 +215,9 @@ export default function ManageRosterPage() {
If blank the password will be randomly generated whenever you host
</FormInfoText>
<label className="mt-3" htmlFor="canHost">
<Label className="mt-3" htmlFor="canHost">
Does your team want to host?
</label>
</Label>
<div className="tournament__manage-roster__radio-with-label">
<input
type="radio"
@@ -225,12 +226,12 @@ export default function ManageRosterPage() {
value="yes"
defaultChecked={ownTeam.canHost}
/>
<label
<Label
className="tournament__manage-roster__radio-label"
htmlFor="yes"
>
Yes
</label>
</Label>
</div>
<div className="tournament__manage-roster__radio-with-label">
@@ -241,12 +242,12 @@ export default function ManageRosterPage() {
value="no"
defaultChecked={!ownTeam.canHost}
/>
<label
<Label
className="tournament__manage-roster__radio-label"
htmlFor="no"
>
No
</label>
</Label>
</div>
<FormInfoText>
You might still have to host if both teams prefer not to
@@ -265,7 +266,7 @@ export default function ManageRosterPage() {
<fieldset className="tournament__manage-roster__actions">
<legend>Add players to your team</legend>
<div className="tournament__manage-roster__actions__section">
<label htmlFor="inviteCodeInput">Share this URL</label>
<Label htmlFor="inviteCodeInput">Share this URL</Label>
<input
id="inviteCodeInput"
className="tournament__manage-roster__input"
@@ -279,9 +280,9 @@ export default function ManageRosterPage() {
<Form method="post">
<input type="hidden" name="_action" value="ADD_PLAYER" />
<input type="hidden" name="teamId" value={ownTeam.id} />
<label htmlFor="userId">
<Label htmlFor="userId">
Add players you previously played with
</label>
</Label>
<select
className="tournament__manage-roster__select"
name="userId"

View File

@@ -26,6 +26,7 @@ import {
friendCodeRegExpString,
} from "~/core/tournament/utils";
import { FormInfoText } from "~/components/FormInfoText";
import { Label } from "~/components/Label";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
@@ -137,7 +138,7 @@ export default function RegisterPage() {
<div className="tournament__register__content">
<Form method="post">
<input type="hidden" name="tournamentId" value={tournamentData.id} />
<label htmlFor="teamName">Team name</label>
<Label htmlFor="teamName">Team name</Label>
<input
name="teamName"
id="teamName"
@@ -148,9 +149,9 @@ export default function RegisterPage() {
data-cy="team-name-input"
/>
<FormErrorMessage errorMsg={actionData?.fieldErrors?.teamName} />
<label className="mt-3" htmlFor="friendCode">
<Label className="mt-3" htmlFor="friendCode">
Friend code for your opponents to add
</label>
</Label>
<input
name="friendCode"
id="friendCode"

View File

@@ -213,7 +213,7 @@ input[type="checkbox"] {
height: 16px;
}
label:not(.plain) {
.label {
display: block;
font-size: var(--fonts-xs);
font-weight: var(--bold);
@@ -338,6 +338,27 @@ select::selection {
margin-block-start: var(--s-3);
}
.mt-4 {
margin-block-start: var(--s-4);
}
.mb-0 {
margin-block-end: 0;
}
.ml-2 {
margin-inline-start: var(--s-2);
}
.my-1-5 {
margin-block-end: var(--s-1-5);
margin-block-start: var(--s-1-5);
}
.text-xs {
font-size: var(--fonts-xs);
}
.invisible {
visibility: hidden;
}
@@ -345,3 +366,15 @@ select::selection {
.width-full {
width: 100%;
}
.flex {
display: flex;
}
.justify-center {
justify-content: center;
}
.items-center {
align-items: center;
}

View File

@@ -61,9 +61,15 @@
.tournament-bracket__infos__columns {
display: grid;
column-gap: var(--s-4);
align-items: center;
margin: 0 auto;
column-gap: var(--s-6);
grid-template-columns: 1fr 1fr;
row-gap: var(--s-2);
row-gap: var(--s-2-5);
}
.tournament-bracket__infos__label {
margin-block-end: 0;
}
.tournament-bracket__stage-banner__top-bar__mode-image {
@@ -76,15 +82,32 @@
margin-top: var(--s-4);
gap: var(--s-4);
grid-template-areas:
"img img"
"infos rosters";
grid-template-columns: 1fr 1fr;
"img"
"infos"
"rosters";
grid-template-columns: 1fr;
}
.tournament-bracket__during-match-actions__actions {
display: flex;
width: 100%;
height: 2rem;
justify-content: center;
margin-top: var(--s-2);
color: var(--theme-warning);
}
.tournament-bracket__during-match-actions__amount-warning-paragraph {
display: flex;
align-items: center;
font-size: var(--fonts-xs);
}
.tournament-bracket__during-match-actions__rosters {
display: flex;
width: 100%;
justify-content: space-evenly;
text-align: center;
}
.tournament-bracket__during-match-actions__team-players {
@@ -243,3 +266,12 @@
content: "";
transform: translate(-50%, -50%);
}
@media screen and (min-width: 480px) {
.tournament-bracket__during-match-actions {
grid-template-areas:
"img img"
"infos rosters";
grid-template-columns: 1fr 1fr;
}
}

View File

@@ -40,8 +40,7 @@
}
.tournament__manage-roster__radio-label {
/* TODO: better way for global styles... <Label> component */
margin-block-end: 0 !important;
margin-block-end: 0;
margin-inline-start: var(--s-2);
}