From 2a7d32b77f33df4289ce29e18fa2d18bcd845c7b Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 11 Aug 2022 23:21:15 +0300 Subject: [PATCH] User profile results page --- TODO.md | 4 +- app/components/Section.tsx | 4 +- app/db/models/calendar.server.ts | 63 +++++++++++++++++++++++ app/routes/calendar/$id/index.tsx | 12 +++-- app/routes/u.$identifier.tsx | 45 ++++++++-------- app/routes/u.$identifier/index.tsx | 20 +++----- app/routes/u.$identifier/results.tsx | 76 ++++++++++++++++++++++++++++ app/styles/calendar-event.css | 25 +-------- app/styles/common.css | 24 +++++++++ app/styles/u.css | 12 +++++ app/utils/strings.ts | 8 +++ public/locales/en/common.json | 4 +- 12 files changed, 225 insertions(+), 72 deletions(-) create mode 100644 app/routes/u.$identifier/results.tsx diff --git a/TODO.md b/TODO.md index cd8bbd06d..80cc591d3 100644 --- a/TODO.md +++ b/TODO.md @@ -16,9 +16,9 @@ Calendar - [x] E2E test adding and editing event - [x] Add winners page - [ ] E2E test add winners page -- [ ] Winners on the event info page +- [x] Winners on the event info page - [ ] There should be a banner on the list page if you have past tournaments to report -- [ ] On the user page tab showing past results +- [x] On the user page tab showing past results - [ ] Translations ## Other diff --git a/app/components/Section.tsx b/app/components/Section.tsx index ece561687..fbe6b3440 100644 --- a/app/components/Section.tsx +++ b/app/components/Section.tsx @@ -3,13 +3,13 @@ export function Section({ children, className, }: { - title: string; + title?: string; children: React.ReactNode; className?: string; }) { return (
-

{title}

