diff --git a/app/routes/u.$identifier/results.tsx b/app/routes/u.$identifier/results.tsx
deleted file mode 100644
index 2a6a431e2..000000000
--- a/app/routes/u.$identifier/results.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { Link, useMatches } from "@remix-run/react";
-import { useTranslation } from "react-i18next";
-import invariant from "tiny-invariant";
-import { Avatar } from "~/components/Avatar";
-import { Placement } from "~/components/Placement";
-import { Section } from "~/components/Section";
-import { databaseTimestampToDate } from "~/utils/dates";
-import { discordFullName } from "~/utils/strings";
-import { calendarEventPage, userPage } from "~/utils/urls";
-import type { UserPageLoaderData } from "../u.$identifier";
-
-export default function UserResultsPage() {
- const { t, i18n } = useTranslation("user");
- const [, parentRoute] = useMatches();
- invariant(parentRoute);
- const data = parentRoute.data as UserPageLoaderData;
-
- return (
-
-
-
-
-
- {t("results.placing")}
- {t("results.team")}
- {t("results.tournament")}
- {t("results.participants")}
- {t("results.date")}
- {t("results.mates")}
-
-
-
- {data.results.map((result) => (
-
-
-
-
- {result.teamName}
-
-
- {result.eventName}
-
-
- {result.participantCount}
-
- {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/routes/u.$identifier/results/components/UserResultsTable.tsx b/app/routes/u.$identifier/results/components/UserResultsTable.tsx
new file mode 100644
index 000000000..b2a251d06
--- /dev/null
+++ b/app/routes/u.$identifier/results/components/UserResultsTable.tsx
@@ -0,0 +1,111 @@
+import { Link } from "@remix-run/react";
+import { useTranslation } from "react-i18next";
+import { Avatar } from "~/components/Avatar";
+import { Placement } from "~/components/Placement";
+import { type UserPageLoaderData } from "~/routes/u.$identifier";
+import { databaseTimestampToDate } from "~/utils/dates";
+import { discordFullName } from "~/utils/strings";
+import { calendarEventPage, userPage } from "~/utils/urls";
+
+export type UserResultsTableProps = {
+ results: UserPageLoaderData["results"];
+ id: string;
+ hasHighlightCheckboxes?: boolean;
+};
+
+export const HIGHLIGHT_CHECKBOX_NAME = "highlightTeamIds";
+
+export function UserResultsTable({
+ results,
+ id,
+ hasHighlightCheckboxes,
+}: UserResultsTableProps) {
+ const { t, i18n } = useTranslation("user");
+
+ const placementHeaderId = `${id}-th-placement`;
+
+ return (
+
+ );
+}
diff --git a/app/routes/u.$identifier/results/highlights.tsx b/app/routes/u.$identifier/results/highlights.tsx
new file mode 100644
index 000000000..70086ff7e
--- /dev/null
+++ b/app/routes/u.$identifier/results/highlights.tsx
@@ -0,0 +1,79 @@
+import { type ActionFunction, redirect } from "@remix-run/node";
+import { Form, useMatches, useTransition } from "@remix-run/react";
+import { useTranslation } from "react-i18next";
+import invariant from "tiny-invariant";
+import { z } from "zod";
+import { Button } from "~/components/Button";
+import { FormErrors } from "~/components/FormErrors";
+import { Main } from "~/components/Main";
+import { db } from "~/db";
+import { requireUser } from "~/modules/auth";
+import { type UserPageLoaderData } from "~/routes/u.$identifier";
+import { normalizeFormFieldArray } from "~/utils/arrays";
+import { parseRequestFormData } from "~/utils/remix";
+import { userResultsPage } from "~/utils/urls";
+import {
+ HIGHLIGHT_CHECKBOX_NAME,
+ UserResultsTable,
+} from "./components/UserResultsTable";
+
+const editHighlightsActionSchema = z.object({
+ [HIGHLIGHT_CHECKBOX_NAME]: z.optional(
+ z.union([z.array(z.string()), z.string()])
+ ),
+});
+
+export const action: ActionFunction = async ({ request }) => {
+ const user = await requireUser(request);
+ const data = await parseRequestFormData({
+ request,
+ schema: editHighlightsActionSchema,
+ });
+
+ const resultTeamIds = normalizeFormFieldArray(
+ data[HIGHLIGHT_CHECKBOX_NAME]
+ ).map((id) => parseInt(id, 10));
+
+ db.users.updateResultHighlights({
+ userId: user.id,
+ resultTeamIds,
+ });
+
+ return redirect(userResultsPage(user));
+};
+
+export default function ResultHighlightsEditPage() {
+ const { t } = useTranslation(["common", "user"]);
+ const [, parentRoute] = useMatches();
+ const transition = useTransition();
+
+ invariant(parentRoute);
+ const userPageData = parentRoute.data as UserPageLoaderData;
+
+ return (
+
+
+
+ );
+}
diff --git a/app/routes/u.$identifier/results/index.tsx b/app/routes/u.$identifier/results/index.tsx
new file mode 100644
index 000000000..954bd9f6b
--- /dev/null
+++ b/app/routes/u.$identifier/results/index.tsx
@@ -0,0 +1,68 @@
+import { useMatches } from "@remix-run/react";
+import { useTranslation } from "react-i18next";
+import invariant from "tiny-invariant";
+import { LinkButton } from "~/components/Button";
+import { Main } from "~/components/Main";
+import { Section } from "~/components/Section";
+import { useUser } from "~/modules/auth";
+import { userResultsEditHighlightsPage } from "~/utils/urls";
+import type { UserPageLoaderData } from "../../u.$identifier";
+import { UserResultsTable } from "./components/UserResultsTable";
+
+export default function UserResultsPage() {
+ const { t } = useTranslation("user");
+ const [, parentRoute] = useMatches();
+ invariant(parentRoute);
+
+ const userPageData = parentRoute.data as UserPageLoaderData;
+ const hasResults = userPageData.results.length > 0;
+
+ const nonHighlights = userPageData.results.filter((r) => !r.isHighlight);
+ const hasNonHighlights = nonHighlights.length > 0;
+
+ const highlights = userPageData.results.filter((r) => r.isHighlight);
+ const hasHighlights = highlights.length > 0;
+
+ const user = useUser();
+ const isOwnResultsPage = user?.id === userPageData.id;
+
+ const showHighlightsSection =
+ hasHighlights || (isOwnResultsPage && hasResults);
+
+ return (
+
+ {showHighlightsSection && (
+
+ {hasHighlights && (
+
+ )}
+ {isOwnResultsPage && (
+
+ {t("results.highlights.choose")}
+
+ )}
+
+ )}
+ {hasNonHighlights && (
+
+ )}
+
+ );
+}
diff --git a/app/styles/common.css b/app/styles/common.css
index 2d1cbf3be..4c4a37b65 100644
--- a/app/styles/common.css
+++ b/app/styles/common.css
@@ -292,6 +292,10 @@ table > tbody > tr > td {
padding-inline: var(--s-1);
}
+td > input[type="checkbox"] {
+ vertical-align: middle;
+}
+
hr {
border-color: var(--theme-transparent);
}
diff --git a/app/styles/u.css b/app/styles/u.css
index 673c38771..b3e504367 100644
--- a/app/styles/u.css
+++ b/app/styles/u.css
@@ -147,10 +147,21 @@
font-weight: var(--bold);
}
-.u__results-section {
+.u__results-table-wrapper {
+ width: 100%;
overflow-x: auto;
}
+.u__results-table-highlights {
+ border: var(--s-2) solid var(--bg-lighter);
+ padding-inline: 0 !important;
+}
+
+.u__results-table-highlights > legend {
+ margin-inline-start: var(--s-2);
+ padding-inline: var(--s-1);
+}
+
.u__results-players {
display: flex;
flex-wrap: wrap;
diff --git a/app/utils/arrays.ts b/app/utils/arrays.ts
index efdccb926..3c29f98a7 100644
--- a/app/utils/arrays.ts
+++ b/app/utils/arrays.ts
@@ -34,3 +34,9 @@ export function joinListToNaturalString(arg: string[]) {
return last ? `${commaJoined} and ${last}` : commaJoined;
}
+
+export function normalizeFormFieldArray(
+ value: undefined | null | string | string[]
+): string[] {
+ return value == null ? [] : typeof value === "string" ? [value] : value;
+}
diff --git a/app/utils/urls.ts b/app/utils/urls.ts
index c9ead88eb..ef4d3c07c 100644
--- a/app/utils/urls.ts
+++ b/app/utils/urls.ts
@@ -54,6 +54,8 @@ export const userBuildsPage = (user: UserLinkArgs) =>
`${userPage(user)}/builds`;
export const userResultsPage = (user: UserLinkArgs) =>
`${userPage(user)}/results`;
+export const userResultsEditHighlightsPage = (user: UserLinkArgs) =>
+ `${userResultsPage(user)}/highlights`;
export const userNewBuildPage = (user: UserLinkArgs) =>
`${userBuildsPage(user)}/new`;
diff --git a/public/locales/de/user.json b/public/locales/de/user.json
index e34fa88cf..e228b04e9 100644
--- a/public/locales/de/user.json
+++ b/public/locales/de/user.json
@@ -10,12 +10,17 @@
"stick": "Stick",
"sens": "Empfindlichkeit",
+ "results.title": "Ergebnisse",
"results.placing": "Platzierung",
"results.team": "Team",
"results.tournament": "Turnier",
"results.participants": "Teilnehmer",
"results.date": "Datum",
"results.mates": "Mitspieler",
+ "results.highlights": "Highlights",
+ "results.nonHighlights": "Weitere Ergebnisse",
+ "results.highlights.choose": "Highlights wählen",
+ "results.highlights.explanation": "Wähle Ergebnisse, die du hervorheben möchtest",
"forms.errors.invalidCustomUrl.numbers": "Benutzerdefinierte URL kann nicht nur aus Zahlen bestehen",
"forms.errors.invalidCustomUrl.strangeCharacter": "Benutzerdefinierte URL kann nicht aus speziellen Zeichen bestehen",
diff --git a/public/locales/en/user.json b/public/locales/en/user.json
index 760f2a9b5..901b3b64a 100644
--- a/public/locales/en/user.json
+++ b/public/locales/en/user.json
@@ -10,12 +10,17 @@
"stick": "Stick",
"sens": "Sens",
+ "results.title": "Results",
"results.placing": "Placing",
"results.team": "Team",
"results.tournament": "Tournament",
"results.participants": "Participants",
"results.date": "Date",
"results.mates": "Mates",
+ "results.highlights": "Highlights",
+ "results.nonHighlights": "Other Results",
+ "results.highlights.choose": "Choose Highlights",
+ "results.highlights.explanation": "Select the results you want to highlight",
"forms.errors.invalidCustomUrl.numbers": "Custom URL can't only contain numbers",
"forms.errors.invalidCustomUrl.strangeCharacter": "Custom URL can't contain special characters",
diff --git a/translation-progress.md b/translation-progress.md
index f51cb0f58..6dc638414 100644
--- a/translation-progress.md
+++ b/translation-progress.md
@@ -78,7 +78,7 @@
### 🟡 user.json
-**7/20**
+**7/25**
Missing
@@ -91,7 +91,12 @@
- motion
- stick
- sens
+- results.title
- results.participants
+- results.highlights
+- results.nonHighlights
+- results.highlights.choose
+- results.highlights.explanation
- forms.errors.invalidCustomUrl.numbers
- forms.errors.invalidCustomUrl.strangeCharacter
- forms.errors.invalidCustomUrl.duplicate
@@ -179,7 +184,7 @@
### 🟢 user.json
-**20/20**
+**25/25**
---
@@ -271,7 +276,7 @@
### 🟡 user.json
-**7/20**
+**7/25**
Missing
@@ -284,7 +289,12 @@
- motion
- stick
- sens
+- results.title
- results.participants
+- results.highlights
+- results.nonHighlights
+- results.highlights.choose
+- results.highlights.explanation
- forms.errors.invalidCustomUrl.numbers
- forms.errors.invalidCustomUrl.strangeCharacter
- forms.errors.invalidCustomUrl.duplicate
@@ -394,7 +404,7 @@
### 🟡 user.json
-**7/20**
+**7/25**
Missing
@@ -407,7 +417,12 @@
- motion
- stick
- sens
+- results.title
- results.participants
+- results.highlights
+- results.nonHighlights
+- results.highlights.choose
+- results.highlights.explanation
- forms.errors.invalidCustomUrl.numbers
- forms.errors.invalidCustomUrl.strangeCharacter
- forms.errors.invalidCustomUrl.duplicate
@@ -468,7 +483,7 @@
### 🔴 user.json
-**0/20**
+**0/25**
---
@@ -650,7 +665,7 @@
### 🟡 user.json
-**7/20**
+**7/25**
Missing
@@ -663,7 +678,12 @@
- motion
- stick
- sens
+- results.title
- results.participants
+- results.highlights
+- results.nonHighlights
+- results.highlights.choose
+- results.highlights.explanation
- forms.errors.invalidCustomUrl.numbers
- forms.errors.invalidCustomUrl.strangeCharacter
- forms.errors.invalidCustomUrl.duplicate
@@ -774,7 +794,7 @@
### 🟡 user.json
-**7/20**
+**7/25**
Missing
@@ -787,7 +807,12 @@
- motion
- stick
- sens
+- results.title
- results.participants
+- results.highlights
+- results.nonHighlights
+- results.highlights.choose
+- results.highlights.explanation
- forms.errors.invalidCustomUrl.numbers
- forms.errors.invalidCustomUrl.strangeCharacter
- forms.errors.invalidCustomUrl.duplicate
@@ -876,12 +901,17 @@
### 🟡 user.json
-**19/20**
+**19/25**
Missing
+- results.title
- results.participants
+- results.highlights
+- results.nonHighlights
+- results.highlights.choose
+- results.highlights.explanation
@@ -988,7 +1018,7 @@
### 🟡 user.json
-**7/20**
+**7/25**
Missing
@@ -1001,7 +1031,12 @@
- motion
- stick
- sens
+- results.title
- results.participants
+- results.highlights
+- results.nonHighlights
+- results.highlights.choose
+- results.highlights.explanation
- forms.errors.invalidCustomUrl.numbers
- forms.errors.invalidCustomUrl.strangeCharacter
- forms.errors.invalidCustomUrl.duplicate
@@ -1112,7 +1147,7 @@
### 🟡 user.json
-**7/20**
+**7/25**
Missing
@@ -1125,7 +1160,12 @@
- motion
- stick
- sens
+- results.title
- results.participants
+- results.highlights
+- results.nonHighlights
+- results.highlights.choose
+- results.highlights.explanation
- forms.errors.invalidCustomUrl.numbers
- forms.errors.invalidCustomUrl.strangeCharacter
- forms.errors.invalidCustomUrl.duplicate