mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
User preferences - ability sorting disabling
This commit is contained in:
parent
986355050d
commit
e43e8eefb9
|
|
@ -52,6 +52,7 @@ interface BuildProps {
|
|||
| "private"
|
||||
> & {
|
||||
abilities: BuildAbilitiesTuple;
|
||||
unsortedAbilities: BuildAbilitiesTuple;
|
||||
modes: ModeShort[] | null;
|
||||
weapons: Array<{
|
||||
weaponSplId: BuildWeapon["weaponSplId"];
|
||||
|
|
@ -61,9 +62,15 @@ interface BuildProps {
|
|||
};
|
||||
owner?: Pick<UserWithPlusTier, "discordId" | "username" | "plusTier">;
|
||||
canEdit?: boolean;
|
||||
withAbilitySorting?: boolean;
|
||||
}
|
||||
|
||||
export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
|
||||
export function BuildCard({
|
||||
build,
|
||||
owner,
|
||||
canEdit = false,
|
||||
withAbilitySorting = true,
|
||||
}: BuildProps) {
|
||||
const user = useUser();
|
||||
const { t } = useTranslation(["weapons", "builds", "common", "game-misc"]);
|
||||
const { i18n } = useTranslation();
|
||||
|
|
@ -77,11 +84,14 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
|
|||
headGearSplId,
|
||||
shoesGearSplId,
|
||||
updatedAt,
|
||||
abilities,
|
||||
modes,
|
||||
weapons,
|
||||
} = build;
|
||||
|
||||
const abilities = withAbilitySorting
|
||||
? build.abilities
|
||||
: build.unsortedAbilities;
|
||||
|
||||
const isNoGear = [headGearSplId, clothesGearSplId, shoesGearSplId].some(
|
||||
(id) => id === -1,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -754,6 +754,10 @@ export const BUILD_SORT_IDENTIFIERS = [
|
|||
|
||||
export type BuildSort = (typeof BUILD_SORT_IDENTIFIERS)[number];
|
||||
|
||||
export interface UserPreferences {
|
||||
disableBuildAbilitySorting?: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
/** 1 = permabanned, timestamp = ban active till then */
|
||||
banned: Generated<number | null>;
|
||||
|
|
@ -798,6 +802,7 @@ export interface User {
|
|||
plusSkippedForSeasonNth: number | null;
|
||||
noScreen: Generated<number>;
|
||||
buildSorting: ColumnType<BuildSort[] | null, string | null, string | null>;
|
||||
preferences: ColumnType<UserPreferences | null, string | null, string | null>;
|
||||
}
|
||||
|
||||
export interface UserResultHighlight {
|
||||
|
|
|
|||
|
|
@ -175,15 +175,18 @@ function augmentBuild<T>({
|
|||
const weapons = (
|
||||
JSON.parse(rawWeapons) as Array<BuildWeaponWithTop500Info>
|
||||
).sort((a, b) => a.weaponSplId - b.weaponSplId);
|
||||
const abilities = JSON.parse(rawAbilities) as Array<
|
||||
Pick<BuildAbility, "ability" | "gearType" | "slotIndex">
|
||||
>;
|
||||
const abilities = dbAbilitiesToArrayOfArrays(
|
||||
JSON.parse(rawAbilities) as Array<
|
||||
Pick<BuildAbility, "ability" | "gearType" | "slotIndex">
|
||||
>,
|
||||
);
|
||||
|
||||
return {
|
||||
...row,
|
||||
modes,
|
||||
weapons,
|
||||
abilities: sortAbilities(dbAbilitiesToArrayOfArrays(abilities)),
|
||||
abilities: sortAbilities(abilities),
|
||||
unsortedAbilities: abilities,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
BUILDS_PAGE_MAX_BUILDS,
|
||||
PATCHES,
|
||||
} from "~/constants";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { safeJSONParse } from "~/utils/json";
|
||||
import { isRevalidation, metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
|
|
@ -163,6 +164,8 @@ const BuildCards = React.memo(function BuildCards({
|
|||
}: {
|
||||
data: SerializeFrom<typeof loader>;
|
||||
}) {
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<div className="builds-container">
|
||||
{data.builds.map((build) => {
|
||||
|
|
@ -172,6 +175,7 @@ const BuildCards = React.memo(function BuildCards({
|
|||
build={build}
|
||||
owner={build}
|
||||
canEdit={false}
|
||||
withAbilitySorting={!user?.preferences.disableBuildAbilitySorting}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
31
app/features/settings/actions/settings.server.ts
Normal file
31
app/features/settings/actions/settings.server.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { ActionFunctionArgs } from "@remix-run/node";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { parseRequestPayload } from "~/utils/remix.server";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { settingsEditSchema } from "../settings-schemas";
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
const data = await parseRequestPayload({
|
||||
request,
|
||||
schema: settingsEditSchema,
|
||||
});
|
||||
|
||||
switch (data._action) {
|
||||
case "UPDATE_DISABLE_BUILD_ABILITY_SORTING": {
|
||||
await UserRepository.updatePreferences(user.id, {
|
||||
disableBuildAbilitySorting: data.newValue,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "PLACEHOLDER": {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -1,13 +1,18 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { useNavigate, useSearchParams } from "@remix-run/react";
|
||||
import { useFetcher, useNavigate, useSearchParams } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormMessage } from "~/components/FormMessage";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { Theme, useTheme } from "~/features/theme/core/provider";
|
||||
import { languages } from "~/modules/i18n/config";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { SETTINGS_PAGE, navIconUrl } from "~/utils/urls";
|
||||
import { action } from "../actions/settings.server";
|
||||
export { action };
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
breadcrumb: () => ({
|
||||
|
|
@ -18,6 +23,7 @@ export const handle: SendouRouteHandle = {
|
|||
};
|
||||
|
||||
export default function SettingsPage() {
|
||||
const user = useUser();
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
return (
|
||||
|
|
@ -26,6 +32,20 @@ export default function SettingsPage() {
|
|||
<h2 className="text-lg">{t("common:pages.settings")}</h2>
|
||||
<LanguageSelector />
|
||||
<ThemeSelector />
|
||||
<div className="mt-6 stack md">
|
||||
<PreferenceSelectorSwitch
|
||||
_action="UPDATE_DISABLE_BUILD_ABILITY_SORTING"
|
||||
defaultSelected={
|
||||
user?.preferences.disableBuildAbilitySorting ?? false
|
||||
}
|
||||
label={t(
|
||||
"common:settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.label",
|
||||
)}
|
||||
bottomText={t(
|
||||
"common:settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Main>
|
||||
);
|
||||
|
|
@ -103,3 +123,38 @@ function ThemeSelector() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreferenceSelectorSwitch({
|
||||
_action,
|
||||
label,
|
||||
bottomText,
|
||||
defaultSelected,
|
||||
}: {
|
||||
_action: string;
|
||||
label: string;
|
||||
bottomText: string;
|
||||
defaultSelected: boolean;
|
||||
}) {
|
||||
const fetcher = useFetcher();
|
||||
|
||||
const onChange = (isSelected: boolean) => {
|
||||
fetcher.submit(
|
||||
{ _action, newValue: isSelected },
|
||||
{ method: "post", encType: "application/json" },
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SendouSwitch
|
||||
defaultSelected={defaultSelected}
|
||||
onChange={onChange}
|
||||
isDisabled={fetcher.state !== "idle"}
|
||||
data-testid={`${_action}-switch`}
|
||||
>
|
||||
{label}
|
||||
</SendouSwitch>
|
||||
<FormMessage type="info">{bottomText}</FormMessage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
12
app/features/settings/settings-schemas.ts
Normal file
12
app/features/settings/settings-schemas.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { z } from "zod";
|
||||
import { _action } from "~/utils/zod";
|
||||
|
||||
export const settingsEditSchema = z.union([
|
||||
z.object({
|
||||
_action: _action("UPDATE_DISABLE_BUILD_ABILITY_SORTING"),
|
||||
newValue: z.boolean(),
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("PLACEHOLDER"),
|
||||
}),
|
||||
]);
|
||||
|
|
@ -2,7 +2,12 @@ import type { ExpressionBuilder, FunctionModule, NotNull } from "kysely";
|
|||
import { sql } from "kysely";
|
||||
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
|
||||
import { db, sql as dbDirect } from "~/db/sql";
|
||||
import type { BuildSort, DB, TablesInsertable } from "~/db/tables";
|
||||
import type {
|
||||
BuildSort,
|
||||
DB,
|
||||
TablesInsertable,
|
||||
UserPreferences,
|
||||
} from "~/db/tables";
|
||||
import type { User } from "~/db/types";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
|
|
@ -291,6 +296,7 @@ export function findLeanById(id: number) {
|
|||
"User.favoriteBadgeId",
|
||||
"User.languages",
|
||||
"User.inGameName",
|
||||
"User.preferences",
|
||||
"PlusTier.tier as plusTier",
|
||||
eb
|
||||
.selectFrom("UserFriendCode")
|
||||
|
|
@ -659,6 +665,35 @@ export function updateProfile(args: UpdateProfileArgs) {
|
|||
});
|
||||
}
|
||||
|
||||
export function updatePreferences(
|
||||
userId: number,
|
||||
newPreferences: UserPreferences,
|
||||
) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const current =
|
||||
(
|
||||
await trx
|
||||
.selectFrom("User")
|
||||
.select("User.preferences")
|
||||
.where("id", "=", userId)
|
||||
.executeTakeFirstOrThrow()
|
||||
).preferences ?? {};
|
||||
|
||||
const mergedPreferences = {
|
||||
...current,
|
||||
...newPreferences,
|
||||
};
|
||||
|
||||
await trx
|
||||
.updateTable("User")
|
||||
.set({
|
||||
preferences: JSON.stringify(mergedPreferences),
|
||||
})
|
||||
.where("id", "=", userId)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
type UpdateResultHighlightsArgs = {
|
||||
userId: number;
|
||||
resultTeamIds: Array<number>;
|
||||
|
|
|
|||
|
|
@ -20,26 +20,19 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
|||
showPrivate: loggedInUser?.id === user.id,
|
||||
});
|
||||
|
||||
const skippedViaSearchParams =
|
||||
new URL(request.url).searchParams.get("exact") === "true";
|
||||
const skipAbilitySorting =
|
||||
loggedInUser?.id === user.id || skippedViaSearchParams;
|
||||
const buildsWithAbilitiesSorted = skipAbilitySorting
|
||||
? builds
|
||||
: builds.map((build) => ({
|
||||
...build,
|
||||
abilities: sortAbilities(build.abilities),
|
||||
}));
|
||||
|
||||
if (buildsWithAbilitiesSorted.length === 0 && loggedInUser?.id !== user.id) {
|
||||
if (builds.length === 0 && loggedInUser?.id !== user.id) {
|
||||
throw new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
const sortedBuilds = sortBuilds({
|
||||
builds: buildsWithAbilitiesSorted,
|
||||
builds,
|
||||
buildSorting: user.buildSorting,
|
||||
weaponPool: user.weapons,
|
||||
});
|
||||
}).map((build) => ({
|
||||
...build,
|
||||
abilities: sortAbilities(build.abilities),
|
||||
unsortedAbilities: build.abilities,
|
||||
}));
|
||||
|
||||
return privatelyCachedJson({
|
||||
buildSorting: user.buildSorting,
|
||||
|
|
|
|||
|
|
@ -98,7 +98,14 @@ export default function UserBuildsPage() {
|
|||
{builds.length > 0 ? (
|
||||
<div className="builds-container">
|
||||
{builds.map((build) => (
|
||||
<BuildCard key={build.id} build={build} canEdit={isOwnPage} />
|
||||
<BuildCard
|
||||
key={build.id}
|
||||
build={build}
|
||||
canEdit={isOwnPage}
|
||||
withAbilitySorting={
|
||||
!isOwnPage && !user?.preferences.disableBuildAbilitySorting
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
isTournamentOrganizer: user.isTournamentOrganizer,
|
||||
inGameName: user.inGameName,
|
||||
friendCode: user.friendCode,
|
||||
preferences: user.preferences ?? {},
|
||||
languages: user.languages ? user.languages.split(",") : [],
|
||||
}
|
||||
: undefined,
|
||||
|
|
|
|||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
41
e2e/settings.spec.ts
Normal file
41
e2e/settings.spec.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import test, { expect } from "@playwright/test";
|
||||
import { impersonate, navigate, seed } from "~/utils/playwright";
|
||||
import { SETTINGS_PAGE } from "~/utils/urls";
|
||||
|
||||
test.describe("Settings", () => {
|
||||
test("updates 'disableBuildAbilitySorting'", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page);
|
||||
|
||||
await navigate({
|
||||
page,
|
||||
url: "/builds/luna-blaster",
|
||||
});
|
||||
|
||||
const oldContents = await page
|
||||
.getByTestId("build-card")
|
||||
.first()
|
||||
.innerHTML();
|
||||
|
||||
await navigate({
|
||||
page,
|
||||
url: SETTINGS_PAGE,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByTestId("UPDATE_DISABLE_BUILD_ABILITY_SORTING-switch")
|
||||
.click();
|
||||
|
||||
await navigate({
|
||||
page,
|
||||
url: "/builds/luna-blaster",
|
||||
});
|
||||
|
||||
const newContents = await page
|
||||
.getByTestId("build-card")
|
||||
.first()
|
||||
.innerHTML();
|
||||
|
||||
expect(newContents).not.toBe(oldContents);
|
||||
});
|
||||
});
|
||||
|
|
@ -242,5 +242,8 @@
|
|||
"chat.systemMsg.userLeft": "{{name}} left the group",
|
||||
"chat.newMessages": "New messages",
|
||||
|
||||
"fc.title": "Friend code"
|
||||
"fc.title": "Friend code",
|
||||
|
||||
"settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.label": "Builds: Disable automatic ability sorting",
|
||||
"settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "Outside of your profile page, build abilities are sorted so that same abilities are next to each other. This setting allows you to see the abilities in the order they were authored everywhere."
|
||||
}
|
||||
|
|
|
|||
5
migrations/081-user-preferences.js
Normal file
5
migrations/081-user-preferences.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export function up(db) {
|
||||
db.transaction(() => {
|
||||
db.prepare(/* sql */ `alter table "User" add "preferences" text`).run();
|
||||
})();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user