diff --git a/frontend/src/html/pages/account-settings.html b/frontend/src/html/pages/account-settings.html deleted file mode 100644 index f809e0eac9cd..000000000000 --- a/frontend/src/html/pages/account-settings.html +++ /dev/null @@ -1,285 +0,0 @@ - diff --git a/frontend/src/index.html b/frontend/src/index.html index 1cd091b68f42..e462f01e88d2 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -61,7 +61,9 @@ - + + ); +} diff --git a/frontend/src/ts/components/pages/account-settings/AccountTab.tsx b/frontend/src/ts/components/pages/account-settings/AccountTab.tsx new file mode 100644 index 000000000000..1e42b86209f9 --- /dev/null +++ b/frontend/src/ts/components/pages/account-settings/AccountTab.tsx @@ -0,0 +1,180 @@ +import { Show } from "solid-js"; + +import Ape from "../../../ape"; +import * as StreakHourOffsetModal from "../../../modals/streak-hour-offset"; +import { showLoaderBar } from "../../../states/loader-bar"; +import { showErrorNotification } from "../../../states/notifications"; +import { getSnapshot } from "../../../states/snapshot"; +import { Button } from "../../common/Button"; +import { Fa } from "../../common/Fa"; +import { + showOptOutOfLeaderboardsModal, + showResetPersonalBestsModal, +} from "../../modals/account-settings/ReauthConfirmModals"; +import { showUnlinkDiscordModal } from "../../modals/account-settings/UnlinkDiscordModal"; +import { showUpdateNameModal } from "../../modals/account-settings/UpdateNameModal"; +import { Section } from "./utils"; + +export function AccountTab() { + return ( + <> + + + + + + + + ); +} + +function Discord() { + const isLinked = () => getSnapshot()?.discordId !== undefined; + return ( +
+ When you connect your monkeytype account to your Discord account, you + will be automatically assigned a new role every time you achieve a new + personal best in a 60 second test. If you link your accounts before + joining the Discord server, the bot will not give you a role. + + button={ + isLinked() + ? undefined + : { + text: "link", + onClick: () => { + showLoaderBar(); + void Ape.users.getDiscordOAuth().then((response) => { + if (response.status === 200) { + window.open(response.body.data.url, "_self"); + } else { + showErrorNotification( + `Failed to get OAuth from discord: ${response.body.message}`, + ); + } + }); + }, + } + } + > + +
+
+ Your accounts are linked! +
+
+
+
+
+
+ ); +} + +function UpdateAccountName() { + return ( +
+ Change the name of your account.{" "} + You can only do this once every 30 days. + + button={{ + text: "update name", + onClick: () => showUpdateNameModal(), + }} + /> + ); +} + +function UpdateStreakOffset() { + return ( +
+ Streaks reset at midnight UTC by default. If this is not convenient for + you (for example if it means that streaks reset in the middle of the + day), you can change the hour offset here.{" "} + You can only do this once! + + button={{ + text: "update hour offset", + onClick: () => StreakHourOffsetModal.show(), + }} + disabled={getSnapshot()?.streakHourOffset !== undefined} + disabledDescription=<> + You have already set your streak + hour offset to{" "} + {`${(getSnapshot()?.streakHourOffset ?? 0) > 0 ? "+" : ""} ${getSnapshot()?.streakHourOffset}`} + . + + /> + ); +} + +function OptOutLeaderboard() { + return ( +
+ Use this if you frequently trigger the anticheat (for example if using + stenography) to opt out of leaderboards.{" "} + You can't undo this action! + + button={{ + text: "opt out", + onClick: () => showOptOutOfLeaderboardsModal(), + }} + disabled={getSnapshot()?.lbOptOut === true} + disabledDescription=<> + + You have opted out of leaderboards. + + /> + ); +} + +function ResetPersonalBests() { + return ( +
+ Resets all your personal bests (but doesn't delete any tests from + your history). You can't undo this! + + button={{ + text: "reset personal bests", + onClick: () => showResetPersonalBestsModal(), + }} + /> + ); +} diff --git a/frontend/src/ts/components/pages/account-settings/ApeKeysTab.tsx b/frontend/src/ts/components/pages/account-settings/ApeKeysTab.tsx new file mode 100644 index 000000000000..ad981bb7bb8b --- /dev/null +++ b/frontend/src/ts/components/pages/account-settings/ApeKeysTab.tsx @@ -0,0 +1,193 @@ +import { ApeKeyNameSchema } from "@monkeytype/schemas/ape-keys"; +import { createColumnHelper } from "@tanstack/solid-table"; +import { format as dateFormat } from "date-fns"; +import { createMemo, Show } from "solid-js"; +import { z } from "zod"; + +import { + ApeKeyEntry, + insertApeKey, + removeApeKey, + renameApeKey, + updateApeKeyEnabled, + useApeKeyLiveQuery, +} from "../../../collections/ape-keys"; +import { isApeKeysDenied } from "../../../states/account-settings"; +import { showModal } from "../../../states/modals"; +import { showSimpleModal } from "../../../states/simple-modal"; +import { replaceSpacesWithUnderscores } from "../../../utils/strings"; +import AsyncContent from "../../common/AsyncContent"; +import { Button } from "../../common/Button"; +import { Fa } from "../../common/Fa"; +import { DataTable, DataTableColumnDef } from "../../ui/table/DataTable"; +import { Section } from "./utils"; + +export function ApeKeysTab() { + const columns = createMemo(() => getColumns()); + const apeKeyQuery = useApeKeyLiveQuery(); + return ( + <> +
+ Generate Ape Keys to access certain API endpoints ( +
+ ); +} + +function ProviderAuthentication(props: { authMethod: ProviderAuthMethod }) { + return ( +
+ Add or remove {getAuthMethodDisplay(props.authMethod)} authentication. + + button={ + isUsingAuthenticationReactive(props.authMethod) + ? { + text: `remove ${getAuthMethodDisplay(props.authMethod)} authentication`, + disabled: !hasAdditionalAuthMethodsReactive(props.authMethod), + onClick: () => + showRemoveAuthMethodModal({ authMethod: props.authMethod }), + } + : { + text: `add ${getAuthMethodDisplay(props.authMethod)} authentication`, + onClick: () => + void addAuthProvider({ authMethod: props.authMethod }), + } + } + /> + ); +} + +function RevokeAllTokens() { + return ( +
+ Revokes all tokens connected to your account. Do this if you think + someone else has access to your account. +
+ This will log you out of all devices. + + button={{ + text: "revoke all tokens", + class: + "[--themable-button-bg:var(--error-color)] [--themable-button-text:var(--bg-color)]", + onClick: () => showRevokeAllTokensModal(), + }} + /> + ); +} diff --git a/frontend/src/ts/components/pages/account-settings/BlockedUsers.tsx b/frontend/src/ts/components/pages/account-settings/BlockedUsersTab.tsx similarity index 90% rename from frontend/src/ts/components/pages/account-settings/BlockedUsers.tsx rename to frontend/src/ts/components/pages/account-settings/BlockedUsersTab.tsx index 8d56f3eff67d..6e8cdd5cb639 100644 --- a/frontend/src/ts/components/pages/account-settings/BlockedUsers.tsx +++ b/frontend/src/ts/components/pages/account-settings/BlockedUsersTab.tsx @@ -10,19 +10,21 @@ import { import { showSimpleModal } from "../../../states/simple-modal"; import AsyncContent from "../../common/AsyncContent"; import { Button } from "../../common/Button"; -import { H3 } from "../../common/Headers"; import { User } from "../../common/User"; import { DataTable, DataTableColumnDef } from "../../ui/table/DataTable"; +import { Section } from "./utils"; -export function BlockedUsers() { +export function BlockedUsersTab() { const query = useBlockedConnectionsQuery(); const columns = createMemo(getColumns); return ( -
-

-

Blocked users cannot send you friend requests.

- +
Blocked users cannot send you friend requests. + > {({ queryData }) => ( )} -

+
); } diff --git a/frontend/src/ts/components/pages/account-settings/DangerZoneTab.tsx b/frontend/src/ts/components/pages/account-settings/DangerZoneTab.tsx new file mode 100644 index 000000000000..11fff1ec0c1c --- /dev/null +++ b/frontend/src/ts/components/pages/account-settings/DangerZoneTab.tsx @@ -0,0 +1,54 @@ +import { + showDeleteAccountModal, + showResetAccountModal, +} from "../../modals/account-settings/ReauthConfirmModals"; +import { Section } from "./utils"; + +export function DangerZoneTab() { + return ( + <> + + + + ); +} + +function ResetAccount() { + return ( +
+ Completely resets your account to a blank state. +
+ You can't undo this action! + + button={{ + text: "reset account", + class: + "[--themable-button-bg:var(--error-color)] [--themable-button-text:var(--bg-color)]", + onClick: () => showResetAccountModal(), + }} + /> + ); +} + +function DeleteAccount() { + return ( +
+ Deletes your account and all data connected to it. +
+ You can't undo this action! + + button={{ + text: "delete account", + class: + "[--themable-button-bg:var(--error-color)] [--themable-button-text:var(--bg-color)]", + onClick: () => showDeleteAccountModal(), + }} + /> + ); +} diff --git a/frontend/src/ts/components/pages/account-settings/utils.tsx b/frontend/src/ts/components/pages/account-settings/utils.tsx new file mode 100644 index 000000000000..6bdcd77a253c --- /dev/null +++ b/frontend/src/ts/components/pages/account-settings/utils.tsx @@ -0,0 +1,30 @@ +import { splitProps } from "solid-js"; + +import { cn } from "../../../utils/cn"; +import { Button, ButtonProps } from "../../common/Button"; +import { Setting, SettingProps } from "../../common/Setting"; + +export function Section( + props: Omit< + SettingProps, + "breakpoints" | "inputs" | "key" | "showDeepLink" | "fullWidthInputs" + > & { + button?: ButtonProps; + fullWidth?: boolean; + }, +) { + const [local, settingsProps] = splitProps(props, ["button", "fullWidth"]); + return ( + + ) : undefined + } + /> + ); +} diff --git a/frontend/src/ts/components/pages/connections/PendingRequests.tsx b/frontend/src/ts/components/pages/connections/PendingRequests.tsx index 24f3fcf713f7..9b1ae72e0c34 100644 --- a/frontend/src/ts/components/pages/connections/PendingRequests.tsx +++ b/frontend/src/ts/components/pages/connections/PendingRequests.tsx @@ -31,7 +31,7 @@ export function PendingRequests() { )} diff --git a/frontend/src/ts/components/pages/settings/Setting.tsx b/frontend/src/ts/components/pages/settings/Setting.tsx deleted file mode 100644 index 4de64430c3d5..000000000000 --- a/frontend/src/ts/components/pages/settings/Setting.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { JSXElement, Show } from "solid-js"; -import { z } from "zod"; -import { serialize } from "zod-urlsearchparams"; - -import { - showErrorNotification, - showSuccessNotification, -} from "../../../states/notifications"; -import { cn } from "../../../utils/cn"; -import { Button } from "../../common/Button"; -import { FaProps } from "../../common/Fa"; -import { H3 } from "../../common/Headers"; - -type Props = { - key: string; - title: string; - fa: FaProps; - description: string | JSXElement; - inputs?: JSXElement; - fullWidthInputs?: JSXElement; -}; - -export function Setting(props: Props): JSXElement { - return ( -
-
-

-

- -
- -
{props.description}
-
-
{props.inputs}
-
-
- {props.fullWidthInputs} -
- ); -} diff --git a/frontend/src/ts/components/pages/settings/SettingsPage.tsx b/frontend/src/ts/components/pages/settings/SettingsPage.tsx index 16da1cc15277..df344727ae74 100644 --- a/frontend/src/ts/components/pages/settings/SettingsPage.tsx +++ b/frontend/src/ts/components/pages/settings/SettingsPage.tsx @@ -25,6 +25,7 @@ import { Anime, AnimeShow } from "../../common/anime"; import { Button } from "../../common/Button"; import { Fa } from "../../common/Fa"; import { Page } from "../../common/Page"; +import { Setting } from "../../common/Setting"; import { CommandlineHotkey } from "../../hotkeys/CommandlineHotkey"; import { InputField } from "../../ui/form/InputField"; import { fromSchema } from "../../ui/form/utils"; @@ -51,7 +52,6 @@ import { SoundVolume } from "./custom-setting/SoundVolume"; import { Tags } from "./custom-setting/Tags"; import { Theme } from "./custom-setting/Theme"; import { QuickNav } from "./QuickNav"; -import { Setting } from "./Setting"; export function SettingsPage(): JSXElement { const [hasLocalBg] = createResource( diff --git a/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx b/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx index c7ad5b19eba3..0f17bc74865f 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx @@ -5,9 +5,9 @@ import { fpsLimitSchema, getfpsLimit, setfpsLimit } from "../../../../anim"; import { useSavedIndicator } from "../../../../hooks/useSavedIndicator"; import { Button } from "../../../common/Button"; import { Separator } from "../../../common/Separator"; +import { Setting } from "../../../common/Setting"; import { InputField } from "../../../ui/form/InputField"; import { fromSchema } from "../../../ui/form/utils"; -import { Setting } from "../Setting"; export function AnimationFpsLimit(): JSXElement { const savedIndicator = useSavedIndicator(); diff --git a/frontend/src/ts/components/pages/settings/custom-setting/AutoSwitchTheme.tsx b/frontend/src/ts/components/pages/settings/custom-setting/AutoSwitchTheme.tsx index cd46bf20c74b..9e53a2e09d5f 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/AutoSwitchTheme.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/AutoSwitchTheme.tsx @@ -7,8 +7,8 @@ import { getConfig } from "../../../../config/store"; import { ThemesList } from "../../../../constants/themes"; import { cn } from "../../../../utils/cn"; import { Button } from "../../../common/Button"; +import { Setting } from "../../../common/Setting"; import SlimSelect from "../../../ui/SlimSelect"; -import { Setting } from "../Setting"; export function AutoSwitchTheme(): JSXElement { return ( diff --git a/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx b/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx index 9f2b526696ea..0e79cc766360 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx @@ -16,9 +16,9 @@ import { getOptions } from "../../../../utils/zod"; import { Button } from "../../../common/Button"; import { Fa } from "../../../common/Fa"; import { Separator } from "../../../common/Separator"; +import { Setting } from "../../../common/Setting"; import { InputField } from "../../../ui/form/InputField"; import { fromSchema } from "../../../ui/form/utils"; -import { Setting } from "../Setting"; export function CustomBackground(): JSXElement { const savedIndicator = useSavedIndicator(); diff --git a/frontend/src/ts/components/pages/settings/custom-setting/CustomBackgroundFilters.tsx b/frontend/src/ts/components/pages/settings/custom-setting/CustomBackgroundFilters.tsx index b4d43e4598c4..5a01de0fabaa 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/CustomBackgroundFilters.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/CustomBackgroundFilters.tsx @@ -4,8 +4,8 @@ import { configMetadata } from "../../../../config/metadata"; import { setConfig } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; import { applyCustomBackgroundFilters } from "../../../../controllers/theme-controller"; +import { Setting } from "../../../common/Setting"; import { Slider } from "../../../common/Slider"; -import { Setting } from "../Setting"; export function CustomBackgroundFilters(): JSXElement { let refBlur: HTMLInputElement | undefined = undefined; diff --git a/frontend/src/ts/components/pages/settings/custom-setting/CustomLayoutfluid.tsx b/frontend/src/ts/components/pages/settings/custom-setting/CustomLayoutfluid.tsx index 51afb4bceb9e..ff8315d15530 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/CustomLayoutfluid.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/CustomLayoutfluid.tsx @@ -6,8 +6,8 @@ import { setConfig } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; import { LayoutsList } from "../../../../constants/layouts"; import { areUnsortedArraysEqual } from "../../../../utils/arrays"; +import { Setting } from "../../../common/Setting"; import SlimSelect from "../../../ui/SlimSelect"; -import { Setting } from "../Setting"; export function CustomLayoutfluid(): JSXElement { return ( diff --git a/frontend/src/ts/components/pages/settings/custom-setting/CustomPolyglot.tsx b/frontend/src/ts/components/pages/settings/custom-setting/CustomPolyglot.tsx index 5de756d72e51..3b65c893504d 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/CustomPolyglot.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/CustomPolyglot.tsx @@ -11,8 +11,8 @@ import { } from "../../../../constants/languages"; import { areUnsortedArraysEqual } from "../../../../utils/arrays"; import { getLanguageDisplayString } from "../../../../utils/strings"; +import { Setting } from "../../../common/Setting"; import SlimSelect from "../../../ui/SlimSelect"; -import { Setting } from "../Setting"; export function CustomPolyglot(): JSXElement { return ( diff --git a/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx b/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx index 19ace4b8a7c4..f3d789623ee2 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx @@ -13,7 +13,7 @@ import { normalizeName } from "../../../../utils/strings"; import { getOptions } from "../../../../utils/zod"; import { Button } from "../../../common/Button"; import { Separator } from "../../../common/Separator"; -import { Setting } from "../Setting"; +import { Setting } from "../../../common/Setting"; export function FontFamily(): JSXElement { const [hasLocalFont, { refetch }] = createResource(async () => diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx index 340654f92456..a13ec89540f9 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx @@ -7,7 +7,7 @@ import { toggleFunbox } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; import { getActiveFunboxNames } from "../../../../test/funbox/list"; import { Button } from "../../../common/Button"; -import { Setting } from "../Setting"; +import { Setting } from "../../../common/Setting"; export function Funbox(): JSXElement { return ( diff --git a/frontend/src/ts/components/pages/settings/custom-setting/ImportExport.tsx b/frontend/src/ts/components/pages/settings/custom-setting/ImportExport.tsx index 14d5d0ea8cc7..83581b4ac7e6 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/ImportExport.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/ImportExport.tsx @@ -9,7 +9,7 @@ import { } from "../../../../states/notifications"; import { showSimpleModal } from "../../../../states/simple-modal"; import { Button } from "../../../common/Button"; -import { Setting } from "../Setting"; +import { Setting } from "../../../common/Setting"; export function ImportExport(): JSXElement { return ( diff --git a/frontend/src/ts/components/pages/settings/custom-setting/KeymapLayout.tsx b/frontend/src/ts/components/pages/settings/custom-setting/KeymapLayout.tsx index bc62e730bf21..fb7bb99b2366 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/KeymapLayout.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/KeymapLayout.tsx @@ -5,8 +5,8 @@ import { configMetadata } from "../../../../config/metadata"; import { setConfig } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; import { LayoutsList } from "../../../../constants/layouts"; +import { Setting } from "../../../common/Setting"; import SlimSelect from "../../../ui/SlimSelect"; -import { Setting } from "../Setting"; export function KeymapLayout(): JSXElement { return ( diff --git a/frontend/src/ts/components/pages/settings/custom-setting/KeymapSize.tsx b/frontend/src/ts/components/pages/settings/custom-setting/KeymapSize.tsx index b1620895b688..3da2f86f1145 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/KeymapSize.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/KeymapSize.tsx @@ -3,8 +3,8 @@ import { JSXElement } from "solid-js"; import { configMetadata } from "../../../../config/metadata"; import { setConfig } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; +import { Setting } from "../../../common/Setting"; import { Slider } from "../../../common/Slider"; -import { Setting } from "../Setting"; export function KeymapSize(): JSXElement { return ( diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Language.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Language.tsx index f3acf14a7cf9..05da846d38b8 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Language.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Language.tsx @@ -10,8 +10,8 @@ import { LanguageGroups, } from "../../../../constants/languages"; import { getLanguageDisplayString } from "../../../../utils/strings"; +import { Setting } from "../../../common/Setting"; import SlimSelect from "../../../ui/SlimSelect"; -import { Setting } from "../Setting"; export function Language(): JSXElement { return ( diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Layout.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Layout.tsx index 1f6271834d80..03b715be812c 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Layout.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Layout.tsx @@ -5,8 +5,8 @@ import { configMetadata } from "../../../../config/metadata"; import { setConfig } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; import { LayoutsList } from "../../../../constants/layouts"; +import { Setting } from "../../../common/Setting"; import SlimSelect from "../../../ui/SlimSelect"; -import { Setting } from "../Setting"; export function Layout(): JSXElement { return ( diff --git a/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx b/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx index 822523b1cb95..59e9d59cdd04 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx @@ -6,10 +6,10 @@ import { configMetadata } from "../../../../config/metadata"; import { setConfig } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; import { useSavedIndicator } from "../../../../hooks/useSavedIndicator"; +import { Setting } from "../../../common/Setting"; // import { showSuccessNotification } from "../../../../states/notifications"; import { InputField } from "../../../ui/form/InputField"; import { fromSchema } from "../../../ui/form/utils"; -import { Setting } from "../Setting"; export function MaxLineWidth(): JSXElement { const { component: SavedIndicator, flash } = useSavedIndicator(); diff --git a/frontend/src/ts/components/pages/settings/custom-setting/MinAcc.tsx b/frontend/src/ts/components/pages/settings/custom-setting/MinAcc.tsx index 2abd3ced0477..7db767a927a7 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/MinAcc.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/MinAcc.tsx @@ -8,9 +8,9 @@ import { getConfig } from "../../../../config/store"; import { useSavedIndicator } from "../../../../hooks/useSavedIndicator"; // import { showSuccessNotification } from "../../../../states/notifications"; import { Button } from "../../../common/Button"; +import { Setting } from "../../../common/Setting"; import { InputField } from "../../../ui/form/InputField"; import { fromSchema } from "../../../ui/form/utils"; -import { Setting } from "../Setting"; export function MinAcc(): JSXElement { const savedIndicator = useSavedIndicator(); diff --git a/frontend/src/ts/components/pages/settings/custom-setting/MinBurst.tsx b/frontend/src/ts/components/pages/settings/custom-setting/MinBurst.tsx index adec7f6979d5..8600b2a825ca 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/MinBurst.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/MinBurst.tsx @@ -8,9 +8,9 @@ import { getConfig } from "../../../../config/store"; import { useSavedIndicator } from "../../../../hooks/useSavedIndicator"; // import { showSuccessNotification } from "../../../../states/notifications"; import { Button } from "../../../common/Button"; +import { Setting } from "../../../common/Setting"; import { InputField } from "../../../ui/form/InputField"; import { fromSchema } from "../../../ui/form/utils"; -import { Setting } from "../Setting"; export function MinBurst(): JSXElement { const savedIndicator = useSavedIndicator(); diff --git a/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx b/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx index b9ae408602a4..34a21200f0af 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx @@ -8,9 +8,9 @@ import { getConfig } from "../../../../config/store"; import { useSavedIndicator } from "../../../../hooks/useSavedIndicator"; // import { showSuccessNotification } from "../../../../states/notifications"; import { Button } from "../../../common/Button"; +import { Setting } from "../../../common/Setting"; import { InputField } from "../../../ui/form/InputField"; import { fromSchema } from "../../../ui/form/utils"; -import { Setting } from "../Setting"; export function MinSpeed(): JSXElement { const savedIndicator = useSavedIndicator(); diff --git a/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx b/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx index 89226f20319e..ea019e169907 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx @@ -11,9 +11,9 @@ import { getConfig } from "../../../../config/store"; import { useSavedIndicator } from "../../../../hooks/useSavedIndicator"; import { getOptions } from "../../../../utils/zod"; import { Button } from "../../../common/Button"; +import { Setting } from "../../../common/Setting"; import { InputField } from "../../../ui/form/InputField"; import { fromSchema } from "../../../ui/form/utils"; -import { Setting } from "../Setting"; export function PaceCaret(): JSXElement { const savedIndicator = useSavedIndicator(); diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Presets.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Presets.tsx index 1bcfc893806f..334cbe43029a 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Presets.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Presets.tsx @@ -9,7 +9,7 @@ import { showEditPresetModal } from "../../../../states/edit-preset-modal"; import { showModal } from "../../../../states/modals"; import { showSimpleModal } from "../../../../states/simple-modal"; import { Button } from "../../../common/Button"; -import { Setting } from "../Setting"; +import { Setting } from "../../../common/Setting"; export function Presets(): JSXElement { const presets = usePresetsLiveQuery(); diff --git a/frontend/src/ts/components/pages/settings/custom-setting/SoundVolume.tsx b/frontend/src/ts/components/pages/settings/custom-setting/SoundVolume.tsx index 4f5dc93cadc0..043cce5fa24e 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/SoundVolume.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/SoundVolume.tsx @@ -7,8 +7,8 @@ import { playClick, previewClick, } from "../../../../controllers/sound-controller"; +import { Setting } from "../../../common/Setting"; import { Slider } from "../../../common/Slider"; -import { Setting } from "../Setting"; export function SoundVolume(): JSXElement { return ( diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Tags.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Tags.tsx index dfc3301f225f..969e7f2655fa 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Tags.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Tags.tsx @@ -13,8 +13,8 @@ import { import { showSimpleModal } from "../../../../states/simple-modal"; import { normalizeName } from "../../../../utils/strings"; import { Button } from "../../../common/Button"; +import { Setting } from "../../../common/Setting"; import { showAddTagModal } from "../../../modals/AddTagModal"; -import { Setting } from "../Setting"; export function Tags(): JSXElement { const tags = useTagsLiveQuery(); diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx index b7ad5a73c554..b72ba8bbda8c 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx @@ -41,7 +41,7 @@ import { AnimeMatch } from "../../../common/anime/AnimeMatch"; import { Button } from "../../../common/Button"; import { Fa } from "../../../common/Fa"; import { Separator } from "../../../common/Separator"; -import { Setting } from "../Setting"; +import { Setting } from "../../../common/Setting"; export const sortedThemes: ThemeWithName[] = [...ThemesList].sort((a, b) => { const b1 = hexToHSL(a.bg); diff --git a/frontend/src/ts/components/ui/table/DataTable.tsx b/frontend/src/ts/components/ui/table/DataTable.tsx index 06aa1d07a757..afff4a24521f 100644 --- a/frontend/src/ts/components/ui/table/DataTable.tsx +++ b/frontend/src/ts/components/ui/table/DataTable.tsx @@ -70,7 +70,7 @@ export type DataTableProps = { }; // oxlint-disable-next-line typescript/no-explicit-any -export function DataTable( +export function DataTable( props: DataTableProps, ): JSXElement { const [sorting, setSorting] = useLocalStorage({ @@ -103,92 +103,100 @@ export function DataTable( } }); - const table = createSolidTable({ - get data() { - return props.data; - }, - get columns() { - return props.columns; - }, - getCoreRowModel: getCoreRowModel(), - onSortingChange: (it) => { - setSorting(it); - props.onSortingChange?.(sorting()); - }, + // Ensure reactivity: always produce a fresh array reference + const data = createMemo(() => props.data.map((it) => ({ ...it }))); - //oxlint-disable-next-line solid/reactivity - ...(props.onSortingChange - ? { manualSorting: true } - : { getSortedRowModel: getSortedRowModel() }), - enableRowSelection: () => props.rowSelection !== undefined, - getRowId: (row, index) => - props.rowSelection !== undefined - ? props.rowSelection.getRowId(row) - : index.toString(), - onRowSelectionChange: setRowSelection, - - state: { - get sorting() { - return sorting(); + // Recreate the table instance whenever data or columns change + const table = createMemo(() => + createSolidTable({ + data: data(), + get columns() { + return props.columns; }, - get rowSelection() { - return rowSelection(); + getCoreRowModel: getCoreRowModel(), + onSortingChange: (it) => { + setSorting(it); + props.onSortingChange?.(sorting()); }, - }, - }); + + //oxlint-disable-next-line solid/reactivity + ...(props.onSortingChange + ? { manualSorting: true } + : { getSortedRowModel: getSortedRowModel() }), + enableRowSelection: () => props.rowSelection !== undefined, + getRowId: (row, index) => + props.rowSelection !== undefined + ? props.rowSelection.getRowId(row) + : typeof (row as Record)["_id"] === "string" + ? ((row as Record)["_id"] as string) + : index.toString(), + onRowSelectionChange: setRowSelection, + + state: { + get sorting() { + return sorting(); + }, + get rowSelection() { + return rowSelection(); + }, + }, + }), + ); //create column visibility classes once, make them accessible via the column.id const columnVisibility = createMemo(() => { return Object.fromEntries( - table.getAllColumns().map((it) => { - const breakpoint = - it.columnDef.meta?.breakpoint === "xxl" - ? "2xl" - : it.columnDef.meta?.breakpoint; - const maxBreakpoint = - it.columnDef.meta?.maxBreakpoint === "xxl" - ? "2xl" - : it.columnDef.meta?.maxBreakpoint; + table() + .getAllColumns() + .map((it) => { + const breakpoint = + it.columnDef.meta?.breakpoint === "xxl" + ? "2xl" + : it.columnDef.meta?.breakpoint; + const maxBreakpoint = + it.columnDef.meta?.maxBreakpoint === "xxl" + ? "2xl" + : it.columnDef.meta?.maxBreakpoint; - // 🚨 Tailwind does not generate CSS for dynamically constructed class names. - const classes = { - hidden: false, - "xxs:table-cell": false, - "xs:table-cell": false, - "sm:table-cell": false, - "md:table-cell": false, - "lg:table-cell": false, - "xl:table-cell": false, - "2xl:table-cell": false, - "xxs:hidden": false, - "xs:hidden": false, - "sm:hidden": false, - "md:hidden": false, - "lg:hidden": false, - "xl:hidden": false, - "2xl:hidden": false, - }; - if (breakpoint !== undefined) { - classes.hidden = true; - classes[`${breakpoint}:table-cell`] = true; - } - if (maxBreakpoint !== undefined) { - classes[`${maxBreakpoint}:hidden`] = true; - } - return [it.id, classes]; - }), + // 🚨 Tailwind does not generate CSS for dynamically constructed class names. + const classes = { + hidden: false, + "xxs:table-cell": false, + "xs:table-cell": false, + "sm:table-cell": false, + "md:table-cell": false, + "lg:table-cell": false, + "xl:table-cell": false, + "2xl:table-cell": false, + "xxs:hidden": false, + "xs:hidden": false, + "sm:hidden": false, + "md:hidden": false, + "lg:hidden": false, + "xl:hidden": false, + "2xl:hidden": false, + }; + if (breakpoint !== undefined) { + classes.hidden = true; + classes[`${breakpoint}:table-cell`] = true; + } + if (maxBreakpoint !== undefined) { + classes[`${maxBreakpoint}:hidden`] = true; + } + return [it.id, classes]; + }), ); }); return ( - + {(headerGroup) => ( @@ -302,7 +310,7 @@ export function DataTable( - + {(row) => ( ( {props.noDataRow !== undefined && diff --git a/frontend/src/ts/controllers/page-controller.ts b/frontend/src/ts/controllers/page-controller.ts index c61925a533e8..5550c12cbe8b 100644 --- a/frontend/src/ts/controllers/page-controller.ts +++ b/frontend/src/ts/controllers/page-controller.ts @@ -7,7 +7,6 @@ import { } from "../states/core"; import * as PageTest from "../pages/test"; import * as PageLoading from "../pages/loading"; -import * as PageAccountSettings from "../pages/account-settings"; import * as PageTransition from "../legacy-states/page-transition"; import * as AdController from "../controllers/ad-controller"; import * as Focus from "../test/focus"; @@ -23,7 +22,7 @@ import { onDOMReady, qsa, qsr } from "../utils/dom"; import * as Skeleton from "../utils/skeleton"; import { LeaderboardUrlParamsSchema, - readGetParameters, + readLeaderboardGetParameters, } from "../states/leaderboard-selection"; import { configurationPromise as serverConfigurationPromise } from "../ape/server-configuration"; import { getSnapshot } from "../db"; @@ -34,6 +33,10 @@ import { isConnectionsReady, waitForConnectionsReady, } from "../collections/connections"; +import { + AccountSettingsUrlParamsSchema, + readAccountSettingsGetParameters, +} from "../states/account-settings"; type ChangeOptions = { force?: boolean; @@ -131,7 +134,12 @@ const pages = { ], }, }), - accountSettings: PageAccountSettings.page, + accountSettings: solidPage("accountSettings", { + urlParamsSchema: AccountSettingsUrlParamsSchema, + beforeShow: async (options) => { + readAccountSettingsGetParameters(options.urlParams); + }, + }), leaderboards: solidPage("leaderboards", { urlParamsSchema: LeaderboardUrlParamsSchema, loadingOptions: { @@ -142,7 +150,7 @@ const pages = { }, }, beforeShow: async (options) => { - readGetParameters(options.urlParams); + readLeaderboardGetParameters(options.urlParams); }, }), }; diff --git a/frontend/src/ts/pages/account-settings.ts b/frontend/src/ts/pages/account-settings.ts deleted file mode 100644 index 6452541bc1f4..000000000000 --- a/frontend/src/ts/pages/account-settings.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { PageWithUrlParams } from "./page"; -import * as Skeleton from "../utils/skeleton"; -import { getAuthenticatedUser } from "../firebase"; -import { getActivePage, isAuthenticated } from "../states/core"; -import { swapElements } from "../utils/misc"; -import { getSnapshot } from "../db"; -import Ape from "../ape"; -import * as StreakHourOffsetModal from "../modals/streak-hour-offset"; -import { showLoaderBar } from "../states/loader-bar"; -import * as ApeKeyTable from "../elements/account-settings/ape-key-table"; -import { showErrorNotification } from "../states/notifications"; -import { z } from "zod"; -import { authEvent } from "../events/auth"; -import { qs, qsa, qsr, onDOMReady } from "../utils/dom"; -import { addAuthProvider } from "../auth"; -import { showUpdateEmailModal } from "../components/modals/account-settings/UpdateEmailModal"; -import { showUpdateNameModal } from "../components/modals/account-settings/UpdateNameModal"; -import { showUpdatePasswordModal } from "../components/modals/account-settings/UpdatePasswordModal"; -import { showRemoveAuthMethodModal } from "../components/modals/account-settings/RemoveAuthMethodModal"; -import { showAddPasswordAuthModal } from "../components/modals/account-settings/AddPasswordAuthModal"; -import { - showDeleteAccountModal, - showOptOutOfLeaderboardsModal, - showResetAccountModal, - showResetPersonalBestsModal, - showRevokeAllTokensModal, -} from "../components/modals/account-settings/ReauthConfirmModals"; -import { showUnlinkDiscordModal } from "../components/modals/account-settings/UnlinkDiscordModal"; - -const pageElement = qsr(".page.pageAccountSettings"); - -const StateSchema = z.object({ - tab: z.enum([ - "authentication", - "account", - "apeKeys", - "dangerZone", - "blockedUsers", - ]), -}); -type State = z.infer; - -const UrlParameterSchema = StateSchema.partial(); - -const state: State = { - tab: "account", -}; - -function updateAuthenticationSections(): void { - pageElement.qsa(".section.passwordAuthSettings button")?.hide(); - pageElement.qsa(".section.googleAuthSettings button")?.hide(); - pageElement.qsa(".section.githubAuthSettings button")?.hide(); - - const user = getAuthenticatedUser(); - if (user === null) return; - - const passwordProvider = user.providerData.some( - (provider) => provider.providerId === "password", - ); - const googleProvider = user.providerData.some( - (provider) => provider.providerId === "google.com", - ); - const githubProvider = user.providerData.some( - (provider) => provider.providerId === "github.com", - ); - - if (passwordProvider) { - pageElement.qs(".section.passwordAuthSettings #emailPasswordAuth")?.show(); - pageElement.qs(".section.passwordAuthSettings #passPasswordAuth")?.show(); - if (googleProvider || githubProvider) { - pageElement - .qs(".section.passwordAuthSettings #removePasswordAuth") - ?.show(); - } - } else { - pageElement.qs(".section.passwordAuthSettings #addPasswordAuth")?.show(); - } - - if (googleProvider) { - pageElement.qs(".section.googleAuthSettings #removeGoogleAuth")?.show(); - if (passwordProvider || githubProvider) { - pageElement.qs(".section.googleAuthSettings #removeGoogleAuth")?.enable(); - } else { - pageElement - .qs(".section.googleAuthSettings #removeGoogleAuth") - ?.disable(); - } - } else { - pageElement.qs(".section.googleAuthSettings #addGoogleAuth")?.show(); - } - if (githubProvider) { - pageElement.qs(".section.githubAuthSettings #removeGithubAuth")?.show(); - if (passwordProvider || googleProvider) { - pageElement.qs(".section.githubAuthSettings #removeGithubAuth")?.enable(); - } else { - pageElement - .qs(".section.githubAuthSettings #removeGithubAuth") - ?.disable(); - } - } else { - pageElement.qs(".section.githubAuthSettings #addGithubAuth")?.show(); - } -} - -function updateIntegrationSections(): void { - //no code and no discord - if (!isAuthenticated()) { - pageElement.qs(".section.discordIntegration")?.hide(); - } else { - if (!getSnapshot()) return; - pageElement.qs(".section.discordIntegration")?.show(); - - if (getSnapshot()?.discordId === undefined) { - //show button - pageElement.qs(".section.discordIntegration .buttons")?.show(); - pageElement.qs(".section.discordIntegration .info")?.hide(); - } else { - pageElement.qs(".section.discordIntegration .buttons")?.hide(); - pageElement.qs(".section.discordIntegration .info")?.show(); - } - } -} - -function updateTabs(): void { - void swapElements( - pageElement.qs(".tab.active"), - pageElement.qs(`.tab[data-tab="${state.tab}"]`), - 250, - async () => { - // - }, - async () => { - pageElement.qsa(".tab")?.removeClass("active"); - pageElement.qs(`.tab[data-tab="${state.tab}"]`)?.addClass("active"); - if (state.tab === "apeKeys") void ApeKeyTable.update(updateUI); - }, - ); - pageElement.qsa("button")?.removeClass("active"); - pageElement.qs(`button[data-tab="${state.tab}"]`)?.addClass("active"); -} - -function updateAccountSections(): void { - pageElement.qs(".section.optOutOfLeaderboards .optedOut")?.hide(); - pageElement.qs(".section.optOutOfLeaderboards .buttons")?.show(); - pageElement.qs(".section.setStreakHourOffset .info")?.hide(); - pageElement.qs(".section.setStreakHourOffset .buttons")?.show(); - - const snapshot = getSnapshot(); - if (snapshot?.lbOptOut === true) { - pageElement.qs(".section.optOutOfLeaderboards .optedOut")?.show(); - pageElement.qs(".section.optOutOfLeaderboards .buttons")?.hide(); - } - if (snapshot?.streakHourOffset !== undefined) { - pageElement.qs(".section.setStreakHourOffset .info")?.show(); - const sign = snapshot?.streakHourOffset > 0 ? "+" : ""; - pageElement - .qs(".section.setStreakHourOffset .info span") - ?.setText(sign + snapshot?.streakHourOffset); - pageElement.qs(".section.setStreakHourOffset .buttons")?.hide(); - } -} - -export function updateUI(): void { - if (getActivePage() !== "accountSettings") return; - updateAuthenticationSections(); - updateIntegrationSections(); - updateAccountSections(); - updateTabs(); - page.setUrlParams(state); -} - -qs(".page.pageAccountSettings")?.onChild("click", ".tabs button", (event) => { - state.tab = (event.target as HTMLElement).getAttribute( - "data-tab", - ) as State["tab"]; - updateTabs(); - page.setUrlParams(state); -}); - -qsa( - ".page.pageAccountSettings .section.discordIntegration .getLinkAndGoToOauth", -)?.on("click", () => { - showLoaderBar(); - void Ape.users.getDiscordOAuth().then((response) => { - if (response.status === 200) { - window.open(response.body.data.url, "_self"); - } else { - showErrorNotification( - `Failed to get OAuth from discord: ${response.body.message}`, - ); - } - }); -}); - -qs(".page.pageAccountSettings #setStreakHourOffset")?.on("click", () => { - StreakHourOffsetModal.show(); -}); - -qs(".pageAccountSettings")?.onChild("click", "#unlinkDiscordButton", () => { - showUnlinkDiscordModal({ callback: updateUI }); -}); - -qs(".pageAccountSettings")?.onChild("click", "#removeGoogleAuth", () => { - showRemoveAuthMethodModal({ authMethod: "google", callback: updateUI }); -}); - -qs(".pageAccountSettings")?.onChild("click", "#removeGithubAuth", () => { - showRemoveAuthMethodModal({ authMethod: "github", callback: updateUI }); -}); - -qs(".pageAccountSettings")?.onChild("click", "#removePasswordAuth", () => { - showRemoveAuthMethodModal({ authMethod: "password", callback: updateUI }); -}); - -qs(".pageAccountSettings")?.onChild("click", "#addPasswordAuth", () => { - showAddPasswordAuthModal({ callback: updateUI }); -}); - -qs(".pageAccountSettings")?.onChild("click", "#updateAccountName", () => { - showUpdateNameModal(); -}); - -qs(".pageAccountSettings")?.onChild("click", "#emailPasswordAuth", () => { - showUpdateEmailModal(); -}); - -qs(".pageAccountSettings")?.onChild("click", "#passPasswordAuth", () => { - showUpdatePasswordModal(); -}); - -qs(".pageAccountSettings")?.onChild("click", "#deleteAccount", () => { - showDeleteAccountModal(); -}); - -qs(".pageAccountSettings")?.onChild("click", "#resetAccount", () => { - showResetAccountModal(); -}); - -qs(".pageAccountSettings")?.onChild( - "click", - "#optOutOfLeaderboardsButton", - () => { - showOptOutOfLeaderboardsModal(); - }, -); - -qs(".pageAccountSettings")?.onChild("click", "#revokeAllTokens", () => { - showRevokeAllTokensModal(); -}); - -qs(".pageAccountSettings")?.onChild( - "click", - "#resetPersonalBestsButton", - () => { - showResetPersonalBestsModal(); - }, -); - -qs(".pageAccountSettings")?.onChild("click", "#addGoogleAuth", () => { - void addAuthProvider("google"); -}); - -qs(".pageAccountSettings")?.onChild("click", "#addGithubAuth", () => { - void addAuthProvider("github"); -}); - -authEvent.subscribe((event) => { - if (event.type === "authConfigUpdated") { - updateUI(); - } -}); - -export const page = new PageWithUrlParams({ - id: "accountSettings", - display: "Account Settings", - element: pageElement, - path: "/account-settings", - urlParamsSchema: UrlParameterSchema, - afterHide: async (): Promise => { - Skeleton.remove("pageAccountSettings"); - }, - beforeShow: async (options): Promise => { - if (options.urlParams?.tab !== undefined) { - state.tab = options.urlParams.tab; - } - Skeleton.append("pageAccountSettings", "main"); - pageElement.qs(`.tab[data-tab="${state.tab}"]`)?.addClass("active"); - updateUI(); - }, -}); - -onDOMReady(() => { - Skeleton.save("pageAccountSettings"); -}); diff --git a/frontend/src/ts/states/account-settings.ts b/frontend/src/ts/states/account-settings.ts index ad24dbe7eac0..c6e396685c64 100644 --- a/frontend/src/ts/states/account-settings.ts +++ b/frontend/src/ts/states/account-settings.ts @@ -1,5 +1,73 @@ import { createSignal } from "solid-js"; +import { z } from "zod"; +import { createEffectOn } from "../hooks/effects"; +import { FaSolidIcon } from "../types/font-awesome"; +import { getActivePage, isAuthenticated } from "./core"; +import { serialize as serializeUrlSearchParams } from "zod-urlsearchparams"; export const [getLastGeneratedApeKey, setLastGeneratedApeKey] = createSignal< string | undefined >(undefined); + +export const AccountSettingsTabSchema = z.enum([ + "account", + "authentication", + "blockedUsers", + "apeKeys", + "dangerZone", +]); +export type AccountSettingsTab = z.infer; + +export const accountSettingsTabs: Record< + AccountSettingsTab, + { icon: FaSolidIcon; text: string } +> = { + account: { text: "account", icon: "fa-user" }, + authentication: { text: "authentication", icon: "fa-key" }, + blockedUsers: { text: "blocked users", icon: "fa-user-shield" }, + apeKeys: { text: "ape keys", icon: "fa-code" }, + dangerZone: { text: "danger zone", icon: "fa-exclamation-triangle" }, +}; + +export const AccountSettingsUrlParamsSchema = z + .object({ + tab: AccountSettingsTabSchema, + }) + .partial(); +export type AccountSettingsUrlParams = z.infer< + typeof AccountSettingsUrlParamsSchema +>; + +export const [getCurrentTab, setCurrentTab] = + createSignal("account"); + +export const [isApeKeysDenied, setApeKeysDenied] = createSignal< + boolean | undefined +>(undefined); + +createEffectOn(isAuthenticated, (hasUser) => { + if (!hasUser) { + setApeKeysDenied(undefined); + } +}); + +export function readAccountSettingsGetParameters( + params: AccountSettingsUrlParams | undefined, +): void { + if (params === undefined || params.tab === undefined) return; + + setCurrentTab(params.tab); +} + +createEffectOn(getCurrentTab, (tab) => { + //make sure we only replace the url if we are on the accountSettings page. If this is missing the url-handler will not work correctly + if (getActivePage() !== "accountSettings") return; + const data: AccountSettingsUrlParams = { tab }; + + const urlParams = serializeUrlSearchParams({ + schema: AccountSettingsUrlParamsSchema, + data, + }); + const newUrl = `${window.location.pathname}?${urlParams.toString()}`; + window.history.replaceState({}, "", newUrl); +}); diff --git a/frontend/src/ts/states/leaderboard-selection.ts b/frontend/src/ts/states/leaderboard-selection.ts index d4771c26dfe5..81dcf8755bc5 100644 --- a/frontend/src/ts/states/leaderboard-selection.ts +++ b/frontend/src/ts/states/leaderboard-selection.ts @@ -67,7 +67,7 @@ export const getSelection = (): Selection => { export { setSelection }; -export function readGetParameters( +export function readLeaderboardGetParameters( params: LeaderboardUrlParams | undefined, ): void { if (params === undefined || params.type === undefined) return;