mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-15 09:45:04 +01:00
Rewrite admin. (#1713)
* Rewrite admin. * Clean up password forms. * Fix naming issues. * CSS Naming.
This commit is contained in:
parent
f4db04c3c6
commit
e1f99a7d01
1
assets/buoy.svg
Normal file
1
assets/buoy.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m457.65 54.783-.441-.441c-14.686-14.656-37.156-16.922-54.325-6.828C359.083 16.393 308.546 0 256 0c-52.545 0-103.082 16.393-146.884 47.515-17.168-10.094-39.635-7.83-54.314 6.817l-.46.46c-14.656 14.689-16.92 37.158-6.827 54.325C16.393 152.918 0 203.455 0 256s16.393 103.082 47.515 146.884c-10.093 17.167-7.828 39.637 6.83 54.328l.446.446c14.622 14.59 37.075 16.971 54.326 6.828C152.919 495.607 203.455 512 256 512c52.546 0 103.082-16.393 146.883-47.514 17.272 10.155 39.721 7.745 54.329-6.831l.445-.445c14.657-14.689 16.922-37.158 6.828-54.326C495.606 359.082 512 308.545 512 256s-16.394-103.081-47.515-146.884c10.094-17.168 7.828-39.638-6.835-54.333zm-42.556 20.915c5.798-5.796 15.184-5.849 20.919-.126l.408.409c5.73 5.743 5.678 15.13-.118 20.925l-74.876 74.875a136.212 136.212 0 0 0-21.209-21.209zM361 256c0 57.897-47.103 105-105 105s-105-47.103-105-105 47.103-105 105-105 105 47.103 105 105zM256 30c44.114 0 86.687 13.18 124.112 38.253l-65.939 65.939C296.548 125.74 276.818 121 256 121s-40.548 4.74-58.174 13.191l-65.938-65.939C169.313 43.18 211.886 30 256 30zM75.572 75.988l.409-.409c5.743-5.73 15.13-5.677 20.926.118l74.875 74.876a136.212 136.212 0 0 0-21.209 21.209L75.697 96.906c-5.795-5.796-5.848-15.183-.125-20.918zM30 256c0-44.113 13.18-86.687 38.253-124.112l65.938 65.939C125.74 215.452 121 235.182 121 256s4.74 40.548 13.191 58.174l-65.938 65.939C43.18 342.687 30 300.113 30 256zm66.906 180.302c-5.796 5.796-15.182 5.849-20.941.103l-.386-.385c-5.73-5.743-5.677-15.13.118-20.926l74.875-74.875a136.168 136.168 0 0 0 21.209 21.209zM256 482c-44.114 0-86.687-13.18-124.112-38.253l65.938-65.939C215.452 386.26 235.182 391 256 391s40.548-4.74 58.174-13.191l65.939 65.939C342.687 468.82 300.114 482 256 482zm180.423-45.983-.404.404c-5.742 5.73-15.128 5.677-20.925-.119l-74.875-74.875a136.168 136.168 0 0 0 21.209-21.209l74.876 74.876c5.795 5.796 5.847 15.183.119 20.923zM482 256c0 44.113-13.18 86.687-38.253 124.112l-65.938-65.938C386.26 296.548 391 276.818 391 256s-4.74-40.548-13.191-58.174l65.938-65.938C468.82 169.314 482 211.887 482 256z"/></svg>
|
After Width: | Height: | Size: 2.1 KiB |
1
assets/card.svg
Normal file
1
assets/card.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="512pt" viewBox="0 -76 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="M452 0H60C26.914 0 0 26.914 0 60v240c0 33.086 26.914 60 60 60h392c33.086 0 60-26.914 60-60V60c0-33.086-26.914-60-60-60zM60 40h392c11.027 0 20 8.973 20 20v20H40V60c0-11.027 8.973-20 20-20zm392 280H60c-11.027 0-20-8.973-20-20V120h432v180c0 11.027-8.973 20-20 20zm-10-55c0 13.809-11.191 25-25 25s-25-11.191-25-25 11.191-25 25-25 25 11.191 25 25zm-70 0c0 13.809-11.191 25-25 25s-25-11.191-25-25 11.191-25 25-25 25 11.191 25 25zm0 0"/></svg>
|
After Width: | Height: | Size: 538 B |
1
assets/users.svg
Normal file
1
assets/users.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 477.869 477.869" style="enable-background:new 0 0 477.869 477.869" xml:space="preserve"><path d="M387.415 233.496c48.976-44.029 52.987-119.424 8.958-168.4C355.991 20.177 288.4 12.546 239.02 47.332c-53.83-37.99-128.264-25.149-166.254 28.68-34.859 49.393-27.259 117.054 17.689 157.483C34.606 262.935-.251 320.976.002 384.108v51.2c0 9.426 7.641 17.067 17.067 17.067h443.733c9.426 0 17.067-7.641 17.067-17.067v-51.2c.252-63.132-34.605-121.173-90.454-150.612zM307.201 59.842c47.062-.052 85.256 38.057 85.309 85.119.037 33.564-19.631 64.023-50.237 77.799-1.314.597-2.628 1.143-3.959 1.707a83.66 83.66 0 0 1-12.988 4.045c-.853.188-1.707.29-2.577.461a85.366 85.366 0 0 1-15.019 1.519c-2.27 0-4.557-.171-6.827-.375-.853 0-1.707 0-2.56-.171a86.219 86.219 0 0 1-27.904-8.226c-.324-.154-.7-.137-1.024-.273-1.707-.819-3.413-1.536-4.932-2.458.137-.171.222-.358.358-.529a119.721 119.721 0 0 0 18.278-33.297l.529-1.434a120.381 120.381 0 0 0 4.523-17.562c.154-.87.273-1.707.41-2.645.987-6.067 1.506-12.2 1.553-18.347a120.041 120.041 0 0 0-1.553-18.313c-.137-.887-.256-1.707-.41-2.645a120.414 120.414 0 0 0-4.523-17.562l-.529-1.434a119.747 119.747 0 0 0-18.278-33.297c-.137-.171-.222-.358-.358-.529a84.787 84.787 0 0 1 42.718-11.553zM85.335 145.176c-.121-47.006 37.886-85.21 84.892-85.331a85.112 85.112 0 0 1 59.134 23.686c.99.956 1.963 1.911 2.918 2.901a87.748 87.748 0 0 1 8.09 9.813c.751 1.058 1.434 2.185 2.133 3.277a83.951 83.951 0 0 1 6.263 11.52c.427.973.751 1.963 1.126 2.935a83.422 83.422 0 0 1 4.233 13.653c.12.512.154 1.024.256 1.553a80.338 80.338 0 0 1 0 32.119c-.102.529-.137 1.041-.256 1.553a83.228 83.228 0 0 1-4.233 13.653c-.375.973-.7 1.963-1.126 2.935a84.251 84.251 0 0 1-6.263 11.503c-.7 1.092-1.382 2.219-2.133 3.277a87.549 87.549 0 0 1-8.09 9.813c-.956.99-1.929 1.946-2.918 2.901a85.187 85.187 0 0 1-23.569 15.906 49.35 49.35 0 0 1-4.198 1.707 85.839 85.839 0 0 1-12.663 3.925c-1.075.239-2.185.375-3.277.563a84.67 84.67 0 0 1-14.046 1.417h-1.877a84.563 84.563 0 0 1-14.046-1.417c-1.092-.188-2.202-.324-3.277-.563a85.802 85.802 0 0 1-12.663-3.925c-1.417-.563-2.816-1.143-4.198-1.707-30.534-13.786-50.173-44.166-50.212-77.667zm221.866 273.066H34.135v-34.133c-.25-57.833 36.188-109.468 90.76-128.614a119.092 119.092 0 0 0 91.546 0 137.138 137.138 0 0 1 16.623 7.356c3.55 1.826 6.827 3.908 10.24 6.007 2.219 1.382 4.471 2.731 6.605 4.25 3.294 2.338 6.4 4.881 9.455 7.492 1.963 1.707 3.908 3.413 5.751 5.12 2.816 2.662 5.461 5.478 8.004 8.363a134.465 134.465 0 0 1 5.291 6.383 132.594 132.594 0 0 1 6.349 8.823c1.707 2.56 3.226 5.222 4.727 7.885 1.707 2.935 3.277 5.871 4.71 8.926 1.434 3.055 2.697 6.4 3.925 9.66 1.075 2.833 2.219 5.649 3.106 8.533 1.195 3.959 2.031 8.055 2.867 12.151.512 2.423 1.178 4.796 1.553 7.253a141.153 141.153 0 0 1 1.553 20.412v34.133zm136.534 0h-102.4v-34.133c0-5.342-.307-10.633-.785-15.872-.137-1.536-.375-3.055-.546-4.591-.461-3.772-.99-7.509-1.707-11.213a246.936 246.936 0 0 0-.973-4.762c-.819-3.8-1.769-7.566-2.85-11.298-.358-1.229-.683-2.475-1.058-3.686a169.105 169.105 0 0 0-20.565-43.127l-.666-.973a168.958 168.958 0 0 0-9.404-12.646l-.119-.154a154.895 154.895 0 0 0-11.008-12.237h.7a120.8 120.8 0 0 0 14.524 1.024h.939c4.496-.039 8.985-.33 13.449-.87 1.399-.171 2.782-.427 4.181-.649a117.43 117.43 0 0 0 10.752-2.167c1.007-.256 2.031-.495 3.055-.785a116.211 116.211 0 0 0 13.653-4.642c54.612 19.127 91.083 70.785 90.829 128.649v34.132z"/></svg>
|
After Width: | Height: | Size: 3.4 KiB |
1
assets/website.svg
Normal file
1
assets/website.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.999 511.999" style="enable-background:new 0 0 511.999 511.999" xml:space="preserve"><path d="M437.019 74.981C388.667 26.628 324.38 0 256 0 187.62 0 123.332 26.628 74.981 74.98 26.628 123.332 0 187.62 0 256s26.628 132.667 74.981 181.019c48.351 48.352 112.639 74.98 181.019 74.98 68.381 0 132.667-26.628 181.02-74.981C485.371 388.667 512 324.379 512 255.999s-26.629-132.667-74.981-181.018zM96.216 96.216c22.511-22.511 48.938-39.681 77.742-50.888-7.672 9.578-14.851 20.587-21.43 32.969-7.641 14.38-14.234 30.173-19.725 47.042-19.022-3.157-36.647-7.039-52.393-11.595a230.423 230.423 0 0 1 15.806-17.528zm-33.987 43.369c18.417 5.897 39.479 10.87 62.461 14.809-6.4 27.166-10.167 56.399-11.066 86.591H30.536c2.36-36.233 13.242-70.813 31.693-101.4zm-1.635 230.053c-17.455-29.899-27.769-63.481-30.059-98.623h83.146c.982 29.329 4.674 57.731 10.858 84.186-23.454 3.802-45.045 8.649-63.945 14.437zm35.622 46.146a229.917 229.917 0 0 1-17.831-20.055c16.323-4.526 34.571-8.359 54.214-11.433 5.53 17.103 12.194 33.105 19.928 47.662 7.17 13.493 15.053 25.349 23.51 35.505-29.61-11.183-56.769-28.629-79.821-51.679zm144.768 62.331c-22.808-6.389-44.384-27.217-61.936-60.249-6.139-11.552-11.531-24.155-16.15-37.587 24.73-2.722 51.045-4.331 78.086-4.709v102.545zm0-132.578c-29.988.409-59.217 2.292-86.59 5.507-6.038-24.961-9.671-51.978-10.668-80.028h97.259v74.521zm0-104.553h-97.315c.911-28.834 4.602-56.605 10.828-82.201 27.198 3.4 56.366 5.468 86.487 6.06v76.141zm0-106.176c-27.146-.547-53.403-2.317-77.958-5.205 4.591-13.292 9.941-25.768 16.022-37.215 17.551-33.032 39.128-53.86 61.936-60.249v102.669zm209.733 6.372c17.874 30.193 28.427 64.199 30.749 99.804h-83.088c-.889-29.844-4.584-58.749-10.85-85.647 23.133-3.736 44.456-8.489 63.189-14.157zm-34.934-44.964a230.122 230.122 0 0 1 16.914 18.91c-16.073 4.389-33.972 8.114-53.204 11.112-5.548-17.208-12.243-33.305-20.02-47.941-6.579-12.382-13.758-23.391-21.43-32.969 28.802 11.207 55.23 28.377 77.74 50.888zm-144.767 174.8h97.259c-1.004 28.268-4.686 55.49-10.81 80.612-27.194-3.381-56.349-5.43-86.449-6.006v-74.606zm0-30.032v-76.041c30.005-.394 59.257-2.261 86.656-5.464 6.125 25.403 9.756 52.932 10.659 81.505h-97.315zm-.002-208.845h.001c22.808 6.389 44.384 27.217 61.936 60.249 6.178 11.627 11.601 24.318 16.24 37.848-24.763 2.712-51.108 4.309-78.177 4.674V32.139zm.002 445.976V375.657c27.12.532 53.357 2.286 77.903 5.156-4.579 13.232-9.911 25.654-15.967 37.053-17.552 33.032-39.128 53.86-61.936 60.249zm144.767-62.331c-23.051 23.051-50.21 40.496-79.821 51.678 8.457-10.156 16.34-22.011 23.51-35.504 7.62-14.341 14.198-30.088 19.68-46.906 19.465 3.213 37.473 7.186 53.515 11.859a230.268 230.268 0 0 1-16.884 18.873zm34.823-44.775c-18.635-5.991-40-11.032-63.326-15.01 6.296-26.68 10.048-55.36 11.041-84.983h83.146c-2.328 35.678-12.918 69.753-30.861 99.993z"/></svg>
|
After Width: | Height: | Size: 2.8 KiB |
@ -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
|
||||
data-tip={tooltip}
|
||||
data-effect="solid"
|
||||
data-for={tooltipId}
|
||||
data-offset={JSON.stringify({ left: 10 })}
|
||||
type={type}
|
||||
className={classNames(styles.button, className, {
|
||||
[styles.large]: size === 'large',
|
||||
[styles.small]: size === 'small',
|
||||
[styles.xsmall]: size === 'xsmall',
|
||||
[styles.action]: variant === 'action',
|
||||
[styles.danger]: variant === 'danger',
|
||||
[styles.light]: variant === 'light',
|
||||
[styles.iconRight]: iconRight,
|
||||
})}
|
||||
disabled={disabled}
|
||||
onClick={!disabled ? onClick : null}
|
||||
{...props}
|
||||
>
|
||||
{icon && <Icon className={styles.icon} icon={icon} size={size} />}
|
||||
{children && <div className={styles.label}>{children}</div>}
|
||||
{tooltip && <ReactTooltip id={tooltipId}>{tooltip}</ReactTooltip>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
@ -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);
|
||||
}
|
@ -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 (
|
||||
<div className={classNames(styles.group, className)}>
|
||||
{items.map(item => {
|
||||
const { label, value } = item;
|
||||
return (
|
||||
<Button
|
||||
key={value}
|
||||
className={classNames(styles.button, { [styles.selected]: selectedItem === value })}
|
||||
size={size}
|
||||
icon={icon}
|
||||
onClick={() => onClick(value)}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
@ -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;
|
||||
}
|
@ -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}
|
||||
<Icon className={styles.icon} icon={selectMonth ? <Cross /> : <Chevron />} size="small" />
|
||||
<Icon className={styles.icon} size="small">
|
||||
{selectMonth ? <Cross /> : <Chevron />}
|
||||
</Icon>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(styles.selector, { [styles.open]: selectYear })}
|
||||
onClick={toggleYearSelect}
|
||||
>
|
||||
{year}
|
||||
<Icon className={styles.icon} icon={selectYear ? <Cross /> : <Chevron />} size="small" />
|
||||
<Icon className={styles.icon} size="small">
|
||||
{selectMonth ? <Cross /> : <Chevron />}
|
||||
</Icon>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
@ -230,12 +233,15 @@ const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
|
||||
<div className={styles.pager}>
|
||||
<div className={styles.left}>
|
||||
<Button
|
||||
icon={<Chevron />}
|
||||
size="small"
|
||||
onClick={handlePrevClick}
|
||||
disabled={years[0] <= minYear}
|
||||
variant="light"
|
||||
/>
|
||||
>
|
||||
<Icon>
|
||||
<Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.middle}>
|
||||
<table>
|
||||
@ -261,12 +267,15 @@ const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
<Button
|
||||
icon={<Chevron />}
|
||||
size="small"
|
||||
onClick={handleNextClick}
|
||||
disabled={years[years.length - 1] > maxYear}
|
||||
variant="light"
|
||||
/>
|
||||
>
|
||||
<Icon>
|
||||
<Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.checkbox} onClick={onClick}>
|
||||
{value && <Icon icon={<Check />} size="small" />}
|
||||
</div>
|
||||
<label className={styles.label} htmlFor={name} onClick={onClick}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
ref={ref}
|
||||
className={styles.input}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
defaultChecked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Checkbox.propTypes = {
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
label: PropTypes.node,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Checkbox;
|
@ -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%;
|
||||
}
|
@ -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 = (
|
||||
|
@ -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: <FormattedMessage id="label.today" defaultMessage="Today" />, value: '1day' },
|
||||
@ -120,7 +119,9 @@ const CustomRange = ({ startDate, endDate, onClick }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon icon={<Calendar />} className="mr-2" onClick={handleClick} />
|
||||
<Icon className="mr-2" onClick={handleClick}>
|
||||
<Calendar />
|
||||
</Icon>
|
||||
{dateFormat(startDate, 'd LLL y', locale)}
|
||||
{!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`}
|
||||
</>
|
||||
|
@ -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 = ()
|
||||
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
|
||||
<div className={styles.value}>
|
||||
<div className={styles.text}>{options.find(e => e.value === value)?.label || value}</div>
|
||||
<Icon icon={<Chevron />} className={styles.icon} size="small" />
|
||||
<Icon className={styles.icon} size="small">
|
||||
<Chevron />
|
||||
</Icon>
|
||||
</div>
|
||||
{showMenu && (
|
||||
<Menu
|
||||
|
@ -1,15 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'components/common/Icon';
|
||||
import { Icon, Flexbox } from 'react-basics';
|
||||
import Logo from 'assets/logo.svg';
|
||||
import styles from './EmptyPlaceholder.module.css';
|
||||
|
||||
function EmptyPlaceholder({ msg, children }) {
|
||||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
|
||||
<Icon className={styles.icon} size="xl">
|
||||
<Logo />
|
||||
</Icon>
|
||||
<h2 className={styles.msg}>{msg}</h2>
|
||||
{children}
|
||||
<Flexbox justifyContent="center" alignItems="center">
|
||||
{children}
|
||||
</Flexbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
||||
<div className={styles.error}>
|
||||
<Icon icon={<Exclamation />} className={styles.icon} size="large" />
|
||||
<Icon className={styles.icon} size="large">
|
||||
<Exclamation />
|
||||
</Icon>
|
||||
<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />
|
||||
</div>
|
||||
);
|
||||
|
@ -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 (
|
||||
<>
|
||||
<Button
|
||||
icon={<List />}
|
||||
tooltip={<FormattedMessage id="label.event-data" defaultMessage="Event" />}
|
||||
tooltipId="button-event"
|
||||
size="small"
|
||||
onClick={handleClick}
|
||||
className={styles.button}
|
||||
>
|
||||
<Icon>
|
||||
<List />
|
||||
</Icon>
|
||||
Event Data
|
||||
</Button>
|
||||
{showEventData && (
|
||||
|
@ -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 (
|
||||
|
@ -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 }) {
|
||||
</Link>
|
||||
{externalUrl && (
|
||||
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">
|
||||
<Icon icon={<External />} className={styles.icon} />
|
||||
<Icon className={styles.icon}>
|
||||
<External />
|
||||
</Icon>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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 (
|
||||
<>
|
||||
<Button
|
||||
className={styles.button}
|
||||
icon={active ? <XMark /> : <Bars />}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<Button className={styles.button} onClick={handleClick}>
|
||||
<Icon>{active ? <XMark /> : <Bars />}</Icon>
|
||||
</Button>
|
||||
{active && <MobileMenu items={menuItems} onClose={handleClose} />}
|
||||
</>
|
||||
);
|
||||
|
@ -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 }) {
|
||||
|
@ -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 (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<Button
|
||||
icon={icon}
|
||||
className={classNames(styles.button, buttonClassName, { [styles.open]: showMenu })}
|
||||
onClick={toggleMenu}
|
||||
variant={buttonVariant}
|
||||
@ -49,6 +48,7 @@ function MenuButton({
|
||||
{!hideLabel && (
|
||||
<div className={styles.text}>{renderValue ? renderValue(selectedOption) : value}</div>
|
||||
)}
|
||||
{children}
|
||||
</Button>
|
||||
{showMenu && (
|
||||
<Menu
|
||||
|
@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import Link from './Link';
|
||||
import Button from './Button';
|
||||
import { Button } from 'react-basics';
|
||||
import XMark from 'assets/xmark.svg';
|
||||
import styles from './MobileMenu.module.css';
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import styles from './Modal.module.css';
|
||||
|
||||
function Modal({ title, children }) {
|
||||
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<animated.div className={styles.modal} style={props}>
|
||||
<div className={styles.content}>
|
||||
{title && <div className={styles.header}>{title}</div>}
|
||||
<div className={styles.body}>{children}</div>
|
||||
</div>
|
||||
</animated.div>,
|
||||
document.getElementById('__modals'),
|
||||
);
|
||||
}
|
||||
|
||||
Modal.propTypes = {
|
||||
title: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Modal;
|
@ -1,46 +0,0 @@
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.modal:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
background: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--base50);
|
||||
min-width: 400px;
|
||||
min-height: 100px;
|
||||
max-width: 100vw;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--base300);
|
||||
padding: 30px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import useStore from 'store/queries';
|
||||
import { setDateRange } from 'store/websites';
|
||||
import Button from './Button';
|
||||
import { Button, Icon } from 'react-basics';
|
||||
import Refresh from 'assets/redo.svg';
|
||||
import Dots from 'assets/ellipsis-h.svg';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
@ -31,12 +31,13 @@ function RefreshButton({ websiteId }) {
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={loading ? <Dots /> : <Refresh />}
|
||||
tooltip={<FormattedMessage id="label.refresh" defaultMessage="Refresh" />}
|
||||
tooltipId="button-refresh"
|
||||
size="small"
|
||||
onClick={handleClick}
|
||||
/>
|
||||
>
|
||||
<Icon>{loading ? <Dots /> : <Refresh />}</Icon>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import NoData from 'components/common/NoData';
|
||||
import styles from './Table.module.css';
|
||||
|
||||
function Table({
|
||||
columns,
|
||||
rows,
|
||||
empty,
|
||||
className,
|
||||
bodyClassName,
|
||||
rowKey,
|
||||
showHeader = true,
|
||||
children,
|
||||
}) {
|
||||
if (empty && rows.length === 0) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.table, className)}>
|
||||
{showHeader && (
|
||||
<div className={classNames(styles.header, 'row')}>
|
||||
{columns.map(({ key, label, className, style, header }) => (
|
||||
<div
|
||||
key={key}
|
||||
className={classNames(styles.head, className, header?.className)}
|
||||
style={{ ...style, ...header?.style }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames(styles.body, bodyClassName)}>
|
||||
{rows.length === 0 && <NoData />}
|
||||
{!children &&
|
||||
rows.map((row, index) => {
|
||||
const id = rowKey ? rowKey(row) : index;
|
||||
return <TableRow key={id} columns={columns} row={row} />;
|
||||
})}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styledObject = PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
});
|
||||
|
||||
Table.propTypes = {
|
||||
columns: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
cell: styledObject,
|
||||
className: PropTypes.string,
|
||||
header: styledObject,
|
||||
key: PropTypes.string,
|
||||
label: PropTypes.node,
|
||||
render: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
}),
|
||||
),
|
||||
rows: PropTypes.arrayOf(PropTypes.object),
|
||||
empty: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
bodyClassName: PropTypes.string,
|
||||
rowKey: PropTypes.func,
|
||||
showHeader: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
||||
export const TableRow = ({ columns, row }) => (
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
{columns.map(({ key, label, render, className, style, cell }, index) => (
|
||||
<div
|
||||
key={`${key}-${index}`}
|
||||
className={classNames(styles.cell, className, cell?.className)}
|
||||
style={{ ...style, ...cell?.style }}
|
||||
>
|
||||
{label && <label>{label}</label>}
|
||||
{render ? render(row) : row[key]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
@ -1,55 +0,0 @@
|
||||
.table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table label {
|
||||
display: none;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header {
|
||||
border-bottom: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.head {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
border-bottom: 1px solid var(--base300);
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.table label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cell {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import Icon from 'components/common/Icon';
|
||||
import Close from 'assets/times.svg';
|
||||
import styles from './Toast.module.css';
|
||||
|
||||
function Toast({ message, timeout = 3000, onClose }) {
|
||||
const props = useSpring({
|
||||
opacity: 1,
|
||||
transform: 'translate3d(0,0px,0)',
|
||||
from: { opacity: 0, transform: 'translate3d(0,-40px,0)' },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(onClose, timeout);
|
||||
}, []);
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<animated.div className={styles.toast} style={props} onClick={onClose}>
|
||||
<div className={styles.message}>{message}</div>
|
||||
<Icon className={styles.close} icon={<Close />} size="small" />
|
||||
</animated.div>,
|
||||
document.getElementById('__modals'),
|
||||
);
|
||||
}
|
||||
|
||||
Toast.propTypes = {
|
||||
message: PropTypes.node,
|
||||
timeout: PropTypes.number,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Toast;
|
@ -1,25 +0,0 @@
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 30px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 300px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
color: var(--msgColor);
|
||||
background: var(--green400);
|
||||
margin: auto;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.close {
|
||||
margin-left: 20px;
|
||||
}
|
@ -4,7 +4,7 @@ import { setItem } from 'next-basics';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import useStore, { checkVersion } from 'store/version';
|
||||
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
||||
import Button from './Button';
|
||||
import { Button } from 'react-basics';
|
||||
import styles from './UpdateNotice.module.css';
|
||||
|
||||
export default function UpdateNotice() {
|
||||
|
@ -1,107 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, {
|
||||
FormButtons,
|
||||
FormError,
|
||||
FormMessage,
|
||||
FormRow,
|
||||
} from 'components/layout/FormLayout';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useUser from 'hooks/useUser';
|
||||
|
||||
const initialValues = {
|
||||
current_password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
};
|
||||
|
||||
const validate = ({ current_password, new_password, confirm_password }) => {
|
||||
const errors = {};
|
||||
|
||||
if (!current_password) {
|
||||
errors.current_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
}
|
||||
if (!new_password) {
|
||||
errors.new_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
}
|
||||
if (!confirm_password) {
|
||||
errors.confirm_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
} else if (new_password !== confirm_password) {
|
||||
errors.confirm_password = (
|
||||
<FormattedMessage id="label.passwords-dont-match" defaultMessage="Passwords don't match" />
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export default function ChangePasswordForm({ values, onSave, onClose }) {
|
||||
const { post } = useApi();
|
||||
const [message, setMessage] = useState();
|
||||
const { user } = useUser();
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { ok, error } = await post(`/users/${user.id}/password`, values);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
||||
} else {
|
||||
setMessage(
|
||||
error || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormLayout>
|
||||
<Formik
|
||||
initialValues={{ ...initialValues, ...values }}
|
||||
validate={validate}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{() => (
|
||||
<Form>
|
||||
<FormRow>
|
||||
<label htmlFor="current_password">
|
||||
<FormattedMessage id="label.current-password" defaultMessage="Current password" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="current_password" type="password" />
|
||||
<FormError name="current_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="new_password">
|
||||
<FormattedMessage id="label.new-password" defaultMessage="New password" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="new_password" type="password" />
|
||||
<FormError name="new_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="confirm_password">
|
||||
<FormattedMessage id="label.confirm-password" defaultMessage="Confirm password" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="confirm_password" type="password" />
|
||||
<FormError name="confirm_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</FormLayout>
|
||||
);
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { isAfter, isBefore, isSameDay } from 'date-fns';
|
||||
import Calendar from 'components/common/Calendar';
|
||||
import Button from 'components/common/Button';
|
||||
import { FormButtons } from 'components/layout/FormLayout';
|
||||
import { isAfter, isBefore, isSameDay } from 'date-fns';
|
||||
import { getDateRangeValues } from 'lib/date';
|
||||
import React, { useState } from 'react';
|
||||
import { Button, ButtonGroup } from 'react-basics';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import styles from './DatePickerForm.module.css';
|
||||
import ButtonGroup from 'components/common/ButtonGroup';
|
||||
|
||||
const FILTER_DAY = 0;
|
||||
const FILTER_RANGE = 1;
|
||||
|
@ -1,105 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, {
|
||||
FormButtons,
|
||||
FormError,
|
||||
FormMessage,
|
||||
FormRow,
|
||||
} from 'components/layout/FormLayout';
|
||||
import Loading from 'components/common/Loading';
|
||||
import useApi from 'hooks/useApi';
|
||||
|
||||
const CONFIRMATION_WORD = 'DELETE';
|
||||
|
||||
const validate = ({ confirmation }) => {
|
||||
const errors = {};
|
||||
|
||||
if (confirmation !== CONFIRMATION_WORD) {
|
||||
errors.confirmation = !confirmation ? (
|
||||
<FormattedMessage id="label.required" defaultMessage="Required" />
|
||||
) : (
|
||||
<FormattedMessage id="label.invalid" defaultMessage="Invalid" />
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export default function DeleteForm({ values, onSave, onClose }) {
|
||||
const { del } = useApi();
|
||||
const [message, setMessage] = useState();
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const handleSubmit = async ({ type, id }) => {
|
||||
setDeleting(true);
|
||||
|
||||
const { ok, data } = await del(`/${type}/${id}`);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
||||
} else {
|
||||
setMessage(
|
||||
data || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
|
||||
);
|
||||
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormLayout>
|
||||
{deleting && <Loading overlay />}
|
||||
<Formik
|
||||
initialValues={{ confirmation: '', ...values }}
|
||||
validate={validate}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{props => (
|
||||
<Form>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="message.confirm-delete"
|
||||
defaultMessage="Are your sure you want to delete {target}?"
|
||||
values={{ target: <b>{values.name}</b> }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="message.delete-warning"
|
||||
defaultMessage="All associated data will be deleted as well."
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="message.type-delete"
|
||||
defaultMessage="Type {delete} in the box below to confirm."
|
||||
values={{ delete: <b>{CONFIRMATION_WORD}</b> }}
|
||||
/>
|
||||
</p>
|
||||
<FormRow>
|
||||
<div>
|
||||
<Field name="confirmation" type="text" />
|
||||
<FormError name="confirmation" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="danger"
|
||||
disabled={props.values.confirmation !== CONFIRMATION_WORD}
|
||||
>
|
||||
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</FormLayout>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import Button from 'components/common/Button';
|
||||
import { Button } from 'react-basics';
|
||||
import DateFilter from 'components/common/DateFilter';
|
||||
import DropDown from 'components/common/DropDown';
|
||||
import FormLayout, {
|
||||
|
@ -1,98 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, {
|
||||
FormButtons,
|
||||
FormError,
|
||||
FormMessage,
|
||||
FormRow,
|
||||
} from 'components/layout/FormLayout';
|
||||
import useApi from 'hooks/useApi';
|
||||
|
||||
const CONFIRMATION_WORD = 'RESET';
|
||||
|
||||
const validate = ({ confirmation }) => {
|
||||
const errors = {};
|
||||
|
||||
if (confirmation !== CONFIRMATION_WORD) {
|
||||
errors.confirmation = !confirmation ? (
|
||||
<FormattedMessage id="label.required" defaultMessage="Required" />
|
||||
) : (
|
||||
<FormattedMessage id="label.invalid" defaultMessage="Invalid" />
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export default function ResetForm({ values, onSave, onClose }) {
|
||||
const { post } = useApi();
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
const handleSubmit = async ({ type, id }) => {
|
||||
const { ok, data } = await post(`/${type}/${id}/reset`);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
||||
} else {
|
||||
setMessage(
|
||||
data || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormLayout>
|
||||
<Formik
|
||||
initialValues={{ confirmation: '', ...values }}
|
||||
validate={validate}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{props => (
|
||||
<Form>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="message.confirm-reset"
|
||||
defaultMessage="Are your sure you want to reset {target}'s statistics?"
|
||||
values={{ target: <b>{values.name}</b> }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="message.reset-warning"
|
||||
defaultMessage="All statistics for this website will be deleted, but your tracking code will remain intact."
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="message.type-reset"
|
||||
defaultMessage="Type {reset} in the box below to confirm."
|
||||
values={{ reset: <b>{CONFIRMATION_WORD}</b> }}
|
||||
/>
|
||||
</p>
|
||||
<FormRow>
|
||||
<div>
|
||||
<Field name="confirmation" type="text" />
|
||||
<FormError name="confirmation" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="danger"
|
||||
disabled={props.values.confirmation !== CONFIRMATION_WORD}
|
||||
>
|
||||
<FormattedMessage id="label.reset" defaultMessage="Reset" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</FormLayout>
|
||||
);
|
||||
}
|
@ -1,42 +1,85 @@
|
||||
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 { useMutation } from '@tanstack/react-query';
|
||||
import { getAuthToken } from 'lib/client';
|
||||
import { getRandomChars, useApi } from 'next-basics';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormRow,
|
||||
HiddenInput,
|
||||
SubmitButton,
|
||||
TextField,
|
||||
Toggle,
|
||||
} from 'react-basics';
|
||||
|
||||
export default function TrackingCodeForm({ values, onClose }) {
|
||||
const ref = useRef();
|
||||
const { basePath } = useRouter();
|
||||
const { name, shareId } = values;
|
||||
export default function ShareUrlForm({ websiteId, data, onSave }) {
|
||||
const { name, shareId } = data;
|
||||
const [id, setId] = useState(shareId);
|
||||
const { post } = useApi(getAuthToken());
|
||||
const { mutate, error } = useMutation(({ shareId }) =>
|
||||
post(`/websites/${websiteId}`, { shareId }),
|
||||
);
|
||||
const ref = useRef(null);
|
||||
const url = useMemo(
|
||||
() => `${process.env.analyticsUrl}/share/${id}/${encodeURIComponent(name)}`,
|
||||
[id, name],
|
||||
);
|
||||
|
||||
const generateId = () => getRandomChars(16);
|
||||
|
||||
const handleSubmit = async data => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
onSave(data);
|
||||
ref.current.reset(data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleGenerate = () => {
|
||||
const id = generateId();
|
||||
ref.current.setValue('shareId', id, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
setId(id);
|
||||
};
|
||||
|
||||
const handleChange = checked => {
|
||||
const data = { shareId: checked ? generateId() : null };
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
onSave(data);
|
||||
},
|
||||
});
|
||||
setId(data.shareId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id && id !== shareId) {
|
||||
ref.current.setValue('shareId', id);
|
||||
}
|
||||
}, [id, shareId]);
|
||||
|
||||
return (
|
||||
<FormLayout>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="message.share-url"
|
||||
defaultMessage="This is the publicly shared URL for {target}."
|
||||
values={{ target: <b>{values.name}</b> }}
|
||||
/>
|
||||
</p>
|
||||
<FormRow>
|
||||
<textarea
|
||||
ref={ref}
|
||||
rows={3}
|
||||
cols={60}
|
||||
spellCheck={false}
|
||||
defaultValue={`${
|
||||
document.location.origin
|
||||
}${basePath}/share/${shareId}/${encodeURIComponent(name)}`}
|
||||
readOnly
|
||||
/>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<CopyButton type="submit" variant="action" element={ref} />
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</FormLayout>
|
||||
<>
|
||||
<Toggle checked={Boolean(id)} onChange={handleChange}>
|
||||
Enable share URL
|
||||
</Toggle>
|
||||
{id && (
|
||||
<Form key={websiteId} ref={ref} onSubmit={handleSubmit} error={error} values={data}>
|
||||
<FormRow>
|
||||
<p>Your website stats are publically available at the following URL:</p>
|
||||
<TextField value={url} readOnly allowCopy />
|
||||
</FormRow>
|
||||
<HiddenInput name="shareId" />
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary">Save</SubmitButton>
|
||||
<Button onClick={handleGenerate}>Regenerate URL</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
36
components/forms/TeamAddForm.js
Normal file
36
components/forms/TeamAddForm.js
Normal file
@ -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 (
|
||||
<Form ref={ref} className={styles.form} onSubmit={handleSubmit} error={error}>
|
||||
<FormInput name="name" label="Name" rules={{ required: 'Required' }}>
|
||||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
<FormButtons flex>
|
||||
<Button type="submit" variant="primary" disabled={isLoading}>
|
||||
Save
|
||||
</Button>
|
||||
<Button disabled={isLoading} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
34
components/forms/TeamEditForm.js
Normal file
34
components/forms/TeamEditForm.js
Normal file
@ -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 (
|
||||
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data}>
|
||||
<FormRow label="Team ID">
|
||||
<TextField value={teamId} readOnly allowCopy />
|
||||
</FormRow>
|
||||
<FormInput name="name" label="Name" rules={{ required: 'Required' }}>
|
||||
<TextField />
|
||||
</FormInput>
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary">Save</SubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
@ -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 = `<script async defer src="${trackerScriptName}" data-website-id="${websiteId}"></script>`;
|
||||
|
||||
return (
|
||||
<FormLayout>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="message.track-stats"
|
||||
defaultMessage="To track stats for {target}, place the following code in the {head} section of your website."
|
||||
values={{ head: '<head>', target: <b>{values.name}</b> }}
|
||||
/>
|
||||
</p>
|
||||
<FormRow>
|
||||
<textarea
|
||||
ref={ref}
|
||||
rows={3}
|
||||
cols={60}
|
||||
spellCheck={false}
|
||||
defaultValue={`<script async defer data-website-id="${values.id}" src="${
|
||||
document.location.origin
|
||||
}${basePath}/${trackerScriptName ? `${trackerScriptName}.js` : 'umami.js'}"></script>`}
|
||||
readOnly
|
||||
/>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<CopyButton type="submit" variant="action" element={ref} />
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</FormLayout>
|
||||
<>
|
||||
<Form ref={ref}>
|
||||
<FormRow>
|
||||
<p>
|
||||
To track stats for this website, place the following code in the{' '}
|
||||
<code><head></code> section of your HTML.
|
||||
</p>
|
||||
<TextArea rows={4} value={code} readOnly allowCopy />
|
||||
</FormRow>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
43
components/forms/UserDeleteForm.js
Normal file
43
components/forms/UserDeleteForm.js
Normal file
@ -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 (
|
||||
<Form className={styles.form} onSubmit={handleSubmit} error={error}>
|
||||
<div>
|
||||
To delete this user, type <b>{CONFIRM_VALUE}</b> in the box below to confirm.
|
||||
</div>
|
||||
<FormInput
|
||||
name="confirmation"
|
||||
label="Confirm"
|
||||
rules={{ validate: value => value === CONFIRM_VALUE }}
|
||||
>
|
||||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
<FormButtons flex>
|
||||
<SubmitButton variant="primary" className={styles.button} disabled={isLoading}>
|
||||
Save
|
||||
</SubmitButton>
|
||||
<Button className={styles.button} disabled={isLoading} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
@ -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 = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
}
|
||||
if (!id && !password) {
|
||||
errors.password = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
}
|
||||
|
||||
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 || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
|
||||
);
|
||||
}
|
||||
const handleSubmit = async data => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
onSave(data);
|
||||
ref.current.reset(data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FormLayout>
|
||||
<Formik
|
||||
initialValues={{ ...initialValues, ...values }}
|
||||
validate={validate}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{() => (
|
||||
<Form>
|
||||
<FormRow>
|
||||
<label htmlFor="username">
|
||||
<FormattedMessage id="label.username" defaultMessage="Username" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="username" type="text" />
|
||||
<FormError name="username" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="password">
|
||||
<FormattedMessage id="label.password" defaultMessage="Password" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="password" type="password" />
|
||||
<FormError name="password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</FormLayout>
|
||||
<Form
|
||||
key={id}
|
||||
className={styles.form}
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit}
|
||||
error={error}
|
||||
values={data}
|
||||
>
|
||||
<FormInput name="username" label="Username">
|
||||
<TextField />
|
||||
</FormInput>
|
||||
<FormInput name="role" label="Role">
|
||||
<Dropdown items={items} style={{ width: 200 }}>
|
||||
{({ value, label }) => <Item key={value}>{label}</Item>}
|
||||
</Dropdown>
|
||||
</FormInput>
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary">Save</SubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
6
components/forms/UserForm.module.css
Normal file
6
components/forms/UserForm.module.css
Normal file
@ -0,0 +1,6 @@
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
width: 300px;
|
||||
}
|
81
components/forms/UserPasswordForm.js
Normal file
81
components/forms/UserPasswordForm.js
Normal file
@ -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 (
|
||||
<Form ref={ref} className={styles.form} onSubmit={handleSubmit} error={error}>
|
||||
{isCurrentUser && (
|
||||
<FormInput
|
||||
name="current_password"
|
||||
label="Current password"
|
||||
rules={{ required: 'Required' }}
|
||||
>
|
||||
<PasswordField autoComplete="off" />
|
||||
</FormInput>
|
||||
)}
|
||||
<FormInput
|
||||
name="new_password"
|
||||
label="New password"
|
||||
rules={{
|
||||
required: 'Required',
|
||||
minLength: { value: 8, message: 'Minimum length 8 characters' },
|
||||
}}
|
||||
>
|
||||
<PasswordField autoComplete="off" />
|
||||
</FormInput>
|
||||
<FormInput
|
||||
name="confirm_password"
|
||||
label="Confirm password"
|
||||
rules={{
|
||||
required: 'Required',
|
||||
minLength: { value: 8, message: 'Minimum length 8 characters' },
|
||||
validate: samePassword,
|
||||
}}
|
||||
>
|
||||
<PasswordField autoComplete="off" />
|
||||
</FormInput>
|
||||
<FormButtons flex>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
Save
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
6
components/forms/UserPasswordForm.module.css
Normal file
6
components/forms/UserPasswordForm.module.css
Normal file
@ -0,0 +1,6 @@
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
width: 300px;
|
||||
}
|
47
components/forms/WebsiteAddForm.js
Normal file
47
components/forms/WebsiteAddForm.js
Normal file
@ -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 (
|
||||
<Form ref={ref} className={styles.form} onSubmit={handleSubmit} error={error}>
|
||||
<FormInput name="name" label="Name" rules={{ required: 'Required' }}>
|
||||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
<FormInput
|
||||
name="domain"
|
||||
label="Domain"
|
||||
rules={{
|
||||
required: 'Required',
|
||||
pattern: { value: DOMAIN_REGEX, message: 'Invalid domain' },
|
||||
}}
|
||||
>
|
||||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
<FormButtons flex>
|
||||
<SubmitButton variant="primary" disabled={false}>
|
||||
Save
|
||||
</SubmitButton>
|
||||
<Button disabled={isLoading} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
43
components/forms/WebsiteDeleteForm.js
Normal file
43
components/forms/WebsiteDeleteForm.js
Normal file
@ -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 (
|
||||
<Form className={styles.form} onSubmit={handleSubmit} error={error}>
|
||||
<div>
|
||||
To delete this website, type <b>{CONFIRM_VALUE}</b> in the box below to confirm.
|
||||
</div>
|
||||
<FormInput
|
||||
name="confirmation"
|
||||
label="Confirm"
|
||||
rules={{ validate: value => value === CONFIRM_VALUE }}
|
||||
>
|
||||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
<FormButtons flex>
|
||||
<SubmitButton variant="primary" className={styles.button} disabled={isLoading}>
|
||||
Save
|
||||
</SubmitButton>
|
||||
<Button className={styles.button} disabled={isLoading} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
@ -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 = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
}
|
||||
if (!domain) {
|
||||
errors.domain = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
} else if (!DOMAIN_REGEX.test(domain)) {
|
||||
errors.domain = <FormattedMessage id="label.invalid-domain" defaultMessage="Invalid 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 (
|
||||
<FormRow>
|
||||
<label htmlFor="owner">
|
||||
<FormattedMessage id="label.owner" defaultMessage="Owner" />
|
||||
</label>
|
||||
<div>
|
||||
<Field as="select" name="owner" className={styles.dropdown}>
|
||||
{users?.map(acc => (
|
||||
<option key={acc.id} value={acc.id}>
|
||||
{acc.username}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
<FormError name="owner" />
|
||||
</div>
|
||||
</FormRow>
|
||||
);
|
||||
} 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 || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
|
||||
);
|
||||
}
|
||||
const handleSubmit = async data => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
ref.current.reset(data);
|
||||
onSave(data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FormLayout>
|
||||
<Formik
|
||||
initialValues={{ ...initialValues, ...values, enableShareUrl: !!values?.shareId }}
|
||||
validate={validate}
|
||||
onSubmit={handleSubmit}
|
||||
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data}>
|
||||
<FormRow label="Website ID">
|
||||
<TextField value={websiteId} readOnly allowCopy />
|
||||
</FormRow>
|
||||
<FormInput name="name" label="Name" rules={{ required: 'Required' }}>
|
||||
<TextField />
|
||||
</FormInput>
|
||||
<FormInput
|
||||
name="domain"
|
||||
label="Domain"
|
||||
rules={{
|
||||
required: 'Required',
|
||||
pattern: {
|
||||
value: DOMAIN_REGEX,
|
||||
message: 'Invalid domain',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
<Form>
|
||||
<FormRow>
|
||||
<label htmlFor="name">
|
||||
<FormattedMessage id="label.name" defaultMessage="Name" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="name" type="text" />
|
||||
<FormError name="name" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="domain">
|
||||
<FormattedMessage id="label.domain" defaultMessage="Domain" />
|
||||
</label>
|
||||
<div>
|
||||
<Field
|
||||
name="domain"
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
spellCheck="false"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
/>
|
||||
<FormError name="domain" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<OwnerDropDown users={users} user={user} />
|
||||
<FormRow>
|
||||
<label />
|
||||
<Field name="enableShareUrl">
|
||||
{({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="label.enable-share-url"
|
||||
defaultMessage="Enable share URL"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</FormLayout>
|
||||
<TextField />
|
||||
</FormInput>
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary">Save</SubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
.dropdown {
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
49
components/forms/WebsiteReset.js
Normal file
49
components/forms/WebsiteReset.js
Normal file
@ -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 (
|
||||
<Form>
|
||||
<FormRow label="Reset website">
|
||||
<p>
|
||||
All statistics for this website will be deleted, but your settings will remain intact.
|
||||
</p>
|
||||
<Button onClick={() => setModal('reset')}>Reset</Button>
|
||||
</FormRow>
|
||||
<FormRow label="Delete website">
|
||||
<p>All website data will be deleted.</p>
|
||||
<Button onClick={() => setModal('delete')}>Delete</Button>
|
||||
</FormRow>
|
||||
{modal === 'reset' && (
|
||||
<Modal title="Reset website" onClose={handleClose}>
|
||||
{close => <WebsiteResetForm websiteId={websiteId} onSave={handleReset} onClose={close} />}
|
||||
</Modal>
|
||||
)}
|
||||
{modal === 'delete' && (
|
||||
<Modal title="Delete website" onClose={handleClose}>
|
||||
{close => (
|
||||
<WebsiteDeleteForm websiteId={websiteId} onSave={handleDelete} onClose={close} />
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
45
components/forms/WebsiteResetForm.js
Normal file
45
components/forms/WebsiteResetForm.js
Normal file
@ -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 (
|
||||
<Form className={styles.form} onSubmit={handleSubmit} error={error}>
|
||||
<div>
|
||||
To reset this website, type <b>{CONFIRM_VALUE}</b> in the box below to confirm.
|
||||
</div>
|
||||
<FormInput
|
||||
name="confirm"
|
||||
label="Confirmation"
|
||||
rules={{ validate: value => value === CONFIRM_VALUE }}
|
||||
>
|
||||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
<FormButtons flex>
|
||||
<SubmitButton variant="primary" className={styles.button} disabled={isLoading}>
|
||||
Save
|
||||
</SubmitButton>
|
||||
<Button className={styles.button} disabled={isLoading} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
@ -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() {
|
||||
<header className={styles.header}>
|
||||
<Row>
|
||||
<Column className={styles.title}>
|
||||
<Icon icon={<Logo />} size="large" className={styles.logo} />
|
||||
<Icon size="lg" className={styles.logo}>
|
||||
<Logo />
|
||||
</Icon>
|
||||
<Link href={isSharePage ? HOMEPAGE_URL : '/'}>umami</Link>
|
||||
</Column>
|
||||
<HamburgerButton />
|
||||
@ -40,7 +41,7 @@ export default function Header() {
|
||||
<Link href="/realtime">
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</Link>
|
||||
<Link href="/settings">
|
||||
<Link href="/websites">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -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 <Banner variant="error">Something went wrong.</Banner>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
export default function Page({ className, children }) {
|
||||
return <div className={classNames(styles.page, className)}>{children}</div>;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 30px;
|
||||
padding: 30px;
|
||||
background: var(--base50);
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
@ -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 <div className={classNames(styles.header, className)}>{children}</div>;
|
||||
export default function PageHeader({ title, backUrl, children, className, style }) {
|
||||
return (
|
||||
<div className={classNames(styles.header, className)} style={style}>
|
||||
<div className={styles.title}>
|
||||
{backUrl && (
|
||||
<Link href={backUrl}>
|
||||
<a>
|
||||
<Button>
|
||||
<Icon icon="arrow-left" /> Back
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{title}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
45
components/nav/Nav.js
Normal file
45
components/nav/Nav.js
Normal file
@ -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: <Website />, label: 'Websites', url: '/websites' },
|
||||
{ icon: <User />, label: 'Users', url: '/users', hidden: role !== 'admin' },
|
||||
{ icon: <Team />, label: 'Teams', url: '/teams' },
|
||||
{ icon: <User />, label: 'Profile', url: '/profile' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Menu items={items} onSelect={handleSelect} className={styles.menu}>
|
||||
{({ icon, label, url, hidden }) =>
|
||||
!hidden && (
|
||||
<Item
|
||||
key={label}
|
||||
className={classNames(styles.item, { [styles.selected]: pathname.startsWith(url) })}
|
||||
>
|
||||
<Link href={url}>
|
||||
<a>
|
||||
<Icon size="lg">{icon}</Icon>
|
||||
<Text>{label}</Text>
|
||||
</a>
|
||||
</Link>
|
||||
</Item>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
);
|
||||
}
|
46
components/nav/Nav.module.css
Normal file
46
components/nav/Nav.module.css
Normal file
@ -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);
|
||||
}
|
@ -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';
|
||||
|
@ -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';
|
||||
|
32
components/pages/ProfileSettings.js
Normal file
32
components/pages/ProfileSettings.js
Normal file
@ -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 (
|
||||
<Page>
|
||||
{toast}
|
||||
<PageHeader>
|
||||
<Breadcrumbs>
|
||||
<Item>Profile</Item>
|
||||
</Breadcrumbs>
|
||||
</PageHeader>
|
||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
|
||||
<Item key="general">General</Item>
|
||||
<Item key="password">Password</Item>
|
||||
</Tabs>
|
||||
{tab === 'general' && <ProfileDetails />}
|
||||
{tab === 'password' && <UserPasswordForm onSave={handleSave} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
@ -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: <FormattedMessage id="label.websites" defaultMessage="Websites" />,
|
||||
value: WEBSITES,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.users" defaultMessage="Users" />,
|
||||
value: ACCOUNTS,
|
||||
hidden: !user?.isAdmin,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
|
||||
value: PROFILE,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<MenuLayout menu={menuOptions} selectedOption={option} onMenuSelect={setOption}>
|
||||
{pathname === WEBSITES && <WebsiteSettings />}
|
||||
{pathname === ACCOUNTS && <UserSettings />}
|
||||
{pathname === PROFILE && <ProfileSettings />}
|
||||
</MenuLayout>
|
||||
</Page>
|
||||
<Layout>
|
||||
<div className={styles.dashboard}>
|
||||
<div className={styles.nav}>
|
||||
<Menu />
|
||||
</div>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
16
components/pages/Settings.module.css
Normal file
16
components/pages/Settings.module.css
Normal file
@ -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;
|
||||
}
|
58
components/pages/TeamDetails.js
Normal file
58
components/pages/TeamDetails.js
Normal file
@ -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 (
|
||||
<Page loading={isLoading || !values}>
|
||||
{toast}
|
||||
<PageHeader>
|
||||
<Breadcrumbs>
|
||||
<Item>
|
||||
<Link href="/teams">Teams</Link>
|
||||
</Item>
|
||||
<Item>{values?.name}</Item>
|
||||
</Breadcrumbs>
|
||||
</PageHeader>
|
||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
|
||||
<Item key="general">General</Item>
|
||||
<Item key="members">Members</Item>
|
||||
<Item key="websites">Websites</Item>
|
||||
</Tabs>
|
||||
{tab === 'general' && <TeamEditForm teamId={teamId} data={values} onSave={handleSave} />}
|
||||
{tab === 'members' && <TeamMembersTable teamId={teamId} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
65
components/pages/TeamsList.js
Normal file
65
components/pages/TeamsList.js
Normal file
@ -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 (
|
||||
<Page loading={isLoading} error={error}>
|
||||
{toast}
|
||||
<PageHeader title="Teams">
|
||||
<Button onClick={handleAdd}>
|
||||
<Icon icon="plus" /> Create team
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
{hasData && <TeamsTable columns={columns} rows={data} />}
|
||||
{!hasData && (
|
||||
<EmptyPlaceholder msg="You don't have any teams configured.">
|
||||
<Flexbox justifyContent="center" alignItems="center">
|
||||
<Button variant="primary" onClick={handleAdd}>
|
||||
<Icon icon="plus" /> Create team
|
||||
</Button>
|
||||
</Flexbox>
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
{edit && (
|
||||
<Modal title="Create team" onClose={handleClose}>
|
||||
{close => <TeamAddForm onSave={handleSave} onClose={close} />}
|
||||
</Modal>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
@ -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() {
|
||||
|
30
components/pages/UserDelete.js
Normal file
30
components/pages/UserDelete.js
Normal file
@ -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 (
|
||||
<Form>
|
||||
<FormRow label="Delete user">
|
||||
<p>All user data will be deleted.</p>
|
||||
<Button onClick={() => setModal('delete')}>Delete</Button>
|
||||
</FormRow>
|
||||
{modal === 'delete' && (
|
||||
<Modal title="Delete user" onClose={handleClose}>
|
||||
{close => <UserDeleteForm userId={userId} onSave={handleDelete} onClose={close} />}
|
||||
</Modal>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
69
components/pages/UserSettings.js
Normal file
69
components/pages/UserSettings.js
Normal file
@ -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 (
|
||||
<Page loading={isLoading || !values}>
|
||||
{toast}
|
||||
<PageHeader>
|
||||
<Breadcrumbs>
|
||||
<Item>
|
||||
<Link href="/users">Users</Link>
|
||||
</Item>
|
||||
<Item>{values?.username}</Item>
|
||||
</Breadcrumbs>
|
||||
</PageHeader>
|
||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
|
||||
<Item key="general">General</Item>
|
||||
<Item key="password">Password</Item>
|
||||
<Item key="delete">Danger Zone</Item>
|
||||
</Tabs>
|
||||
{tab === 'general' && <UserEditForm userId={userId} data={values} onSave={handleSave} />}
|
||||
{tab === 'password' && <UserPasswordForm userId={userId} data={values} onSave={handleSave} />}
|
||||
{tab === 'delete' && <UserDelete userId={userId} onSave={handleDelete} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
45
components/pages/UsersList.js
Normal file
45
components/pages/UsersList.js
Normal file
@ -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 (
|
||||
<Page loading={loading || isLoading} error={error}>
|
||||
{toast}
|
||||
<PageHeader title="Users">
|
||||
<Button onClick={handleSave}>
|
||||
<Icon icon="plus" /> Create user
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<UsersTable
|
||||
onLoading={({ isLoading, error }) => {
|
||||
setLoading(isLoading);
|
||||
setError(error);
|
||||
}}
|
||||
onAddKeyClick={handleSave}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
}
|
@ -36,7 +36,7 @@ export default function WebsiteList({ websites, showCharts, limit }) {
|
||||
return (
|
||||
<Page>
|
||||
<EmptyPlaceholder msg={formatMessage(messages.noWebsites)}>
|
||||
<Link href="/settings" icon={<Arrow />} iconRight>
|
||||
<Link href="/websites" icon={<Arrow />} iconRight>
|
||||
{formatMessage(messages.goToSettngs)}
|
||||
</Link>
|
||||
</EmptyPlaceholder>
|
||||
|
76
components/pages/WebsiteSettings.js
Normal file
76
components/pages/WebsiteSettings.js
Normal file
@ -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 (
|
||||
<Page loading={isLoading || !values}>
|
||||
{toast}
|
||||
<PageHeader>
|
||||
<Breadcrumbs>
|
||||
<Item>
|
||||
<Link href="/websites">Websites</Link>
|
||||
</Item>
|
||||
<Item>{values?.name}</Item>
|
||||
</Breadcrumbs>
|
||||
<Link href={`/analytics/websites/${websiteId}`}>
|
||||
<a target="_blank">
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
<ExternalLink />
|
||||
</Icon>
|
||||
View
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</PageHeader>
|
||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
|
||||
<Item key="general">General</Item>
|
||||
<Item key="tracking">Tracking code</Item>
|
||||
<Item key="share">Share URL</Item>
|
||||
<Item key="danger">Danger zone</Item>
|
||||
</Tabs>
|
||||
{tab === 'general' && (
|
||||
<WebsiteEditForm websiteId={websiteId} data={values} onSave={handleSave} />
|
||||
)}
|
||||
{tab === 'tracking' && <TrackingCodeForm websiteId={websiteId} data={values} />}
|
||||
{tab === 'share' && <ShareUrlForm websiteId={websiteId} data={values} onSave={handleSave} />}
|
||||
{tab === 'danger' && <WebsiteReset websiteId={websiteId} onSave={handleSave} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
70
components/pages/WebsitesList.js
Normal file
70
components/pages/WebsitesList.js
Normal file
@ -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 (
|
||||
<Page loading={isLoading} error={error}>
|
||||
{toast}
|
||||
<PageHeader title="Websites">
|
||||
<Button onClick={handleAdd}>
|
||||
<Icon icon="plus" /> Add website
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
{hasData && <WebsitesTable columns={columns} rows={data} />}
|
||||
{!hasData && (
|
||||
<EmptyPlaceholder msg="You don't have any websites configured.">
|
||||
<Flexbox justifyContent="center" alignItems="center">
|
||||
<Button variant="primary" onClick={handleAdd}>
|
||||
<Icon icon="plus" /> Add website
|
||||
</Button>
|
||||
</Flexbox>
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
{edit && (
|
||||
<Modal title="Add website" onClose={handleClose}>
|
||||
{close => <WebsiteAddForm onSave={handleSave} onClose={close} />}
|
||||
</Modal>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
@ -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 <MenuButton icon={<Gear />} options={menuOptions} onSelect={handleSelect} hideLabel />;
|
||||
return (
|
||||
<MenuButton options={menuOptions} onSelect={handleSelect} hideLabel>
|
||||
<Icon>
|
||||
<Gear />
|
||||
</Icon>
|
||||
</MenuButton>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
<Button className={styles.button} size="small" onClick={handleReset}>
|
||||
<Button className={styles.button} size="sm" onClick={handleReset}>
|
||||
<FormattedMessage id="label.reset" defaultMessage="Reset" />
|
||||
</Button>
|
||||
</>
|
||||
|
@ -4,6 +4,7 @@ import useLocale from 'hooks/useLocale';
|
||||
import MenuButton from 'components/common/MenuButton';
|
||||
import Globe from 'assets/globe.svg';
|
||||
import styles from './LanguageButton.module.css';
|
||||
import { Icon } from 'react-basics';
|
||||
|
||||
export default function LanguageButton() {
|
||||
const { locale, saveLocale } = useLocale();
|
||||
@ -15,13 +16,16 @@ export default function LanguageButton() {
|
||||
|
||||
return (
|
||||
<MenuButton
|
||||
icon={<Globe />}
|
||||
options={menuOptions}
|
||||
value={locale}
|
||||
menuClassName={styles.menu}
|
||||
buttonVariant="light"
|
||||
onSelect={handleSelect}
|
||||
hideLabel
|
||||
/>
|
||||
>
|
||||
<Icon>
|
||||
<Globe />
|
||||
</Icon>
|
||||
</MenuButton>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import DropDown from 'components/common/DropDown';
|
||||
import Button from 'components/common/Button';
|
||||
import { Button } from 'react-basics';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { DEFAULT_LOCALE } from 'lib/constants';
|
||||
import styles from './TimezoneSetting.module.css';
|
||||
@ -23,7 +23,7 @@ export default function LanguageSetting() {
|
||||
options={options}
|
||||
onChange={saveLocale}
|
||||
/>
|
||||
<Button className={styles.button} size="small" onClick={handleReset}>
|
||||
<Button className={styles.button} size="sm" onClick={handleReset}>
|
||||
<FormattedMessage id="label.reset" defaultMessage="Reset" />
|
||||
</Button>
|
||||
</>
|
||||
|
53
components/settings/ProfileDetails.js
Normal file
53
components/settings/ProfileDetails.js
Normal file
@ -0,0 +1,53 @@
|
||||
import TimezoneSetting from 'components/settings/TimezoneSetting';
|
||||
import useUser from 'hooks/useUser';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import DateRangeSetting from './DateRangeSetting';
|
||||
import LanguageSetting from './LanguageSetting';
|
||||
import styles from './ProfileSettings.module.css';
|
||||
import ThemeSetting from './ThemeSetting';
|
||||
|
||||
export default function ProfileDetails() {
|
||||
const { user } = useUser();
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { username } = user;
|
||||
|
||||
return (
|
||||
<>
|
||||
<dl className={styles.list}>
|
||||
<dt>
|
||||
<FormattedMessage id="label.username" defaultMessage="Username" />
|
||||
</dt>
|
||||
<dd>{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>
|
||||
<dt>
|
||||
<FormattedMessage id="label.language" defaultMessage="Language" />
|
||||
</dt>
|
||||
<dd>
|
||||
<LanguageSetting />
|
||||
</dd>
|
||||
<dt>
|
||||
<FormattedMessage id="label.theme" defaultMessage="Theme" />
|
||||
</dt>
|
||||
<dd>
|
||||
<ThemeSetting />
|
||||
</dd>
|
||||
</dl>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
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';
|
||||
import useUser from 'hooks/useUser';
|
||||
import LanguageSetting from './LanguageSetting';
|
||||
import ThemeSetting from './ThemeSetting';
|
||||
|
||||
export default function ProfileSettings() {
|
||||
const { user } = useUser();
|
||||
const [changePassword, setChangePassword] = useState(false);
|
||||
const [message, setMessage] = useState(null);
|
||||
|
||||
function handleSave() {
|
||||
setChangePassword(false);
|
||||
setMessage(<FormattedMessage id="message.save-success" defaultMessage="Saved successfully." />);
|
||||
}
|
||||
|
||||
useEscapeKey(() => {
|
||||
setChangePassword(false);
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { userId, username } = user;
|
||||
|
||||
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>{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>
|
||||
<dt>
|
||||
<FormattedMessage id="label.language" defaultMessage="Language" />
|
||||
</dt>
|
||||
<dd>
|
||||
<LanguageSetting />
|
||||
</dd>
|
||||
<dt>
|
||||
<FormattedMessage id="label.theme" defaultMessage="Theme" />
|
||||
</dt>
|
||||
<dd>
|
||||
<ThemeSetting />
|
||||
</dd>
|
||||
</dl>
|
||||
{changePassword && (
|
||||
<Modal
|
||||
title={<FormattedMessage id="label.change-password" defaultMessage="Change password" />}
|
||||
>
|
||||
<ChangePasswordForm
|
||||
values={{ userId }}
|
||||
onSave={handleSave}
|
||||
onClose={() => setChangePassword(false)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{message && <Toast message={message} onClose={() => setMessage(null)} />}
|
||||
</>
|
||||
);
|
||||
}
|
@ -2,7 +2,7 @@ import React, { useRef, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import TimezoneSetting from './TimezoneSetting';
|
||||
import DateRangeSetting from './DateRangeSetting';
|
||||
import Button from 'components/common/Button';
|
||||
import { Button, Icon } from 'react-basics';
|
||||
import styles from './SettingsButton.module.css';
|
||||
import Gear from 'assets/gear.svg';
|
||||
import useDocumentClick from '../../hooks/useDocumentClick';
|
||||
@ -23,7 +23,11 @@ export default function SettingsButton() {
|
||||
|
||||
return (
|
||||
<div className={styles.button} ref={ref}>
|
||||
<Button icon={<Gear />} variant="light" onClick={handleClick} />
|
||||
<Button variant="light" onClick={handleClick}>
|
||||
<Icon>
|
||||
<Gear />
|
||||
</Icon>
|
||||
</Button>
|
||||
{show && (
|
||||
<div className={styles.panel}>
|
||||
<dt>
|
||||
|
@ -4,7 +4,7 @@ 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';
|
||||
import { Icon } from 'react-basics';
|
||||
|
||||
export default function ThemeButton() {
|
||||
const [theme, setTheme] = useTheme();
|
||||
@ -30,7 +30,7 @@ export default function ThemeButton() {
|
||||
<div className={styles.button} onClick={handleClick}>
|
||||
{transitions((styles, item) => (
|
||||
<animated.div key={item} style={styles}>
|
||||
<Icon icon={item === 'light' ? <Sun /> : <Moon />} />
|
||||
<Icon>{item === 'light' ? <Sun /> : <Moon />}</Icon>
|
||||
</animated.div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import Button from 'components/common/Button';
|
||||
import { Button, Icon } from 'react-basics';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import Sun from 'assets/sun.svg';
|
||||
import Moon from 'assets/moon.svg';
|
||||
@ -12,14 +12,20 @@ export default function ThemeSetting() {
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={classNames({ [styles.active]: theme === 'light' })}
|
||||
icon={<Sun />}
|
||||
onClick={() => setTheme('light')}
|
||||
/>
|
||||
>
|
||||
<Icon>
|
||||
<Sun />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Button
|
||||
className={classNames({ [styles.active]: theme === 'dark' })}
|
||||
icon={<Moon />}
|
||||
onClick={() => setTheme('dark')}
|
||||
/>
|
||||
>
|
||||
<Icon>
|
||||
<Moon />
|
||||
</Icon>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ 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 { Button } from 'react-basics';
|
||||
import useTimezone from 'hooks/useTimezone';
|
||||
import { getTimezone } from 'lib/date';
|
||||
import styles from './TimezoneSetting.module.css';
|
||||
@ -23,7 +23,7 @@ export default function TimezoneSetting() {
|
||||
options={options}
|
||||
onChange={saveTimezone}
|
||||
/>
|
||||
<Button className={styles.button} size="small" onClick={handleReset}>
|
||||
<Button className={styles.button} size="sm" onClick={handleReset}>
|
||||
<FormattedMessage id="label.reset" defaultMessage="Reset" />
|
||||
</Button>
|
||||
</>
|
||||
|
@ -1,16 +1,18 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useRouter } from 'next/router';
|
||||
import { removeItem } from 'next-basics';
|
||||
import MenuButton from 'components/common/MenuButton';
|
||||
import Icon from 'components/common/Icon';
|
||||
import User from 'assets/user.svg';
|
||||
import styles from './UserButton.module.css';
|
||||
import { AUTH_TOKEN } from 'lib/constants';
|
||||
import useUser from 'hooks/useUser';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import useUser from 'hooks/useUser';
|
||||
import { AUTH_TOKEN } from 'lib/constants';
|
||||
import { removeItem } from 'next-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Button, Icon, Item, Menu, Popup, Text } from 'react-basics';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import styles from './UserButton.module.css';
|
||||
import useDocumentClick from '../../hooks/useDocumentClick';
|
||||
|
||||
export default function UserButton() {
|
||||
const [show, setShow] = useState(false);
|
||||
const ref = useRef();
|
||||
const { user } = useUser();
|
||||
const router = useRouter();
|
||||
const { adminDisabled } = useConfig();
|
||||
@ -31,26 +33,48 @@ export default function UserButton() {
|
||||
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
|
||||
value: 'profile',
|
||||
hidden: adminDisabled,
|
||||
divider: true,
|
||||
},
|
||||
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: 'logout' },
|
||||
];
|
||||
|
||||
function handleClick() {
|
||||
setShow(state => !state);
|
||||
}
|
||||
|
||||
function handleSelect(value) {
|
||||
if (value === 'logout') {
|
||||
removeItem(AUTH_TOKEN);
|
||||
router.push('/login');
|
||||
} else if (value === 'profile') {
|
||||
router.push('/settings/profile');
|
||||
router.push('/profile');
|
||||
}
|
||||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
if (!ref.current?.contains(e.target)) {
|
||||
setShow(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MenuButton
|
||||
icon={<Icon icon={<User />} size="large" />}
|
||||
buttonVariant="light"
|
||||
options={menuOptions}
|
||||
onSelect={handleSelect}
|
||||
hideLabel
|
||||
/>
|
||||
<div className={styles.button} ref={ref}>
|
||||
<Button variant="light" onClick={handleClick}>
|
||||
<Icon className={styles.icon} size="large">
|
||||
<User />
|
||||
</Icon>
|
||||
</Button>
|
||||
{show && (
|
||||
<Popup className={styles.menu} position="bottom" gap={5}>
|
||||
<Menu items={menuOptions} onSelect={handleSelect}>
|
||||
{({ label, value }) => (
|
||||
<Item key={value}>
|
||||
<Text>{label}</Text>
|
||||
</Item>
|
||||
)}
|
||||
</Menu>
|
||||
</Popup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
.button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.username {
|
||||
border-bottom: 1px solid var(--base500);
|
||||
}
|
||||
@ -5,3 +9,18 @@
|
||||
.username:hover {
|
||||
background: var(--base50);
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
font-size: 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
left: -50%;
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
}
|
||||
|
@ -1,133 +0,0 @@
|
||||
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 UserEditForm from 'components/forms/UserEditForm';
|
||||
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 './UserSettings.module.css';
|
||||
|
||||
export default function UserSettings() {
|
||||
const [addUser, setAddUser] = useState();
|
||||
const [editUser, setEditUser] = useState();
|
||||
const [deleteUser, setDeleteUser] = useState();
|
||||
const [saved, setSaved] = useState(0);
|
||||
const [message, setMessage] = useState();
|
||||
const { data } = useFetch(`/users`, {}, [saved]);
|
||||
|
||||
const Checkmark = ({ isAdmin }) => (isAdmin ? <Icon icon={<Check />} size="medium" /> : null);
|
||||
|
||||
const DashboardLink = row => {
|
||||
return (
|
||||
<Link href={`/dashboard/${row.id}/${row.username}`}>
|
||||
<a>
|
||||
<Icon icon={<LinkIcon />} />
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const Buttons = row => (
|
||||
<ButtonLayout align="right">
|
||||
<Button icon={<Pen />} size="small" onClick={() => setEditUser(row)}>
|
||||
<FormattedMessage id="label.edit" defaultMessage="Edit" />
|
||||
</Button>
|
||||
{!row.isAdmin && (
|
||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteUser(row)}>
|
||||
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
)}
|
||||
</ButtonLayout>
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'username',
|
||||
label: <FormattedMessage id="label.username" defaultMessage="Username" />,
|
||||
className: 'col-12 col-lg-4',
|
||||
},
|
||||
{
|
||||
key: 'isAdmin',
|
||||
label: <FormattedMessage id="label.administrator" defaultMessage="Administrator" />,
|
||||
className: 'col-12 col-lg-3',
|
||||
render: Checkmark,
|
||||
},
|
||||
{
|
||||
key: 'dashboard',
|
||||
label: <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />,
|
||||
className: 'col-12 col-lg-3',
|
||||
render: DashboardLink,
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
className: classNames(styles.buttons, 'col-12 col-lg-2 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() {
|
||||
setEditUser(null);
|
||||
setAddUser(null);
|
||||
setDeleteUser(null);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader>
|
||||
<div>
|
||||
<FormattedMessage id="label.users" defaultMessage="Users" />
|
||||
</div>
|
||||
<Button icon={<Plus />} size="small" onClick={() => setAddUser(true)}>
|
||||
<FormattedMessage id="label.add-user" defaultMessage="Add user" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Table columns={columns} rows={data} />
|
||||
{editUser && (
|
||||
<Modal title={<FormattedMessage id="label.edit-user" defaultMessage="Edit user" />}>
|
||||
<UserEditForm
|
||||
values={{ ...editUser, password: '' }}
|
||||
onSave={handleSave}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{addUser && (
|
||||
<Modal title={<FormattedMessage id="label.add-user" defaultMessage="Add user" />}>
|
||||
<UserEditForm onSave={handleSave} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
{deleteUser && (
|
||||
<Modal title={<FormattedMessage id="label.delete-user" defaultMessage="Delete user" />}>
|
||||
<DeleteForm
|
||||
values={{ type: 'users', id: deleteUser.id, name: deleteUser.username }}
|
||||
onSave={handleSave}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{message && <Toast message={message} onClose={() => setMessage(null)} />}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
}
|
@ -1,232 +0,0 @@
|
||||
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 OverflowText from 'components/common/OverflowText';
|
||||
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 useUser from 'hooks/useUser';
|
||||
import styles from './WebsiteSettings.module.css';
|
||||
|
||||
export default function WebsiteSettings() {
|
||||
const { user } = useUser();
|
||||
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('/websites', { params: { include_all: !!user?.isAdmin } }, [saved]);
|
||||
|
||||
const Buttons = row => (
|
||||
<ButtonLayout align="right">
|
||||
{row.shareId && (
|
||||
<Button
|
||||
icon={<LinkIcon />}
|
||||
size="small"
|
||||
tooltip={<FormattedMessage id="message.get-share-url" defaultMessage="Get share URL" />}
|
||||
tooltipId={`button-share-${row.id}`}
|
||||
onClick={() => setShowUrl(row)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon={<Code />}
|
||||
size="small"
|
||||
tooltip={
|
||||
<FormattedMessage id="message.get-tracking-code" defaultMessage="Get tracking code" />
|
||||
}
|
||||
tooltipId={`button-code-${row.id}`}
|
||||
onClick={() => setShowCode(row)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Pen />}
|
||||
size="small"
|
||||
tooltip={<FormattedMessage id="label.edit" defaultMessage="Edit" />}
|
||||
tooltipId={`button-edit-${row.id}`}
|
||||
onClick={() => setEditWebsite(row)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Reset />}
|
||||
size="small"
|
||||
tooltip={<FormattedMessage id="label.reset" defaultMessage="Reset" />}
|
||||
tooltipId={`button-reset-${row.id}`}
|
||||
onClick={() => setResetWebsite(row)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Trash />}
|
||||
size="small"
|
||||
tooltip={<FormattedMessage id="label.delete" defaultMessage="Delete" />}
|
||||
tooltipId={`button-delete-${row.id}`}
|
||||
onClick={() => setDeleteWebsite(row)}
|
||||
/>
|
||||
</ButtonLayout>
|
||||
);
|
||||
|
||||
const DetailsLink = ({ id, name, domain }) => (
|
||||
<Link className={styles.detailLink} href="/websites/[...id]" as={`/websites/${id}/${name}`}>
|
||||
<Favicon domain={domain} />
|
||||
<OverflowText tooltipId={`${id}-name`}>{name}</OverflowText>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const Domain = ({ domain, id }) => (
|
||||
<OverflowText tooltipId={`${id}-domain`}>{domain}</OverflowText>
|
||||
);
|
||||
|
||||
const adminColumns = [
|
||||
{
|
||||
key: 'name',
|
||||
label: <FormattedMessage id="label.name" defaultMessage="Name" />,
|
||||
className: 'col-12 col-lg-4 col-xl-3',
|
||||
render: DetailsLink,
|
||||
},
|
||||
{
|
||||
key: 'domain',
|
||||
label: <FormattedMessage id="label.domain" defaultMessage="Domain" />,
|
||||
className: 'col-12 col-lg-4 col-xl-3',
|
||||
render: Domain,
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: <FormattedMessage id="label.owner" defaultMessage="Owner" />,
|
||||
className: 'col-12 col-lg-4 col-xl-1',
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
className: classNames(styles.buttons, 'col-12 col-xl-5 pt-2 pt-xl-0'),
|
||||
render: Buttons,
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
label: <FormattedMessage id="label.name" defaultMessage="Name" />,
|
||||
className: 'col-12 col-lg-6 col-xl-4',
|
||||
render: DetailsLink,
|
||||
},
|
||||
{
|
||||
key: 'domain',
|
||||
label: <FormattedMessage id="label.domain" defaultMessage="Domain" />,
|
||||
className: 'col-12 col-lg-6 col-xl-4',
|
||||
render: Domain,
|
||||
},
|
||||
{
|
||||
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={user.isAdmin ? adminColumns : 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: 'websites', id: resetWebsite.id, name: resetWebsite.name }}
|
||||
onSave={handleSave}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{deleteWebsite && (
|
||||
<Modal
|
||||
title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}
|
||||
>
|
||||
<DeleteForm
|
||||
values={{ type: 'websites', id: deleteWebsite.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)} />}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
.col {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detailLink {
|
||||
width: 100%;
|
||||
}
|
62
components/tables/TeamMembersTable.js
Normal file
62
components/tables/TeamMembersTable.js
Normal file
@ -0,0 +1,62 @@
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
Button,
|
||||
Icon,
|
||||
} from 'react-basics';
|
||||
import styles from './TeamsTable.module.css';
|
||||
|
||||
export default function TeamMembersTable({ columns = [], rows = [] }) {
|
||||
return (
|
||||
<Table className={styles.table} columns={columns} rows={rows}>
|
||||
<TableHeader>
|
||||
{(column, index) => {
|
||||
return (
|
||||
<TableColumn key={index} style={{ ...column.style }}>
|
||||
{column.label}
|
||||
</TableColumn>
|
||||
);
|
||||
}}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(row, keys, rowIndex) => {
|
||||
const { id } = row;
|
||||
|
||||
row.action = (
|
||||
<div className={styles.actions}>
|
||||
<Link href={`/teams/${id}`} shallow>
|
||||
<a>
|
||||
<Button>
|
||||
<Icon icon="arrow-right" />
|
||||
Settings
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={rowIndex} data={row} keys={keys}>
|
||||
{(data, key, colIndex) => {
|
||||
return (
|
||||
<TableCell
|
||||
key={colIndex}
|
||||
className={styles.cell}
|
||||
style={{ ...columns[colIndex]?.style }}
|
||||
>
|
||||
{data[key]}
|
||||
</TableCell>
|
||||
);
|
||||
}}
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
62
components/tables/TeamsTable.js
Normal file
62
components/tables/TeamsTable.js
Normal file
@ -0,0 +1,62 @@
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
Button,
|
||||
Icon,
|
||||
} from 'react-basics';
|
||||
import styles from './TeamsTable.module.css';
|
||||
|
||||
export default function TeamsTable({ columns = [], rows = [] }) {
|
||||
return (
|
||||
<Table className={styles.table} columns={columns} rows={rows}>
|
||||
<TableHeader>
|
||||
{(column, index) => {
|
||||
return (
|
||||
<TableColumn key={index} style={{ ...column.style }}>
|
||||
{column.label}
|
||||
</TableColumn>
|
||||
);
|
||||
}}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(row, keys, rowIndex) => {
|
||||
const { id } = row;
|
||||
|
||||
row.action = (
|
||||
<div className={styles.actions}>
|
||||
<Link href={`/teams/${id}`} shallow>
|
||||
<a>
|
||||
<Button>
|
||||
<Icon icon="arrow-right" />
|
||||
Settings
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={rowIndex} data={row} keys={keys}>
|
||||
{(data, key, colIndex) => {
|
||||
return (
|
||||
<TableCell
|
||||
key={colIndex}
|
||||
className={styles.cell}
|
||||
style={{ ...columns[colIndex]?.style }}
|
||||
>
|
||||
{data[key]}
|
||||
</TableCell>
|
||||
);
|
||||
}}
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
18
components/tables/TeamsTable.module.css
Normal file
18
components/tables/TeamsTable.module.css
Normal file
@ -0,0 +1,18 @@
|
||||
.table th,
|
||||
.table td {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cell:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
100
components/tables/UsersTable.js
Normal file
100
components/tables/UsersTable.js
Normal file
@ -0,0 +1,100 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { getAuthToken } from 'lib/client';
|
||||
import { useApi } from 'next-basics';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from 'react-basics';
|
||||
import styles from './UsersTable.module.css';
|
||||
|
||||
const defaultColumns = [
|
||||
{ name: 'username', label: 'Username', style: { flex: 2 } },
|
||||
{ name: 'role', label: 'Role', style: { flex: 2 } },
|
||||
{ name: 'created', label: 'Created' },
|
||||
{ name: 'action', label: ' ' },
|
||||
];
|
||||
|
||||
export default function UsersTable({ columns = defaultColumns, onLoading, onAddKeyClick }) {
|
||||
const [values, setValues] = useState(null);
|
||||
const { get } = useApi(getAuthToken());
|
||||
const { data, isLoading, error } = useQuery(['user'], () => get(`/users`));
|
||||
const hasData = data && data.length !== 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setValues(data);
|
||||
onLoading({ data, isLoading, error });
|
||||
}
|
||||
}, [onLoading, data, isLoading, error]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasData && (
|
||||
<Table className={styles.table} columns={columns} rows={values}>
|
||||
<TableHeader>
|
||||
{(column, index) => {
|
||||
return (
|
||||
<TableColumn key={index} className={styles.header} style={{ ...column.style }}>
|
||||
{column.label}
|
||||
</TableColumn>
|
||||
);
|
||||
}}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(row, keys, rowIndex) => {
|
||||
row.created = formatDistance(new Date(row.createdAt), new Date(), {
|
||||
addSuffix: true,
|
||||
});
|
||||
|
||||
row.action = (
|
||||
<div className={styles.actions}>
|
||||
<Link href={`/users/${row.id}`} shallow>
|
||||
<a>
|
||||
<Button>
|
||||
<Icon icon="arrow-right" />
|
||||
Settings
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={rowIndex} data={row} keys={keys}>
|
||||
{(data, key, colIndex) => {
|
||||
return (
|
||||
<TableCell
|
||||
key={colIndex}
|
||||
className={styles.cell}
|
||||
style={{ ...columns[colIndex]?.style }}
|
||||
>
|
||||
{data[key]}
|
||||
</TableCell>
|
||||
);
|
||||
}}
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
{!hasData && (
|
||||
<EmptyPlaceholder className={styles.empty} msg="You don't have any Users.">
|
||||
<Button variant="primary" onClick={onAddKeyClick}>
|
||||
<Icon icon="plus" /> Create User
|
||||
</Button>
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
26
components/tables/UsersTable.module.css
Normal file
26
components/tables/UsersTable.module.css
Normal file
@ -0,0 +1,26 @@
|
||||
.table th,
|
||||
.table td {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.cell:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
min-height: 300px;
|
||||
}
|
73
components/tables/WebsitesTable.js
Normal file
73
components/tables/WebsitesTable.js
Normal file
@ -0,0 +1,73 @@
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
Button,
|
||||
Icon,
|
||||
} from 'react-basics';
|
||||
import ExternalLink from 'assets/external-link.svg';
|
||||
import styles from './WebsitesTable.module.css';
|
||||
|
||||
export default function WebsitesTable({ columns = [], rows = [] }) {
|
||||
return (
|
||||
<Table className={styles.table} columns={columns} rows={rows}>
|
||||
<TableHeader>
|
||||
{(column, index) => {
|
||||
return (
|
||||
<TableColumn key={index} style={{ ...column.style }}>
|
||||
{column.label}
|
||||
</TableColumn>
|
||||
);
|
||||
}}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(row, keys, rowIndex) => {
|
||||
const { id } = row;
|
||||
|
||||
row.action = (
|
||||
<div className={styles.actions}>
|
||||
<Link href={`/websites/${id}/settings`} shallow>
|
||||
<a>
|
||||
<Button>
|
||||
<Icon icon="arrow-right" />
|
||||
Settings
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/websites/${id}`}>
|
||||
<a>
|
||||
<Button>
|
||||
<Icon>
|
||||
<ExternalLink />
|
||||
</Icon>
|
||||
View
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={rowIndex} data={row} keys={keys}>
|
||||
{(data, key, colIndex) => {
|
||||
return (
|
||||
<TableCell
|
||||
key={colIndex}
|
||||
className={styles.cell}
|
||||
style={{ ...columns[colIndex]?.style }}
|
||||
>
|
||||
{data[key]}
|
||||
</TableCell>
|
||||
);
|
||||
}}
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
18
components/tables/WebsitesTable.module.css
Normal file
18
components/tables/WebsitesTable.module.css
Normal file
@ -0,0 +1,18 @@
|
||||
.table th,
|
||||
.table td {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cell:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
@ -33,7 +33,7 @@ export const ROLES = {
|
||||
teamOwner: 'team-owner',
|
||||
teamMember: 'team-member',
|
||||
teamGuest: 'team-guest',
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const PERMISSIONS = {
|
||||
all: 'all',
|
||||
|
@ -89,7 +89,7 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^18.2.0",
|
||||
"react-basics": "^0.40.0",
|
||||
"react-basics": "^0.44.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-intl": "^5.24.7",
|
||||
|
@ -26,7 +26,7 @@ export default async (
|
||||
const { id } = req.query;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (await canViewUser(userId, id)) {
|
||||
if (!isAdmin && !(await canViewUser(userId, id))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ export default async (
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
if (await canUpdateUser(userId, id)) {
|
||||
if (!isAdmin && !(await canUpdateUser(userId, id))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
@ -46,7 +46,8 @@ export default async (
|
||||
|
||||
const data: any = {};
|
||||
|
||||
if (password) {
|
||||
// Only admin can change these fields
|
||||
if (password && isAdmin) {
|
||||
data.password = hashPassword(password);
|
||||
}
|
||||
|
||||
@ -70,7 +71,7 @@ export default async (
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
if (isAdmin) {
|
||||
if (!isAdmin) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user