Reworked settings screens.
This commit is contained in:
26
src/app/(main)/settings/profile/DateRangeSetting.tsx
Normal file
26
src/app/(main)/settings/profile/DateRangeSetting.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { DateFilter } from '@/components/input/DateFilter';
|
||||
import { Button, Row } from '@umami/react-zen';
|
||||
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, saveDateRange } = useDateRange();
|
||||
const { value } = dateRange;
|
||||
|
||||
const handleChange = (value: string | DateRange) => saveDateRange(value);
|
||||
const handleReset = () => saveDateRange(DEFAULT_DATE_RANGE);
|
||||
|
||||
return (
|
||||
<Row gap="3">
|
||||
<DateFilter
|
||||
value={value}
|
||||
startDate={dateRange.startDate}
|
||||
endDate={dateRange.endDate}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Button onPress={handleReset}>{formatMessage(labels.reset)}</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
48
src/app/(main)/settings/profile/LanguageSetting.tsx
Normal file
48
src/app/(main)/settings/profile/LanguageSetting.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Select, ListItem, Row } from '@umami/react-zen';
|
||||
import { useLocale, useMessages } from '@/components/hooks';
|
||||
import { DEFAULT_LOCALE } from '@/lib/constants';
|
||||
import { languages } from '@/lib/lang';
|
||||
|
||||
export function LanguageSetting() {
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale, saveLocale } = useLocale();
|
||||
const items = 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 handleOpen = isOpen => {
|
||||
if (isOpen) {
|
||||
setSearch('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gap="3">
|
||||
<Select
|
||||
selectedKey={locale}
|
||||
onChange={val => saveLocale(val as string)}
|
||||
allowSearch
|
||||
onSearch={setSearch}
|
||||
onOpenChange={handleOpen}
|
||||
listProps={{ style: { maxHeight: '300px' } }}
|
||||
>
|
||||
{items.map(item => (
|
||||
<ListItem key={item} id={item}>
|
||||
{languages[item].label}
|
||||
</ListItem>
|
||||
))}
|
||||
{!items.length && <ListItem></ListItem>}
|
||||
</Select>
|
||||
<Button onPress={handleReset}>{formatMessage(labels.reset)}</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
29
src/app/(main)/settings/profile/PasswordChangeButton.tsx
Normal file
29
src/app/(main)/settings/profile/PasswordChangeButton.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Button, Icon, Text, useToast, DialogTrigger, Dialog, Modal } from '@umami/react-zen';
|
||||
import { PasswordEditForm } from './PasswordEditForm';
|
||||
import { Icons } from '@/components/icons';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export function PasswordChangeButton() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleSave = () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button>
|
||||
<Icon fillColor="currentColor">
|
||||
<Icons.Lock />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.changePassword)}</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.changePassword)}>
|
||||
{({ close }) => <PasswordEditForm onSave={handleSave} onClose={close} />}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
70
src/app/(main)/settings/profile/PasswordEditForm.tsx
Normal file
70
src/app/(main)/settings/profile/PasswordEditForm.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
Form,
|
||||
FormField,
|
||||
FormButtons,
|
||||
PasswordField,
|
||||
Button,
|
||||
FormSubmitButton,
|
||||
} from '@umami/react-zen';
|
||||
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 handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
onSave();
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const samePassword = (value: string, values: { [key: string]: any }) => {
|
||||
if (value !== values.newPassword) {
|
||||
return formatMessage(messages.noMatchPassword);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={error}>
|
||||
<FormField
|
||||
label={formatMessage(labels.currentPassword)}
|
||||
name="currentPassword"
|
||||
rules={{ required: 'Required' }}
|
||||
>
|
||||
<PasswordField autoComplete="current-password" />
|
||||
</FormField>
|
||||
<FormField
|
||||
name="newPassword"
|
||||
label={formatMessage(labels.newPassword)}
|
||||
rules={{
|
||||
required: 'Required',
|
||||
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
|
||||
}}
|
||||
>
|
||||
<PasswordField autoComplete="new-password" />
|
||||
</FormField>
|
||||
<FormField
|
||||
name="confirmPassword"
|
||||
label={formatMessage(labels.confirmPassword)}
|
||||
rules={{
|
||||
required: formatMessage(labels.required),
|
||||
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
|
||||
validate: samePassword,
|
||||
}}
|
||||
>
|
||||
<PasswordField autoComplete="confirm-password" />
|
||||
</FormField>
|
||||
<FormButtons>
|
||||
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
<FormSubmitButton isDisabled={isPending}>{formatMessage(labels.save)}</FormSubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
8
src/app/(main)/settings/profile/ProfileHeader.tsx
Normal file
8
src/app/(main)/settings/profile/ProfileHeader.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export function ProfileHeader() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return <SectionHeader title={formatMessage(labels.profile)}></SectionHeader>;
|
||||
}
|
||||
16
src/app/(main)/settings/profile/ProfilePage.tsx
Normal file
16
src/app/(main)/settings/profile/ProfilePage.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
import { ProfileSettings } from './ProfileSettings';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
|
||||
export function ProfilePage() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title={formatMessage(labels.profile)} />
|
||||
|
||||
<ProfileSettings />
|
||||
</>
|
||||
);
|
||||
}
|
||||
77
src/app/(main)/settings/profile/ProfileSettings.tsx
Normal file
77
src/app/(main)/settings/profile/ProfileSettings.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Row, Column, Label } from '@umami/react-zen';
|
||||
import { useLoginQuery, useMessages } from '@/components/hooks';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
import { TimezoneSetting } from './TimezoneSetting';
|
||||
import { DateRangeSetting } from './DateRangeSetting';
|
||||
import { LanguageSetting } from './LanguageSetting';
|
||||
import { ThemeSetting } from './ThemeSetting';
|
||||
import { PasswordChangeButton } from './PasswordChangeButton';
|
||||
|
||||
export function ProfileSettings() {
|
||||
const { user } = useLoginQuery();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { username, role } = user;
|
||||
|
||||
const renderRole = (value: string) => {
|
||||
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 (
|
||||
<Column gap="6">
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.username)}</Label>
|
||||
{username}
|
||||
</Column>
|
||||
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.role)}</Label>
|
||||
{renderRole(role)}
|
||||
</Column>
|
||||
|
||||
{!cloudMode && (
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.password)}</Label>
|
||||
<Row>
|
||||
<PasswordChangeButton />
|
||||
</Row>
|
||||
</Column>
|
||||
)}
|
||||
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.defaultDateRange)}</Label>
|
||||
<DateRangeSetting />
|
||||
</Column>
|
||||
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.language)}</Label>
|
||||
<LanguageSetting />
|
||||
</Column>
|
||||
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.timezone)}</Label>
|
||||
<TimezoneSetting />
|
||||
</Column>
|
||||
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.theme)}</Label>
|
||||
<ThemeSetting />
|
||||
</Column>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
24
src/app/(main)/settings/profile/ThemeSetting.tsx
Normal file
24
src/app/(main)/settings/profile/ThemeSetting.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Row, Button, Icon, useTheme } from '@umami/react-zen';
|
||||
import { Icons } from '@/components/icons';
|
||||
|
||||
export function ThemeSetting() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<Row gap>
|
||||
<Button
|
||||
variant={theme === 'light' ? 'primary' : 'secondary'}
|
||||
onPress={() => setTheme('light')}
|
||||
>
|
||||
<Icon fillColor="currentColor">
|
||||
<Icons.Sun />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Button variant={theme === 'dark' ? 'primary' : 'secondary'} onPress={() => setTheme('dark')}>
|
||||
<Icon fillColor="currentColor">
|
||||
<Icons.Moon />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
44
src/app/(main)/settings/profile/TimezoneSetting.tsx
Normal file
44
src/app/(main)/settings/profile/TimezoneSetting.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useState } from 'react';
|
||||
import { Row, Select, ListItem, Button } from '@umami/react-zen';
|
||||
import { useTimezone, useMessages } from '@/components/hooks';
|
||||
import { getTimezone } from '@/lib/date';
|
||||
|
||||
const timezones = Intl.supportedValuesOf('timeZone');
|
||||
|
||||
export function TimezoneSetting() {
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { timezone, saveTimezone } = useTimezone();
|
||||
const items = search
|
||||
? timezones.filter(n => n.toLowerCase().includes(search.toLowerCase()))
|
||||
: timezones;
|
||||
|
||||
const handleReset = () => saveTimezone(getTimezone());
|
||||
|
||||
const handleOpen = isOpen => {
|
||||
if (isOpen) {
|
||||
setSearch('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gap="3">
|
||||
<Select
|
||||
selectedKey={timezone}
|
||||
onChange={(value: any) => saveTimezone(value)}
|
||||
allowSearch={true}
|
||||
onSearch={setSearch}
|
||||
onOpenChange={handleOpen}
|
||||
listProps={{ style: { maxHeight: '300px' } }}
|
||||
>
|
||||
{items.map((item: any) => (
|
||||
<ListItem key={item} id={item}>
|
||||
{item}
|
||||
</ListItem>
|
||||
))}
|
||||
{!items.length && <ListItem></ListItem>}
|
||||
</Select>
|
||||
<Button onPress={handleReset}>{formatMessage(labels.reset)}</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
10
src/app/(main)/settings/profile/page.tsx
Normal file
10
src/app/(main)/settings/profile/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Metadata } from 'next';
|
||||
import { ProfilePage } from './ProfilePage';
|
||||
|
||||
export default function () {
|
||||
return <ProfilePage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Profile',
|
||||
};
|
||||
Reference in New Issue
Block a user