mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 07:12:36 -05:00
Score reporting UI pretty complete ver
This commit is contained in:
10
app/components/Label.tsx
Normal file
10
app/components/Label.tsx
Normal 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} />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user