Initial commit

Created from https://vercel.com/new
This commit is contained in:
WaylonWalker 2021-12-08 18:25:42 +00:00
commit 4047a7ec23
378 changed files with 29334 additions and 0 deletions

View file

@ -0,0 +1,132 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import Link from 'next/link';
import classNames from 'classnames';
import PageHeader from 'components/layout/PageHeader';
import Button from 'components/common/Button';
import Icon from 'components/common/Icon';
import Table from 'components/common/Table';
import Modal from 'components/common/Modal';
import Toast from 'components/common/Toast';
import AccountEditForm from 'components/forms/AccountEditForm';
import ButtonLayout from 'components/layout/ButtonLayout';
import DeleteForm from 'components/forms/DeleteForm';
import useFetch from 'hooks/useFetch';
import Pen from 'assets/pen.svg';
import Plus from 'assets/plus.svg';
import Trash from 'assets/trash.svg';
import Check from 'assets/check.svg';
import LinkIcon from 'assets/external-link.svg';
import styles from './AccountSettings.module.css';
export default function AccountSettings() {
const [addAccount, setAddAccount] = useState();
const [editAccount, setEditAccount] = useState();
const [deleteAccount, setDeleteAccount] = useState();
const [saved, setSaved] = useState(0);
const [message, setMessage] = useState();
const { data } = useFetch(`/api/accounts`, {}, [saved]);
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
const DashboardLink = row => (
<Link href={`/dashboard/${row.user_id}/${row.username}`}>
<a>
<Icon icon={<LinkIcon />} />
</a>
</Link>
);
const Buttons = row =>
row.username !== 'admin' ? (
<ButtonLayout align="right">
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
<FormattedMessage id="label.edit" defaultMessage="Edit" />
</Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
<FormattedMessage id="label.delete" defaultMessage="Delete" />
</Button>
</ButtonLayout>
) : null;
const columns = [
{
key: 'username',
label: <FormattedMessage id="label.username" defaultMessage="Username" />,
className: 'col-4 col-md-3',
},
{
key: 'is_admin',
label: <FormattedMessage id="label.administrator" defaultMessage="Administrator" />,
className: 'col-4 col-md-3',
render: Checkmark,
},
{
key: 'dashboard',
label: <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />,
className: 'col-4 col-md-3',
render: DashboardLink,
},
{
key: 'actions',
className: classNames(styles.buttons, 'col-12 col-md-3 pt-2 pt-md-0'),
render: Buttons,
},
];
function handleSave() {
setSaved(state => state + 1);
setMessage(<FormattedMessage id="message.save-success" defaultMessage="Saved successfully." />);
handleClose();
}
function handleClose() {
setEditAccount(null);
setAddAccount(null);
setDeleteAccount(null);
}
if (!data) {
return null;
}
return (
<>
<PageHeader>
<div>
<FormattedMessage id="label.accounts" defaultMessage="Accounts" />
</div>
<Button icon={<Plus />} size="small" onClick={() => setAddAccount(true)}>
<FormattedMessage id="label.add-account" defaultMessage="Add account" />
</Button>
</PageHeader>
<Table columns={columns} rows={data} />
{editAccount && (
<Modal title={<FormattedMessage id="label.edit-account" defaultMessage="Edit account" />}>
<AccountEditForm
values={{ ...editAccount, password: '' }}
onSave={handleSave}
onClose={handleClose}
/>
</Modal>
)}
{addAccount && (
<Modal title={<FormattedMessage id="label.add-account" defaultMessage="Add account" />}>
<AccountEditForm onSave={handleSave} onClose={handleClose} />
</Modal>
)}
{deleteAccount && (
<Modal
title={<FormattedMessage id="label.delete-account" defaultMessage="Delete account" />}
>
<DeleteForm
values={{ type: 'account', id: deleteAccount.user_id, name: deleteAccount.username }}
onSave={handleSave}
onClose={handleClose}
/>
</Modal>
)}
{message && <Toast message={message} onClose={() => setMessage(null)} />}
</>
);
}

View file

@ -0,0 +1,5 @@
.buttons {
display: flex;
justify-content: flex-end;
flex: 1;
}

View file

