mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-08 12:16:12 -05:00
Merge pull request #1020 from Sendouc/highlight-results
User result highlights
This commit is contained in:
@@ -12,7 +12,16 @@ select
|
||||
"CalendarEventDate"
|
||||
where
|
||||
"eventId" = "CalendarEvent"."id"
|
||||
) as "startTime"
|
||||
) as "startTime",
|
||||
exists (
|
||||
select
|
||||
1
|
||||
from
|
||||
"UserResultHighlight"
|
||||
where
|
||||
"userId" = @userId and
|
||||
"teamId" = "CalendarEventResultTeam"."id"
|
||||
) as "isHighlight"
|
||||
from
|
||||
"CalendarEventResultPlayer"
|
||||
join "CalendarEventResultTeam" on "CalendarEventResultTeam"."id" = "CalendarEventResultPlayer"."teamId"
|
||||
|
||||
@@ -244,9 +244,11 @@ export function findResultsByUserId(userId: User["id"]) {
|
||||
placement: CalendarEventResultTeam["placement"];
|
||||
participantCount: CalendarEvent["participantCount"];
|
||||
startTime: CalendarEventDate["startTime"];
|
||||
isHighlight: number;
|
||||
}>
|
||||
).map((row) => ({
|
||||
...row,
|
||||
isHighlight: Boolean(row.isHighlight),
|
||||
mates: (
|
||||
findMatesByResultTeamIdStm.all({
|
||||
teamId: row.teamId,
|
||||
|
||||
10
app/db/models/users/addResultHighlight.sql
Normal file
10
app/db/models/users/addResultHighlight.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
insert into
|
||||
"UserResultHighlight" (
|
||||
"userId",
|
||||
"teamId"
|
||||
)
|
||||
values
|
||||
(
|
||||
@userId,
|
||||
@teamId
|
||||
)
|
||||
4
app/db/models/users/deleteAllResultHighlights.sql
Normal file
4
app/db/models/users/deleteAllResultHighlights.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
delete from
|
||||
"UserResultHighlight"
|
||||
where
|
||||
"userId" = @userId
|
||||
@@ -1,5 +1,9 @@
|
||||
import { sql } from "../../sql";
|
||||
import type { User, UserWithPlusTier } from "../../types";
|
||||
import type {
|
||||
CalendarEventResultTeam,
|
||||
User,
|
||||
UserWithPlusTier,
|
||||
} from "../../types";
|
||||
|
||||
import upsertSql from "./upsert.sql";
|
||||
import updateProfileSql from "./updateProfile.sql";
|
||||
@@ -12,6 +16,8 @@ import updateDiscordIdSql from "./updateDiscordId.sql";
|
||||
import findByIdentifierSql from "./findByIdentifier.sql";
|
||||
import findAllPlusMembersSql from "./findAllPlusMembers.sql";
|
||||
import findAllPatronsSql from "./findAllPatrons.sql";
|
||||
import addResultHighlightSql from "./addResultHighlight.sql";
|
||||
import deleteAllResultHighlightsSql from "./deleteAllResultHighlights.sql";
|
||||
|
||||
const upsertStm = sql.prepare(upsertSql);
|
||||
export function upsert(
|
||||
@@ -123,3 +129,18 @@ export type FindAllPatrons = Array<
|
||||
export function findAllPatrons() {
|
||||
return findAllPatronsStm.all() as FindAllPatrons;
|
||||
}
|
||||
|
||||
const deleteAllResultHighlightsStm = sql.prepare(deleteAllResultHighlightsSql);
|
||||
const addResultHighlightStm = sql.prepare(addResultHighlightSql);
|
||||
export type UpdateResultHighlightsArgs = {
|
||||
userId: User["id"];
|
||||
resultTeamIds: Array<CalendarEventResultTeam["id"]>;
|
||||
};
|
||||
export const updateResultHighlights = sql.transaction(
|
||||
({ userId, resultTeamIds }: UpdateResultHighlightsArgs) => {
|
||||
deleteAllResultHighlightsStm.run({ userId });
|
||||
for (const teamId of resultTeamIds) {
|
||||
addResultHighlightStm.run({ userId, teamId });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<main className="main layout__main">
|
||||
<Section className="u__results-section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("results.placing")}</th>
|
||||
<th>{t("results.team")}</th>
|
||||
<th>{t("results.tournament")}</th>
|
||||
<th>{t("results.participants")}</th>
|
||||
<th>{t("results.date")}</th>
|
||||
<th>{t("results.mates")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.results.map((result) => (
|
||||
<tr key={result.eventId}>
|
||||
<td className="pl-4">
|
||||
<Placement placement={result.placement} />
|
||||
</td>
|
||||
<td>{result.teamName}</td>
|
||||
<td>
|
||||
<Link to={calendarEventPage(result.eventId)}>
|
||||
{result.eventName}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{result.participantCount}</td>
|
||||
<td>
|
||||
{databaseTimestampToDate(result.startTime).toLocaleDateString(
|
||||
i18n.language,
|
||||
{
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
}
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<ul className="u__results-players">
|
||||
{result.mates.map((player) => (
|
||||
<li
|
||||
key={typeof player === "string" ? player : player.id}
|
||||
className="flex items-center"
|
||||
>
|
||||
{typeof player === "string" ? (
|
||||
player
|
||||
) : (
|
||||
<Link
|
||||
to={userPage(player)}
|
||||
className="stack horizontal xs items-center"
|
||||
>
|
||||
<Avatar user={player} size="xxs" />{" "}
|
||||
{discordFullName(player)}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
111
app/routes/u.$identifier/results/components/UserResultsTable.tsx
Normal file
111
app/routes/u.$identifier/results/components/UserResultsTable.tsx
Normal file
@@ -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 (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{hasHighlightCheckboxes && <th />}
|
||||
<th id={placementHeaderId}>{t("results.placing")}</th>
|
||||
<th>{t("results.team")}</th>
|
||||
<th>{t("results.tournament")}</th>
|
||||
<th>{t("results.participants")}</th>
|
||||
<th>{t("results.date")}</th>
|
||||
<th>{t("results.mates")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.map((result) => {
|
||||
// We are trying to construct a reasonable label for the checkbox
|
||||
// which shouldn't contain the whole information of the table row as
|
||||
// that can be also accessed when needed.
|
||||
// e.g. "20xx Placing 2nd", "Big House 10 Placing 20th"
|
||||
const placementCellId = `${id}-${result.teamId}-placement`;
|
||||
const nameCellId = `${id}-${result.teamId}-name`;
|
||||
const checkboxLabelIds = `${nameCellId} ${placementHeaderId} ${placementCellId}`;
|
||||
|
||||
return (
|
||||
<tr key={result.teamId}>
|
||||
{hasHighlightCheckboxes && (
|
||||
<td>
|
||||
<input
|
||||
value={result.teamId}
|
||||
aria-labelledby={checkboxLabelIds}
|
||||
name={HIGHLIGHT_CHECKBOX_NAME}
|
||||
type="checkbox"
|
||||
defaultChecked={result.isHighlight}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
<td className="pl-4" id={placementCellId}>
|
||||
<Placement placement={result.placement} />
|
||||
</td>
|
||||
<td>{result.teamName}</td>
|
||||
<td id={nameCellId}>
|
||||
<Link to={calendarEventPage(result.eventId)}>
|
||||
{result.eventName}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{result.participantCount}</td>
|
||||
<td>
|
||||
{databaseTimestampToDate(result.startTime).toLocaleDateString(
|
||||
i18n.language,
|
||||
{
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
}
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<ul className="u__results-players">
|
||||
{result.mates.map((player) => (
|
||||
<li
|
||||
key={typeof player === "string" ? player : player.id}
|
||||
className="flex items-center"
|
||||
>
|
||||
{typeof player === "string" ? (
|
||||
player
|
||||
) : (
|
||||
<Link
|
||||
to={userPage(player)}
|
||||
className="stack horizontal xs items-center"
|
||||
>
|
||||
<Avatar user={player} size="xxs" />
|
||||
{discordFullName(player)}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
79
app/routes/u.$identifier/results/highlights.tsx
Normal file
79
app/routes/u.$identifier/results/highlights.tsx
Normal file
@@ -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 (
|
||||
<Main>
|
||||
<Form method="post" className="stack md items-start">
|
||||
<h2 className="text-start">{t("user:results.highlights.choose")}</h2>
|
||||
<div className="u__results-table-wrapper">
|
||||
<fieldset className="u__results-table-highlights">
|
||||
<legend>{t("user:results.highlights.explanation")}</legend>
|
||||
<UserResultsTable
|
||||
id="user-results-highlight-selection"
|
||||
results={userPageData.results}
|
||||
hasHighlightCheckboxes
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
<Button
|
||||
loadingText={t("common:actions.saving")}
|
||||
type="submit"
|
||||
loading={transition.state === "submitting"}
|
||||
data-cy="submit-button"
|
||||
>
|
||||
{t("common:actions.save")}
|
||||
</Button>
|
||||
<FormErrors namespace="user" />
|
||||
</Form>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
68
app/routes/u.$identifier/results/index.tsx
Normal file
68
app/routes/u.$identifier/results/index.tsx
Normal file
@@ -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 (
|
||||
<Main className="stack lg">
|
||||
{showHighlightsSection && (
|
||||
<Section
|
||||
title={t("results.highlights")}
|
||||
className="u__results-table-wrapper u__results-table-highlights stack md items-center"
|
||||
>
|
||||
{hasHighlights && (
|
||||
<UserResultsTable
|
||||
id="user-results-highlight-table"
|
||||
results={highlights}
|
||||
/>
|
||||
)}
|
||||
{isOwnResultsPage && (
|
||||
<LinkButton
|
||||
variant="outlined"
|
||||
tiny
|
||||
to={userResultsEditHighlightsPage(userPageData)}
|
||||
>
|
||||
{t("results.highlights.choose")}
|
||||
</LinkButton>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
{hasNonHighlights && (
|
||||
<Section
|
||||
title={
|
||||
hasHighlights ? t("results.nonHighlights") : t("results.title")
|
||||
}
|
||||
className="u__results-table-wrapper"
|
||||
>
|
||||
<UserResultsTable id="user-results-table" results={nonHighlights} />
|
||||
</Section>
|
||||
)}
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
|
||||
|
||||
25
migrations/011-user-result-highlights.js
Normal file
25
migrations/011-user-result-highlights.js
Normal file
@@ -0,0 +1,25 @@
|
||||
module.exports.up = function (db) {
|
||||
db.prepare(
|
||||
`
|
||||
create table "UserResultHighlight" (
|
||||
"teamId" integer not null,
|
||||
"userId" integer not null,
|
||||
foreign key ("teamId") references "CalendarEventResultTeam"("id") on delete cascade,
|
||||
foreign key ("userId") references "User"("id") on delete cascade,
|
||||
unique("teamId", "userId") on conflict rollback
|
||||
) strict
|
||||
`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`create index user_result_highlight_user_id on "UserResultHighlight"("userId")`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`create index user_result_highlight_team_id on "UserResultHighlight"("teamId")`
|
||||
).run();
|
||||
};
|
||||
|
||||
module.exports.down = function (db) {
|
||||
db.prepare(`drop table "UserResultHighlight"`).run();
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
### 🟡 user.json
|
||||
|
||||
**7/20**
|
||||
**7/25**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -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**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -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**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -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**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -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**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -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**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- results.title
|
||||
- results.participants
|
||||
- results.highlights
|
||||
- results.nonHighlights
|
||||
- results.highlights.choose
|
||||
- results.highlights.explanation
|
||||
|
||||
</details>
|
||||
|
||||
@@ -988,7 +1018,7 @@
|
||||
|
||||
### 🟡 user.json
|
||||
|
||||
**7/20**
|
||||
**7/25**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -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**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user