make settings page fully functional

This commit is contained in:
mrjvs 2023-11-24 21:54:44 +01:00
parent b38e5768e3
commit a9abe14810
12 changed files with 359 additions and 121 deletions

View File

@ -12,6 +12,10 @@ export interface SessionResponse {
userAgent: string; userAgent: string;
} }
export interface SessionUpdate {
deviceName: string;
}
export async function getSessions(url: string, account: AccountWithToken) { export async function getSessions(url: string, account: AccountWithToken) {
return ofetch<SessionResponse[]>(`/users/${account.userId}/sessions`, { return ofetch<SessionResponse[]>(`/users/${account.userId}/sessions`, {
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
@ -19,6 +23,19 @@ export async function getSessions(url: string, account: AccountWithToken) {
}); });
} }
export async function updateSession(
url: string,
account: AccountWithToken,
update: SessionUpdate
) {
return ofetch<SessionResponse[]>(`/sessions/${account.sessionId}`, {
method: "PATCH",
headers: getAuthHeaders(account.token),
body: update,
baseURL: url,
});
}
export async function removeSession( export async function removeSession(
url: string, url: string,
token: string, token: string,

View File

@ -5,7 +5,7 @@ import { AccountWithToken } from "@/stores/auth";
export interface SettingsInput { export interface SettingsInput {
applicationLanguage?: string; applicationLanguage?: string;
applicationTheme?: string; applicationTheme?: string | null;
defaultSubtitleLanguage?: string; defaultSubtitleLanguage?: string;
} }

View File

@ -18,6 +18,14 @@ export interface UserResponse {
}; };
} }
export interface UserEdit {
profile?: {
colorA: string;
colorB: string;
icon: string;
};
}
export interface BookmarkResponse { export interface BookmarkResponse {
tmdbId: string; tmdbId: string;
meta: { meta: {
@ -122,6 +130,22 @@ export async function getUser(
); );
} }
export async function editUser(
url: string,
account: AccountWithToken,
object: UserEdit
): Promise<{ user: UserResponse; session: SessionResponse }> {
return ofetch<{ user: UserResponse; session: SessionResponse }>(
`/users/${account.userId}`,
{
method: "PATCH",
headers: getAuthHeaders(account.token),
body: object,
baseURL: url,
}
);
}
export async function deleteUser( export async function deleteUser(
url: string, url: string,
account: AccountWithToken account: AccountWithToken

View File

@ -9,20 +9,31 @@ export interface AvatarProps {
profile: AccountProfile["profile"]; profile: AccountProfile["profile"];
sizeClass?: string; sizeClass?: string;
iconClass?: string; iconClass?: string;
bottom?: React.ReactNode;
} }
export function Avatar(props: AvatarProps) { export function Avatar(props: AvatarProps) {
return ( return (
<div <div className="relative inline-block">
className={classNames( <div
props.sizeClass, className={classNames(
"rounded-full overflow-hidden flex items-center justify-center text-white" props.sizeClass,
)} "rounded-full overflow-hidden flex items-center justify-center text-white"
style={{ )}
background: `linear-gradient(to bottom right, ${props.profile.colorA}, ${props.profile.colorB})`, style={{
}} background: `linear-gradient(to bottom right, ${props.profile.colorA}, ${props.profile.colorB})`,
> }}
<UserIcon className={props.iconClass} icon={props.profile.icon as any} /> >
<UserIcon
className={props.iconClass}
icon={props.profile.icon as any}
/>
</div>
{props.bottom ? (
<div className="absolute bottom-0 left-1/2 transform translate-y-1/2 -translate-x-1/2">
{props.bottom}
</div>
) : null}
</div> </div>
); );
} }
@ -35,18 +46,12 @@ export function UserAvatar(props: {
const auth = useAuthStore(); const auth = useAuthStore();
if (!auth.account) return null; if (!auth.account) return null;
return ( return (
<div className="relative inline-block"> <Avatar
<Avatar profile={auth.account.profile}
profile={auth.account.profile} sizeClass={props.sizeClass ?? "w-[2rem] h-[2rem]"}
sizeClass={props.sizeClass ?? "w-[2rem] h-[2rem]"} iconClass={props.iconClass}
iconClass={props.iconClass} bottom={props.bottom}
/> />
{props.bottom ? (
<div className="absolute bottom-0 left-1/2 transform translate-y-1/2 -translate-x-1/2">
{props.bottom}
</div>
) : null}
</div>
); );
} }

View File

@ -3,7 +3,7 @@ import { useCopyToClipboard, useMountedState } from "react-use";
import { Icon, Icons } from "./Icon"; import { Icon, Icons } from "./Icon";
export function PassphaseDisplay(props: { mnemonic: string }) { export function PassphraseDisplay(props: { mnemonic: string }) {
const individualWords = props.mnemonic.split(" "); const individualWords = props.mnemonic.split(" ");
const [, copy] = useCopyToClipboard(); const [, copy] = useCopyToClipboard();
@ -23,7 +23,7 @@ export function PassphaseDisplay(props: { mnemonic: string }) {
return ( return (
<div className="rounded-lg border border-authentication-border/50 "> <div className="rounded-lg border border-authentication-border/50 ">
<div className="px-4 py-2 flex justify-between border-b border-authentication-border/50"> <div className="px-4 py-2 flex justify-between border-b border-authentication-border/50">
<p className="font-bold text-sm text-white">Passphase</p> <p className="font-bold text-sm text-white">Passphrase</p>
<button <button
type="button" type="button"
className="text-authentication-copyText hover:text-authentication-copyTextHover transition-colors flex gap-2 items-center cursor-pointer" className="text-authentication-copyText hover:text-authentication-copyTextHover transition-colors flex gap-2 items-center cursor-pointer"
@ -37,10 +37,12 @@ export function PassphaseDisplay(props: { mnemonic: string }) {
</button> </button>
</div> </div>
<div className="px-4 py-4 grid grid-cols-4 gap-2"> <div className="px-4 py-4 grid grid-cols-4 gap-2">
{individualWords.map((word) => ( {individualWords.map((word, i) => (
<div <div
className="px-4 rounded-md py-2 bg-authentication-wordBackground text-white font-medium text-center" className="px-4 rounded-md py-2 bg-authentication-wordBackground text-white font-medium text-center"
key={word} // this doesn't get rerendered nor does it have state so its fine
// eslint-disable-next-line react/no-array-index-key
key={i}
> >
{word} {word}
</div> </div>

View File

@ -1,11 +1,18 @@
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import { useCallback, useEffect, useMemo, useState } from "react"; import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { SubtitleStyling } from "@/stores/subtitles"; import { SubtitleStyling } from "@/stores/subtitles";
export function useDerived<T>( export function useDerived<T>(
initial: T initial: T
): [T, (v: T) => void, () => void, boolean] { ): [T, Dispatch<SetStateAction<T>>, () => void, boolean] {
const [overwrite, setOverwrite] = useState<T | undefined>(undefined); const [overwrite, setOverwrite] = useState<T | undefined>(undefined);
useEffect(() => { useEffect(() => {
setOverwrite(undefined); setOverwrite(undefined);
@ -14,19 +21,39 @@ export function useDerived<T>(
() => !isEqual(overwrite, initial) && overwrite !== undefined, () => !isEqual(overwrite, initial) && overwrite !== undefined,
[overwrite, initial] [overwrite, initial]
); );
const setter = useCallback<Dispatch<SetStateAction<T>>>(
(inp) => {
if (!(inp instanceof Function)) setOverwrite(inp);
else setOverwrite((s) => inp(s ?? initial));
},
[initial, setOverwrite]
);
const data = overwrite === undefined ? initial : overwrite; const data = overwrite === undefined ? initial : overwrite;
const reset = useCallback(() => setOverwrite(undefined), [setOverwrite]); const reset = useCallback(() => setOverwrite(undefined), [setOverwrite]);
return [data, setOverwrite, reset, changed]; return [data, setter, reset, changed];
} }
export function useSettingsState( export function useSettingsState(
theme: string | null, theme: string | null,
appLanguage: string, appLanguage: string,
subtitleStyling: SubtitleStyling, subtitleStyling: SubtitleStyling,
deviceName?: string deviceName: string,
proxyUrls: string[] | null,
backendUrl: string | null,
profile:
| {
colorA: string;
colorB: string;
icon: string;
}
| undefined
) { ) {
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
useDerived(proxyUrls);
const [backendUrlState, setBackendUrl, resetBackendUrl, backendUrlChanged] =
useDerived(backendUrl);
const [themeState, setTheme, resetTheme, themeChanged] = useDerived(theme); const [themeState, setTheme, resetTheme, themeChanged] = useDerived(theme);
const [ const [
appLanguageState, appLanguageState,
@ -42,22 +69,27 @@ export function useSettingsState(
resetDeviceName, resetDeviceName,
deviceNameChanged, deviceNameChanged,
] = useDerived(deviceName); ] = useDerived(deviceName);
const [profileState, setProfileState, resetProfile, profileChanged] =
useDerived(profile);
function reset() { function reset() {
resetTheme(); resetTheme();
resetAppLanguage(); resetAppLanguage();
resetSubStyling(); resetSubStyling();
resetProxyUrls();
resetBackendUrl();
resetDeviceName(); resetDeviceName();
resetProfile();
} }
const changed = useMemo( const changed =
() => themeChanged ||
themeChanged || appLanguageChanged ||
appLanguageChanged || subStylingChanged ||
subStylingChanged || deviceNameChanged ||
deviceNameChanged, backendUrlChanged ||
[themeChanged, appLanguageChanged, subStylingChanged, deviceNameChanged] proxyUrlsChanged ||
); profileChanged;
return { return {
reset, reset,
@ -65,18 +97,37 @@ export function useSettingsState(
theme: { theme: {
state: themeState, state: themeState,
set: setTheme, set: setTheme,
changed: themeChanged,
}, },
appLanguage: { appLanguage: {
state: appLanguageState, state: appLanguageState,
set: setAppLanguage, set: setAppLanguage,
changed: appLanguageChanged,
}, },
subtitleStyling: { subtitleStyling: {
state: subStylingState, state: subStylingState,
set: setSubStyling, set: setSubStyling,
changed: subStylingChanged,
}, },
deviceName: { deviceName: {
state: deviceNameState, state: deviceNameState,
set: setDeviceNameState, set: setDeviceNameState,
changed: deviceNameChanged,
},
proxyUrls: {
state: proxyUrlsState,
set: setProxyUrls,
changed: proxyUrlsChanged,
},
backendUrl: {
state: backendUrlState,
set: setBackendUrl,
changed: backendUrlChanged,
},
profile: {
state: profileState,
set: setProfileState,
changed: profileChanged,
}, },
}; };
} }

View File

@ -2,12 +2,19 @@ import classNames from "classnames";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import { useAsyncFn } from "react-use"; import { useAsyncFn } from "react-use";
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto"; import {
import { getSessions } from "@/backend/accounts/sessions"; base64ToBuffer,
decryptData,
encryptData,
} from "@/backend/accounts/crypto";
import { getSessions, updateSession } from "@/backend/accounts/sessions";
import { updateSettings } from "@/backend/accounts/settings"; import { updateSettings } from "@/backend/accounts/settings";
import { editUser } from "@/backend/accounts/user";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { WideContainer } from "@/components/layout/WideContainer"; import { WideContainer } from "@/components/layout/WideContainer";
import { UserIcons } from "@/components/UserIcon";
import { Heading1 } from "@/components/utils/Text"; import { Heading1 } from "@/components/utils/Text";
import { useAuth } from "@/hooks/auth/useAuth";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { useIsMobile } from "@/hooks/useIsMobile"; import { useIsMobile } from "@/hooks/useIsMobile";
import { useSettingsState } from "@/hooks/useSettingsState"; import { useSettingsState } from "@/hooks/useSettingsState";
@ -45,7 +52,17 @@ function SettingsLayout(props: { children: React.ReactNode }) {
); );
} }
export function AccountSettings(props: { account: AccountWithToken }) { export function AccountSettings(props: {
account: AccountWithToken;
deviceName: string;
setDeviceName: (s: string) => void;
colorA: string;
setColorA: (s: string) => void;
colorB: string;
setColorB: (s: string) => void;
userIcon: UserIcons;
setUserIcon: (s: UserIcons) => void;
}) {
const url = useBackendUrl(); const url = useBackendUrl();
const { account } = props; const { account } = props;
const [sessionsResult, execSessions] = useAsyncFn(() => { const [sessionsResult, execSessions] = useAsyncFn(() => {
@ -57,7 +74,16 @@ export function AccountSettings(props: { account: AccountWithToken }) {
return ( return (
<> <>
<AccountEditPart /> <AccountEditPart
deviceName={props.deviceName}
setDeviceName={props.setDeviceName}
colorA={props.colorA}
setColorA={props.setColorA}
colorB={props.colorB}
setColorB={props.setColorB}
userIcon={props.userIcon}
setUserIcon={props.setUserIcon}
/>
<DeviceListPart <DeviceListPart
error={!!sessionsResult.error} error={!!sessionsResult.error}
loading={sessionsResult.loading} loading={sessionsResult.loading}
@ -79,7 +105,15 @@ export function SettingsPage() {
const subStyling = useSubtitleStore((s) => s.styling); const subStyling = useSubtitleStore((s) => s.styling);
const setSubStyling = useSubtitleStore((s) => s.updateStyling); const setSubStyling = useSubtitleStore((s) => s.updateStyling);
const proxySet = useAuthStore((s) => s.proxySet);
const setProxySet = useAuthStore((s) => s.setProxySet);
const backendUrlSetting = useAuthStore((s) => s.backendUrl);
const setBackendUrl = useAuthStore((s) => s.setBackendUrl);
const account = useAuthStore((s) => s.account); const account = useAuthStore((s) => s.account);
const updateProfile = useAuthStore((s) => s.setAccountProfile);
const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
const decryptedName = useMemo(() => { const decryptedName = useMemo(() => {
if (!account) return ""; if (!account) return "";
return decryptData(account.deviceName, base64ToBuffer(account.seed)); return decryptData(account.deviceName, base64ToBuffer(account.seed));
@ -87,29 +121,71 @@ export function SettingsPage() {
const backendUrl = useBackendUrl(); const backendUrl = useBackendUrl();
const { logout } = useAuth();
const user = useAuthStore(); const user = useAuthStore();
const state = useSettingsState( const state = useSettingsState(
activeTheme, activeTheme,
appLanguage, appLanguage,
subStyling, subStyling,
decryptedName decryptedName,
proxySet,
backendUrlSetting,
account?.profile
); );
const saveChanges = useCallback(async () => { const saveChanges = useCallback(async () => {
console.log(state);
if (account) { if (account) {
await updateSettings(backendUrl, account, { if (state.appLanguage.changed || state.theme.changed) {
applicationLanguage: state.appLanguage.state, await updateSettings(backendUrl, account, {
applicationTheme: state.theme.state ?? undefined, applicationLanguage: state.appLanguage.state,
}); applicationTheme: state.theme.state,
});
}
if (state.deviceName.changed) {
const newDeviceName = await encryptData(
state.deviceName.state,
base64ToBuffer(account.seed)
);
await updateSession(backendUrl, account, {
deviceName: newDeviceName,
});
updateDeviceName(newDeviceName);
}
if (state.profile.changed) {
await editUser(backendUrl, account, {
profile: state.profile.state,
});
}
} }
setAppLanguage(state.appLanguage.state); setAppLanguage(state.appLanguage.state);
setTheme(state.theme.state); setTheme(state.theme.state);
setSubStyling(state.subtitleStyling.state); setSubStyling(state.subtitleStyling.state);
}, [state, account, backendUrl, setAppLanguage, setTheme, setSubStyling]); setProxySet(state.proxyUrls.state);
if (state.profile.state) {
updateProfile(state.profile.state);
}
// when backend url gets changed, log the user out first
if (state.backendUrl.changed) {
await logout();
setBackendUrl(state.backendUrl.state);
}
}, [
state,
account,
backendUrl,
setAppLanguage,
setTheme,
setSubStyling,
updateDeviceName,
updateProfile,
setProxySet,
setBackendUrl,
logout,
]);
return ( return (
<SubPageLayout> <SubPageLayout>
<SettingsLayout> <SettingsLayout>
@ -117,8 +193,24 @@ export function SettingsPage() {
<Heading1 border className="!mb-0"> <Heading1 border className="!mb-0">
Account Account
</Heading1> </Heading1>
{user.account ? ( {user.account && state.profile.state ? (
<AccountSettings account={user.account} /> <AccountSettings
account={user.account}
deviceName={state.deviceName.state}
setDeviceName={state.deviceName.set}
colorA={state.profile.state.colorA}
setColorA={(v) => {
state.profile.set((s) => (s ? { ...s, colorA: v } : undefined));
}}
colorB={state.profile.state.colorB}
setColorB={(v) =>
state.profile.set((s) => (s ? { ...s, colorB: v } : undefined))
}
userIcon={state.profile.state.icon as any}
setUserIcon={(v) =>
state.profile.set((s) => (s ? { ...s, icon: v } : undefined))
}
/>
) : ( ) : (
<RegisterCalloutPart /> <RegisterCalloutPart />
)} )}
@ -139,7 +231,12 @@ export function SettingsPage() {
/> />
</div> </div>
<div id="settings-connection" className="mt-48"> <div id="settings-connection" className="mt-48">
<ConnectionsPart /> <ConnectionsPart
backendUrl={state.backendUrl.state}
setBackendUrl={state.backendUrl.set}
proxyUrls={state.proxyUrls.state}
setProxyUrls={state.proxyUrls.set}
/>
</div> </div>
</SettingsLayout> </SettingsLayout>
<div <div

View File

@ -8,7 +8,7 @@ import {
LargeCardButtons, LargeCardButtons,
LargeCardText, LargeCardText,
} from "@/components/layout/LargeCard"; } from "@/components/layout/LargeCard";
import { PassphaseDisplay } from "@/components/PassphraseDisplay"; import { PassphraseDisplay } from "@/components/PassphraseDisplay";
interface PassphraseGeneratePartProps { interface PassphraseGeneratePartProps {
onNext?: (mnemonic: string) => void; onNext?: (mnemonic: string) => void;
@ -23,7 +23,7 @@ export function PassphraseGeneratePart(props: PassphraseGeneratePartProps) {
If you lose this, you&apos;re a silly goose and will be posted on the If you lose this, you&apos;re a silly goose and will be posted on the
wall of shame wall of shame
</LargeCardText> </LargeCardText>
<PassphaseDisplay mnemonic={mnemonic} /> <PassphraseDisplay mnemonic={mnemonic} />
<LargeCardButtons> <LargeCardButtons>
<Button theme="purple" onClick={() => props.onNext?.(mnemonic)}> <Button theme="purple" onClick={() => props.onNext?.(mnemonic)}>

View File

@ -1,13 +1,23 @@
import { UserAvatar } from "@/components/Avatar"; import { Avatar } from "@/components/Avatar";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { SettingsCard } from "@/components/layout/SettingsCard"; import { SettingsCard } from "@/components/layout/SettingsCard";
import { useModal } from "@/components/overlays/Modal"; import { useModal } from "@/components/overlays/Modal";
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
import { UserIcons } from "@/components/UserIcon";
import { useAuth } from "@/hooks/auth/useAuth"; import { useAuth } from "@/hooks/auth/useAuth";
import { ProfileEditModal } from "@/pages/parts/settings/ProfileEditModal"; import { ProfileEditModal } from "@/pages/parts/settings/ProfileEditModal";
export function AccountEditPart() { export function AccountEditPart(props: {
deviceName: string;
setDeviceName: (s: string) => void;
colorA: string;
setColorA: (s: string) => void;
colorB: string;
setColorB: (s: string) => void;
userIcon: UserIcons;
setUserIcon: (s: UserIcons) => void;
}) {
const { logout } = useAuth(); const { logout } = useAuth();
const profileEditModal = useModal("profile-edit"); const profileEditModal = useModal("profile-edit");
@ -16,10 +26,21 @@ export function AccountEditPart() {
<ProfileEditModal <ProfileEditModal
id={profileEditModal.id} id={profileEditModal.id}
close={profileEditModal.hide} close={profileEditModal.hide}
colorA={props.colorA}
setColorA={props.setColorA}
colorB={props.colorB}
setColorB={props.setColorB}
userIcon={props.userIcon}
setUserIcon={props.setUserIcon}
/> />
<div className="grid lg:grid-cols-[auto,1fr] gap-8"> <div className="grid lg:grid-cols-[auto,1fr] gap-8">
<div> <div>
<UserAvatar <Avatar
profile={{
colorA: props.colorA,
colorB: props.colorB,
icon: props.userIcon,
}}
iconClass="text-5xl" iconClass="text-5xl"
sizeClass="w-32 h-32" sizeClass="w-32 h-32"
bottom={ bottom={
@ -36,9 +57,13 @@ export function AccountEditPart() {
</div> </div>
<div> <div>
<div className="space-y-8 max-w-xs"> <div className="space-y-8 max-w-xs">
<AuthInputBox label="Device name" placeholder="Fremen tablet" /> <AuthInputBox
label="Device name"
placeholder="Fremen tablet"
value={props.deviceName}
onChange={(value) => props.setDeviceName(value)}
/>
<div className="flex space-x-3"> <div className="flex space-x-3">
<Button theme="purple">Save account</Button>
<Button theme="danger" onClick={logout}> <Button theme="danger" onClick={logout}>
Log out Log out
</Button> </Button>

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from "react"; import { Dispatch, SetStateAction, useCallback } from "react";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Toggle } from "@/components/buttons/Toggle"; import { Toggle } from "@/components/buttons/Toggle";
@ -8,45 +8,38 @@ import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
import { Divider } from "@/components/utils/Divider"; import { Divider } from "@/components/utils/Divider";
import { Heading1 } from "@/components/utils/Text"; import { Heading1 } from "@/components/utils/Text";
let idNum = 0; interface ProxyEditProps {
proxyUrls: string[] | null;
interface ProxyItem { setProxyUrls: Dispatch<SetStateAction<string[] | null>>;
url: string;
id: number;
} }
function ProxyEdit() { interface BackendEditProps {
const [customWorkers, setCustomWorkers] = useState<ProxyItem[] | null>(null); backendUrl: string | null;
setBackendUrl: Dispatch<SetStateAction<string | null>>;
}
function ProxyEdit({ proxyUrls, setProxyUrls }: ProxyEditProps) {
const add = useCallback(() => { const add = useCallback(() => {
idNum += 1; setProxyUrls((s) => [...(s ?? []), ""]);
setCustomWorkers((s) => [ }, [setProxyUrls]);
...(s ?? []),
{
id: idNum,
url: "",
},
]);
}, [setCustomWorkers]);
const changeItem = useCallback( const changeItem = useCallback(
(id: number, val: string) => { (index: number, val: string) => {
setCustomWorkers((s) => [ setProxyUrls((s) => [
...(s ?? []).map((v) => { ...(s ?? []).map((v, i) => {
if (v.id !== id) return v; if (i !== index) return v;
v.url = val; return val;
return v;
}), }),
]); ]);
}, },
[setCustomWorkers] [setProxyUrls]
); );
const removeItem = useCallback( const removeItem = useCallback(
(id: number) => { (index: number) => {
setCustomWorkers((s) => [...(s ?? []).filter((v) => v.id !== id)]); setProxyUrls((s) => [...(s ?? []).filter((v, i) => i !== index)]);
}, },
[setCustomWorkers] [setProxyUrls]
); );
return ( return (
@ -61,30 +54,35 @@ function ProxyEdit() {
</div> </div>
<div> <div>
<Toggle <Toggle
onClick={() => setCustomWorkers((s) => (s === null ? [] : null))} onClick={() => setProxyUrls((s) => (s === null ? [] : null))}
enabled={customWorkers !== null} enabled={proxyUrls !== null}
/> />
</div> </div>
</div> </div>
{customWorkers !== null ? ( {proxyUrls !== null ? (
<> <>
<Divider marginClass="my-6 px-8 box-content -mx-8" /> <Divider marginClass="my-6 px-8 box-content -mx-8" />
<p className="text-white font-bold mb-3">Worker URLs</p> <p className="text-white font-bold mb-3">Worker URLs</p>
<div className="my-6 space-y-2 max-w-md"> <div className="my-6 space-y-2 max-w-md">
{(customWorkers?.length ?? 0) === 0 ? ( {(proxyUrls?.length ?? 0) === 0 ? (
<p>No workers yet, add one below</p> <p>No workers yet, add one below</p>
) : null} ) : null}
{(customWorkers ?? []).map((v) => ( {(proxyUrls ?? []).map((v, i) => (
<div className="grid grid-cols-[1fr,auto] items-center gap-2"> <div
// not the best but we can live with it
// eslint-disable-next-line react/no-array-index-key
key={i}
className="grid grid-cols-[1fr,auto] items-center gap-2"
>
<AuthInputBox <AuthInputBox
value={v.url} value={v}
onChange={(val) => changeItem(v.id, val)} onChange={(val) => changeItem(i, val)}
placeholder="https://" placeholder="https://"
/> />
<button <button
type="button" type="button"
onClick={() => removeItem(v.id)} onClick={() => removeItem(i)}
className="h-full scale-90 hover:scale-100 rounded-full aspect-square bg-authentication-inputBg hover:bg-authentication-inputBgHover flex justify-center items-center transition-transform duration-200 hover:text-white cursor-pointer" className="h-full scale-90 hover:scale-100 rounded-full aspect-square bg-authentication-inputBg hover:bg-authentication-inputBgHover flex justify-center items-center transition-transform duration-200 hover:text-white cursor-pointer"
> >
<Icon className="text-xl" icon={Icons.X} /> <Icon className="text-xl" icon={Icons.X} />
@ -102,9 +100,7 @@ function ProxyEdit() {
); );
} }
function BackendEdit() { function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
const [customBackendUrl, setCustomBackendUrl] = useState<string | null>(null);
return ( return (
<SettingsCard> <SettingsCard>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@ -117,32 +113,35 @@ function BackendEdit() {
</div> </div>
<div> <div>
<Toggle <Toggle
onClick={() => setCustomBackendUrl((s) => (s === null ? "" : null))} onClick={() => setBackendUrl((s) => (s === null ? "" : null))}
enabled={customBackendUrl !== null} enabled={backendUrl !== null}
/> />
</div> </div>
</div> </div>
{customBackendUrl !== null ? ( {backendUrl !== null ? (
<> <>
<Divider marginClass="my-6 px-8 box-content -mx-8" /> <Divider marginClass="my-6 px-8 box-content -mx-8" />
<p className="text-white font-bold mb-3">Custom server URL</p> <p className="text-white font-bold mb-3">Custom server URL</p>
<AuthInputBox <AuthInputBox onChange={setBackendUrl} value={backendUrl ?? ""} />
onChange={setCustomBackendUrl}
value={customBackendUrl ?? ""}
/>
</> </>
) : null} ) : null}
</SettingsCard> </SettingsCard>
); );
} }
export function ConnectionsPart() { export function ConnectionsPart(props: BackendEditProps & ProxyEditProps) {
return ( return (
<div> <div>
<Heading1 border>Connections</Heading1> <Heading1 border>Connections</Heading1>
<div className="space-y-6"> <div className="space-y-6">
<ProxyEdit /> <ProxyEdit
<BackendEdit /> proxyUrls={props.proxyUrls}
setProxyUrls={props.setProxyUrls}
/>
<BackendEdit
backendUrl={props.backendUrl}
setBackendUrl={props.setBackendUrl}
/>
</div> </div>
</div> </div>
); );

View File

@ -1,5 +1,3 @@
import { useState } from "react";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { ColorPicker } from "@/components/form/ColorPicker"; import { ColorPicker } from "@/components/form/ColorPicker";
import { IconPicker } from "@/components/form/IconPicker"; import { IconPicker } from "@/components/form/IconPicker";
@ -10,28 +8,34 @@ import { Heading2 } from "@/components/utils/Text";
export interface ProfileEditModalProps { export interface ProfileEditModalProps {
id: string; id: string;
close?: () => void; close?: () => void;
colorA: string;
setColorA: (s: string) => void;
colorB: string;
setColorB: (s: string) => void;
userIcon: UserIcons;
setUserIcon: (s: UserIcons) => void;
} }
export function ProfileEditModal(props: ProfileEditModalProps) { export function ProfileEditModal(props: ProfileEditModalProps) {
const [colorA, setColorA] = useState("#2E65CF");
const [colorB, setColorB] = useState("#2E65CF");
const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER);
return ( return (
<Modal id={props.id}> <Modal id={props.id}>
<ModalCard> <ModalCard>
<Heading2 className="!mt-0">Edit profile picture</Heading2> <Heading2 className="!mt-0">Edit profile picture</Heading2>
<div className="space-y-6"> <div className="space-y-6">
<ColorPicker label="First color" value={colorA} onInput={setColorA} /> <ColorPicker
label="First color"
value={props.colorA}
onInput={props.setColorA}
/>
<ColorPicker <ColorPicker
label="Second color" label="Second color"
value={colorB} value={props.colorB}
onInput={setColorB} onInput={props.setColorB}
/> />
<IconPicker <IconPicker
label="User icon" label="User icon"
value={userIcon} value={props.userIcon}
onInput={setUserIcon} onInput={props.setUserIcon}
/> />
</div> </div>
<div className="flex justify-center mt-8"> <div className="flex justify-center mt-8">

View File

@ -26,7 +26,9 @@ interface AuthStore {
setAccount(acc: AccountWithToken): void; setAccount(acc: AccountWithToken): void;
updateDeviceName(deviceName: string): void; updateDeviceName(deviceName: string): void;
updateAccount(acc: Account): void; updateAccount(acc: Account): void;
setAccountProfile(acc: Account["profile"]): void;
setBackendUrl(url: null | string): void; setBackendUrl(url: null | string): void;
setProxySet(urls: null | string[]): void;
} }
export const useAuthStore = create( export const useAuthStore = create(
@ -50,6 +52,18 @@ export const useAuthStore = create(
s.backendUrl = v; s.backendUrl = v;
}); });
}, },
setProxySet(urls) {
set((s) => {
s.proxySet = urls;
});
},
setAccountProfile(profile) {
set((s) => {
if (s.account) {
s.account.profile = profile;
}
});
},
updateAccount(acc) { updateAccount(acc) {
set((s) => { set((s) => {
if (!s.account) return; if (!s.account) return;