mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-11 05:36:10 -05:00
Event results initial table component
This commit is contained in:
1
TODO.md
1
TODO.md
@@ -23,6 +23,7 @@ Calendar
|
||||
|
||||
## Other
|
||||
|
||||
- [ ] Refactor Avatar to accept player object instead of separate fields
|
||||
- [ ] Redirect to event page after submitting results
|
||||
- [x] date input take in account time zone difference between server and client
|
||||
- [x] User selector allow passing users from top level
|
||||
|
||||
@@ -3,11 +3,12 @@ import type { User } from "~/db/types";
|
||||
import * as React from "react";
|
||||
|
||||
const dimensions = {
|
||||
xxs: 24,
|
||||
xs: 36,
|
||||
sm: 44,
|
||||
md: 81,
|
||||
lg: 125,
|
||||
};
|
||||
} as const;
|
||||
|
||||
export function Avatar({
|
||||
discordId,
|
||||
@@ -17,7 +18,7 @@ export function Avatar({
|
||||
...rest
|
||||
}: Pick<User, "discordId" | "discordAvatar"> & {
|
||||
className?: string;
|
||||
size: "xs" | "sm" | "md" | "lg";
|
||||
size: keyof typeof dimensions;
|
||||
} & React.ButtonHTMLAttributes<HTMLImageElement>) {
|
||||
const [isErrored, setIsErrored] = React.useState(false);
|
||||
// TODO: just show text... my profile?
|
||||
|
||||
@@ -212,12 +212,20 @@ const findWinnersByEventIdStm = sql.prepare(`
|
||||
"CalendarEventResultTeam"."name" as "teamName",
|
||||
"CalendarEventResultTeam"."placement",
|
||||
"CalendarEventResultPlayer"."userId" as "playerId",
|
||||
"CalendarEventResultPlayer"."name" as "playerName"
|
||||
"CalendarEventResultPlayer"."name" as "playerName",
|
||||
"User"."discordName" as "playerDiscordName",
|
||||
"User"."discordDiscriminator" as "playerDiscordDiscriminator",
|
||||
"User"."discordId" as "playerDiscordId",
|
||||
"User"."discordAvatar" as "playerDiscordAvatar"
|
||||
from "CalendarEventResultTeam"
|
||||
join
|
||||
"CalendarEventResultPlayer"
|
||||
on
|
||||
"CalendarEventResultPlayer"."teamId" = "CalendarEventResultTeam"."id"
|
||||
join
|
||||
"User"
|
||||
on
|
||||
"User"."id" = "CalendarEventResultPlayer"."userId"
|
||||
where "CalendarEventResultTeam"."eventId" = $eventId
|
||||
order by "placement" asc
|
||||
`);
|
||||
@@ -228,18 +236,40 @@ export function findResultsByEventId(eventId: CalendarEvent["id"]) {
|
||||
teamName: CalendarEventResultTeam["name"];
|
||||
placement: CalendarEventResultTeam["placement"];
|
||||
playerId: CalendarEventResultPlayer["userId"];
|
||||
playerName: CalendarEventResultPlayer["name"];
|
||||
playerName: CalendarEventResultPlayer["name"] | null;
|
||||
playerDiscordName: User["discordName"] | null;
|
||||
playerDiscordDiscriminator: User["discordDiscriminator"] | null;
|
||||
playerDiscordId: User["discordId"] | null;
|
||||
playerDiscordAvatar: User["discordAvatar"];
|
||||
}>;
|
||||
|
||||
const result: Array<{
|
||||
teamName: CalendarEventResultTeam["name"];
|
||||
placement: CalendarEventResultTeam["placement"];
|
||||
players: Array<string | { id: number }>;
|
||||
players: Array<
|
||||
| string
|
||||
| Pick<
|
||||
User,
|
||||
| "id"
|
||||
| "discordId"
|
||||
| "discordName"
|
||||
| "discordDiscriminator"
|
||||
| "discordAvatar"
|
||||
>
|
||||
>;
|
||||
}> = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const team = result.find((team) => team.teamName === row.teamName);
|
||||
const player = row.playerName ?? { id: row.playerId! };
|
||||
const player = row.playerName ?? {
|
||||
// player name and user id are mutually exclusive
|
||||
// also if user id exists we know a joined user also has to exist
|
||||
id: row.playerId!,
|
||||
discordId: row.playerDiscordId!,
|
||||
discordName: row.playerDiscordName!,
|
||||
discordDiscriminator: row.playerDiscordDiscriminator!,
|
||||
discordAvatar: row.playerDiscordAvatar,
|
||||
};
|
||||
|
||||
if (team) {
|
||||
team.players.push(player);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import type { UseDataFunctionReturn } from "@remix-run/react/dist/components";
|
||||
import { Link } from "@remix-run/react/dist/components";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
calendarEditPage,
|
||||
calendarReportWinnersPage,
|
||||
resolveBaseUrl,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import { actualNumber, id } from "~/utils/zod";
|
||||
import { Tags } from "../components/Tags";
|
||||
@@ -69,7 +71,7 @@ export const loader = async ({ params, request }: LoaderArgs) => {
|
||||
|
||||
export default function CalendarEventPage() {
|
||||
const user = useUser();
|
||||
const { event, badgePrizes } = useLoaderData<typeof loader>();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const { i18n } = useTranslation();
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
@@ -77,11 +79,11 @@ export default function CalendarEventPage() {
|
||||
<Main className="stack lg">
|
||||
<section className="stack sm">
|
||||
<div className="event__times">
|
||||
{event.startTimes.map((startTime, i) => (
|
||||
{data.event.startTimes.map((startTime, i) => (
|
||||
<React.Fragment key={startTime}>
|
||||
<span
|
||||
className={clsx("event__day", {
|
||||
hidden: event.startTimes.length === 1,
|
||||
hidden: data.event.startTimes.length === 1,
|
||||
})}
|
||||
>
|
||||
Day {i + 1}
|
||||
@@ -105,13 +107,13 @@ export default function CalendarEventPage() {
|
||||
</div>
|
||||
<div className="stack md">
|
||||
<div className="stack xs">
|
||||
<h2>{event.name}</h2>
|
||||
<Tags tags={event.tags} badges={badgePrizes} />
|
||||
<h2>{data.event.name}</h2>
|
||||
<Tags tags={data.event.tags} badges={data.badgePrizes} />
|
||||
</div>
|
||||
<div className="stack horizontal sm flex-wrap">
|
||||
{event.discordUrl ? (
|
||||
{data.event.discordUrl ? (
|
||||
<LinkButton
|
||||
to={event.discordUrl}
|
||||
to={data.event.discordUrl}
|
||||
variant="outlined"
|
||||
tiny
|
||||
isExternal
|
||||
@@ -120,17 +122,17 @@ export default function CalendarEventPage() {
|
||||
</LinkButton>
|
||||
) : null}
|
||||
<LinkButton
|
||||
to={event.bracketUrl}
|
||||
to={data.event.bracketUrl}
|
||||
variant="outlined"
|
||||
tiny
|
||||
isExternal
|
||||
>
|
||||
{resolveBaseUrl(event.bracketUrl)}
|
||||
{resolveBaseUrl(data.event.bracketUrl)}
|
||||
</LinkButton>
|
||||
{canEditCalendarEvent({ user, event }) && (
|
||||
{canEditCalendarEvent({ user, event: data.event }) && (
|
||||
<LinkButton
|
||||
tiny
|
||||
to={calendarEditPage(event.eventId)}
|
||||
to={calendarEditPage(data.event.eventId)}
|
||||
data-cy="edit-button"
|
||||
>
|
||||
Edit
|
||||
@@ -138,12 +140,12 @@ export default function CalendarEventPage() {
|
||||
)}
|
||||
{canReportCalendarEventWinners({
|
||||
user,
|
||||
event,
|
||||
startTimes: event.startTimes,
|
||||
event: data.event,
|
||||
startTimes: data.event.startTimes,
|
||||
}) && (
|
||||
<LinkButton
|
||||
tiny
|
||||
to={calendarReportWinnersPage(event.eventId)}
|
||||
to={calendarReportWinnersPage(data.event.eventId)}
|
||||
data-cy="edit-button"
|
||||
>
|
||||
Report winners
|
||||
@@ -152,17 +154,68 @@ export default function CalendarEventPage() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Results />
|
||||
<div className="stack sm">
|
||||
<div className="event__author">
|
||||
<Avatar
|
||||
discordAvatar={event.discordAvatar}
|
||||
discordId={event.discordId}
|
||||
discordAvatar={data.event.discordAvatar}
|
||||
discordId={data.event.discordId}
|
||||
size="xs"
|
||||
/>
|
||||
{discordFullName(event)}
|
||||
{discordFullName(data.event)}
|
||||
</div>
|
||||
<div data-cy="event-description">{event.description}</div>
|
||||
<div data-cy="event-description">{data.event.description}</div>
|
||||
</div>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
||||
function Results() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{data.event.participantCount} teams participated</div>
|
||||
<table className="event__results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Placement</th>
|
||||
<th>Name</th>
|
||||
<th>Members</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.results.map((result, i) => (
|
||||
<tr key={i}>
|
||||
<td>{result.placement}</td>
|
||||
<td>{result.teamName}</td>
|
||||
<td>
|
||||
<ul className="event__results-players">
|
||||
{result.players.map((player) => (
|
||||
<li key={typeof player === "string" ? player : player.id}>
|
||||
{typeof player === "string" ? (
|
||||
player
|
||||
) : (
|
||||
<Link
|
||||
to={userPage(player.discordId)}
|
||||
className="stack horizontal sm justify-center"
|
||||
>
|
||||
<Avatar
|
||||
discordAvatar={player.discordAvatar}
|
||||
discordId={player.discordId}
|
||||
size="xxs"
|
||||
/>{" "}
|
||||
{discordFullName(player)}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,3 +24,14 @@
|
||||
font-weight: var(--semi-bold);
|
||||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
.event__results-table {
|
||||
font-size: var(--fonts-sm);
|
||||
}
|
||||
|
||||
.event__results-players {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--s-2);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user