Added settings layout.

This commit is contained in:
Mike Cao 2024-02-04 22:35:14 -08:00
parent d818bf5aaf
commit 6802093d69
22 changed files with 102 additions and 76 deletions

View file

@ -0,0 +1,29 @@
'use client';
import DateFilter from 'components/input/DateFilter';
import { Button, Flexbox } from 'react-basics';
import { useDateRange, useMessages } from 'components/hooks';
import { DEFAULT_DATE_RANGE } from 'lib/constants';
import { DateRange } from 'lib/types';
export function DateRangeSetting() {
const { formatMessage, labels } = useMessages();
const [dateRange, setDateRange] = useDateRange();
const { value } = dateRange;
const handleChange = (value: string | DateRange) => setDateRange(value);
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
return (
<Flexbox gap={10}>
<DateFilter
value={value}
startDate={dateRange.startDate}
endDate={dateRange.endDate}
onChange={handleChange}
/>
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button>
</Flexbox>
);
}
export default DateRangeSetting;

View file

@ -0,0 +1,4 @@
div.menu {
max-height: 300px;
width: 300px;
}

View file

@ -0,0 +1,44 @@
'use client';
import { useState } from 'react';
import { Button, Dropdown, Item, Flexbox } from 'react-basics';
import { useLocale, useMessages } from 'components/hooks';
import { DEFAULT_LOCALE } from 'lib/constants';
import { languages } from 'lib/lang';
import styles from './LanguageSetting.module.css';
export function LanguageSetting() {
const [search, setSearch] = useState('');
const { formatMessage, labels } = useMessages();
const { locale, saveLocale } = useLocale();
const options = search
? Object.keys(languages).filter(n => {
return (
n.toLowerCase().includes(search.toLowerCase()) ||
languages[n].label.toLowerCase().includes(search.toLowerCase())
);
})
: Object.keys(languages);
const handleReset = () => saveLocale(DEFAULT_LOCALE);
const renderValue = (value: string | number) => languages[value].label;
return (
<Flexbox gap={10}>
<Dropdown
items={options}
value={locale}
renderValue={renderValue}
onChange={val => saveLocale(val as string)}
allowSearch={true}
onSearch={setSearch}
menuProps={{ className: styles.menu }}
>
{item => <Item key={item}>{languages[item].label}</Item>}
</Dropdown>
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button>
</Flexbox>
);
}
export default LanguageSetting;

View file

@ -0,0 +1,32 @@
'use client';
import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics';
import PasswordEditForm from 'app/(main)/profile/PasswordEditForm';
import Icons from 'components/icons';
import { useMessages } from 'components/hooks';
export function PasswordChangeButton() {
const { formatMessage, labels, messages } = useMessages();
const { showToast } = useToasts();
const handleSave = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
return (
<>
<ModalTrigger>
<Button>
<Icon>
<Icons.Lock />
</Icon>
<Text>{formatMessage(labels.changePassword)}</Text>
</Button>
<Modal title={formatMessage(labels.changePassword)}>
{close => <PasswordEditForm onSave={handleSave} onClose={close} />}
</Modal>
</ModalTrigger>
</>
);
}
export default PasswordChangeButton;

View file

@ -0,0 +1,70 @@
'use client';
import { useRef } from 'react';
import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
import { useApi, useMessages } from 'components/hooks';
export function PasswordEditForm({ onSave, onClose }) {
const { formatMessage, labels, messages } = useMessages();
const { post, useMutation } = useApi();
const { mutate, error, isPending } = useMutation({
mutationFn: (data: any) => post('/me/password', data),
});
const ref = useRef(null);
const handleSubmit = async (data: any) => {
mutate(data, {
onSuccess: async () => {
onSave();
onClose();
},
});
};
const samePassword = (value: string) => {
if (value !== ref?.current?.getValues('newPassword')) {
return formatMessage(messages.noMatchPassword);
}
return true;
};
return (
<Form ref={ref} onSubmit={handleSubmit} error={error}>
<FormRow label={formatMessage(labels.currentPassword)}>
<FormInput name="currentPassword" rules={{ required: 'Required' }}>
<PasswordField autoComplete="current-password" />
</FormInput>
</FormRow>
<FormRow label={formatMessage(labels.newPassword)}>
<FormInput
name="newPassword"
rules={{
required: 'Required',
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
}}
>
<PasswordField autoComplete="new-password" />
</FormInput>
</FormRow>
<FormRow label={formatMessage(labels.confirmPassword)}>
<FormInput
name="confirmPassword"
rules={{
required: formatMessage(labels.required),
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
validate: samePassword,
}}
>
<PasswordField autoComplete="confirm-password" />
</FormInput>
</FormRow>
<FormButtons flex>
<Button type="submit" variant="primary" disabled={isPending}>
{formatMessage(labels.save)}
</Button>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
</FormButtons>
</Form>
);
}
export default PasswordEditForm;