+ {title &&

{title}

}
{children}
); diff --git a/app/db/models/calendar.server.ts b/app/db/models/calendar.server.ts index eabaeeb4b..549ee56e0 100644 --- a/app/db/models/calendar.server.ts +++ b/app/db/models/calendar.server.ts @@ -285,6 +285,69 @@ export function findResultsByEventId(eventId: CalendarEvent["id"]) { return result; } +const findResultsByUserIdStm = sql.prepare(` + select + "CalendarEvent"."id" as "eventId", + "CalendarEvent"."name" as "eventName", + "CalendarEventResultTeam"."name" as "teamName", + "CalendarEventResultTeam"."placement", + "CalendarEvent"."participantCount", + (select max("startTime") + from "CalendarEventDate" + where "eventId" = "CalendarEvent"."id") + as "startTime" + from "CalendarEventResultPlayer" + join "CalendarEventResultTeam" + on "CalendarEventResultTeam"."id" = "CalendarEventResultPlayer"."teamId" + join "CalendarEvent" + on "CalendarEvent"."id" = "CalendarEventResultTeam"."eventId" + where "CalendarEventResultPlayer"."userId" = $userId + order by "startTime" desc +`); + +const findMatesByResultTeamIdStm = sql.prepare(` + select + "CalendarEventResultPlayer"."name", + "User"."id", + "User"."discordName" as "discordName", + "User"."discordDiscriminator" as "discordDiscriminator", + "User"."discordId" as "discordId", + "User"."discordAvatar" as "discordAvatar" + from "CalendarEventResultPlayer" + join "User" + on "User"."id" = "CalendarEventResultPlayer"."userId" + where "teamId" = $teamId + and "userId" != $userId +`); + +export function findResultsByUserId(userId: User["id"]) { + return ( + findResultsByUserIdStm.all({ userId }) as Array<{ + eventId: CalendarEvent["id"]; + eventName: CalendarEvent["name"]; + teamName: CalendarEventResultTeam["name"]; + placement: CalendarEventResultTeam["placement"]; + participantCount: CalendarEvent["participantCount"]; + startTime: CalendarEventDate["startTime"]; + }> + ).map((row) => ({ + ...row, + mates: ( + findMatesByResultTeamIdStm.all({ + teamId: row.eventId, + userId, + }) as Array<{ + name: CalendarEventResultPlayer["name"]; + id: User["id"]; + discordName: User["discordName"]; + discordDiscriminator: User["discordDiscriminator"]; + discordId: User["discordId"]; + discordAvatar: User["discordAvatar"]; + }> + ).map(({ name, ...mate }) => name ?? mate), + })); +} + const findAllBetweenTwoTimestampsStm = sql.prepare(` select "CalendarEvent"."name", diff --git a/app/routes/calendar/$id/index.tsx b/app/routes/calendar/$id/index.tsx index 50aa53a96..a03605baf 100644 --- a/app/routes/calendar/$id/index.tsx +++ b/app/routes/calendar/$id/index.tsx @@ -26,7 +26,7 @@ import { import styles from "~/styles/calendar-event.css"; import { databaseTimestampToDate } from "~/utils/dates"; import { notFoundIfFalsy } from "~/utils/remix"; -import { discordFullName, makeTitle } from "~/utils/strings"; +import { discordFullName, makeTitle, placementString } from "~/utils/strings"; import { calendarEditPage, calendarReportWinnersPage, @@ -171,10 +171,10 @@ function Results() {
{data.event.participantCount} teams participated
- +
- + @@ -182,7 +182,9 @@ function Results() { {data.results.map((result, i) => ( - +
PlacementPlacing Name Members
{result.placement} + {placementString(result.placement)} + {result.teamName}
    @@ -193,7 +195,7 @@ function Results() { ) : ( { + return [{ rel: "stylesheet", href: styles }]; +}; export const meta: MetaFunction = ({ data }: { data: UserPageLoaderData }) => { return { @@ -28,23 +31,9 @@ export const handle = { export const userParamsSchema = z.object({ identifier: z.string() }); -export type UserPageLoaderData = Pick< - User, - | "id" - | "discordName" - | "discordAvatar" - | "discordDiscriminator" - | "discordId" - | "youtubeId" - | "twitch" - | "twitter" - | "bio" -> & { - country?: { emoji: string; code: string; name: string }; - badges: CountsByUserId; -}; +export type UserPageLoaderData = UseDataFunctionReturn; -export const loader: LoaderFunction = async ({ request, params }) => { +export const loader = async ({ request, params }: LoaderArgs) => { const locale = await i18next.getLocale(request); const { identifier } = userParamsSchema.parse(params); const user = notFoundIfFalsy(db.users.findByIdentifier(identifier)); @@ -53,7 +42,7 @@ export const loader: LoaderFunction = async ({ request, params }) => { ? countries[user.country as keyof typeof countries] : undefined; - return json({ + return json({ id: user.id, discordAvatar: user.discordAvatar, discordDiscriminator: user.discordDiscriminator, @@ -76,11 +65,12 @@ export const loader: LoaderFunction = async ({ request, params }) => { } : undefined, badges: db.badges.countsByUserId(user.id), + results: db.calendarEvents.findResultsByUserId(user.id), }); }; export default function UserPageLayout() { - const data = useLoaderData(); + const data = useLoaderData(); const user = useUser(); const { t } = useTranslation(); @@ -92,11 +82,16 @@ export default function UserPageLayout() { {t("header.profile")} - {isOwnPage ? ( + {isOwnPage && ( {t("actions.edit")} - ) : null} + )} + {data.results.length > 0 && ( + + {t("results")} + + )}
    diff --git a/app/routes/u.$identifier/index.tsx b/app/routes/u.$identifier/index.tsx index b2f64ed63..e0baf46fa 100644 --- a/app/routes/u.$identifier/index.tsx +++ b/app/routes/u.$identifier/index.tsx @@ -1,23 +1,17 @@ -import type { LinksFunction } from "@remix-run/node"; import { useMatches } from "@remix-run/react"; +import clsx from "clsx"; +import * as React from "react"; +import { useTranslation } from "react-i18next"; import invariant from "tiny-invariant"; import { Avatar } from "~/components/Avatar"; -import styles from "~/styles/u.css"; -import type { UserPageLoaderData } from "../u.$identifier"; -import * as React from "react"; -import type { Unpacked } from "~/utils/types"; -import clsx from "clsx"; -import { assertUnreachable } from "~/utils/types"; +import { Badge } from "~/components/Badge"; import { TwitchIcon } from "~/components/icons/Twitch"; import { TwitterIcon } from "~/components/icons/Twitter"; import { YouTubeIcon } from "~/components/icons/YouTube"; +import type { Unpacked } from "~/utils/types"; +import { assertUnreachable } from "~/utils/types"; import { badgeExplanationText } from "../badges/$id"; -import { Badge } from "~/components/Badge"; -import { useTranslation } from "react-i18next"; - -export const links: LinksFunction = () => { - return [{ rel: "stylesheet", href: styles }]; -}; +import type { UserPageLoaderData } from "../u.$identifier"; export const handle = { i18n: "badges", diff --git a/app/routes/u.$identifier/results.tsx b/app/routes/u.$identifier/results.tsx new file mode 100644 index 000000000..d9b5f9ea6 --- /dev/null +++ b/app/routes/u.$identifier/results.tsx @@ -0,0 +1,76 @@ +import { Link, useMatches } from "@remix-run/react"; +import { useTranslation } from "react-i18next"; +import invariant from "tiny-invariant"; +import { Avatar } from "~/components/Avatar"; +import { Section } from "~/components/Section"; +import { databaseTimestampToDate } from "~/utils/dates"; +import { discordFullName, placementString } from "~/utils/strings"; +import { userPage } from "~/utils/urls"; +import type { UserPageLoaderData } from "../u.$identifier"; + +export default function UserResultsPage() { + const { i18n } = useTranslation(); + const [, parentRoute] = useMatches(); + invariant(parentRoute); + const data = parentRoute.data as UserPageLoaderData; + + return ( +
    + + + + + + + + + + + + {data.results.map((result) => ( + + + + + + + + ))} + +
    PlacingTeamTournamentDateMembers
    + {placementString(result.placement)} + {result.teamName}{result.eventName} + {databaseTimestampToDate(result.startTime).toLocaleDateString( + i18n.language, + { + day: "numeric", + month: "numeric", + year: "numeric", + } + )} + +
      + {result.mates.map((player) => ( +
    • + {typeof player === "string" ? ( + player + ) : ( + + {" "} + {discordFullName(player)} + + )} +
    • + ))} +
    +
    +
    + ); +} diff --git a/app/styles/calendar-event.css b/app/styles/calendar-event.css index b18b67781..eb6d0c311 100644 --- a/app/styles/calendar-event.css +++ b/app/styles/calendar-event.css @@ -25,33 +25,10 @@ gap: var(--s-1); } -.event__results-table { - width: 100%; - border-collapse: separate; - border-spacing: 0 var(--s-1-5); - font-size: var(--fonts-sm); - text-align: left; -} - -.event__results-table > thead { - font-size: var(--fonts-xxs); -} - -.event__results-table > thead > tr { - text-align: center; -} - -.event__results-table > tbody > tr { - padding-block: var(--s-2); -} - -.event__results-table > tbody > tr:nth-child(2n) { - background-color: var(--bg); -} - .event__results-players { display: flex; flex-wrap: wrap; + padding: 0; gap: var(--s-3); list-style: none; } diff --git a/app/styles/common.css b/app/styles/common.css index 332625d53..4559b8eba 100644 --- a/app/styles/common.css +++ b/app/styles/common.css @@ -263,6 +263,30 @@ select:focus { outline: 2px solid var(--theme); } +table { + width: 100%; + border-collapse: separate; + border-spacing: 0 var(--s-1-5); + font-size: var(--fonts-xs); + text-align: left; +} + +table > thead { + font-size: var(--fonts-xxs); +} + +table > tbody > tr:nth-child(2n) { + background-color: var(--bg); +} + +table > thead > tr > th { + padding-inline: var(--s-1); +} + +table > tbody > tr > td { + padding-inline: var(--s-1); +} + hr { border-color: var(--theme-transparent); } diff --git a/app/styles/u.css b/app/styles/u.css index 52b8732ae..713a71c62 100644 --- a/app/styles/u.css +++ b/app/styles/u.css @@ -129,3 +129,15 @@ font-size: var(--fonts-xxxs); font-weight: var(--bold); } + +.u__results-section { + overflow-x: auto; +} + +.u__results-players { + display: flex; + flex-wrap: wrap; + padding: 0; + gap: var(--s-3); + list-style: none; +} diff --git a/app/utils/strings.ts b/app/utils/strings.ts index eb3fe3bd1..01094b7ed 100644 --- a/app/utils/strings.ts +++ b/app/utils/strings.ts @@ -9,3 +9,11 @@ export function discordFullName( export function makeTitle(title: string | string[]) { return `${Array.isArray(title) ? title.join(" | ") : title} | sendou.ink`; } + +export function placementString(placement: number) { + if (placement === 1) return "🥇"; + if (placement === 2) return "🥈"; + if (placement === 3) return "🥉"; + + return `${placement}th`; +} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index d5fc1c09f..20953d587 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -18,5 +18,7 @@ "actions.save": "Save", "actions.saving": "Saving...", - "actions.edit": "Edit" + "actions.edit": "Edit", + + "results": "Results" }