@ -0,0 +1,28 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import DateFilter from 'components/common/DateFilter';
import Button from 'components/common/Button';
import useDateRange from 'hooks/useDateRange';
import { DEFAULT_DATE_RANGE } from 'lib/constants';
import { getDateRange } from 'lib/date';
import styles from './DateRangeSetting.module.css';
import useLocale from 'hooks/useLocale';
export default function DateRangeSetting() {
const { locale } = useLocale();
const [dateRange, setDateRange] = useDateRange();
const { startDate, endDate, value } = dateRange;
function handleReset() {
setDateRange(getDateRange(DEFAULT_DATE_RANGE, locale));
}
return (
<>
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={setDateRange} />
<Button className={styles.button} size="small" onClick={handleReset}>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
</>
);
}

View file

@ -0,0 +1,3 @@
.button {
margin-left: 10px;
}

View file

@ -0,0 +1,26 @@
import React from 'react';
import { languages } from 'lib/lang';
import useLocale from 'hooks/useLocale';
import MenuButton from 'components/common/MenuButton';
import Globe from 'assets/globe.svg';
import styles from './LanguageButton.module.css';
export default function LanguageButton() {
const { locale, saveLocale } = useLocale();
const menuOptions = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
function handleSelect(value) {
saveLocale(value);
}
return (
<MenuButton
icon={<Globe />}
options={menuOptions}
value={locale}
menuClassName={styles.menu}
onSelect={handleSelect}
hideLabel
/>
);
}

View file

@ -0,0 +1,25 @@
.menu {
display: flex;
flex-flow: row wrap;
min-width: 560px;
max-width: 100vw;
padding: 10px;
}
.menu div {
border-radius: 5px;
min-width: calc(100% / 3);
}
@media only screen and (max-width: 992px) {
.menu {
min-width: 90vw;
transform: translateX(calc(40vw));
}
}
@media only screen and (max-width: 768px) {
.menu div {
min-width: 50%;
}
}

View file

@ -0,0 +1,72 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import PageHeader from 'components/layout/PageHeader';
import Button from 'components/common/Button';
import Modal from 'components/common/Modal';
import Toast from 'components/common/Toast';
import ChangePasswordForm from 'components/forms/ChangePasswordForm';
import TimezoneSetting from 'components/settings/TimezoneSetting';
import Dots from 'assets/ellipsis-h.svg';
import styles from './ProfileSettings.module.css';
import DateRangeSetting from './DateRangeSetting';
import useEscapeKey from 'hooks/useEscapeKey';
export default function ProfileSettings() {
const user = useSelector(state => state.user);
const [changePassword, setChangePassword] = useState(false);
const [message, setMessage] = useState();
const { user_id } = user;
function handleSave() {
setChangePassword(false);
setMessage(<FormattedMessage id="message.save-success" defaultMessage="Saved successfully." />);
}
useEscapeKey(() => {
setChangePassword(false);
});
return (
<>
<PageHeader>
<div>
<FormattedMessage id="label.profile" defaultMessage="Profile" />
</div>
<Button icon={<Dots />} size="small" onClick={() => setChangePassword(true)}>
<FormattedMessage id="label.change-password" defaultMessage="Change password" />
</Button>
</PageHeader>
<dl className={styles.list}>
<dt>
<FormattedMessage id="label.username" defaultMessage="Username" />
</dt>
<dd>{user.username}</dd>
<dt>
<FormattedMessage id="label.timezone" defaultMessage="Timezone" />
</dt>
<dd>
<TimezoneSetting />
</dd>
<dt>
<FormattedMessage id="label.default-date-range" defaultMessage="Default date range" />
</dt>
<dd>
<DateRangeSetting />
</dd>
</dl>
{changePassword && (
<Modal
title={<FormattedMessage id="label.change-password" defaultMessage="Change password" />}
>
<ChangePasswordForm
values={{ user_id }}
onSave={handleSave}
onClose={() => setChangePassword(false)}
/>
</Modal>
)}
{message && <Toast message={message} onClose={() => setMessage(null)} />}
</>
);
}

View file

@ -0,0 +1,3 @@
.list dd {
display: flex;
}

View file