View file

@ -0,0 +1,11 @@
'use client';
import PageHeader from 'components/layout/PageHeader';
import { useMessages } from 'components/hooks';
export function ProfileHeader() {
const { formatMessage, labels } = useMessages();
return <PageHeader title={formatMessage(labels.profile)}></PageHeader>;
}
export default ProfileHeader;

View file

@ -0,0 +1,61 @@
'use client';
import { Form, FormRow } from 'react-basics';
import TimezoneSetting from 'app/(main)/profile/TimezoneSetting';
import DateRangeSetting from 'app/(main)/profile/DateRangeSetting';
import LanguageSetting from 'app/(main)/profile/LanguageSetting';
import ThemeSetting from 'app/(main)/profile/ThemeSetting';
import PasswordChangeButton from './PasswordChangeButton';
import { useLogin, useMessages } from 'components/hooks';
import { ROLES } from 'lib/constants';
export function ProfileSettings() {
const { user } = useLogin();
const { formatMessage, labels } = useMessages();
const cloudMode = Boolean(process.env.cloudMode);
if (!user) {
return null;
}
const { username, role } = user;
const renderRole = value => {
if (value === ROLES.user) {
return formatMessage(labels.user);
}
if (value === ROLES.admin) {
return formatMessage(labels.admin);
}
if (value === ROLES.viewOnly) {
return formatMessage(labels.viewOnly);
}
return formatMessage(labels.unknown);
};
return (
<Form>
<FormRow label={formatMessage(labels.username)}>{username}</FormRow>
<FormRow label={formatMessage(labels.role)}>{renderRole(role)}</FormRow>
{!cloudMode && (
<FormRow label={formatMessage(labels.password)}>
<PasswordChangeButton />
</FormRow>
)}
<FormRow label={formatMessage(labels.defaultDateRange)}>
<DateRangeSetting />
</FormRow>
<FormRow label={formatMessage(labels.language)}>
<LanguageSetting />
</FormRow>
<FormRow label={formatMessage(labels.timezone)}>
<TimezoneSetting />
</FormRow>
<FormRow label={formatMessage(labels.theme)}>
<ThemeSetting />
</FormRow>
</Form>
);
}
export default ProfileSettings;

View file

@ -0,0 +1,8 @@
.buttons {
display: flex;
gap: 10px;
}
.active {
border: 2px solid var(--primary400);
}

View file

@ -0,0 +1,34 @@
'use client';
import classNames from 'classnames';
import { Button, Icon } from 'react-basics';
import { useTheme } from 'components/hooks';
import Sun from 'assets/sun.svg';
import Moon from 'assets/moon.svg';
import styles from './ThemeSetting.module.css';
export function ThemeSetting() {
const { theme, saveTheme } = useTheme();
return (
<div className={styles.buttons}>
<Button
className={classNames({ [styles.active]: theme === 'light' })}
onClick={() => saveTheme('light')}
>
<Icon>
<Sun />
</Icon>
</Button>
<Button
className={classNames({ [styles.active]: theme === 'dark' })}
onClick={() => saveTheme('dark')}
>
<Icon>
<Moon />
</Icon>
</Button>
</div>
);
}
export default ThemeSetting;

View file

@ -0,0 +1,4 @@
div.menu {
max-height: 300px;
width: 300px;
}

View file

@ -0,0 +1,36 @@
'use client';
import { useState } from 'react';
import { Dropdown, Item, Button, Flexbox } from 'react-basics';
import { listTimeZones } from 'timezone-support';
import { useTimezone, useMessages } from 'components/hooks';
import { getTimezone } from 'lib/date';
import styles from './TimezoneSetting.module.css';
export function TimezoneSetting() {
const [search, setSearch] = useState('');
const { formatMessage, labels } = useMessages();
const [timezone, saveTimezone] = useTimezone();
const options = search
? listTimeZones().filter(n => n.toLowerCase().includes(search.toLowerCase()))
: listTimeZones();
const handleReset = () => saveTimezone(getTimezone());
return (
<Flexbox gap={10}>
<Dropdown
items={options}
value={timezone}
onChange={saveTimezone}
menuProps={{ className: styles.menu }}
allowSearch={true}
onSearch={setSearch}
>
{item => <Item key={item}>{item}</Item>}
</Dropdown>
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button>
</Flexbox>
);
}
export default TimezoneSetting;

View file

@ -0,0 +1,16 @@
import ProfileHeader from './ProfileHeader';
import ProfileSettings from './ProfileSettings';
import { Metadata } from 'next';
export default function () {
return (
<>
<ProfileHeader />
<ProfileSettings />
</>
);
}
export const metadata: Metadata = {
title: 'Profile | Umami',
};