Updated settings components and date filter.

This commit is contained in:
Mike Cao 2023-01-10 17:18:59 -08:00
parent 70ef857dc7
commit 4b5b4db108
16 changed files with 172 additions and 194 deletions

View File

@ -1 +1 @@
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M437.02 74.98C388.668 26.63 324.379 0 256 0S123.332 26.629 74.98 74.98C26.63 123.332 0 187.621 0 256s26.629 132.668 74.98 181.02C123.332 485.37 187.621 512 256 512s132.668-26.629 181.02-74.98C485.37 388.668 512 324.379 512 256s-26.629-132.668-74.98-181.02zM111.105 429.297c8.454-72.735 70.989-128.89 144.895-128.89 38.96 0 75.598 15.179 103.156 42.734 23.281 23.285 37.965 53.687 41.742 86.152C361.641 462.172 311.094 482 256 482s-105.637-19.824-144.895-52.703zM256 269.507c-42.871 0-77.754-34.882-77.754-77.753C178.246 148.879 213.13 114 256 114s77.754 34.879 77.754 77.754c0 42.871-34.883 77.754-77.754 77.754zm170.719 134.427a175.9 175.9 0 0 0-46.352-82.004c-18.437-18.438-40.25-32.27-64.039-40.938 28.598-19.394 47.426-52.16 47.426-89.238C363.754 132.34 315.414 84 256 84s-107.754 48.34-107.754 107.754c0 37.098 18.844 69.875 47.465 89.266-21.887 7.976-42.14 20.308-59.566 36.542-25.235 23.5-42.758 53.465-50.883 86.348C50.852 364.242 30 312.512 30 256 30 131.383 131.383 30 256 30s226 101.383 226 226c0 56.523-20.86 108.266-55.281 147.934zm0 0"/></svg> <svg height="512pt" viewBox="-56 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m267 236.375c36.253906-22.582031 60.433594-62.796875 60.433594-108.5625 0-70.476562-57.335938-127.8125-127.8125-127.8125-70.476563 0-127.8125 57.335938-127.8125 127.8125 0 45.765625 24.179687 85.976562 60.429687 108.558594-77.015625 27.699218-132.238281 101.46875-132.238281 187.902344v72.242187c0 8.550781 6.933594 15.484375 15.484375 15.484375h368.265625c8.550781 0 15.480469-6.933594 15.480469-15.484375v-72.242187c0-86.429688-55.21875-160.195313-132.230469-187.898438zm101.265625 244.65625h-337.296875v-56.757812c0-92.992188 75.652344-168.644532 168.648438-168.644532 92.992187 0 168.648437 75.652344 168.648437 168.644532zm-71.800781-353.21875c0 53.402344-43.441406 96.847656-96.84375 96.847656s-96.84375-43.445312-96.84375-96.847656c0-53.398438 43.441406-96.84375 96.84375-96.84375s96.84375 43.445312 96.84375 96.84375zm0 0"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 940 B

View File

