adding profile detail modal

This commit is contained in:
balzack 2024-09-08 08:25:04 -07:00
parent 4c7f5315cc
commit 214782d9c9
3 changed files with 181 additions and 72 deletions

View File

@ -16,7 +16,7 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 8px; padding: 8px;
gap: 8px; gap: 2px;
.header { .header {
font-size: 22px; font-size: 22px;
@ -115,20 +115,19 @@
width: 100%; width: 100%;
padding-left: 8px; padding-left: 8px;
display: flex; display: flex;
align-items: center;
.radio { .radio {
position: relative; position: relative;
top: -4px; top: -8px;
} }
.entryIcon { .entryIcon {
width: 32px; width: 32px;
display: flex;
} }
.entrySet { .entrySet {
font-size: 14px; font-size: 14px;
white-space: preserve;
} }
.entryUnset { .entryUnset {
@ -166,6 +165,8 @@
align-items: center; align-items: center;
max-width: 40%; max-width: 40%;
padding: 0; padding: 0;
position: relative;
top: -4px;
} }
} }
} }

View File

@ -1,14 +1,18 @@
import { useSettings } from './useSettings.hook'; import { useSettings } from './useSettings.hook';
import { Modal, TextInput, PasswordInput, Radio, Group, Select, Switch, Text, Image, Button, UnstyledButton } from '@mantine/core'; import { Modal, Textarea, TextInput, PasswordInput, Radio, Group, Select, Switch, Text, Image, Button, UnstyledButton } from '@mantine/core';
import classes from './Settings.module.css'; import classes from './Settings.module.css';
import { IconLock, IconUser, IconClock, IconCalendar, IconUsers, IconVideo, IconMicrophone, IconWorld, IconBrightness, IconTicket, IconCloudLock, IconBell, IconEye, IconBook, IconMapPin, IconLogout, IconLogin } from '@tabler/icons-react' import { IconLock, IconUser, IconClock, IconIdBadge, IconCalendar, IconUsers, IconVideo, IconMicrophone, IconWorld, IconBrightness, IconTicket, IconCloudLock, IconBell, IconEye, IconBook, IconMapPin, IconLogout, IconLogin } from '@tabler/icons-react'
import avatar from '../images/avatar.png' import avatar from '../images/avatar.png'
import { modals } from '@mantine/modals'; import { modals } from '@mantine/modals';
import { useDisclosure } from '@mantine/hooks' import { useDisclosure } from '@mantine/hooks'
import { useState } from 'react'
export function Settings({ showLogout }) { export function Settings({ showLogout }) {
const { state, actions } = useSettings(); const { state, actions } = useSettings();
const [changeOpened, { open: changeOpen, close: changeClose }] = useDisclosure(false) const [changeOpened, { open: changeOpen, close: changeClose }] = useDisclosure(false)
const [detailsOpened, { open: detailsOpen, close: detailsClose }] = useDisclosure(false)
const [savingLogin, setSavingLogin] = useState(false);
const [savingDetails, setSavingDetails] = useState(false);
const logout = () => modals.openConfirmModal({ const logout = () => modals.openConfirmModal({
title: state.strings.confirmLogout, title: state.strings.confirmLogout,
@ -24,28 +28,57 @@ export function Settings({ showLogout }) {
onConfirm: actions.logout, onConfirm: actions.logout,
}); });
const setLogin = async () => { const setDetails = async () => {
try { if (!savingDetails) {
throw new Error("NO"); setSavingDetails(true);
await actions.setLogin(); try {
changeClose(); await actions.setDetails();
detailsClose();
}
catch (err) {
console.log(err);
modals.openConfirmModal({
title: state.strings.operationFailed,
withCloseButton: true,
overlayProps: {
backgroundOpacity: 0.55,
blur: 3,
},
children: (
<Text>{state.strings.tryAgain}</Text>
),
cancelProps: { display: 'none' },
confirmProps: { display: 'none' },
});
}
setSavingDetails(false);
} }
catch (err) { }
console.log(err);
modals.openConfirmModal({
title: state.strings.operationFailed,
withCloseButton: true,
overlayProps: {
backgroundOpacity: 0.55,
blur: 3,
},
children: (
<Text>{state.strings.tryAgain}</Text>
),
cancelProps: { display: 'none' },
confirmProps: { display: 'none' },
});
const setLogin = async () => {
if (!savingLogin) {
setSavingLogin(true);
try {
await actions.setLogin();
changeClose();
}
catch (err) {
console.log(err);
modals.openConfirmModal({
title: state.strings.operationFailed,
withCloseButton: true,
overlayProps: {
backgroundOpacity: 0.55,
blur: 3,
},
children: (
<Text>{state.strings.tryAgain}</Text>
),
cancelProps: { display: 'none' },
confirmProps: { display: 'none' },
});
}
setSavingLogin(false);
} }
} }
@ -72,7 +105,7 @@ throw new Error("NO");
</div> </div>
<div className={classes.section}> <div className={classes.section}>
<div className={classes.divider} /> <div className={classes.divider} />
<UnstyledButton className={classes.sectionEdit}>{ state.strings.edit }</UnstyledButton> <UnstyledButton className={classes.sectionEdit} onClick={detailsOpen}>{ state.strings.edit }</UnstyledButton>
</div> </div>
{ !state.profile.name && ( { !state.profile.name && (
<Text className={classes.nameUnset}>{state.strings.name}</Text> <Text className={classes.nameUnset}>{state.strings.name}</Text>
@ -146,43 +179,41 @@ throw new Error("NO");
<Text className={classes.entryLabel} onClick={changeOpen}>{ state.strings.changeLogin }</Text> <Text className={classes.entryLabel} onClick={changeOpen}>{ state.strings.changeLogin }</Text>
</div> </div>
<div className={classes.divider} /> <div className={classes.divider} />
<div className={classes.entry}>
<div className={classes.entryIcon}>
<IconClock />
</div>
<Text className={classes.controlLabel}>{ state.strings.timeFormat }</Text>
<Radio.Group
name="timeFormat"
className={classes.radio}
value={state.timeFormat}
onChange={actions.setTimeFormat}
>
<Group mt="xs">
<Radio value="12h" label={ state.strings.timeUs } />
<Radio value="24h" label={ state.strings.timeEu } />
</Group>
</Radio.Group>
</div>
<div className={classes.entry}>
<div className={classes.entryIcon}>
<IconCalendar />
</div>
<Text className={classes.controlLabel}>{ state.strings.dateFormat }</Text>
<Radio.Group
name="dateFormat"
className={classes.radio}
value={state.dateFormat}
onChange={actions.setDateFormat}
>
<Group mt="xs">
<Radio value="mm/dd" label={ state.strings.dateUs } />
<Radio value="dd/mm" label={ state.strings.dateEu } />
</Group>
</Radio.Group>
</div>
<div className={classes.selects}> <div className={classes.selects}>
<div className={classes.entry}>
<div className={classes.entryIcon}>
<IconClock />
</div>
<Text className={classes.controlLabel}>{ state.strings.timeFormat }</Text>
<Radio.Group
name="timeFormat"
className={classes.radio}
value={state.timeFormat}
onChange={actions.setTimeFormat}
>
<Group mt="xs">
<Radio value="12h" label={ state.strings.timeUs } />
<Radio value="24h" label={ state.strings.timeEu } />
</Group>
</Radio.Group>
</div>
<div className={classes.entry}>
<div className={classes.entryIcon}>
<IconCalendar />
</div>
<Text className={classes.controlLabel}>{ state.strings.dateFormat }</Text>
<Radio.Group
name="dateFormat"
className={classes.radio}
value={state.dateFormat}
onChange={actions.setDateFormat}
>
<Group mt="xs">
<Radio value="mm/dd" label={ state.strings.dateUs } />
<Radio value="dd/mm" label={ state.strings.dateEu } />
</Group>
</Radio.Group>
</div>
<div className={classes.entry}> <div className={classes.entry}>
<div className={classes.entryIcon}> <div className={classes.entryIcon}>
<IconBrightness /> <IconBrightness />
@ -249,13 +280,13 @@ throw new Error("NO");
<TextInput <TextInput
className={classes.input} className={classes.input}
size="md" size="md"
value={state.username} value={state.handle}
leftSectionPointerEvents="none" leftSectionPointerEvents="none"
leftSection={<IconUser />} leftSection={<IconUser />}
rightSection={state.taken ? <IconUsers /> : null} rightSection={state.taken ? <IconUsers /> : null}
placeholder={state.strings.username} placeholder={state.strings.username}
onChange={(event) => onChange={(event) =>
actions.setUsername(event.currentTarget.value) actions.setHandle(event.currentTarget.value)
} }
error={state.taken} error={state.taken}
/> />
@ -286,8 +317,66 @@ throw new Error("NO");
<Button <Button
variant="filled" variant="filled"
onClick={setLogin} onClick={setLogin}
loading={state.loading} loading={savingLogin}
disabled={state.taken || !state.checked || !state.username || !state.password || state.confirm !== state.password} disabled={state.taken || !state.checked || !state.handle || !state.password || state.confirm !== state.password}
>
{state.strings.save}
</Button>
</div>
</div>
</Modal>
<Modal
title={state.strings.profileDetails}
opened={detailsOpened}
onClose={detailsClose}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
centered
>
<div className={classes.change}>
<TextInput
className={classes.input}
size="md"
value={state.name}
leftSectionPointerEvents="none"
leftSection={<IconIdBadge />}
placeholder={state.strings.name}
onChange={(event) =>
actions.setName(event.currentTarget.value)
}
/>
<TextInput
className={classes.input}
size="md"
value={state.location}
leftSectionPointerEvents="none"
leftSection={<IconMapPin />}
placeholder={state.strings.location}
onChange={(event) =>
actions.setLocation(event.currentTarget.value)
}
/>
<Textarea
className={classes.input}
size="md"
minRows={1}
maxRows={4}
autosize={true}
value={state.description}
leftSectionPointerEvents="none"
leftSection={<IconBook />}
placeholder={state.strings.description}
onChange={(event) =>
actions.setDescription(event.currentTarget.value)
}
/>
<div className={classes.control}>
<Button variant="default" onClick={detailsClose}>
{state.strings.cancel}
</Button>
<Button
variant="filled"
onClick={setDetails}
loading={savingDetails}
> >
{state.strings.save} {state.strings.save}
</Button> </Button>

View File

@ -34,6 +34,10 @@ export function useSettings() {
username: '', username: '',
taken: false, taken: false,
checked: true, checked: true,
name: '',
description: '',
location: '',
handle: '',
}) })
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -58,7 +62,8 @@ export function useSettings() {
} }
settings.addConfigListener(setConfig); settings.addConfigListener(setConfig);
const setProfile = (profile: Profile) => { const setProfile = (profile: Profile) => {
updateState({ profile, username: profile.handle, profileSet: true, imageUrl: identity.getProfileImageUrl() }) const { handle, name, location, description } = profile;
updateState({ profile, handle, name, location, description, profileSet: true, imageUrl: identity.getProfileImageUrl() })
} }
identity.addProfileListener(setProfile) identity.addProfileListener(setProfile)
return () => { return () => {
@ -176,16 +181,16 @@ console.log(audioInputs, videoInputs);
logout: async () => { logout: async () => {
await app.actions.accountLogout(state.all); await app.actions.accountLogout(state.all);
}, },
setUsername: (username) => { setHandle: (handle) => {
updateState({ username, taken: false, checked: false }); updateState({ handle, taken: false, checked: false });
clearTimeout(debounce.current); clearTimeout(debounce.current);
if (!username || username === state.profile.handle) { if (!handle || handle === state.profile.handle) {
updateState({ available: true, checked: true}); updateState({ available: true, checked: true});
} }
else { else {
debounce.current = setTimeout(async () => { debounce.current = setTimeout(async () => {
const { settings } = getSession(); const { settings } = getSession();
const available = await settings.getUsernameStatus(username); const available = await settings.getUsernameStatus(handle);
updateState({ taken: !available, checked: true }); updateState({ taken: !available, checked: true });
}, DEBOUNCE_MS); }, DEBOUNCE_MS);
} }
@ -199,6 +204,20 @@ console.log(audioInputs, videoInputs);
setLogin: async () => { setLogin: async () => {
const { settings } = getSession(); const { settings } = getSession();
await settings.setLogin(state.username, state.password); await settings.setLogin(state.username, state.password);
},
setName: (name) => {
updateState({ name });
},
setLocation: (location) => {
updateState({ location });
},
setDescription: (description) => {
updateState({ description });
},
setDetails: async () => {
const { identity } = getSession();
const { name, location, description } = state;
await identity.setProfileData(name, location, description);
} }
} }