@ -0,0 +1,43 @@
import React from 'react';
import { useTransition, animated } from 'react-spring';
import useTheme from 'hooks/useTheme';
import Sun from 'assets/sun.svg';
import Moon from 'assets/moon.svg';
import styles from './ThemeButton.module.css';
import Icon from '../common/Icon';
export default function ThemeButton() {
const [theme, setTheme] = useTheme();
const transitions = useTransition(theme, theme => theme, {
from: {
opacity: 0,
transform: `translateY(${theme === 'light' ? '20px' : '-20px'}) scale(0.5)`,
},
enter: { opacity: 1, transform: 'translateY(0px) scale(1)' },
leave: {
opacity: 0,
transform: `translateY(${theme === 'light' ? '-20px' : '20px'}) scale(0.5)`,
},
});
function handleClick() {
setTheme(theme === 'light' ? 'dark' : 'light');
}
return (
<div className={styles.button} onClick={handleClick}>
{transitions.map(({ item, key, props }) =>
item === 'light' ? (
<animated.div key={key} style={props}>
<Icon icon={<Sun />} />
</animated.div>
) : (
<animated.div key={key} style={props}>
<Icon icon={<Moon />} />
</animated.div>
),
)}
</div>
);
}

View file

@ -0,0 +1,13 @@
.button {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
padding-bottom: 3px;
}
.button svg {
position: absolute;
}

View file

@ -0,0 +1,31 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { listTimeZones } from 'timezone-support';
import DropDown from 'components/common/DropDown';
import Button from 'components/common/Button';
import useTimezone from 'hooks/useTimezone';
import { getTimezone } from 'lib/date';
import styles from './TimezoneSetting.module.css';
export default function TimezoneSetting() {
const [timezone, saveTimezone] = useTimezone();
const options = listTimeZones().map(n => ({ label: n, value: n }));
function handleReset() {
saveTimezone(getTimezone());
}
return (
<>
<DropDown
menuClassName={styles.menu}
value={timezone}
options={options}
onChange={saveTimezone}
/>
<Button className={styles.button} size="small" onClick={handleReset}>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
</>
);
}

View file

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

View file

@ -0,0 +1,47 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import { useRouter } from 'next/router';
import MenuButton from 'components/common/MenuButton';
import Icon from 'components/common/Icon';
import User from 'assets/user.svg';
import Chevron from 'assets/chevron-down.svg';
import styles from './UserButton.module.css';
export default function UserButton() {
const user = useSelector(state => state.user);
const router = useRouter();
const menuOptions = [
{
label: (
<FormattedMessage
id="label.logged-in-as"
defaultMessage="Logged in as {username}"
values={{ username: <b>{user.username}</b> }}
/>
),
value: 'username',
className: styles.username,
},
{ label: <FormattedMessage id="label.profile" defaultMessage="Profile" />, value: 'profile' },
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: 'logout' },
];
function handleSelect(value) {
if (value === 'logout') {
router.push('/logout');
} else if (value === 'profile') {
router.push('/settings/profile');
}
}
return (
<MenuButton
icon={<Icon icon={<User />} size="large" />}
value={<Icon icon={<Chevron />} size="small" />}
options={menuOptions}
onSelect={handleSelect}
/>
);
}

View file

@ -0,0 +1,7 @@
.username {
border-bottom: 1px solid var(--gray500);
}
.username:hover {
background: var(--gray50);
}

View file