@ -1,99 +1,109 @@
import Calendar from 'assets/calendar-alt.svg';
import DatePickerForm from 'components/metrics/DatePickerForm';
import { endOfYear, isSameDay } from 'date-fns'; import { endOfYear, isSameDay } from 'date-fns';
import { useState } from 'react';
import { Icon, Modal, Dropdown, Item } from 'react-basics';
import { useIntl, defineMessages } from 'react-intl';
import DatePickerForm from 'components/metrics/DatePickerForm';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import { dateFormat } from 'lib/date'; import { dateFormat } from 'lib/date';
import PropTypes from 'prop-types'; import Calendar from 'assets/calendar-alt.svg';
import { useState } from 'react';
import { Icon, Modal } from 'react-basics';
import { FormattedMessage } from 'react-intl';
import DropDown from './DropDown';
export const filterOptions = [ const messages = defineMessages({
{ label: <FormattedMessage id="label.today" defaultMessage="Today" />, value: '1day' }, today: { id: 'label.today', defaultMessage: 'Today' },
{ lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' },
label: ( yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' },
<FormattedMessage id="label.last-hours" defaultMessage="Last {x} hours" values={{ x: 24 }} /> thisWeek: { id: 'label.this-week', defaultMessage: 'This week' },
), lastDays: { id: 'label.last-days', defaultMessage: 'Last {x} days' },
value: '24hour', thisMonth: { id: 'label.this-month', defaultMessage: 'This month' },
}, thisYear: { id: 'label.this-year', defaultMessage: 'This year' },
{ allTime: { id: 'label.all-time', defaultMessage: 'All time' },
label: <FormattedMessage id="label.yesterday" defaultMessage="Yesterday" />, customRange: { id: 'label.custom-range', defaultMessage: 'Custom-range' },
value: '-1day', });
},
{
label: <FormattedMessage id="label.this-week" defaultMessage="This week" />,
value: '1week',
divider: true,
},
{
label: (
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 7 }} />
),
value: '7day',
},
{
label: <FormattedMessage id="label.this-month" defaultMessage="This month" />,
value: '1month',
divider: true,
},
{
label: (
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 30 }} />
),
value: '30day',
},
{
label: (
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 90 }} />
),
value: '90day',
},
{ label: <FormattedMessage id="label.this-year" defaultMessage="This year" />, value: '1year' },
{
label: <FormattedMessage id="label.all-time" defaultMessage="All time" />,
value: 'all',
divider: true,
},
{
label: <FormattedMessage id="label.custom-range" defaultMessage="Custom range" />,
value: 'custom',
divider: true,
},
];
function DateFilter({ value, startDate, endDate, onChange, className, options }) { function DateFilter({ value, startDate, endDate, onChange, className }) {
const { formatMessage } = useIntl();
const [showPicker, setShowPicker] = useState(false); const [showPicker, setShowPicker] = useState(false);
const displayValue =
value === 'custom' ? ( const options = [
{ label: formatMessage(messages.today), value: '1day' },
{
label: formatMessage(messages.lastHours, { x: 24 }),
value: '24hour',
},
{
label: formatMessage(messages.yesterday),
value: '-1day',
},
{
label: formatMessage(messages.thisWeek),
value: '1week',
divider: true,
},
{
label: formatMessage(messages.lastDays, { x: 7 }),
value: '7day',
},
{
label: formatMessage(messages.thisMonth),
value: '1month',
divider: true,
},
{
label: formatMessage(messages.lastDays, { x: 30 }),
value: '30day',
},
{
label: formatMessage(messages.lastDays, { x: 90 }),
value: '90day',
},
{ label: formatMessage(messages.thisYear), value: '1year' },
{
label: formatMessage(messages.allTime),
value: 'all',
divider: true,
},
{
label: formatMessage(messages.customRange),
value: 'custom',
divider: true,
},
];
const renderValue = value => {
return value === 'custom' ? (
<CustomRange startDate={startDate} endDate={endDate} onClick={() => handleChange('custom')} /> <CustomRange startDate={startDate} endDate={endDate} onClick={() => handleChange('custom')} />
) : ( ) : (
value options.find(e => e.value === value).label
); );
};
async function handleChange(value) { const handleChange = async value => {
if (value === 'custom') { if (value === 'custom') {
setShowPicker(true); setShowPicker(true);
return; return;
} }
onChange(value); onChange(value);
} };
function handlePickerChange(value) { const handlePickerChange = value => {
setShowPicker(false); setShowPicker(false);
onChange(value); onChange(value);
} };
const handleClose = () => setShowPicker(false);
return ( return (
<> <>
<DropDown <Dropdown
className={className} className={className}
value={displayValue} items={options}
options={options || filterOptions} renderValue={renderValue}
value={value}
onChange={handleChange} onChange={handleChange}
/> >
{({ label, value }) => <Item key={value}>{label}</Item>}
</Dropdown>
{showPicker && ( {showPicker && (
<Modal> <Modal onClose={handleClose}>
<DatePickerForm <DatePickerForm
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
@ -128,12 +138,4 @@ const CustomRange = ({ startDate, endDate, onClick }) => {
); );
}; };
DateFilter.propTypes = {
value: PropTypes.string,
startDate: PropTypes.instanceOf(Date),
endDate: PropTypes.instanceOf(Date),
onChange: PropTypes.func,
className: PropTypes.string,
};
export default DateFilter; export default DateFilter;

View File

@ -66,8 +66,9 @@ export default function WebsiteChart({
async function handleDateChange(value) { async function handleDateChange(value) {
if (value === 'all') { if (value === 'all') {
const { data, ok } = await get(`/websites/${websiteId}`); const data = await get(`/websites/${websiteId}`);
if (ok) {
if (data) {
setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }); setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) });
} }
} else { } else {

View File

@ -1,12 +1,13 @@
import User from 'assets/user.svg';
import Team from 'assets/users.svg';
import Website from 'assets/website.svg';
import classNames from 'classnames'; import classNames from 'classnames';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { Icon, Item, Menu, Text } from 'react-basics'; import { Icon, Item, Menu, Text } from 'react-basics';
import styles from './Nav.module.css';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import User from 'assets/user.svg';
import Team from 'assets/users.svg';
import Website from 'assets/website.svg';
import Profile from 'assets/profile.svg';
import styles from './Nav.module.css';
export default function Nav() { export default function Nav() {
const user = useUser(); const user = useUser();
@ -22,7 +23,7 @@ export default function Nav() {
{ icon: <Website />, label: 'Websites', url: '/settings/websites' }, { icon: <Website />, label: 'Websites', url: '/settings/websites' },
{ icon: <User />, label: 'Users', url: '/settings/users' }, { icon: <User />, label: 'Users', url: '/settings/users' },
{ icon: <Team />, label: 'Teams', url: '/settings/teams' }, { icon: <Team />, label: 'Teams', url: '/settings/teams' },
{ icon: <User />, label: 'Profile', url: '/settings/profile' }, { icon: <Profile />, label: 'Profile', url: '/settings/profile' },
]; ];
return ( return (

View File

@ -27,7 +27,7 @@
} }
.item a { .item a {
color: var(--base700); color: var(--base600);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
@ -42,5 +42,4 @@
.item.selected a { .item.selected a {
color: var(--base900); color: var(--base900);
background: var(--base100);
} }

View File

@ -2,13 +2,12 @@ import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import ProfileDetails from 'components/settings/ProfileDetails'; import ProfileDetails from 'components/settings/ProfileDetails';
import { useState } from 'react'; import { useState } from 'react';
import { Breadcrumbs, Icon, Item, Tabs, useToast, Modal, Button } from 'react-basics'; import { Breadcrumbs, Icon, Item, useToast, Modal, Button } from 'react-basics';
import UserPasswordForm from 'components/pages/settings/users/UserPasswordForm'; import UserPasswordForm from 'components/pages/settings/users/UserPasswordForm';
import Pen from 'assets/pen.svg'; import Lock from 'assets/lock.svg';
export default function ProfileSettings() { export default function ProfileSettings() {
const [edit, setEdit] = useState(false); const [edit, setEdit] = useState(false);
const [tab, setTab] = useState('general');
const { toast, showToast } = useToast(); const { toast, showToast } = useToast();
const handleSave = () => { const handleSave = () => {
@ -33,17 +32,14 @@ export default function ProfileSettings() {
</Breadcrumbs> </Breadcrumbs>
<Button onClick={handleAdd}> <Button onClick={handleAdd}>
<Icon> <Icon>
<Pen /> <Lock />
</Icon> </Icon>
Change Password Change Password
</Button> </Button>
</PageHeader> </PageHeader>
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}> <ProfileDetails />
<Item key="general">General</Item>
</Tabs>
{tab === 'general' && <ProfileDetails />}
{edit && ( {edit && (
<Modal title="Add website" onClose={handleClose}> <Modal title="Change password" onClose={handleClose}>
{close => <UserPasswordForm onSave={handleSave} onClose={close} />} {close => <UserPasswordForm onSave={handleSave} onClose={close} />}
</Modal> </Modal>
)} )}

View File

@ -12,7 +12,6 @@ import { useRef } from 'react';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import styles from './UserForm.module.css';
const items = [ const items = [
{ {
@ -41,14 +40,7 @@ export default function UserEditForm({ data, onSave }) {
}; };
return ( return (
<Form <Form key={id} ref={ref} onSubmit={handleSubmit} error={error} values={data}>
key={id}
className={styles.form}
ref={ref}
onSubmit={handleSubmit}
error={error}
values={data}
>
<FormRow label="Username"> <FormRow label="Username">
<FormInput name="username"> <FormInput name="username">
<TextField /> <TextField />

View File

@ -1,30 +1,28 @@
import { FormattedMessage } from 'react-intl'; import { useIntl, defineMessages } from 'react-intl';
import DropDown from 'components/common/DropDown'; import { Button, Dropdown, Item, Flexbox } from 'react-basics';
import { Button } from 'react-basics';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import { DEFAULT_LOCALE } from 'lib/constants'; import { DEFAULT_LOCALE } from 'lib/constants';
import styles from './TimezoneSetting.module.css';
import { languages } from 'lib/lang'; import { languages } from 'lib/lang';
const messages = defineMessages({
reset: { id: 'label.reset', defaultMessage: 'Reset' },
});
export default function LanguageSetting() { export default function LanguageSetting() {
const { formatMessage } = useIntl();
const { locale, saveLocale } = useLocale(); const { locale, saveLocale } = useLocale();
const options = Object.keys(languages).map(key => ({ ...languages[key], value: key })); const options = Object.keys(languages);
function handleReset() { function handleReset() {
saveLocale(DEFAULT_LOCALE); saveLocale(DEFAULT_LOCALE);
} }
return ( return (
<> <Flexbox gap={10} style={{ width: 400 }}>
<DropDown <Dropdown items={options} value={locale} onChange={saveLocale}>
menuClassName={styles.menu} {item => <Item key={item}>{languages[item].label}</Item>}
value={locale} </Dropdown>
options={options} <Button onClick={handleReset}>{formatMessage(messages.reset)}</Button>
onChange={saveLocale} </Flexbox>
/>
<Button className={styles.button} size="sm" onClick={handleReset}>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
</>
); );
} }

View File

@ -1,53 +1,46 @@
import { Form, FormRow } from 'react-basics';
import { useIntl, defineMessages } from 'react-intl';
import TimezoneSetting from 'components/settings/TimezoneSetting'; import TimezoneSetting from 'components/settings/TimezoneSetting';
import DateRangeSetting from 'components/settings/DateRangeSetting';
import LanguageSetting from 'components/settings/LanguageSetting';
import ThemeSetting from 'components/settings/ThemeSetting';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import { FormattedMessage } from 'react-intl'; const messages = defineMessages({
import DateRangeSetting from './DateRangeSetting'; username: { id: 'label.username', defaultMessage: 'Username' },
import LanguageSetting from './LanguageSetting'; role: { id: 'label.role', defaultMessage: 'Role' },
import styles from './ProfileDetails.module.css'; timezone: { id: 'label.timezone', defaultMessage: 'Timezone' },
import ThemeSetting from './ThemeSetting'; dateRange: { id: 'label.default-date-range', defaultMessage: 'Default date range' },
language: { id: 'label.language', defaultMessage: 'Language' },
theme: { id: 'label.theme', defaultMessage: 'Theme' },
});
export default function ProfileDetails() { export default function ProfileDetails() {
const user = useUser(); const user = useUser();
const { formatMessage } = useIntl();
if (!user) { if (!user) {
return null; return null;
} }
const { username } = user; const { username, role } = user;
return ( return (
<> <Form>
<dl className={styles.list}> <FormRow label={formatMessage(messages.username)}>{username}</FormRow>
<dt> <FormRow label={formatMessage(messages.role)}>{role}</FormRow>
<FormattedMessage id="label.username" defaultMessage="Username" /> <FormRow label={formatMessage(messages.language)} inline>
</dt> <LanguageSetting />
<dd>{username}</dd> </FormRow>
<dt> <FormRow label={formatMessage(messages.timezone)} inline>
<FormattedMessage id="label.timezone" defaultMessage="Timezone" /> <TimezoneSetting />
</dt> </FormRow>
<dd> <FormRow label={formatMessage(messages.dateRange)} inline>
<TimezoneSetting /> <DateRangeSetting />
</dd> </FormRow>
<dt> <FormRow label={formatMessage(messages.theme)}>
<FormattedMessage id="label.default-date-range" defaultMessage="Default date range" /> <ThemeSetting />
</dt> </FormRow>
<dd> </Form>
<DateRangeSetting />
</dd>
<dt>
<FormattedMessage id="label.language" defaultMessage="Language" />
</dt>
<dd>
<LanguageSetting />
</dd>
<dt>
<FormattedMessage id="label.theme" defaultMessage="Theme" />
</dt>
<dd>
<ThemeSetting />
</dd>
</dl>
</>
); );
} }

View File

@ -1,14 +1,17 @@
import { FormattedMessage } from 'react-intl'; import { Dropdown, Item, Button } from 'react-basics';
import { useIntl, defineMessages } from 'react-intl';
import { listTimeZones } from 'timezone-support'; import { listTimeZones } from 'timezone-support';
import DropDown from 'components/common/DropDown';
import { Button } from 'react-basics';
import useTimezone from 'hooks/useTimezone'; import useTimezone from 'hooks/useTimezone';
import { getTimezone } from 'lib/date'; import { getTimezone } from 'lib/date';
import styles from './TimezoneSetting.module.css';
const messages = defineMessages({
reset: { id: 'label.reset', defaultMessage: 'Reset' },
});
export default function TimezoneSetting() { export default function TimezoneSetting() {
const { formatMessage } = useIntl();
const [timezone, saveTimezone] = useTimezone(); const [timezone, saveTimezone] = useTimezone();
const options = listTimeZones().map(n => ({ label: n, value: n })); const options = listTimeZones();
function handleReset() { function handleReset() {
saveTimezone(getTimezone()); saveTimezone(getTimezone());
@ -16,15 +19,10 @@ export default function TimezoneSetting() {
return ( return (
<> <>
<DropDown <Dropdown items={options} value={timezone} onChange={saveTimezone}>
menuClassName={styles.menu} {item => <Item key={item}>{item}</Item>}
value={timezone} </Dropdown>
options={options} <Button onClick={handleReset}>{formatMessage(messages.reset)}</Button>
onChange={saveTimezone}
/>
<Button className={styles.button} size="sm" onClick={handleReset}>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
</> </>
); );
} }

View File

@ -1,8 +0,0 @@
.menu {
max-height: 300px;
overflow-y: auto;
}
.button {
margin-left: 10px;
}

View File

@ -1,4 +1,3 @@
import User from 'assets/user.svg';
import useConfig from 'hooks/useConfig'; import useConfig from 'hooks/useConfig';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import { AUTH_TOKEN } from 'lib/constants'; import { AUTH_TOKEN } from 'lib/constants';
@ -7,8 +6,9 @@ import { useRouter } from 'next/router';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { Button, Icon, Item, Menu, Popup, Text } from 'react-basics'; import { Button, Icon, Item, Menu, Popup, Text } from 'react-basics';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import useDocumentClick from 'hooks/useDocumentClick';
import Profile from 'assets/profile.svg';
import styles from './UserButton.module.css'; import styles from './UserButton.module.css';
import useDocumentClick from '../../hooks/useDocumentClick';
export default function UserButton() { export default function UserButton() {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
@ -61,7 +61,7 @@ export default function UserButton() {
<div className={styles.button} ref={ref}> <div className={styles.button} ref={ref}>
<Button variant="light" onClick={handleClick}> <Button variant="light" onClick={handleClick}>
<Icon className={styles.icon} size="large"> <Icon className={styles.icon} size="large">
<User /> <Profile />
</Icon> </Icon>
</Button> </Button>
{show && ( {show && (

View File

@ -12,9 +12,9 @@ export default function useCountryNames(locale) {
const { basePath } = useRouter(); const { basePath } = useRouter();
async function loadData(locale) { async function loadData(locale) {
const { ok, data } = await get(`${basePath}/intl/country/${locale}.json`); const data = await get(`${basePath}/intl/country/${locale}.json`);
if (ok) { if (data) {
countryNames[locale] = data; countryNames[locale] = data;
setList(countryNames[locale]); setList(countryNames[locale]);
} else { } else {

View File

@ -12,9 +12,9 @@ export default function useLanguageNames(locale) {
const { basePath } = useRouter(); const { basePath } = useRouter();
async function loadData(locale) { async function loadData(locale) {
const { ok, data } = await get(`${basePath}/intl/language/${locale}.json`); const data = await get(`${basePath}/intl/language/${locale}.json`);
if (ok) { if (data) {
languageNames[locale] = data; languageNames[locale] = data;
setList(languageNames[locale]); setList(languageNames[locale]);
} else { } else {

View File

@ -21,9 +21,9 @@ export default function useLocale() {
const dateLocale = getDateLocale(locale); const dateLocale = getDateLocale(locale);
async function loadMessages(locale) { async function loadMessages(locale) {
const { ok, data } = await get(`${basePath}/intl/messages/${locale}.json`); const data = await get(`${basePath}/intl/messages/${locale}.json`);
if (ok) { if (data) {
messages[locale] = data; messages[locale] = data;
} }
} }

View File

@ -11,7 +11,13 @@ import 'styles/index.css';
import '@fontsource/inter/400.css'; import '@fontsource/inter/400.css';
import '@fontsource/inter/600.css'; import '@fontsource/inter/600.css';
const client = new QueryClient(); const client = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
export default function App({ Component, pageProps }) { export default function App({ Component, pageProps }) {
const { locale, messages } = useLocale(); const { locale, messages } = useLocale();