diff --git a/assets/buoy.svg b/assets/buoy.svg
new file mode 100644
index 00000000..847a814e
--- /dev/null
+++ b/assets/buoy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/card.svg b/assets/card.svg
new file mode 100644
index 00000000..ecda0e46
--- /dev/null
+++ b/assets/card.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/users.svg b/assets/users.svg
new file mode 100644
index 00000000..f775ea91
--- /dev/null
+++ b/assets/users.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/website.svg b/assets/website.svg
new file mode 100644
index 00000000..cfa9e565
--- /dev/null
+++ b/assets/website.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/common/Button.js b/components/common/Button.js
deleted file mode 100644
index d3b7d2ad..00000000
--- a/components/common/Button.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ReactTooltip from 'react-tooltip';
-import classNames from 'classnames';
-import Icon from './Icon';
-import styles from './Button.module.css';
-
-function Button({
- type = 'button',
- icon,
- size,
- variant,
- children,
- className,
- tooltip,
- tooltipId,
- disabled,
- iconRight,
- onClick = () => {},
- ...props
-}) {
- return (
-
- );
-}
-
-Button.propTypes = {
- type: PropTypes.oneOf(['button', 'submit', 'reset']),
- icon: PropTypes.node,
- size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
- variant: PropTypes.oneOf(['action', 'danger', 'light']),
- children: PropTypes.node,
- className: PropTypes.string,
- tooltip: PropTypes.node,
- tooltipId: PropTypes.string,
- disabled: PropTypes.bool,
- iconRight: PropTypes.bool,
- onClick: PropTypes.func,
-};
-
-export default Button;
diff --git a/components/common/Button.module.css b/components/common/Button.module.css
deleted file mode 100644
index b6edc60e..00000000
--- a/components/common/Button.module.css
+++ /dev/null
@@ -1,102 +0,0 @@
-.button {
- display: flex;
- justify-content: center;
- align-items: center;
- font-size: var(--font-size-md);
- color: var(--base900);
- background: var(--base100);
- padding: 8px 16px;
- border-radius: 4px;
- border: 0;
- outline: none;
- cursor: pointer;
- position: relative;
-}
-
-.button:hover {
- background: var(--base200);
-}
-
-.button:active {
- color: var(--base900);
-}
-
-.label {
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- max-width: 300px;
-}
-
-.large {
- font-size: var(--font-size-lg);
-}
-
-.small {
- font-size: var(--font-size-sm);
-}
-
-.xsmall {
- font-size: var(--font-size-xs);
-}
-
-.action,
-.action:active {
- color: var(--base50);
- background: var(--base900);
-}
-
-.action:hover {
- background: var(--base800);
-}
-
-.danger,
-.danger:active {
- color: var(--base50);
- background: var(--red500);
-}
-
-.danger:hover {
- background: var(--red400);
-}
-
-.light,
-.light:active {
- color: var(--base900);
- background: transparent;
-}
-
-.light:hover {
- background: inherit;
-}
-
-.button .icon + * {
- margin-left: 10px;
-}
-
-.button.iconRight .icon {
- order: 1;
- margin-left: 10px;
-}
-
-.button.iconRight .icon + * {
- margin: 0;
-}
-
-.button:disabled {
- cursor: default;
- color: var(--base500);
- background: var(--base75);
-}
-
-.button:disabled:active {
- color: var(--base500);
-}
-
-.button:disabled:hover {
- background: var(--base75);
-}
-
-.button.light:disabled {
- background: var(--base50);
-}
diff --git a/components/common/ButtonGroup.js b/components/common/ButtonGroup.js
deleted file mode 100644
index 353ce690..00000000
--- a/components/common/ButtonGroup.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import Button from './Button';
-import styles from './ButtonGroup.module.css';
-
-function ButtonGroup({ items = [], selectedItem, className, size, icon, onClick = () => {} }) {
- return (
-
- {items.map(item => {
- const { label, value } = item;
- return (
-
- );
- })}
-
- );
-}
-
-ButtonGroup.propTypes = {
- items: PropTypes.arrayOf(
- PropTypes.shape({
- label: PropTypes.node,
- value: PropTypes.any.isRequired,
- }),
- ),
- selectedItem: PropTypes.any,
- className: PropTypes.string,
- size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
- icon: PropTypes.node,
- onClick: PropTypes.func,
-};
-
-export default ButtonGroup;
diff --git a/components/common/ButtonGroup.module.css b/components/common/ButtonGroup.module.css
deleted file mode 100644
index 04d33d22..00000000
--- a/components/common/ButtonGroup.module.css
+++ /dev/null
@@ -1,31 +0,0 @@
-.group {
- display: inline-flex;
- border-radius: 4px;
- overflow: hidden;
- border: 1px solid var(--base500);
-}
-
-.group .button {
- border-radius: 0;
- color: var(--base800);
- background: var(--base50);
- border-left: 1px solid var(--base500);
- padding: 4px 8px;
-}
-
-.group .button:first-child {
- border: 0;
-}
-
-.group .button:hover {
- background: var(--base100);
-}
-
-.group .button + .button {
- margin: 0;
-}
-
-.group .button.selected {
- color: var(--base900);
- font-weight: 600;
-}
diff --git a/components/common/Calendar.js b/components/common/Calendar.js
index b6c5cd0b..077382d1 100644
--- a/components/common/Calendar.js
+++ b/components/common/Calendar.js
@@ -16,7 +16,7 @@ import {
isBefore,
isAfter,
} from 'date-fns';
-import Button from './Button';
+import { Button, Icon } from 'react-basics';
import useLocale from 'hooks/useLocale';
import { dateFormat } from 'lib/date';
import { chunk } from 'lib/array';
@@ -24,7 +24,6 @@ import { getDateLocale } from 'lib/lang';
import Chevron from 'assets/chevron-down.svg';
import Cross from 'assets/times.svg';
import styles from './Calendar.module.css';
-import Icon from './Icon';
export default function Calendar({ date, minDate, maxDate, onChange }) {
const { locale } = useLocale();
@@ -61,14 +60,18 @@ export default function Calendar({ date, minDate, maxDate, onChange }) {
onClick={toggleMonthSelect}
>
{month}
- : } size="small" />
+
+ {selectMonth ? : }
+
{year}
- : } size="small" />
+
+ {selectMonth ? : }
+
@@ -230,12 +233,15 @@ const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
}
size="small"
onClick={handlePrevClick}
disabled={years[0] <= minYear}
variant="light"
- />
+ >
+
+
+
+
@@ -261,12 +267,15 @@ const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
}
size="small"
onClick={handleNextClick}
disabled={years[years.length - 1] > maxYear}
variant="light"
- />
+ >
+
+
+
+
);
diff --git a/components/common/Checkbox.js b/components/common/Checkbox.js
deleted file mode 100644
index 0cd0dcad..00000000
--- a/components/common/Checkbox.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { useRef } from 'react';
-import PropTypes from 'prop-types';
-import Icon from 'components/common/Icon';
-import Check from 'assets/check.svg';
-import styles from './Checkbox.module.css';
-
-function Checkbox({ name, value, label, onChange }) {
- const ref = useRef();
-
- const onClick = () => ref.current.click();
-
- return (
-
-
- {value && } size="small" />}
-
-
-
-
- );
-}
-
-Checkbox.propTypes = {
- name: PropTypes.string,
- value: PropTypes.any,
- label: PropTypes.node,
- onChange: PropTypes.func,
-};
-
-export default Checkbox;
diff --git a/components/common/Checkbox.module.css b/components/common/Checkbox.module.css
deleted file mode 100644
index edd2b776..00000000
--- a/components/common/Checkbox.module.css
+++ /dev/null
@@ -1,30 +0,0 @@
-.container {
- display: flex;
- align-items: center;
- position: relative;
- overflow: hidden;
-}
-
-.checkbox {
- display: flex;
- justify-content: center;
- align-items: center;
- width: 20px;
- height: 20px;
- border: 1px solid var(--base500);
- border-radius: 4px;
-}
-
-.label {
- margin-left: 10px;
- user-select: none; /* disable text selection when clicking to toggle the checkbox */
-}
-
-.input {
- position: absolute;
- visibility: hidden;
- height: 0;
- width: 0;
- bottom: 100%;
- right: 100%;
-}
diff --git a/components/common/CopyButton.js b/components/common/CopyButton.js
index b300ef31..610c1c1c 100644
--- a/components/common/CopyButton.js
+++ b/components/common/CopyButton.js
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
-import Button from './Button';
+import { Button } from 'react-basics';
import { FormattedMessage } from 'react-intl';
const defaultText = (
diff --git a/components/common/DateFilter.js b/components/common/DateFilter.js
index d568a889..1769a55c 100644
--- a/components/common/DateFilter.js
+++ b/components/common/DateFilter.js
@@ -1,14 +1,13 @@
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
-import { FormattedMessage } from 'react-intl';
-import { endOfYear, isSameDay } from 'date-fns';
-import Modal from './Modal';
-import DropDown from './DropDown';
+import Calendar from 'assets/calendar-alt.svg';
import DatePickerForm from 'components/forms/DatePickerForm';
+import { endOfYear, isSameDay } from 'date-fns';
import useLocale from 'hooks/useLocale';
import { dateFormat } from 'lib/date';
-import Calendar from 'assets/calendar-alt.svg';
-import Icon from './Icon';
+import PropTypes from 'prop-types';
+import React, { useState } from 'react';
+import { Icon, Modal } from 'react-basics';
+import { FormattedMessage } from 'react-intl';
+import DropDown from './DropDown';
export const filterOptions = [
{ label: , value: '1day' },
@@ -120,7 +119,9 @@ const CustomRange = ({ startDate, endDate, onClick }) => {
return (
<>
- } className="mr-2" onClick={handleClick} />
+
+
+
{dateFormat(startDate, 'd LLL y', locale)}
{!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`}
>
diff --git a/components/common/DropDown.js b/components/common/DropDown.js
index 00d20e34..c84a9d04 100644
--- a/components/common/DropDown.js
+++ b/components/common/DropDown.js
@@ -5,7 +5,7 @@ import Menu from './Menu';
import useDocumentClick from 'hooks/useDocumentClick';
import Chevron from 'assets/chevron-down.svg';
import styles from './Dropdown.module.css';
-import Icon from './Icon';
+import { Icon } from 'react-basics';
function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) {
const [showMenu, setShowMenu] = useState(false);
@@ -33,7 +33,9 @@ function DropDown({ value, className, menuClassName, options = [], onChange = ()
{options.find(e => e.value === value)?.label || value}
-
} className={styles.icon} size="small" />
+
+
+
{showMenu && (
);
}
diff --git a/components/common/ErrorMessage.js b/components/common/ErrorMessage.js
index 5747f226..efc6cc31 100644
--- a/components/common/ErrorMessage.js
+++ b/components/common/ErrorMessage.js
@@ -1,13 +1,15 @@
+import Exclamation from 'assets/exclamation-triangle.svg';
import React from 'react';
import { FormattedMessage } from 'react-intl';
-import Icon from './Icon';
-import Exclamation from 'assets/exclamation-triangle.svg';
import styles from './ErrorMessage.module.css';
+import { Icon } from 'react-basics';
export default function ErrorMessage() {
return (
- } className={styles.icon} size="large" />
+
+
+
);
diff --git a/components/common/EventDataButton.js b/components/common/EventDataButton.js
index 2b895840..706d07d1 100644
--- a/components/common/EventDataButton.js
+++ b/components/common/EventDataButton.js
@@ -1,10 +1,9 @@
import List from 'assets/list-ul.svg';
-import Modal from 'components/common/Modal';
+import EventDataForm from 'components/forms/EventDataForm';
import PropTypes from 'prop-types';
import { useState } from 'react';
+import { Button, Icon, Modal } from 'react-basics';
import { FormattedMessage } from 'react-intl';
-import Button from './Button';
-import EventDataForm from 'components/forms/EventDataForm';
import styles from './EventDataButton.module.css';
function EventDataButton({ websiteId }) {
@@ -23,13 +22,15 @@ function EventDataButton({ websiteId }) {
return (
<>
}
tooltip={}
tooltipId="button-event"
size="small"
onClick={handleClick}
className={styles.button}
>
+
+
+
Event Data
{showEventData && (
diff --git a/components/common/FilterButtons.js b/components/common/FilterButtons.js
index ea811216..79962c51 100644
--- a/components/common/FilterButtons.js
+++ b/components/common/FilterButtons.js
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ButtonLayout from 'components/layout/ButtonLayout';
-import ButtonGroup from './ButtonGroup';
+import { ButtonGroup } from 'react-basics';
function FilterButtons({ buttons, selected, onClick }) {
return (
diff --git a/components/common/FilterLink.js b/components/common/FilterLink.js
index f16258f1..7500ffdb 100644
--- a/components/common/FilterLink.js
+++ b/components/common/FilterLink.js
@@ -4,7 +4,7 @@ import Link from 'next/link';
import { safeDecodeURI } from 'next-basics';
import usePageQuery from 'hooks/usePageQuery';
import External from 'assets/arrow-up-right-from-square.svg';
-import Icon from './Icon';
+import { Icon } from 'react-basics';
import styles from './FilterLink.module.css';
export default function FilterLink({ id, value, label, externalUrl }) {
@@ -26,7 +26,9 @@ export default function FilterLink({ id, value, label, externalUrl }) {
{externalUrl && (
- } className={styles.icon} />
+
+
+
)}
diff --git a/components/common/HamburgerButton.js b/components/common/HamburgerButton.js
index 501b8c95..67a1c83a 100644
--- a/components/common/HamburgerButton.js
+++ b/components/common/HamburgerButton.js
@@ -1,4 +1,4 @@
-import Button from 'components/common/Button';
+import { Button, Icon } from 'react-basics';
import XMark from 'assets/xmark.svg';
import Bars from 'assets/bars.svg';
import { useState } from 'react';
@@ -33,11 +33,9 @@ export default function HamburgerButton() {
return (
<>
- : }
- onClick={handleClick}
- />
+
{active && }
>
);
diff --git a/components/common/Link.js b/components/common/Link.js
index f683f5df..1e05d8b2 100644
--- a/components/common/Link.js
+++ b/components/common/Link.js
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import NextLink from 'next/link';
-import Icon from './Icon';
+import { Icon } from 'react-basics';
import styles from './Link.module.css';
function Link({ className, icon, children, size, iconRight, onClick, ...props }) {
diff --git a/components/common/MenuButton.js b/components/common/MenuButton.js
index efe150f0..9eb4554f 100644
--- a/components/common/MenuButton.js
+++ b/components/common/MenuButton.js
@@ -2,12 +2,12 @@ import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Menu from 'components/common/Menu';
-import Button from 'components/common/Button';
import useDocumentClick from 'hooks/useDocumentClick';
import styles from './MenuButton.module.css';
+import { Button } from 'react-basics';
function MenuButton({
- icon,
+ children,
value,
options,
buttonClassName,
@@ -41,7 +41,6 @@ function MenuButton({
return (
)}
+ {children}
{showMenu && (
+ )}
+ >
);
}
diff --git a/components/forms/TeamAddForm.js b/components/forms/TeamAddForm.js
new file mode 100644
index 00000000..a18fc769
--- /dev/null
+++ b/components/forms/TeamAddForm.js
@@ -0,0 +1,36 @@
+import { useRef } from 'react';
+import { Form, FormInput, FormButtons, TextField, Button } from 'react-basics';
+import { useApi } from 'next-basics';
+import styles from './Form.module.css';
+import { useMutation } from '@tanstack/react-query';
+import { getAuthToken } from 'lib/client';
+
+export default function TeamAddForm({ onSave, onClose }) {
+ const { post } = useApi(getAuthToken());
+ const { mutate, error, isLoading } = useMutation(data => post('/teams', data));
+ const ref = useRef(null);
+
+ const handleSubmit = async data => {
+ mutate(data, {
+ onSuccess: async () => {
+ onSave();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/components/forms/TeamEditForm.js b/components/forms/TeamEditForm.js
new file mode 100644
index 00000000..769ccf61
--- /dev/null
+++ b/components/forms/TeamEditForm.js
@@ -0,0 +1,34 @@
+import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics';
+import { useMutation } from '@tanstack/react-query';
+import { useRef } from 'react';
+import { useApi } from 'next-basics';
+import { getAuthToken } from 'lib/client';
+
+export default function TeamEditForm({ teamId, data, onSave }) {
+ const { post } = useApi(getAuthToken());
+ const { mutate, error } = useMutation(data => post(`/teams/${teamId}`, data));
+ const ref = useRef(null);
+
+ const handleSubmit = async data => {
+ mutate(data, {
+ onSuccess: async () => {
+ ref.current.reset(data);
+ onSave(data);
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/components/forms/TrackingCodeForm.js b/components/forms/TrackingCodeForm.js
index 5a098b8d..191248f5 100644
--- a/components/forms/TrackingCodeForm.js
+++ b/components/forms/TrackingCodeForm.js
@@ -1,43 +1,23 @@
-import React, { useRef } from 'react';
-import { FormattedMessage } from 'react-intl';
-import { useRouter } from 'next/router';
-import Button from 'components/common/Button';
-import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
-import CopyButton from 'components/common/CopyButton';
import useConfig from 'hooks/useConfig';
+import { useRef } from 'react';
+import { Form, FormRow, TextArea } from 'react-basics';
-export default function TrackingCodeForm({ values, onClose }) {
- const ref = useRef();
- const { basePath } = useRouter();
+export default function TrackingCodeForm({ websiteId }) {
+ const ref = useRef(null);
const { trackerScriptName } = useConfig();
+ const code = ``;
return (
-
-
- ', target: {values.name} }}
- />
-
-
-
-
-
-
-
-
+ <>
+
+ >
);
}
diff --git a/components/forms/UserDeleteForm.js b/components/forms/UserDeleteForm.js
new file mode 100644
index 00000000..99bea443
--- /dev/null
+++ b/components/forms/UserDeleteForm.js
@@ -0,0 +1,43 @@
+import { useMutation } from '@tanstack/react-query';
+import { getAuthToken } from 'lib/client';
+import { useApi } from 'next-basics';
+import { Button, Form, FormButtons, FormInput, SubmitButton, TextField } from 'react-basics';
+import styles from './Form.module.css';
+
+const CONFIRM_VALUE = 'DELETE';
+
+export default function UserDeleteForm({ userId, onSave, onClose }) {
+ const { del } = useApi(getAuthToken());
+ const { mutate, error, isLoading } = useMutation(data => del(`/users/${userId}`, data));
+
+ const handleSubmit = async data => {
+ mutate(data, {
+ onSuccess: async () => {
+ onSave();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/components/forms/UserEditForm.js b/components/forms/UserEditForm.js
index 0d7e392f..7fb6e5c6 100644
--- a/components/forms/UserEditForm.js
+++ b/components/forms/UserEditForm.js
@@ -1,89 +1,65 @@
-import React, { useState } from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Formik, Form, Field } from 'formik';
-import Button from 'components/common/Button';
-import FormLayout, {
+import {
+ Dropdown,
+ Item,
+ Form,
FormButtons,
- FormError,
- FormMessage,
- FormRow,
-} from 'components/layout/FormLayout';
-import useApi from 'hooks/useApi';
+ FormInput,
+ TextField,
+ SubmitButton,
+} from 'react-basics';
+import { useRef } from 'react';
+import { useMutation } from '@tanstack/react-query';
+import { useApi } from 'next-basics';
+import { getAuthToken } from 'lib/client';
+import { ROLES } from 'lib/constants';
+import styles from './UserForm.module.css';
-const initialValues = {
- username: '',
- password: '',
-};
+const items = [
+ {
+ value: ROLES.user,
+ label: 'User',
+ },
+ {
+ value: ROLES.admin,
+ label: 'Admin',
+ },
+];
-const validate = ({ id, username, password }) => {
- const errors = {};
+export default function UserEditForm({ data, onSave }) {
+ const { id } = data;
+ const { post } = useApi(getAuthToken());
+ const { mutate, error } = useMutation(({ username }) => post(`/user/${id}`, { username }));
+ const ref = useRef(null);
- if (!username) {
- errors.username = ;
- }
- if (!id && !password) {
- errors.password = ;
- }
-
- return errors;
-};
-
-export default function UserEditForm({ values, onSave, onClose }) {
- const { post } = useApi();
- const [message, setMessage] = useState();
-
- const handleSubmit = async values => {
- const { id } = values;
- const { ok, data } = await post(id ? `/users/${id}` : '/users', values);
-
- if (ok) {
- onSave();
- } else {
- setMessage(
- data || ,
- );
- }
+ const handleSubmit = async data => {
+ mutate(data, {
+ onSuccess: async () => {
+ onSave(data);
+ ref.current.reset(data);
+ },
+ });
};
return (
-
-
- {() => (
-
- )}
-
-
+
);
}
diff --git a/components/forms/UserForm.module.css b/components/forms/UserForm.module.css
new file mode 100644
index 00000000..793682e1
--- /dev/null
+++ b/components/forms/UserForm.module.css
@@ -0,0 +1,6 @@
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+ width: 300px;
+}
diff --git a/components/forms/UserPasswordForm.js b/components/forms/UserPasswordForm.js
new file mode 100644
index 00000000..76748405
--- /dev/null
+++ b/components/forms/UserPasswordForm.js
@@ -0,0 +1,81 @@
+import { useRef } from 'react';
+import { Form, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
+import { useApi } from 'next-basics';
+import { useMutation } from '@tanstack/react-query';
+import { getAuthToken } from 'lib/client';
+import styles from './UserPasswordForm.module.css';
+import useUser from 'hooks/useUser';
+
+export default function UserPasswordForm({ onSave, userId }) {
+ const {
+ user: { id },
+ } = useUser();
+
+ const isCurrentUser = !userId || id === userId;
+ const url = isCurrentUser ? `/users/${id}/password` : `/users/${id}`;
+ const { post } = useApi(getAuthToken());
+ const { mutate, error, isLoading } = useMutation(data => post(url, data));
+ const ref = useRef(null);
+
+ const handleSubmit = async data => {
+ const payload = isCurrentUser
+ ? data
+ : {
+ password: data.new_password,
+ };
+
+ mutate(payload, {
+ onSuccess: async () => {
+ onSave();
+ ref.current.reset();
+ },
+ });
+ };
+
+ const samePassword = value => {
+ if (value !== ref?.current?.getValues('new_password')) {
+ return "Passwords don't match";
+ }
+ return true;
+ };
+
+ return (
+
+ );
+}
diff --git a/components/forms/UserPasswordForm.module.css b/components/forms/UserPasswordForm.module.css
new file mode 100644
index 00000000..793682e1
--- /dev/null
+++ b/components/forms/UserPasswordForm.module.css
@@ -0,0 +1,6 @@
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+ width: 300px;
+}
diff --git a/components/forms/WebsiteAddForm.js b/components/forms/WebsiteAddForm.js
new file mode 100644
index 00000000..f14b8b74
--- /dev/null
+++ b/components/forms/WebsiteAddForm.js
@@ -0,0 +1,47 @@
+import { useRef } from 'react';
+import { Form, FormInput, FormButtons, TextField, Button, SubmitButton } from 'react-basics';
+import { useApi } from 'next-basics';
+import styles from './Form.module.css';
+import { useMutation } from '@tanstack/react-query';
+import { getAuthToken } from 'lib/client';
+import { DOMAIN_REGEX } from 'lib/constants';
+
+export default function WebsiteAddForm({ onSave, onClose }) {
+ const { post } = useApi(getAuthToken());
+ const { mutate, error, isLoading } = useMutation(data => post('/websites', data));
+ const ref = useRef(null);
+
+ const handleSubmit = async data => {
+ mutate(data, {
+ onSuccess: async () => {
+ onSave();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/components/forms/WebsiteDeleteForm.js b/components/forms/WebsiteDeleteForm.js
new file mode 100644
index 00000000..d90e88a4
--- /dev/null
+++ b/components/forms/WebsiteDeleteForm.js
@@ -0,0 +1,43 @@
+import { useMutation } from '@tanstack/react-query';
+import { getAuthToken } from 'lib/client';
+import { useApi } from 'next-basics';
+import { Button, Form, FormButtons, FormInput, SubmitButton, TextField } from 'react-basics';
+import styles from './Form.module.css';
+
+const CONFIRM_VALUE = 'DELETE';
+
+export default function WebsiteDeleteForm({ websiteId, onSave, onClose }) {
+ const { del } = useApi(getAuthToken());
+ const { mutate, error, isLoading } = useMutation(data => del(`/websites/${websiteId}`, data));
+
+ const handleSubmit = async data => {
+ mutate(data, {
+ onSuccess: async () => {
+ onSave();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/components/forms/WebsiteEditForm.js b/components/forms/WebsiteEditForm.js
index 491a8bfe..8e1abc10 100644
--- a/components/forms/WebsiteEditForm.js
+++ b/components/forms/WebsiteEditForm.js
@@ -1,159 +1,48 @@
-import React, { useEffect, useState } from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Formik, Form, Field, useFormikContext } from 'formik';
-import Button from 'components/common/Button';
-import FormLayout, {
- FormButtons,
- FormError,
- FormMessage,
- FormRow,
-} from 'components/layout/FormLayout';
-import Checkbox from 'components/common/Checkbox';
+import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics';
+import { useMutation } from '@tanstack/react-query';
+import { useRef } from 'react';
+import { useApi } from 'next-basics';
+import { getAuthToken } from 'lib/client';
import { DOMAIN_REGEX } from 'lib/constants';
-import useApi from 'hooks/useApi';
-import useFetch from 'hooks/useFetch';
-import useUser from 'hooks/useUser';
-import styles from './WebsiteEditForm.module.css';
-const initialValues = {
- name: '',
- domain: '',
- owner: '',
- public: false,
-};
+export default function WebsiteEditForm({ websiteId, data, onSave }) {
+ const { post } = useApi(getAuthToken());
+ const { mutate, error } = useMutation(data => post(`/websites/${websiteId}`, data));
+ const ref = useRef(null);
-const validate = ({ name, domain }) => {
- const errors = {};
-
- if (!name) {
- errors.name = ;
- }
- if (!domain) {
- errors.domain = ;
- } else if (!DOMAIN_REGEX.test(domain)) {
- errors.domain = ;
- }
-
- return errors;
-};
-
-const OwnerDropDown = ({ user, users }) => {
- const { setFieldValue, values } = useFormikContext();
-
- useEffect(() => {
- if (values.userId != null && values.owner === '') {
- setFieldValue('owner', values.userId.toString());
- } else if (user?.id && values.owner === '') {
- setFieldValue('owner', user.id.toString());
- }
- }, [users, setFieldValue, user, values]);
-
- if (user?.isAdmin) {
- return (
-
-
-
-
- {users?.map(acc => (
-
- ))}
-
-
-
-
- );
- } else {
- return null;
- }
-};
-
-export default function WebsiteEditForm({ values, onSave, onClose }) {
- const { post } = useApi();
- const { data: users } = useFetch(`/users`);
- const { user } = useUser();
- const [message, setMessage] = useState();
-
- const handleSubmit = async values => {
- const { id } = values;
-
- const { ok, data } = await post(id ? `/websites/${id}` : '/websites', values);
-
- if (ok) {
- onSave();
- } else {
- setMessage(
- data || ,
- );
- }
+ const handleSubmit = async data => {
+ mutate(data, {
+ onSuccess: async () => {
+ ref.current.reset(data);
+ onSave(data);
+ },
+ });
};
return (
-
-
+
+
+
+
+
+
+
- {() => (
-
- )}
-
-
+
+
+
+ Save
+
+
);
}
diff --git a/components/forms/WebsiteEditForm.module.css b/components/forms/WebsiteEditForm.module.css
deleted file mode 100644
index c45cca79..00000000
--- a/components/forms/WebsiteEditForm.module.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.dropdown {
- -moz-appearance: none;
- -webkit-appearance: none;
- appearance: none;
-}
diff --git a/components/forms/WebsiteReset.js b/components/forms/WebsiteReset.js
new file mode 100644
index 00000000..6534798b
--- /dev/null
+++ b/components/forms/WebsiteReset.js
@@ -0,0 +1,49 @@
+import WebsiteDeleteForm from 'components/forms/WebsiteDeleteForm';
+import WebsiteResetForm from 'components/forms/WebsiteResetForm';
+import { useRouter } from 'next/router';
+import { useState } from 'react';
+import { Button, Form, FormRow, Modal } from 'react-basics';
+
+export default function WebsiteReset({ websiteId, onSave }) {
+ const [modal, setModal] = useState(null);
+ const router = useRouter();
+
+ const handleReset = async () => {
+ setModal(null);
+ onSave();
+ };
+
+ const handleDelete = async () => {
+ onSave();
+ await router.push('/websites');
+ };
+
+ const handleClose = () => setModal(null);
+
+ return (
+
+ );
+}
diff --git a/components/forms/WebsiteResetForm.js b/components/forms/WebsiteResetForm.js
new file mode 100644
index 00000000..7dda5f04
--- /dev/null
+++ b/components/forms/WebsiteResetForm.js
@@ -0,0 +1,45 @@
+import { useMutation } from '@tanstack/react-query';
+import { getAuthToken } from 'lib/client';
+import { useApi } from 'next-basics';
+import { Button, Form, FormButtons, FormInput, SubmitButton, TextField } from 'react-basics';
+import styles from './Form.module.css';
+
+const CONFIRM_VALUE = 'RESET';
+
+export default function WebsiteResetForm({ websiteId, onSave, onClose }) {
+ const { post } = useApi(getAuthToken());
+ const { mutate, error, isLoading } = useMutation(data =>
+ post(`/websites/${websiteId}/reset`, data),
+ );
+
+ const handleSubmit = async data => {
+ mutate(data, {
+ onSuccess: async () => {
+ onSave();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/components/layout/Header.js b/components/layout/Header.js
index a0063cfb..43f2192a 100644
--- a/components/layout/Header.js
+++ b/components/layout/Header.js
@@ -1,19 +1,18 @@
-import { Row, Column } from 'react-basics';
-import { useRouter } from 'next/router';
-import { FormattedMessage } from 'react-intl';
+import Logo from 'assets/logo.svg';
+import HamburgerButton from 'components/common/HamburgerButton';
import Link from 'components/common/Link';
-import Icon from 'components/common/Icon';
+import UpdateNotice from 'components/common/UpdateNotice';
import LanguageButton from 'components/settings/LanguageButton';
import ThemeButton from 'components/settings/ThemeButton';
-import HamburgerButton from 'components/common/HamburgerButton';
-import UpdateNotice from 'components/common/UpdateNotice';
import UserButton from 'components/settings/UserButton';
-import { HOMEPAGE_URL } from 'lib/constants';
import useConfig from 'hooks/useConfig';
import useUser from 'hooks/useUser';
-import Logo from 'assets/logo.svg';
-import styles from './Header.module.css';
+import { HOMEPAGE_URL } from 'lib/constants';
+import { useRouter } from 'next/router';
+import { Column, Icon, Row } from 'react-basics';
+import { FormattedMessage } from 'react-intl';
import SettingsButton from '../settings/SettingsButton';
+import styles from './Header.module.css';
export default function Header() {
const { user } = useUser();
@@ -28,7 +27,9 @@ export default function Header() {
- } size="large" className={styles.logo} />
+
+
+
umami
@@ -40,7 +41,7 @@ export default function Header() {
-
+
diff --git a/components/layout/Page.js b/components/layout/Page.js
index bcb61395..cc402e56 100644
--- a/components/layout/Page.js
+++ b/components/layout/Page.js
@@ -1,6 +1,15 @@
import classNames from 'classnames';
import styles from './Page.module.css';
+import { Banner, Loading } from 'react-basics';
+
+export default function Page({ className, error, loading, children }) {
+ if (error) {
+ return Something went wrong.;
+ }
+
+ if (loading) {
+ return ;
+ }
-export default function Page({ className, children }) {
return {children}
;
}
diff --git a/components/layout/Page.module.css b/components/layout/Page.module.css
index d3b03fff..4bd02d05 100644
--- a/components/layout/Page.module.css
+++ b/components/layout/Page.module.css
@@ -2,7 +2,7 @@
flex: 1;
display: flex;
flex-direction: column;
- padding: 0 30px;
+ padding: 30px;
background: var(--base50);
- border-radius: 8px;
+ position: relative;
}
diff --git a/components/layout/PageHeader.js b/components/layout/PageHeader.js
index f356235e..2de1f915 100644
--- a/components/layout/PageHeader.js
+++ b/components/layout/PageHeader.js
@@ -1,7 +1,25 @@
import React from 'react';
+import Link from 'next/link';
import classNames from 'classnames';
+import { Button, Icon } from 'react-basics';
import styles from './PageHeader.module.css';
-export default function PageHeader({ children, className }) {
- return {children}
;
+export default function PageHeader({ title, backUrl, children, className, style }) {
+ return (
+
+ );
}
diff --git a/components/layout/PageHeader.module.css b/components/layout/PageHeader.module.css
index 5bbe893f..6fe45e8b 100644
--- a/components/layout/PageHeader.module.css
+++ b/components/layout/PageHeader.module.css
@@ -3,7 +3,23 @@
justify-content: space-between;
align-items: center;
align-content: center;
- min-height: 80px;
align-self: stretch;
+ margin-bottom: 40px;
+ font-size: 18px;
font-weight: bold;
+ height: 50px;
+}
+
+.header a {
+ color: var(--base600);
+}
+
+.header a:hover {
+ color: var(--base900);
+}
+
+.title {
+ display: flex;
+ align-items: center;
+ gap: 20px;
}
diff --git a/components/metrics/FilterTags.js b/components/metrics/FilterTags.js
index bb4174b8..2fc04ced 100644
--- a/components/metrics/FilterTags.js
+++ b/components/metrics/FilterTags.js
@@ -1,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { safeDecodeURI } from 'next-basics';
-import Button from 'components/common/Button';
+import { Button } from 'react-basics';
import Times from 'assets/times.svg';
import styles from './FilterTags.module.css';
diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js
index 1cf0326b..0a8a0ec2 100644
--- a/components/metrics/RealtimeLog.js
+++ b/components/metrics/RealtimeLog.js
@@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { FixedSizeList } from 'react-window';
import firstBy from 'thenby';
-import Icon from 'components/common/Icon';
+import { Icon } from 'react-basics';
import Dot from 'components/common/Dot';
import FilterButtons from 'components/common/FilterButtons';
import NoData from 'components/common/NoData';
diff --git a/components/nav/Nav.js b/components/nav/Nav.js
new file mode 100644
index 00000000..91f9c966
--- /dev/null
+++ b/components/nav/Nav.js
@@ -0,0 +1,45 @@
+import User from 'assets/user.svg';
+import Team from 'assets/users.svg';
+import Website from 'assets/website.svg';
+import classNames from 'classnames';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { Icon, Item, Menu, Text } from 'react-basics';
+import styles from './Nav.module.css';
+import useRequireLogin from 'hooks/useRequireLogin';
+
+export default function Nav() {
+ const {
+ user: { role },
+ } = useRequireLogin();
+ const { pathname } = useRouter();
+
+ const handleSelect = () => {};
+
+ const items = [
+ { icon: , label: 'Websites', url: '/websites' },
+ { icon: , label: 'Users', url: '/users', hidden: role !== 'admin' },
+ { icon: , label: 'Teams', url: '/teams' },
+ { icon: , label: 'Profile', url: '/profile' },
+ ];
+
+ return (
+
+ );
+}
diff --git a/components/nav/Nav.module.css b/components/nav/Nav.module.css
new file mode 100644
index 00000000..cfd8ba05
--- /dev/null
+++ b/components/nav/Nav.module.css
@@ -0,0 +1,46 @@
+.menu {
+ display: flex;
+ flex-direction: column;
+ width: 200px;
+ gap: 10px;
+ background: transparent;
+ margin-right: 16px;
+}
+
+.menu svg {
+ width: 20px;
+ height: 20px;
+}
+
+.item {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ font-weight: 600;
+ background: transparent;
+ padding: 0;
+ border-radius: 8px;
+}
+
+.item:hover {
+ background: var(--base100);
+}
+
+.item a {
+ color: var(--base700);
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ flex: 1;
+ padding: 16px;
+ border-radius: 8px;
+}
+
+.item a:hover {
+ color: var(--base900);
+}
+
+.item.selected a {
+ color: var(--base900);
+ background: var(--base100);
+}
diff --git a/components/pages/Dashboard.js b/components/pages/Dashboard.js
index 70a4d624..343ac3f7 100644
--- a/components/pages/Dashboard.js
+++ b/components/pages/Dashboard.js
@@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import WebsiteList from 'components/pages/WebsiteList';
-import Button from 'components/common/Button';
+import { Button } from 'react-basics';
import DashboardSettingsButton from 'components/settings/DashboardSettingsButton';
import useFetch from 'hooks/useFetch';
import useDashboard from 'store/dashboard';
diff --git a/components/pages/DashboardEdit.js b/components/pages/DashboardEdit.js
index 6af1644a..707a7a66 100644
--- a/components/pages/DashboardEdit.js
+++ b/components/pages/DashboardEdit.js
@@ -2,7 +2,7 @@ import { useState, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import classNames from 'classnames';
-import Button from 'components/common/Button';
+import { Button } from 'react-basics';
import { firstBy } from 'thenby';
import useDashboard, { saveDashboard } from 'store/dashboard';
import styles from './DashboardEdit.module.css';
diff --git a/components/pages/ProfileSettings.js b/components/pages/ProfileSettings.js
new file mode 100644
index 00000000..ab6dbb8a
--- /dev/null
+++ b/components/pages/ProfileSettings.js
@@ -0,0 +1,32 @@
+import Page from 'components/layout/Page';
+import PageHeader from 'components/layout/PageHeader';
+import ProfileDetails from 'components/settings/ProfileDetails';
+import { useState } from 'react';
+import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
+import UserPasswordForm from 'components/forms/UserPasswordForm';
+
+export default function ProfileSettings() {
+ const [tab, setTab] = useState('general');
+ const { toast, showToast } = useToast();
+
+ const handleSave = () => {
+ showToast({ message: 'Saved successfully.', variant: 'success' });
+ };
+
+ return (
+
+ {toast}
+
+
+ - Profile
+
+
+
+ - General
+ - Password
+
+ {tab === 'general' && }
+ {tab === 'password' && }
+
+ );
+}
diff --git a/components/pages/Settings.js b/components/pages/Settings.js
index bc5022b9..20d86e4e 100644
--- a/components/pages/Settings.js
+++ b/components/pages/Settings.js
@@ -1,50 +1,23 @@
-import React, { useState } from 'react';
-import { FormattedMessage } from 'react-intl';
-import { useRouter } from 'next/router';
-import Page from 'components/layout/Page';
-import MenuLayout from 'components/layout/MenuLayout';
-import WebsiteSettings from 'components/settings/WebsiteSettings';
-import UserSettings from 'components/settings/UserSettings';
-import ProfileSettings from 'components/settings/ProfileSettings';
-import useUser from 'hooks/useUser';
+import Layout from 'components/layout/Layout';
+import Menu from 'components/nav/Nav';
+import useRequireLogin from 'hooks/useRequireLogin';
+import styles from './Settings.module.css';
-const WEBSITES = '/settings';
-const ACCOUNTS = '/settings/users';
-const PROFILE = '/settings/profile';
+export default function Settings({ children }) {
+ const { user: loggedIn } = useRequireLogin();
-export default function Settings() {
- const { user } = useUser();
- const [option, setOption] = useState(WEBSITES);
- const router = useRouter();
- const { pathname } = router;
-
- if (!user) {
+ if (!loggedIn) {
return null;
}
- const menuOptions = [
- {
- label: ,
- value: WEBSITES,
- },
- {
- label: ,
- value: ACCOUNTS,
- hidden: !user?.isAdmin,
- },
- {
- label: ,
- value: PROFILE,
- },
- ];
-
return (
-
-
- {pathname === WEBSITES && }
- {pathname === ACCOUNTS && }
- {pathname === PROFILE && }
-
-
+
+
+
);
}
diff --git a/components/pages/Settings.module.css b/components/pages/Settings.module.css
new file mode 100644
index 00000000..ad0a22f8
--- /dev/null
+++ b/components/pages/Settings.module.css
@@ -0,0 +1,16 @@
+.dashboard {
+ display: flex;
+ flex: 1;
+}
+
+.nav {
+ margin-top: 20px;
+}
+
+.content {
+ position: relative;
+ background: var(--base50);
+ flex: 1;
+ border-radius: 8px;
+ overflow: hidden;
+}
diff --git a/components/pages/TeamDetails.js b/components/pages/TeamDetails.js
new file mode 100644
index 00000000..30d54a9b
--- /dev/null
+++ b/components/pages/TeamDetails.js
@@ -0,0 +1,58 @@
+import { useEffect, useState } from 'react';
+import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
+import { useQuery } from '@tanstack/react-query';
+import { useApi } from 'next-basics';
+import Link from 'next/link';
+import Page from 'components/layout/Page';
+import TeamEditForm from 'components/forms/TeamEditForm';
+import PageHeader from 'components/layout/PageHeader';
+import { getAuthToken } from 'lib/client';
+import TeamMembersTable from '../tables/TeamMembersTable';
+
+export default function TeamDetails({ teamId }) {
+ const [values, setValues] = useState(null);
+ const [tab, setTab] = useState('general');
+ const { get } = useApi(getAuthToken());
+ const { toast, showToast } = useToast();
+ const { data, isLoading } = useQuery(
+ ['team', teamId],
+ () => {
+ if (teamId) {
+ return get(`/teams/${teamId}`);
+ }
+ },
+ { cacheTime: 0 },
+ );
+
+ const handleSave = data => {
+ showToast({ message: 'Saved successfully.', variant: 'success' });
+ setValues(state => ({ ...state, ...data }));
+ };
+
+ useEffect(() => {
+ if (data) {
+ setValues(data);
+ }
+ }, [data]);
+
+ return (
+
+ {toast}
+
+
+ -
+ Teams
+
+ - {values?.name}
+
+
+
+ - General
+ - Members
+ - Websites
+
+ {tab === 'general' && }
+ {tab === 'members' && }
+
+ );
+}
diff --git a/components/pages/TeamsList.js b/components/pages/TeamsList.js
new file mode 100644
index 00000000..30a21b95
--- /dev/null
+++ b/components/pages/TeamsList.js
@@ -0,0 +1,65 @@
+import { useState } from 'react';
+import { Button, Icon, Modal, useToast, Flexbox } from 'react-basics';
+import { useApi } from 'next-basics';
+import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
+import TeamAddForm from 'components/forms/TeamAddForm';
+import PageHeader from 'components/layout/PageHeader';
+import TeamsTable from 'components/tables/TeamsTable';
+import Page from 'components/layout/Page';
+import { getAuthToken } from 'lib/client';
+import { useQuery } from '@tanstack/react-query';
+
+export default function TeamsList() {
+ const [edit, setEdit] = useState(false);
+ const [update, setUpdate] = useState(0);
+ const { get } = useApi(getAuthToken());
+ const { data, isLoading, error } = useQuery(['teams', update], () => get(`/teams`));
+ const hasData = data && data.length !== 0;
+ const { toast, showToast } = useToast();
+
+ const columns = [
+ { name: 'name', label: 'Name', style: { flex: 2 } },
+ { name: 'action', label: ' ' },
+ ];
+
+ const handleAdd = () => {
+ setEdit(true);
+ };
+
+ const handleSave = () => {
+ setEdit(false);
+ setUpdate(state => state + 1);
+ showToast({ message: 'Team saved.', variant: 'success' });
+ };
+
+ const handleClose = () => {
+ setEdit(false);
+ };
+
+ return (
+
+ {toast}
+
+
+
+
+ {hasData && }
+ {!hasData && (
+
+
+
+
+
+ )}
+ {edit && (
+
+ {close => }
+
+ )}
+
+ );
+}
diff --git a/components/pages/TestConsole.js b/components/pages/TestConsole.js
index c505030c..9da67663 100644
--- a/components/pages/TestConsole.js
+++ b/components/pages/TestConsole.js
@@ -1,14 +1,13 @@
-import { Row, Column } from 'react-basics';
+import DropDown from 'components/common/DropDown';
+import Page from 'components/layout/Page';
+import PageHeader from 'components/layout/PageHeader';
+import EventsChart from 'components/metrics/EventsChart';
+import WebsiteChart from 'components/metrics/WebsiteChart';
+import useFetch from 'hooks/useFetch';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
-import Page from 'components/layout/Page';
-import PageHeader from 'components/layout/PageHeader';
-import DropDown from 'components/common/DropDown';
-import WebsiteChart from 'components/metrics/WebsiteChart';
-import EventsChart from 'components/metrics/EventsChart';
-import Button from 'components/common/Button';
-import useFetch from 'hooks/useFetch';
+import { Button, Column, Row } from 'react-basics';
import styles from './TestConsole.module.css';
export default function TestConsole() {
diff --git a/components/pages/UserDelete.js b/components/pages/UserDelete.js
new file mode 100644
index 00000000..fe4423b9
--- /dev/null
+++ b/components/pages/UserDelete.js
@@ -0,0 +1,30 @@
+import UserDeleteForm from 'components/forms/UserDeleteForm';
+import { useRouter } from 'next/router';
+import { useState } from 'react';
+import { Button, Form, FormRow, Modal } from 'react-basics';
+
+export default function UserDelete({ userId, onSave }) {
+ const [modal, setModal] = useState(null);
+ const router = useRouter();
+
+ const handleDelete = async () => {
+ onSave();
+ await router.push('/users');
+ };
+
+ const handleClose = () => setModal(null);
+
+ return (
+
+ );
+}
diff --git a/components/pages/UserSettings.js b/components/pages/UserSettings.js
new file mode 100644
index 00000000..80705133
--- /dev/null
+++ b/components/pages/UserSettings.js
@@ -0,0 +1,69 @@
+import { useQuery } from '@tanstack/react-query';
+import UserDelete from 'components/pages/UserDelete';
+import UserEditForm from 'components/forms/UserEditForm';
+import UserPasswordForm from 'components/forms/UserPasswordForm';
+import Page from 'components/layout/Page';
+import PageHeader from 'components/layout/PageHeader';
+import { getAuthToken } from 'lib/client';
+import { useApi } from 'next-basics';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
+
+export default function UserSettings({ userId }) {
+ const [values, setValues] = useState(null);
+ const [tab, setTab] = useState('general');
+ const { get } = useApi(getAuthToken());
+ const { toast, showToast } = useToast();
+ const router = useRouter();
+ const { data, isLoading } = useQuery(
+ ['user', userId],
+ () => {
+ if (userId) {
+ return get(`/users/${userId}`);
+ }
+ },
+ { cacheTime: 0 },
+ );
+
+ const handleSave = data => {
+ showToast({ message: 'Saved successfully.', variant: 'success' });
+ if (data) {
+ setValues(state => ({ ...state, ...data }));
+ }
+ };
+
+ const handleDelete = async () => {
+ showToast({ message: 'Deleted successfully.', variant: 'danger' });
+ await router.push('/users');
+ };
+
+ useEffect(() => {
+ if (data) {
+ setValues(data);
+ }
+ }, [data]);
+
+ return (
+
+ {toast}
+
+
+ -
+ Users
+
+ - {values?.username}
+
+
+
+ - General
+ - Password
+ - Danger Zone
+
+ {tab === 'general' && }
+ {tab === 'password' && }
+ {tab === 'delete' && }
+
+ );
+}
diff --git a/components/pages/UsersList.js b/components/pages/UsersList.js
new file mode 100644
index 00000000..726a1cdc
--- /dev/null
+++ b/components/pages/UsersList.js
@@ -0,0 +1,45 @@
+import Page from 'components/layout/Page';
+import PageHeader from 'components/layout/PageHeader';
+import UsersTable from 'components/tables/UsersTable';
+import { useState } from 'react';
+import { Button, Icon, useToast } from 'react-basics';
+import { getAuthToken } from 'lib/client';
+import { useMutation } from '@tanstack/react-query';
+import { useApi } from 'next-basics';
+
+export default function UsersList() {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState();
+ const { toast, showToast } = useToast();
+ const { post } = useApi(getAuthToken());
+ const { mutate, isLoading } = useMutation(data => post('/api-key', data));
+
+ const handleSave = () => {
+ mutate(
+ {},
+ {
+ onSuccess: async () => {
+ showToast({ message: 'API key saved.', variant: 'success' });
+ },
+ },
+ );
+ };
+
+ return (
+
+ {toast}
+
+
+
+ {
+ setLoading(isLoading);
+ setError(error);
+ }}
+ onAddKeyClick={handleSave}
+ />
+
+ );
+}
diff --git a/components/pages/WebsiteList.js b/components/pages/WebsiteList.js
index dfd041a1..3bdf56a1 100644
--- a/components/pages/WebsiteList.js
+++ b/components/pages/WebsiteList.js
@@ -36,7 +36,7 @@ export default function WebsiteList({ websites, showCharts, limit }) {
return (
- } iconRight>
+ } iconRight>
{formatMessage(messages.goToSettngs)}
diff --git a/components/pages/WebsiteSettings.js b/components/pages/WebsiteSettings.js
new file mode 100644
index 00000000..3dcd8a59
--- /dev/null
+++ b/components/pages/WebsiteSettings.js
@@ -0,0 +1,76 @@
+import { useEffect, useState } from 'react';
+import { Breadcrumbs, Item, Tabs, useToast, Button, Icon } from 'react-basics';
+import { useQuery } from '@tanstack/react-query';
+import { useApi } from 'next-basics';
+import Link from 'next/link';
+import Page from 'components/layout/Page';
+import WebsiteEditForm from 'components/forms/WebsiteEditForm';
+import WebsiteReset from 'components/forms/WebsiteReset';
+import PageHeader from 'components/layout/PageHeader';
+import TrackingCodeForm from 'components/forms/TrackingCodeForm';
+import ShareUrlForm from 'components/forms/ShareUrlForm';
+import { getAuthToken } from 'lib/client';
+import ExternalLink from 'assets/external-link.svg';
+
+export default function Websites({ websiteId }) {
+ const [values, setValues] = useState(null);
+ const [tab, setTab] = useState('general');
+ const { get } = useApi(getAuthToken());
+ const { toast, showToast } = useToast();
+ const { data, isLoading } = useQuery(
+ ['website', websiteId],
+ () => {
+ if (websiteId) {
+ return get(`/websites/${websiteId}`);
+ }
+ },
+ { cacheTime: 0 },
+ );
+
+ const handleSave = data => {
+ showToast({ message: 'Saved successfully.', variant: 'success' });
+ setValues(state => ({ ...state, ...data }));
+ };
+
+ useEffect(() => {
+ if (data) {
+ setValues(data);
+ }
+ }, [data]);
+
+ return (
+
+ {toast}
+
+
+ -
+ Websites
+
+ - {values?.name}
+
+
+
+
+
+
+
+
+ - General
+ - Tracking code
+ - Share URL
+ - Danger zone
+
+ {tab === 'general' && (
+
+ )}
+ {tab === 'tracking' && }
+ {tab === 'share' && }
+ {tab === 'danger' && }
+
+ );
+}
diff --git a/components/pages/WebsitesList.js b/components/pages/WebsitesList.js
new file mode 100644
index 00000000..0bb5f854
--- /dev/null
+++ b/components/pages/WebsitesList.js
@@ -0,0 +1,70 @@
+import { useState } from 'react';
+import { Button, Icon, Modal, useToast, Flexbox } from 'react-basics';
+import { useApi } from 'next-basics';
+import { useQuery } from '@tanstack/react-query';
+import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
+import WebsiteAddForm from 'components/forms/WebsiteAddForm';
+import PageHeader from 'components/layout/PageHeader';
+import WebsitesTable from 'components/tables/WebsitesTable';
+import Page from 'components/layout/Page';
+import { getAuthToken } from 'lib/client';
+import useUser from 'hooks/useUser';
+
+export default function WebsitesList() {
+ const [edit, setEdit] = useState(false);
+ const [update, setUpdate] = useState(0);
+ const { get } = useApi(getAuthToken());
+ const { user } = useUser();
+ const { data, isLoading, error } = useQuery(['websites', update], () =>
+ get(`/users/${user.id}/websites`),
+ );
+ const hasData = data && data.length !== 0;
+ const { toast, showToast } = useToast();
+
+ const columns = [
+ { name: 'name', label: 'Name', style: { flex: 2 } },
+ { name: 'domain', label: 'Domain' },
+ { name: 'action', label: ' ' },
+ ];
+
+ const handleAdd = () => {
+ setEdit(true);
+ };
+
+ const handleSave = () => {
+ setEdit(false);
+ setUpdate(state => state + 1);
+ showToast({ message: 'Website saved.', variant: 'success' });
+ };
+
+ const handleClose = () => {
+ setEdit(false);
+ };
+
+ return (
+
+ {toast}
+
+
+
+
+ {hasData && }
+ {!hasData && (
+
+
+
+
+
+ )}
+ {edit && (
+
+ {close => }
+
+ )}
+
+ );
+}
diff --git a/components/settings/DashboardSettingsButton.js b/components/settings/DashboardSettingsButton.js
index 15611d0e..64781ef0 100644
--- a/components/settings/DashboardSettingsButton.js
+++ b/components/settings/DashboardSettingsButton.js
@@ -3,6 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
import MenuButton from 'components/common/MenuButton';
import Gear from 'assets/gear.svg';
import { saveDashboard } from 'store/dashboard';
+import { Icon } from 'react-basics';
const messages = defineMessages({
toggleCharts: { id: 'message.toggle-charts', defaultMessage: 'Toggle charts' },
@@ -32,5 +33,11 @@ export default function DashboardSettingsButton() {
}
}
- return } options={menuOptions} onSelect={handleSelect} hideLabel />;
+ return (
+
+
+
+
+
+ );
}
diff --git a/components/settings/DateRangeSetting.js b/components/settings/DateRangeSetting.js
index 9c59d3ea..a6c2e4c8 100644
--- a/components/settings/DateRangeSetting.js
+++ b/components/settings/DateRangeSetting.js
@@ -1,7 +1,7 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import DateFilter, { filterOptions } from 'components/common/DateFilter';
-import Button from 'components/common/Button';
+import { Button } from 'react-basics';
import useDateRange from 'hooks/useDateRange';
import { DEFAULT_DATE_RANGE } from 'lib/constants';
import styles from './DateRangeSetting.module.css';
@@ -28,7 +28,7 @@ export default function DateRangeSetting() {
endDate={endDate}
onChange={handleChange}
/>
-