mirror of
https://github.com/umami-software/umami.git
synced 2026-02-14 01:25:37 +01:00
Added settings layout.
This commit is contained in:
parent
d818bf5aaf
commit
6802093d69
22 changed files with 102 additions and 76 deletions
29
src/app/(main)/profile/DateRangeSetting.tsx
Normal file
29
src/app/(main)/profile/DateRangeSetting.tsx
Normal 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;
|
||||
4
src/app/(main)/profile/LanguageSetting.module.css
Normal file
4
src/app/(main)/profile/LanguageSetting.module.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
div.menu {
|
||||
max-height: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
44
src/app/(main)/profile/LanguageSetting.tsx
Normal file
44
src/app/(main)/profile/LanguageSetting.tsx
Normal 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;
|
||||
32
src/app/(main)/profile/PasswordChangeButton.tsx
Normal file
32
src/app/(main)/profile/PasswordChangeButton.tsx
Normal 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;
|
||||
70
src/app/(main)/profile/PasswordEditForm.tsx
Normal file
70
src/app/(main)/profile/PasswordEditForm.tsx
Normal 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;
|
||||
11
src/app/(main)/profile/ProfileHeader.tsx
Normal file
11
src/app/(main)/profile/ProfileHeader.tsx
Normal 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;
|
||||
61
src/app/(main)/profile/ProfileSettings.tsx
Normal file
61
src/app/(main)/profile/ProfileSettings.tsx
Normal 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;
|
||||
8
src/app/(main)/profile/ThemeSetting.module.css
Normal file
8
src/app/(main)/profile/ThemeSetting.module.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.active {
|
||||
border: 2px solid var(--primary400);
|
||||
}
|
||||
34
src/app/(main)/profile/ThemeSetting.tsx
Normal file
34
src/app/(main)/profile/ThemeSetting.tsx
Normal 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;
|
||||
4
src/app/(main)/profile/TimezoneSetting.module.css
Normal file
4
src/app/(main)/profile/TimezoneSetting.module.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
div.menu {
|
||||
max-height: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
36
src/app/(main)/profile/TimezoneSetting.tsx
Normal file
36
src/app/(main)/profile/TimezoneSetting.tsx
Normal 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;
|
||||
16
src/app/(main)/profile/page.tsx
Normal file
16
src/app/(main)/profile/page.tsx
Normal 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',
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue