diff --git a/src/renderer/src/components/ui/SettingsSection.tsx b/src/renderer/src/components/ui/SettingsSection.tsx new file mode 100644 index 0000000..c15301f --- /dev/null +++ b/src/renderer/src/components/ui/SettingsSection.tsx @@ -0,0 +1,51 @@ +import type { ReactNode } from "react"; +import { Check, X } from "lucide-react"; + +export function SettingsSection({ + title, + description, + icon, + danger, + children, +}: { + title: string; + description?: ReactNode; + icon?: ReactNode; + danger?: boolean; + children: ReactNode; +}) { + return ( +
+
+

+ {icon ? {icon} : null} + {title} +

+ {description ?

{description}

: null} +
+ {children} +
+ ); +} + +export function Notice({ error, ok }: { error?: string | null; ok?: string | null }) { + if (error) + return ( +

+ {error} +

+ ); + if (ok) + return ( +

+ {ok} +

+ ); + return null; +} diff --git a/src/renderer/src/components/ui/index.ts b/src/renderer/src/components/ui/index.ts index f6ba166..6ac6bea 100644 --- a/src/renderer/src/components/ui/index.ts +++ b/src/renderer/src/components/ui/index.ts @@ -15,6 +15,7 @@ export { CollapsibleCard } from "./CollapsibleCard"; export { Modal } from "./Modal"; export { Toggle } from "./Toggle"; export { Section, Fact } from "./Section"; +export { SettingsSection, Notice } from "./SettingsSection"; export { StatTile } from "./StatTile"; export { SkeletonGrid } from "./SkeletonGrid"; export { CardGrid } from "./CardGrid"; @@ -26,4 +27,4 @@ export { Card } from "./Card"; export { HoverImage } from "./HoverImage"; export { ContextMenu } from "./ContextMenu"; export type { ContextMenuEntry, ContextMenuItem } from "./ContextMenu"; -export { LABEL_HEADING } from "./styles"; +export { LABEL_HEADING, PAGE_TITLE } from "./styles"; diff --git a/src/renderer/src/components/ui/styles.ts b/src/renderer/src/components/ui/styles.ts index 90dfbdb..381b814 100644 --- a/src/renderer/src/components/ui/styles.ts +++ b/src/renderer/src/components/ui/styles.ts @@ -1 +1,2 @@ export const LABEL_HEADING = "text-[11px] font-semibold uppercase tracking-wide text-faint"; +export const PAGE_TITLE = "text-[26px] font-bold tracking-[-0.4px]"; diff --git a/src/renderer/src/features/account/AccountSettingsView.tsx b/src/renderer/src/features/account/AccountSettingsView.tsx index 23f08a7..2259b4c 100644 --- a/src/renderer/src/features/account/AccountSettingsView.tsx +++ b/src/renderer/src/features/account/AccountSettingsView.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { Trans } from "react-i18next"; import { useI18n } from "../../lib/i18n"; -import { Banner, Loader, Tabs } from "../../components/ui"; +import { Banner, Loader, PAGE_TITLE, Tabs } from "../../components/ui"; import { useAccountSettings } from "./useAccountSettings"; import { DisplayNameSection } from "./sections/DisplayNameSection"; import { EmailSection } from "./sections/EmailSection"; @@ -32,11 +32,11 @@ export function AccountSettingsView() { ); - const s = state.settings; + const s = state.data; return (
-

{t("account:title")}

+

{t("account:title")}

["t"]; @@ -14,39 +17,6 @@ export type SectionProps = { export const WEBSITE_ACCOUNT = "https://vrchat.com/home/profile"; -export function Section({ - title, - description, - icon, - danger, - children, -}: { - title: string; - description?: ReactNode; - icon?: ReactNode; - danger?: boolean; - children: ReactNode; -}) { - return ( -

-
-

- {icon ? {icon} : null} - {title} -

- {description ?

{description}

: null} -
- {children} -
- ); -} - export function useAction() { const { t } = useI18n(); const [busy, setBusy] = useState(false); @@ -80,22 +50,6 @@ export function useAction() { }; } -export function Notice({ error, ok }: { error?: string | null; ok?: string | null }) { - if (error) - return ( -

- {error} -

- ); - if (ok) - return ( -

- {ok} -

- ); - return null; -} - export function ToggleRow({ label, hint, diff --git a/src/renderer/src/features/account/useAccountSettings.ts b/src/renderer/src/features/account/useAccountSettings.ts index 0afa9f4..9fba324 100644 --- a/src/renderer/src/features/account/useAccountSettings.ts +++ b/src/renderer/src/features/account/useAccountSettings.ts @@ -1,43 +1,36 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import type { AccountSettings } from "../../../../shared/types/settings"; -import { api, errorMessage } from "../../lib/api"; +import { api } from "../../lib/api"; +import { useAsync, type Async } from "../../lib/useAsync"; import { useAuth } from "../auth/AuthContext"; -type State = - | { status: "loading" } - | { status: "error"; message: string } - | { status: "ready"; settings: AccountSettings }; - export function useAccountSettings(): { - state: State; + state: Async; reload: () => void; set: (s: AccountSettings) => void; } { const { status } = useAuth(); const activeId = status.state === "authenticated" ? status.user.id : null; - const [state, setState] = useState({ status: "loading" }); + + const [nonce, setNonce] = useState(0); + const [override, setOverride] = useState(null); + + const fetched = useAsync( + () => api.settings.get(), + [activeId, nonce], + "Failed to load settings.", + ); + + const state: Async = override + ? { status: "ready", data: override } + : fetched; const reload = useCallback(() => { - setState({ status: "loading" }); - api.settings - .get() - .then((settings) => setState({ status: "ready", settings })) - .catch((err) => - setState({ - status: "error", - message: errorMessage(err, "Failed to load settings."), - }), - ); + setOverride(null); + setNonce((n) => n + 1); }, []); - useEffect(() => { - reload(); - }, [reload, activeId]); - - const set = useCallback( - (settings: AccountSettings) => setState({ status: "ready", settings }), - [], - ); + const set = useCallback((settings: AccountSettings) => setOverride(settings), []); return { state, reload, set }; } diff --git a/src/renderer/src/features/enhancements/EnhancementsView.tsx b/src/renderer/src/features/enhancements/EnhancementsView.tsx index e61c114..8b8bfe8 100644 --- a/src/renderer/src/features/enhancements/EnhancementsView.tsx +++ b/src/renderer/src/features/enhancements/EnhancementsView.tsx @@ -8,7 +8,7 @@ import type { } from "../../../../shared/types/enhancements"; import { api, errorMessage } from "../../lib/api"; import { useI18n } from "../../lib/i18n"; -import { Badge, Banner, Loader, Toggle } from "../../components/ui"; +import { Badge, Banner, Loader, PAGE_TITLE, Toggle } from "../../components/ui"; const SHELL = "mx-auto flex w-full max-w-[760px] flex-col gap-[18px] px-12 pb-16 pt-10"; @@ -62,7 +62,7 @@ export function EnhancementsView() { return (
-

{t("enhancements:title")}

+

{t("enhancements:title")}

{t("enhancements:subtitle")}

diff --git a/src/renderer/src/features/profile/MyWorldsView.tsx b/src/renderer/src/features/profile/MyWorldsView.tsx index db72a9a..055f58d 100644 --- a/src/renderer/src/features/profile/MyWorldsView.tsx +++ b/src/renderer/src/features/profile/MyWorldsView.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Banner, Loader, Tabs } from "../../components/ui"; +import { Banner, Loader, PAGE_TITLE, Tabs } from "../../components/ui"; import { useT } from "../../lib/i18n"; import { useProfile } from "./useProfile"; import { WorldsSection, FavoriteWorldsSection, WorldSearch } from "./WorldsSection"; @@ -26,7 +26,7 @@ export function MyWorldsView() { return (
-

{t("nav:worlds")}

+

{t("nav:worlds")}

-

{t("search:title")}

+

{t("search:title")}

-

{t("settings:title")}

+

{t("settings:title")}

{t("settings:subtitle")}

@@ -84,31 +91,6 @@ export function SettingsView() { ); } -function Section({ - title, - description, - icon, - children, -}: { - title: string; - description?: ReactNode; - icon?: ReactNode; - children: ReactNode; -}) { - return ( -
-
-

- {icon ? {icon} : null} - {title} -

- {description ?

{description}

: null} -
- {children} -
- ); -} - const SCHEME_OPTIONS: { mode: SchemeMode; labelKey: string; hintKey: string; icon: ReactNode }[] = [ { mode: "auto", @@ -378,12 +360,6 @@ function GameSection() { ); } -function Notice({ error, ok }: { error?: string | null; ok?: string | null }) { - if (error) return

{error}

; - if (ok) return

{ok}

; - return null; -} - const REGION_OPTIONS: PreferredRegion[] = ["auto", "us", "use", "eu", "jp"]; function RegionSection() {