@ -0,0 +1,186 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Link from 'components/common/Link';
import Table from 'components/common/Table';
import Button from 'components/common/Button';
import PageHeader from 'components/layout/PageHeader';
import Modal from 'components/common/Modal';
import WebsiteEditForm from 'components/forms/WebsiteEditForm';
import ResetForm from 'components/forms/ResetForm';
import DeleteForm from 'components/forms/DeleteForm';
import TrackingCodeForm from 'components/forms/TrackingCodeForm';
import ShareUrlForm from 'components/forms/ShareUrlForm';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import ButtonLayout from 'components/layout/ButtonLayout';
import Toast from 'components/common/Toast';
import Favicon from 'components/common/Favicon';
import Pen from 'assets/pen.svg';
import Trash from 'assets/trash.svg';
import Reset from 'assets/redo.svg';
import Plus from 'assets/plus.svg';
import Code from 'assets/code.svg';
import LinkIcon from 'assets/link.svg';
import useFetch from 'hooks/useFetch';
import styles from './WebsiteSettings.module.css';
export default function WebsiteSettings() {
const [editWebsite, setEditWebsite] = useState();
const [resetWebsite, setResetWebsite] = useState();
const [deleteWebsite, setDeleteWebsite] = useState();
const [addWebsite, setAddWebsite] = useState();
const [showCode, setShowCode] = useState();
const [showUrl, setShowUrl] = useState();
const [saved, setSaved] = useState(0);
const [message, setMessage] = useState();
const { data } = useFetch(`/api/websites`, {}, [saved]);
const Buttons = row => (
<ButtonLayout align="right">
{row.share_id && (
<Button
icon={<LinkIcon />}
size="small"
tooltip={<FormattedMessage id="message.get-share-url" defaultMessage="Get share URL" />}
tooltipId={`button-share-${row.website_id}`}
onClick={() => setShowUrl(row)}
/>
)}
<Button
icon={<Code />}
size="small"
tooltip={
<FormattedMessage id="message.get-tracking-code" defaultMessage="Get tracking code" />
}
tooltipId={`button-code-${row.website_id}`}
onClick={() => setShowCode(row)}
/>
<Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}>
<FormattedMessage id="label.edit" defaultMessage="Edit" />
</Button>
<Button icon={<Reset />} size="small" onClick={() => setResetWebsite(row)}>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
<FormattedMessage id="label.delete" defaultMessage="Delete" />
</Button>
</ButtonLayout>
);
const DetailsLink = ({ website_id, name, domain }) => (
<Link href="/website/[...id]" as={`/website/${website_id}/${name}`}>
<Favicon domain={domain} />
{name}
</Link>
);
const columns = [
{
key: 'name',
label: <FormattedMessage id="label.name" defaultMessage="Name" />,
className: 'col-6 col-xl-4',
render: DetailsLink,
},
{
key: 'domain',
label: <FormattedMessage id="label.domain" defaultMessage="Domain" />,
className: 'col-6 col-xl-4',
},
{
key: 'action',
className: classNames(styles.buttons, 'col-12 col-xl-4 pt-2 pt-xl-0'),
render: Buttons,
},
];
function handleSave() {
setSaved(state => state + 1);
setMessage(<FormattedMessage id="message.save-success" defaultMessage="Saved successfully." />);
handleClose();
}
function handleClose() {
setAddWebsite(null);
setEditWebsite(null);
setResetWebsite(null);
setDeleteWebsite(null);
setShowCode(null);
setShowUrl(null);
}
if (!data) {
return null;
}
const empty = (
<EmptyPlaceholder
msg={
<FormattedMessage
id="message.no-websites-configured"
defaultMessage="You don't have any websites configured."
/>
}
>
<Button icon={<Plus />} size="medium" onClick={() => setAddWebsite(true)}>
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
</Button>
</EmptyPlaceholder>
);
return (
<>
<PageHeader>
<div>
<FormattedMessage id="label.websites" defaultMessage="Websites" />
</div>
<Button icon={<Plus />} size="small" onClick={() => setAddWebsite(true)}>
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
</Button>
</PageHeader>
<Table columns={columns} rows={data} empty={empty} />
{editWebsite && (
<Modal title={<FormattedMessage id="label.edit-website" defaultMessage="Edit website" />}>
<WebsiteEditForm values={editWebsite} onSave={handleSave} onClose={handleClose} />
</Modal>
)}
{addWebsite && (
<Modal title={<FormattedMessage id="label.add-website" defaultMessage="Add website" />}>
<WebsiteEditForm onSave={handleSave} onClose={handleClose} />
</Modal>
)}
{resetWebsite && (
<Modal
title={<FormattedMessage id="label.reset-website" defaultMessage="Reset statistics" />}
>
<ResetForm
values={{ type: 'website', id: resetWebsite.website_id, name: resetWebsite.name }}
onSave={handleSave}
onClose={handleClose}
/>
</Modal>
)}
{deleteWebsite && (
<Modal
title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}
>
<DeleteForm
values={{ type: 'website', id: deleteWebsite.website_id, name: deleteWebsite.name }}
onSave={handleSave}
onClose={handleClose}
/>
</Modal>
)}
{showCode && (
<Modal title={<FormattedMessage id="label.tracking-code" defaultMessage="Tracking code" />}>
<TrackingCodeForm values={showCode} onClose={handleClose} />
</Modal>
)}
{showUrl && (
<Modal title={<FormattedMessage id="label.share-url" defaultMessage="Share URL" />}>
<ShareUrlForm values={showUrl} onClose={handleClose} />
</Modal>
)}
{message && <Toast message={message} onClose={() => setMessage(null)} />}
</>
);
}

View file

@ -0,0 +1,7 @@
.col {
flex: 2;
}
.buttons {
justify-content: flex-end;
}