More refactoring, cleaned up icons, nav buttons, add messages.

This commit is contained in:
Mike Cao 2023-01-27 21:53:13 -08:00
parent 4b1013c8c6
commit 5f15ad0807
68 changed files with 391 additions and 790 deletions

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="m216.464 36.465-7.071 7.07c-4.686 4.686-4.686 12.284 0 16.971L387.887 239H12c-6.627 0-12 5.373-12 12v10c0 6.627 5.373 12 12 12h375.887L209.393 451.494c-4.686 4.686-4.686 12.284 0 16.971l7.071 7.07c4.686 4.686 12.284 4.686 16.97 0l211.051-211.05c4.686-4.686 4.686-12.284 0-16.971L233.434 36.465c-4.686-4.687-12.284-4.687-16.97 0z"/></svg>

Before

Width:  |  Height:  |  Size: 408 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M392 320c-13.25 0-24 10.75-24 24v112c0 4.406-3.594 8-8 8H56c-4.406 0-8-3.594-8-8V152c0-4.406 3.594-8 8-8h112c13.25 0 24-10.75 24-24s-10.75-24-24-24H56c-30.875 0-56 25.125-56 56v304c0 30.875 25.125 56 56 56h304c30.875 0 56-25.125 56-56V344c0-13.25-10.75-24-24-24ZM488 0H320c-13.25 0-24 10.75-24 24s10.75 24 24 24h110.062L183.031 295.031c-9.375 9.375-9.375 24.563 0 33.938A23.9 23.9 0 0 0 200 336a23.9 23.9 0 0 0 16.969-7.031L464 81.938V192c0 13.25 10.75 24 24 24s24-10.75 24-24V24c0-13.25-10.75-24-24-24Z"/></svg>

Before

Width:  |  Height:  |  Size: 583 B

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 538 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M396.8 352h22.4c6.4 0 12.8-6.4 12.8-12.8V108.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v230.4c0 6.4 6.4 12.8 12.8 12.8zm-192 0h22.4c6.4 0 12.8-6.4 12.8-12.8V140.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v198.4c0 6.4 6.4 12.8 12.8 12.8zm96 0h22.4c6.4 0 12.8-6.4 12.8-12.8V204.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v134.4c0 6.4 6.4 12.8 12.8 12.8zM496 400H48V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v336c0 17.67 14.33 32 32 32h464c8.84 0 16-7.16 16-16v-16c0-8.84-7.16-16-16-16zm-387.2-48h22.4c6.4 0 12.8-6.4 12.8-12.8v-70.4c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v70.4c0 6.4 6.4 12.8 12.8 12.8z"/></svg>

Before

Width:  |  Height:  |  Size: 748 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M435.848 83.466 172.804 346.51l-96.652-96.652c-4.686-4.686-12.284-4.686-16.971 0l-28.284 28.284c-4.686 4.686-4.686 12.284 0 16.971l133.421 133.421c4.686 4.686 12.284 4.686 16.971 0l299.813-299.813c4.686-4.686 4.686-12.284 0-16.971l-28.284-28.284c-4.686-4.686-12.284-4.686-16.97 0z"/></svg>

Before

Width:  |  Height:  |  Size: 360 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="m441.9 167.3-19.8-19.8c-4.7-4.7-12.3-4.7-17 0L224 328.2 42.9 147.5c-4.7-4.7-12.3-4.7-17 0L6.1 167.3c-4.7 4.7-4.7 12.3 0 17l209.4 209.4c4.7 4.7 12.3 4.7 17 0l209.4-209.4c4.7-4.7 4.7-12.3 0-17z"/></svg>

Before

Width:  |  Height:  |  Size: 271 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M234.8 511.7 196 500.4c-4.2-1.2-6.7-5.7-5.5-9.9L331.3 5.8c1.2-4.2 5.7-6.7 9.9-5.5L380 11.6c4.2 1.2 6.7 5.7 5.5 9.9L244.7 506.2c-1.2 4.3-5.6 6.7-9.9 5.5zm-83.2-121.1 27.2-29c3.1-3.3 2.8-8.5-.5-11.5L72.2 256l106.1-94.1c3.4-3 3.6-8.2.5-11.5l-27.2-29c-3-3.2-8.1-3.4-11.3-.4L2.5 250.2c-3.4 3.2-3.4 8.5 0 11.7L140.3 391c3.2 3 8.2 2.8 11.3-.4zm284.1.4 137.7-129.1c3.4-3.2 3.4-8.5 0-11.7L435.7 121c-3.2-3-8.3-2.9-11.3.4l-27.2 29c-3.1 3.3-2.8 8.5.5 11.5L503.8 256l-106.1 94.1c-3.4 3-3.6 8.2-.5 11.5l27.2 29c3.1 3.2 8.1 3.4 11.3.4z"/></svg>

Before

Width:  |  Height:  |  Size: 601 B

View File

@ -1 +0,0 @@
<svg clip-rule="evenodd" fill-rule="evenodd" height="512" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><g id="Icon"><path d="m13.647 3.25c.414 0 .75.336.75.75s-.336.75-.75.75h-8.647c-1.243 0-2.25 1.007-2.25 2.25v12c0 1.243 1.007 2.25 2.25 2.25h12c1.243 0 2.25-1.007 2.25-2.25v-8.647c0-.414.336-.75.75-.75s.75.336.75.75v8.647c0 2.071-1.679 3.75-3.75 3.75h-12c-2.071 0-3.75-1.679-3.75-3.75v-12c0-2.071 1.679-3.75 3.75-3.75z"/><path d="m22.092 5.09-8.841 8.841c-.082.082-.182.144-.293.181l-3.182 1.06c-.269.09-.566.02-.767-.181s-.271-.498-.181-.767l1.06-3.182c.037-.111.099-.211.181-.293l8.841-8.841c.878-.877 2.301-.877 3.179 0l.003.003c.877.878.877 2.301 0 3.179zm-1.061-1.06c.292-.292.292-.766 0-1.058l-.003-.003c-.292-.292-.766-.292-1.058 0l-8.715 8.715-.53 1.591 1.591-.53z"/><path d="m20.322 5.799c.293.293.293.768 0 1.061-.292.292-.768.292-1.06 0l-2.122-2.122c-.292-.292-.292-.768 0-1.06.293-.293.768-.293 1.061 0z"/></g></svg>

Before

Width:  |  Height:  |  Size: 999 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M304 256c0 26.5-21.5 48-48 48s-48-21.5-48-48 21.5-48 48-48 48 21.5 48 48zm120-48c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48zm-336 0c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z"/></svg>

Before

Width:  |  Height:  |  Size: 297 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M270.2 160h35.5c3.4 0 6.1 2.8 6 6.2l-7.5 196c-.1 3.2-2.8 5.8-6 5.8h-20.5c-3.2 0-5.9-2.5-6-5.8l-7.5-196c-.1-3.4 2.6-6.2 6-6.2zM288 388c-15.5 0-28 12.5-28 28s12.5 28 28 28 28-12.5 28-28-12.5-28-28-28zm281.5 52L329.6 24c-18.4-32-64.7-32-83.2 0L6.5 440c-18.4 31.9 4.6 72 41.6 72H528c36.8 0 60-40 41.5-72zM528 480H48c-12.3 0-20-13.3-13.9-24l240-416c6.1-10.6 21.6-10.7 27.7 0l240 416c6.2 10.6-1.5 24-13.8 24z"/></svg>

Before

Width:  |  Height:  |  Size: 482 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M497.6 0 334.4.17a14.4 14.4 0 0 0-14.4 14.4v33.31a14.4 14.4 0 0 0 14.69 14.4l73.63-2.72 2.06 2.06-278.86 278.87a12 12 0 0 0 0 17l23 23a12 12 0 0 0 17 0l278.86-278.87 2.06 2.06-2.72 73.63a14.4 14.4 0 0 0 14.4 14.69h33.31a14.4 14.4 0 0 0 14.4-14.4L512 14.4A14.4 14.4 0 0 0 497.6 0ZM432 288h-16a16 16 0 0 0-16 16v154a6 6 0 0 1-6 6H54a6 6 0 0 1-6-6V118a6 6 0 0 1 6-6h154a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16H48a48 48 0 0 0-48 48v352a48 48 0 0 0 48 48h352a48 48 0 0 0 48-48V304a16 16 0 0 0-16-16Z"/></svg>

Before

Width:  |  Height:  |  Size: 573 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M48 368a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm0-160a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm0-160a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm448 24H176a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V88a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-16a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-16a16 16 0 0 0-16-16z"/></svg>

Before

Width:  |  Height:  |  Size: 492 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m493.26 56.26-37.51-37.51C443.25 6.25 426.87 0 410.49 0s-32.76 6.25-45.25 18.74l-74.49 74.49L256 127.98 12.85 371.12.15 485.34C-1.45 499.72 9.88 512 23.95 512c.89 0 1.79-.05 2.69-.15l114.14-12.61L384.02 256l34.74-34.74 74.49-74.49c25-25 25-65.52.01-90.51zM118.75 453.39l-67.58 7.46 7.53-67.69 231.24-231.24 31.02-31.02 60.14 60.14-31.02 31.02-231.33 231.33zm340.56-340.57-44.28 44.28-60.13-60.14 44.28-44.28c4.08-4.08 8.84-4.69 11.31-4.69s7.24.61 11.31 4.69l37.51 37.51c6.24 6.25 6.24 16.4 0 22.63z"/></svg>

Before

Width:  |  Height:  |  Size: 578 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M368 224H224V80c0-8.84-7.16-16-16-16h-32c-8.84 0-16 7.16-16 16v144H16c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h144v144c0 8.84 7.16 16 16 16h32c8.84 0 16-7.16 16-16V288h144c8.84 0 16-7.16 16-16v-32c0-8.84-7.16-16-16-16z"/></svg>

Before

Width:  |  Height:  |  Size: 303 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="m207.6 256 107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>

Before

Width:  |  Height:  |  Size: 468 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M432 80h-82.4l-34-56.7A48 48 0 0 0 274.4 0H173.6a48 48 0 0 0-41.2 23.3L98.4 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16l21.2 339a48 48 0 0 0 47.9 45h245.8a48 48 0 0 0 47.9-45L416 128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM173.6 48h100.8l19.2 32H154.4zm173.3 416H101.11l-21-336h287.8z"/></svg>

Before

Width:  |  Height:  |  Size: 370 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M312.973 375.032c9.369 9.369 9.369 24.572 0 33.941s-24.572 9.369-33.941 0L160 289.941 40.968 408.973c-9.369 9.369-24.572 9.369-33.941 0s-9.369-24.572 0-33.941L126.059 256 7.027 136.968c-9.369-9.369-9.369-24.572 0-33.941s24.572-9.369 33.941 0L160 222.059l119.032-119.032c9.369-9.369 24.572-9.369 33.941 0s9.369 24.572 0 33.941L193.941 256l119.032 119.032Z"/></svg>

Before

Width:  |  Height:  |  Size: 434 B

View File

@ -1,30 +1,51 @@
import { Icon, Button, PopupTrigger, Popup, Tooltip, Icons, Text } from 'react-basics';
import { useIntl } from 'react-intl';
import classNames from 'classnames';
import { languages } from 'lib/lang'; import { languages } from 'lib/lang';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import MenuButton from 'components/common/MenuButton'; import { Globe } from 'components/icons';
import Globe from 'assets/globe.svg'; import { labels } from 'components/messages';
import styles from './LanguageButton.module.css'; import styles from './LanguageButton.module.css';
import { Icon } from 'react-basics';
export default function LanguageButton() { export default function LanguageButton({ tooltipPosition = 'top' }) {
const { formatMessage } = useIntl();
const { locale, saveLocale } = useLocale(); const { locale, saveLocale } = useLocale();
const menuOptions = Object.keys(languages).map(key => ({ ...languages[key], value: key })); const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
function handleSelect(value) { function handleSelect(value) {
saveLocale(value); saveLocale(value);
} }
return ( return (
<MenuButton <PopupTrigger>
options={menuOptions} <PopupTrigger action="hover">
value={locale} <Button variant="quiet">
menuClassName={styles.menu}
buttonVariant="light"
onSelect={handleSelect}
hideLabel
>
<Icon> <Icon>
<Globe /> <Globe />
</Icon> </Icon>
</MenuButton> </Button>
<Tooltip position={tooltipPosition}>{formatMessage(labels.language)}</Tooltip>
</PopupTrigger>
<Popup position="right" alignment="end">
<div className={styles.menu}>
{items.map(({ value, label }) => {
return (
<div
key={value}
className={classNames(styles.item, { [styles.selected]: value === locale })}
onClick={handleSelect.bind(null, value)}
>
<Text>{label}</Text>
{value === locale && (
<Icon className={styles.icon}>
<Icons.Check />
</Icon>
)}
</div>
);
})}
</div>
</Popup>
</PopupTrigger>
); );
} }

View File

@ -1,25 +1,35 @@
.menu { .menu {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
min-width: 560px; min-width: 600px;
max-width: 100vw; max-width: 100vw;
padding: 10px; padding: 10px;
} background: var(--base50);
z-index: var(--z-index100);
.menu div {
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--border-color);
margin-left: 10px;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
min-width: calc(100% / 3); min-width: calc(100% / 3);
border-radius: 5px;
padding: 5px 10px;
} }
@media only screen and (max-width: 992px) { .item:hover {
.menu { background: var(--base75);
min-width: 90vw; cursor: pointer;
transform: translateX(calc(40vw));
}
} }
@media only screen and (max-width: 768px) { .selected {
.menu div { font-weight: 700;
min-width: 50%; background: var(--blue100);
} }
.icon {
color: var(--primary400);
} }

View File

@ -0,0 +1,22 @@
import { Button, Icon, Icons, PopupTrigger, Tooltip } from 'react-basics';
import Link from 'next/link';
import { labels } from 'components/messages';
import { useIntl } from 'react-intl';
export default function LogoutButton({ tooltipPosition = 'top' }) {
const { formatMessage } = useIntl();
return (
<Link href="/logout">
<a>
<PopupTrigger action="hover">
<Button variant="quiet">
<Icon>
<Icons.Logout />
</Icon>
</Button>
<Tooltip position={tooltipPosition}>{formatMessage(labels.logout)}</Tooltip>
</PopupTrigger>
</a>
</Link>
);
}

View File

@ -1,12 +1,14 @@
import { useTransition, animated } from 'react-spring'; import { useTransition, animated } from 'react-spring';
import { Button, Icon, PopupTrigger, Tooltip } from 'react-basics';
import { useIntl } from 'react-intl';
import useTheme from 'hooks/useTheme'; import useTheme from 'hooks/useTheme';
import Sun from 'assets/sun.svg'; import { Sun, Moon } from 'components/icons';
import Moon from 'assets/moon.svg'; import { labels } from 'components/messages';
import styles from './ThemeButton.module.css'; import styles from './ThemeButton.module.css';
import { Icon } from 'react-basics';
export default function ThemeButton() { export default function ThemeButton({ tooltipPosition = 'top' }) {
const [theme, setTheme] = useTheme(); const [theme, setTheme] = useTheme();
const { formatMessage } = useIntl();
const transitions = useTransition(theme, { const transitions = useTransition(theme, {
initial: { opacity: 1 }, initial: { opacity: 1 },
@ -14,7 +16,7 @@ export default function ThemeButton() {
opacity: 0, opacity: 0,
transform: `translateY(${theme === 'light' ? '20px' : '-20px'}) scale(0.5)`, transform: `translateY(${theme === 'light' ? '20px' : '-20px'}) scale(0.5)`,
}, },
enter: { opacity: 1, transform: 'translateY(0px) scale(1)' }, enter: { opacity: 1, transform: 'translateY(0px) scale(1.0)' },
leave: { leave: {
opacity: 0, opacity: 0,
transform: `translateY(${theme === 'light' ? '-20px' : '20px'}) scale(0.5)`, transform: `translateY(${theme === 'light' ? '-20px' : '20px'}) scale(0.5)`,
@ -26,12 +28,15 @@ export default function ThemeButton() {
} }
return ( return (
<div className={styles.button} onClick={handleClick}> <PopupTrigger action="hover" popupProps={{ position: 'top' }}>
{transitions((styles, item) => ( <Button variant="quiet" className={styles.button} onClick={handleClick}>
<animated.div key={item} style={styles}> {transitions((style, item) => (
<Icon>{item === 'light' ? <Sun /> : <Moon />}</Icon> <animated.div key={item} style={style}>
<Icon className={styles.icon}>{item === 'light' ? <Sun /> : <Moon />}</Icon>
</animated.div> </animated.div>
))} ))}
</div> </Button>
<Tooltip position={tooltipPosition}>{formatMessage(labels.theme)}</Tooltip>
</PopupTrigger>
); );
} }

View File

@ -5,9 +5,11 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
padding-bottom: 3px;
} }
.button svg { .button > div {
display: flex;
justify-content: center;
align-items: center;
position: absolute; position: absolute;
} }

View File

@ -16,13 +16,11 @@ import {
isBefore, isBefore,
isAfter, isAfter,
} from 'date-fns'; } from 'date-fns';
import { Button, Icon } from 'react-basics'; import { Button, Icon, Icons } from 'react-basics';
import { chunkArray } from 'next-basics'; import { chunkArray } from 'next-basics';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import { dateFormat } from 'lib/date'; import { dateFormat } from 'lib/date';
import { getDateLocale } from 'lib/lang'; 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 styles from './Calendar.module.css';
export default function Calendar({ date, minDate, maxDate, onChange }) { export default function Calendar({ date, minDate, maxDate, onChange }) {
@ -61,7 +59,7 @@ export default function Calendar({ date, minDate, maxDate, onChange }) {
> >
{month} {month}
<Icon className={styles.icon} size="small"> <Icon className={styles.icon} size="small">
{selectMonth ? <Cross /> : <Chevron />} {selectMonth ? <Icons.Close /> : <Icons.ChevronDown />}
</Icon> </Icon>
</div> </div>
<div <div
@ -70,7 +68,7 @@ export default function Calendar({ date, minDate, maxDate, onChange }) {
> >
{year} {year}
<Icon className={styles.icon} size="small"> <Icon className={styles.icon} size="small">
{selectMonth ? <Cross /> : <Chevron />} {selectMonth ? <Icons.Close /> : <Icons.ChevronDown />}
</Icon> </Icon>
</div> </div>
</div> </div>
@ -239,7 +237,7 @@ const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
variant="light" variant="light"
> >
<Icon> <Icon>
<Chevron /> <Icons.ChevronDown />
</Icon> </Icon>
</Button> </Button>
</div> </div>
@ -273,7 +271,7 @@ const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
variant="light" variant="light"
> >
<Icon> <Icon>
<Chevron /> <Icons.ChevronDown />
</Icon> </Icon>
</Button> </Button>
</div> </div>

View File

@ -1,13 +1,13 @@
import { Icon, Text, Flexbox } from 'react-basics'; import { Icon, Text, Flexbox } from 'react-basics';
import Logo from 'assets/logo.svg'; import Logo from 'assets/logo.svg';
function EmptyPlaceholder({ msg, children }) { function EmptyPlaceholder({ message, children }) {
return ( return (
<Flexbox direction="column" alignItems="center" justifyContent="center" gap={60} height={600}> <Flexbox direction="column" alignItems="center" justifyContent="center" gap={60} height={600}>
<Icon size="xl"> <Icon size="xl">
<Logo /> <Logo />
</Icon> </Icon>
<Text size="lg">{msg}</Text> <Text size="lg">{message}</Text>
<div>{children}</div> <div>{children}</div>
</Flexbox> </Flexbox>
); );

View File

@ -1,13 +1,12 @@
import Exclamation from 'assets/exclamation-triangle.svg';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Icon, Icons } from 'react-basics';
import styles from './ErrorMessage.module.css'; import styles from './ErrorMessage.module.css';
import { Icon } from 'react-basics';
export default function ErrorMessage() { export default function ErrorMessage() {
return ( return (
<div className={styles.error}> <div className={styles.error}>
<Icon className={styles.icon} size="large"> <Icon className={styles.icon} size="large">
<Exclamation /> <Icons.Alert />
</Icon> </Icon>
<FormattedMessage id="message.failure" defaultMessage="Something went wrong." /> <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />
</div> </div>

View File

@ -1,8 +1,7 @@
import List from 'assets/list-ul.svg';
import EventDataForm from 'components/metrics/EventDataForm'; import EventDataForm from 'components/metrics/EventDataForm';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useState } from 'react'; import { useState } from 'react';
import { Button, Icon, Modal } from 'react-basics'; import { Button, Icon, Modal, Icons } from 'react-basics';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import styles from './EventDataButton.module.css'; import styles from './EventDataButton.module.css';
@ -29,7 +28,7 @@ function EventDataButton({ websiteId }) {
className={styles.button} className={styles.button}
> >
<Icon> <Icon>
<List /> <Icons.More />
</Icon> </Icon>
Event Data Event Data
</Button> </Button>

View File

@ -1,7 +1,7 @@
import Bolt from 'assets/bolt.svg';
import Calendar from 'assets/calendar.svg'; import Calendar from 'assets/calendar.svg';
import Clock from 'assets/clock.svg'; import Clock from 'assets/clock.svg';
import Dashboard from 'assets/dashboard.svg'; import Dashboard from 'assets/dashboard.svg';
import Edit from 'assets/edit.svg';
import Gear from 'assets/gear.svg'; import Gear from 'assets/gear.svg';
import Globe from 'assets/globe.svg'; import Globe from 'assets/globe.svg';
import Lock from 'assets/lock.svg'; import Lock from 'assets/lock.svg';
@ -9,15 +9,14 @@ import Logo from 'assets/logo.svg';
import Moon from 'assets/moon.svg'; import Moon from 'assets/moon.svg';
import Profile from 'assets/profile.svg'; import Profile from 'assets/profile.svg';
import Sun from 'assets/sun.svg'; import Sun from 'assets/sun.svg';
import Trash from 'assets/trash.svg';
import User from 'assets/user.svg'; import User from 'assets/user.svg';
import Users from 'assets/users.svg'; import Users from 'assets/users.svg';
export { export {
Bolt,
Calendar, Calendar,
Clock, Clock,
Dashboard, Dashboard,
Edit,
Gear, Gear,
Globe, Globe,
Lock, Lock,
@ -25,7 +24,6 @@ export {
Moon, Moon,
Profile, Profile,
Sun, Sun,
Trash,
User, User,
Users, Users,
}; };

View File

@ -1,30 +1,33 @@
import { useState } from 'react'; import { useState } from 'react';
import { useIntl } from 'react-intl';
import { Icon, Text, Icons } from 'react-basics'; import { Icon, Text, Icons } from 'react-basics';
import classNames from 'classnames'; import classNames from 'classnames';
import { Dashboard, Logo, Profile, User, Users, Clock, Globe } from 'components/icons'; import { Dashboard, Logo, Profile, User, Users, Clock, Globe } from 'components/icons';
import ThemeButton from '../buttons/ThemeButton';
import LanguageButton from 'components/buttons/LanguageButton';
import LogoutButton from 'components/buttons/LogoutButton';
import { labels } from 'components/messages';
import NavGroup from './NavGroup'; import NavGroup from './NavGroup';
import styles from './NavBar.module.css'; import styles from './NavBar.module.css';
import ThemeButton from '../buttons/ThemeButton';
import LanguageButton from '../buttons/LanguageButton';
const { ChevronDown, Search } = Icons; export default function NavBar() {
const { formatMessage } = useIntl();
const [minimized, setMinimized] = useState(false);
const tooltipPosition = minimized ? 'right' : 'top';
const analytics = [ const analytics = [
{ label: 'Dashboard', url: '/dashboard', icon: <Dashboard /> }, { label: formatMessage(labels.dashboard), url: '/dashboard', icon: <Dashboard /> },
{ label: 'Realtime', url: '/realtime', icon: <Clock /> }, { label: formatMessage(labels.realtime), url: '/realtime', icon: <Clock /> },
{ label: 'Queries', url: '/queries', icon: <Search /> }, { label: formatMessage(labels.queries), url: '/queries', icon: <Icons.Search /> },
]; ];
const settings = [ const settings = [
{ label: 'Websites', url: '/settings/websites', icon: <Globe /> }, { label: formatMessage(labels.websites), url: '/settings/websites', icon: <Globe /> },
{ label: 'Users', url: '/settings/users', icon: <User /> }, { label: formatMessage(labels.users), url: '/settings/users', icon: <User /> },
{ label: 'Teams', url: '/settings/teams', icon: <Users /> }, { label: formatMessage(labels.teams), url: '/settings/teams', icon: <Users /> },
{ label: 'Profile', url: '/settings/profile', icon: <Profile /> }, { label: formatMessage(labels.profile), url: '/settings/profile', icon: <Profile /> },
]; ];
export default function NavBar() {
const [minimized, setMinimized] = useState(false);
const handleMinimize = () => setMinimized(state => !state); const handleMinimize = () => setMinimized(state => !state);
return ( return (
@ -35,15 +38,16 @@ export default function NavBar() {
</Icon> </Icon>
<Text className={styles.text}>umami</Text> <Text className={styles.text}>umami</Text>
<Icon size="sm" rotate={minimized ? -90 : 90} className={styles.icon}> <Icon size="sm" rotate={minimized ? -90 : 90} className={styles.icon}>
<ChevronDown /> <Icons.ChevronDown />
</Icon> </Icon>
</div> </div>
<NavGroup title="Analytics" items={analytics} minimized={minimized} /> <NavGroup title={formatMessage(labels.analytics)} items={analytics} minimized={minimized} />
<NavGroup title="Settings" items={settings} minimized={minimized} /> <NavGroup title={formatMessage(labels.settings)} items={settings} minimized={minimized} />
<div className={styles.footer}> <div className={styles.footer}>
<div className={styles.buttons}> <div className={styles.buttons}>
<ThemeButton /> <ThemeButton tooltipPosition={tooltipPosition} />
<LanguageButton /> <LanguageButton tooltipPosition={tooltipPosition} />
<LogoutButton tooltipPosition={tooltipPosition} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -51,3 +51,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.minimized .buttons {
flex-direction: column;
}

View File

@ -5,8 +5,6 @@ import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import styles from './NavGroup.module.css'; import styles from './NavGroup.module.css';
const { ChevronDown } = Icons;
export default function NavGroup({ export default function NavGroup({
title, title,
items, items,
@ -30,7 +28,7 @@ export default function NavGroup({
<div className={styles.header} onClick={allowExpand ? handleExpand : undefined}> <div className={styles.header} onClick={allowExpand ? handleExpand : undefined}>
<Text>{title}</Text> <Text>{title}</Text>
<Icon size="sm" rotate={expanded ? 0 : -90}> <Icon size="sm" rotate={expanded ? 0 : -90}>
<ChevronDown /> <Icons.ChevronDown />
</Icon> </Icon>
</div> </div>
)} )}

View File

@ -52,17 +52,24 @@ export const labels = defineMessages({
language: { id: 'label.language', defaultMessage: 'Language' }, language: { id: 'label.language', defaultMessage: 'Language' },
theme: { id: 'label.theme', defaultMessage: 'Theme' }, theme: { id: 'label.theme', defaultMessage: 'Theme' },
profile: { id: 'label.profile', defaultMessage: 'Profile' }, profile: { id: 'label.profile', defaultMessage: 'Profile' },
dashboard: { id: 'label.dashboard', defaultMessage: 'Dashboard' },
more: { id: 'label.more', defaultMessage: 'More' },
realtime: { id: 'label.realtime', defaultMessage: 'Realtime' },
queries: { id: 'label.queries', defaultMessage: 'Queries' },
teams: { id: 'label.teams', defaultMessage: 'Teams' },
analytics: { id: 'label.analytics', defaultMessage: 'Analytics' },
logout: { id: 'label.logout', defaultMessage: 'Logout' },
}); });
export const messages = defineMessages({ export const messages = defineMessages({
error: { id: 'message.error', defaultMessage: 'Something went wrong.' }, error: { id: 'message.error', defaultMessage: 'Something went wrong.' },
saved: { id: 'message.saved', defaultMessage: 'Saved successfully.' }, saved: { id: 'message.saved', defaultMessage: 'Saved.' },
noUsers: { id: 'message.no-users', defaultMessage: 'There are no users.' }, noUsers: { id: 'message.no-users', defaultMessage: 'There are no users.' },
userDeleted: { id: 'message.user-deleted', defaultMessage: 'User deleted successfully.' }, userDeleted: { id: 'message.user-deleted', defaultMessage: 'User deleted.' },
noData: { id: 'message.no-data', defaultMessage: 'No data available.' }, noData: { id: 'message.no-data', defaultMessage: 'No data available.' },
deleteUserWarning: { deleteUserWarning: {
id: 'message.delete-user-warning', id: 'message.delete-user-warning',
defaultMessage: 'Are you sure you want to delete {username}?', defaultMessage: 'Are you sure you want to delete the user {username}?',
}, },
minPasswordLength: { minPasswordLength: {
id: 'message.min-password-length', id: 'message.min-password-length',
@ -79,7 +86,7 @@ export const messages = defineMessages({
trackingCode: { trackingCode: {
id: 'message.tracking-code', id: 'message.tracking-code',
defaultMessage: defaultMessage:
'To track stats for this website, place the following code in the <head> section of your HTML.', 'To track stats for this website, place the following code in the <head>...</head> section of your HTML.',
}, },
deleteWebsite: { deleteWebsite: {
id: 'message.delete-website', id: 'message.delete-website',
@ -107,6 +114,10 @@ export const messages = defineMessages({
defaultMessage: 'You do not have any websites configured.', defaultMessage: 'You do not have any websites configured.',
}, },
noMatchPassword: { id: 'message.no-match-password', defaultMessage: 'Passwords do not match.' }, noMatchPassword: { id: 'message.no-match-password', defaultMessage: 'Passwords do not match.' },
goToSettings: {
id: 'message.go-to-settings',
defaultMessage: 'Go to settings',
},
}); });
export const devices = defineMessages({ export const devices = defineMessages({

View File

@ -1,7 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { safeDecodeURI } from 'next-basics'; import { safeDecodeURI } from 'next-basics';
import { Button, Icon } from 'react-basics'; import { Button, Icon, Icons } from 'react-basics';
import Times from 'assets/times.svg';
import styles from './FilterTags.module.css'; import styles from './FilterTags.module.css';
export default function FilterTags({ className, params, onClick }) { export default function FilterTags({ className, params, onClick }) {
@ -19,7 +18,7 @@ export default function FilterTags({ className, params, onClick }) {
<Button onClick={() => onClick(key)} variant="action" iconRight> <Button onClick={() => onClick(key)} variant="action" iconRight>
{`${key}: ${safeDecodeURI(params[key])}`} {`${key}: ${safeDecodeURI(params[key])}`}
<Icon> <Icon>
<Times /> <Icons.Close />
</Icon> </Icon>
</Button> </Button>
</div> </div>

View File

@ -1,53 +1,62 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button, Loading } from 'react-basics'; import { Button, Icon, Icons, Text, Flexbox } from 'react-basics';
import { defineMessages, useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import Link from 'next/link';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import WebsiteChartList from 'components/pages/websites/WebsiteChartList'; import WebsiteChartList from 'components/pages/websites/WebsiteChartList';
import DashboardSettingsButton from 'components/pages/dashboard/DashboardSettingsButton'; import DashboardSettingsButton from 'components/pages/dashboard/DashboardSettingsButton';
import DashboardEdit from 'components/pages/dashboard/DashboardEdit'; import DashboardEdit from 'components/pages/dashboard/DashboardEdit';
import styles from 'components/pages/websites/WebsiteList.module.css'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import useUser from 'hooks/useUser';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { labels, messages } from 'components/messages';
import useDashboard from 'store/dashboard'; import useDashboard from 'store/dashboard';
const messages = defineMessages({
dashboard: { id: 'label.dashboard', defaultMessage: 'Dashboard' },
more: { id: 'label.more', defaultMessage: 'More' },
});
export default function Dashboard({ userId }) { export default function Dashboard({ userId }) {
const { user } = useUser();
const dashboard = useDashboard(); const dashboard = useDashboard();
const { showCharts, limit, editing } = dashboard; const { showCharts, limit, editing } = dashboard;
const [max, setMax] = useState(limit); const [max, setMax] = useState(limit);
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['websites'], () => get('/websites', { userId })); const { data, isLoading, error } = useQuery(['websites'], () => get('/websites', { userId }));
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const hasData = data && data.length !== 0;
function handleMore() { function handleMore() {
setMax(max + limit); setMax(max + limit);
} }
if (!user || isLoading) {
return <Loading />;
}
if (!data) {
return null;
}
return ( return (
<Page> <Page loading={isLoading} error={error}>
<PageHeader title={formatMessage(messages.dashboard)}> <PageHeader title={formatMessage(labels.dashboard)}>
{!editing && <DashboardSettingsButton />} {!editing && hasData && <DashboardSettingsButton />}
</PageHeader> </PageHeader>
{!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noWebsites)}>
<Link href="/settings/websites">
<Button>
<Icon>
<Icons.ArrowRight />
</Icon>
<Text>{formatMessage(messages.goToSettings)}</Text>
</Button>
</Link>
</EmptyPlaceholder>
)}
{hasData && (
<>
{editing && <DashboardEdit websites={data} />} {editing && <DashboardEdit websites={data} />}
{!editing && <WebsiteChartList websites={data} showCharts={showCharts} limit={max} />} {!editing && <WebsiteChartList websites={data} showCharts={showCharts} limit={max} />}
{max < data.length && ( {max < data.length && (
<Button className={styles.button} onClick={handleMore}> <Flexbox justifyContent="center">
{formatMessage(messages.more)} <Button onClick={handleMore}>
<Icon>
<Icons.More />
</Icon>
<Text>{formatMessage(labels.more)}</Text>
</Button> </Button>
</Flexbox>
)}
</>
)} )}
</Page> </Page>
); );

View File

@ -1,69 +0,0 @@
import { Button, Modal, useToast, Icon, Tabs, Item } from 'react-basics';
import { useEffect, useState } from 'react';
import useApi from 'hooks/useApi';
import PasswordEditForm from 'components/pages/settings/account/PasswordEditForm';
import PageHeader from 'components/layout/PageHeader';
import AccountEditForm from 'components/pages/settings/account/AccountEditForm';
import Lock from 'assets/lock.svg';
import Page from 'components/layout/Page';
import ApiKeysList from 'components/pages/settings/account/ApiKeysList';
import useUser from 'hooks/useUser';
export default function AccountDetails() {
const { user } = useUser();
const [values, setValues] = useState(null);
const [tab, setTab] = useState('detail');
const [showForm, setShowForm] = useState(false);
const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['account'], () => get(`/accounts/${user.id}`), {
cacheTime: 0,
});
const { toast, showToast } = useToast();
const handleChangePassword = () => setShowForm(true);
const handleClose = () => {
setShowForm(false);
};
const handleSave = data => {
setValues(data);
showToast({ message: 'Saved successfully.', variant: 'success' });
};
const handlePasswordSave = () => {
setShowForm(false);
showToast({ message: 'Password successfully changed', variant: 'success' });
};
useEffect(() => {
if (data) {
setValues(data);
}
}, [data]);
return (
<Page loading={isLoading || !values}>
{toast}
<PageHeader title="Account">
<Button onClick={handleChangePassword}>
<Icon>
<Lock />
</Icon>
Change password
</Button>
</PageHeader>
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
<Item key="detail">Details</Item>
<Item key="apiKey">API Keys</Item>
</Tabs>
{tab === 'detail' && <AccountEditForm data={values} onSave={handleSave} />}
{tab === 'apiKey' && <ApiKeysList />}
{data && showForm && (
<Modal title="Change password" onClose={handleClose} style={{ fontWeight: 'bold' }}>
{close => <PasswordEditForm onSave={handlePasswordSave} onClose={close} />}
</Modal>
)}
</Page>
);
}

View File

@ -1,39 +0,0 @@
import { Form, FormRow, FormButtons, FormInput, TextField, SubmitButton } from 'react-basics';
import { useRef } from 'react';
import useApi from 'hooks/useApi';
export default function AccountEditForm({ data, onSave }) {
const { id } = data;
const { post, useMutation } = useApi();
const { mutate, error } = useMutation(({ name }) => post(`/accounts/${id}`, { name }));
const ref = useRef(null);
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave(data);
ref.current.reset(data);
},
});
};
return (
<>
<Form key={id} ref={ref} onSubmit={handleSubmit} error={error} values={data}>
<FormRow label="Name">
<FormInput name="name">
<TextField autoComplete="off" />
</FormInput>
</FormRow>
<FormRow label="Email">
<FormInput name="email">
<TextField readOnly />
</FormInput>
</FormRow>
<FormButtons>
<SubmitButton variant="primary">Save</SubmitButton>
</FormButtons>
</Form>
</>
);
}

View File

@ -1,29 +0,0 @@
import useApi from 'hooks/useApi';
import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
export default function ApiKeyDeleteForm({ apiKeyId, onSave, onClose }) {
const { del, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => del(`/api-key/${apiKeyId}`, data));
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave();
},
});
};
return (
<Form onSubmit={handleSubmit} error={error}>
<div>Are you sure you want to delete this API KEY?</div>
<FormButtons flex>
<SubmitButton variant="primary" disabled={isLoading}>
Delete
</SubmitButton>
<Button disabled={isLoading} onClick={onClose}>
Cancel
</Button>
</FormButtons>
</Form>
);
}

View File

@ -1,46 +0,0 @@
import { Text, Icon, useToast, Banner, LoadingButton, Loading } from 'react-basics';
import useApi from 'hooks/useApi';
import ApiKeysTable from 'components/pages/settings/account/ApiKeysTable';
export default function ApiKeysList() {
const { toast, showToast } = useToast();
const { get, post, useQuery, useMutation } = useApi();
const { mutate, isLoading: isUpdating } = useMutation(data => post('/api-key', data));
const { data, refetch, isLoading, error } = useQuery(['api-key'], () => get(`/api-key`));
const hasData = data && data.length !== 0;
const handleCreate = () => {
mutate(
{},
{
onSuccess: async () => {
showToast({ message: 'API key saved.', variant: 'success' });
await handleSave();
},
},
);
};
const handleSave = async () => {
await refetch();
};
if (error) {
return <Banner variant="error">Something went wrong.</Banner>;
}
if (isLoading) {
return <Loading icon="dots" position="block" />;
}
return (
<>
{toast}
<LoadingButton loading={isUpdating} onClick={handleCreate}>
<Icon icon="plus" /> Create key
</LoadingButton>
{hasData && <ApiKeysTable data={data} onSave={handleSave} />}
{!hasData && <Text>You don&apos;t have any API keys.</Text>}
</>
);
}

View File

@ -1,100 +0,0 @@
import { formatDistance } from 'date-fns';
import { useState } from 'react';
import {
Button,
Icon,
Modal,
PasswordField,
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
Text,
} from 'react-basics';
import ApiKeyDeleteForm from 'components/pages/settings/account/ApiKeyDeleteForm';
import Trash from 'assets/trash.svg';
import styles from './ApiKeysTable.module.css';
const columns = [
{ name: 'apiKey', label: 'Key', style: { flex: 3 } },
{ name: 'created', label: 'Created', style: { flex: 1 } },
{ name: 'action', label: ' ', style: { flex: 1 } },
];
export default function ApiKeysTable({ data = [], onSave }) {
const [apiKeyId, setApiKeyId] = useState(null);
const handleSave = () => {
setApiKeyId(null);
onSave();
};
const handleClose = () => {
setApiKeyId(null);
};
const handleDelete = id => {
setApiKeyId(id);
};
return (
<>
<Table className={styles.table} columns={columns} rows={data}>
<TableHeader>
{(column, index) => {
return (
<TableColumn key={index} className={styles.header} style={{ ...column.style }}>
{column.label}
</TableColumn>
);
}}
</TableHeader>
<TableBody>
{(row, keys, rowIndex) => {
row.apiKey = <PasswordField className={styles.input} value={row.key} readOnly={true} />;
row.created = formatDistance(new Date(row.createdAt), new Date(), {
addSuffix: true,
});
row.action = (
<div className={styles.actions}>
<a target="_blank">
<Button onClick={() => handleDelete(row.id)}>
<Icon>
<Trash />
</Icon>
<Text>Delete</Text>
</Button>
</a>
</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>
{apiKeyId && (
<Modal title="Delete API key" onClose={handleClose}>
{close => <ApiKeyDeleteForm apiKeyId={apiKeyId} onSave={handleSave} onClose={close} />}
</Modal>
)}
</>
);
}

View File

@ -1,31 +0,0 @@
.table th,
.table td {
flex: 2;
}
.cell {
display: flex;
align-items: center;
}
.header:first-child,
.cell:first-child {
min-width: 320px;
}
.input {
flex: 2;
}
.cell:last-child {
justify-content: flex-end;
}
.actions {
display: flex;
gap: 12px;
}
.empty {
min-height: 300px;
}

View File

@ -1,67 +0,0 @@
import { useRef } from 'react';
import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser';
export default function PasswordEditForm({ onSave, onClose }) {
const { post, useMutation } = useApi();
const { user } = useUser();
const { mutate, error, isLoading } = useMutation(data =>
post(`/accounts/${user.id}/change-password`, data),
);
const ref = useRef(null);
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave();
},
});
};
const samePassword = value => {
if (value !== ref?.current?.getValues('newPassword')) {
return "Passwords don't match";
}
return true;
};
return (
<Form ref={ref} onSubmit={handleSubmit} error={error}>
<FormRow label="Current password">
<FormInput name="currentPassword" rules={{ required: 'Required' }}>
<PasswordField autoComplete="off" />
</FormInput>
</FormRow>
<FormRow label="New password">
<FormInput
name="newPassword"
rules={{
required: 'Required',
minLength: { value: 8, message: 'Minimum length 8 characters' },
}}
>
<PasswordField autoComplete="off" />
</FormInput>
</FormRow>
<FormRow label="Confirm password">
<FormInput
name="confirmPassword"
rules={{
required: 'Required',
minLength: { value: 8, message: 'Minimum length 8 characters' },
validate: samePassword,
}}
>
<PasswordField autoComplete="off" />
</FormInput>
</FormRow>
<FormButtons flex>
<Button type="submit" variant="primary" disabled={isLoading}>
Save
</Button>
<Button onClick={onClose}>Cancel</Button>
</FormButtons>
</Form>
);
}

View File

@ -1,50 +0,0 @@
import { useRef } from 'react';
import { Form, FormRow, FormInput, FormButtons, PasswordField, SubmitButton } from 'react-basics';
import useApi from 'hooks/useApi';
export default function PasswordResetForm({ token, onSave }) {
const { post, useMutation } = useApi();
const { mutate, error } = useMutation(data =>
post('/accounts/reset-password', { ...data, token }),
);
const ref = useRef(null);
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave();
},
});
};
const samePassword = value => {
if (value !== ref?.current?.getValues('newPassword')) {
return "Passwords don't match";
}
return true;
};
return (
<>
<Form ref={ref} onSubmit={handleSubmit} error={error}>
<h2>Reset your password</h2>
<FormRow label="New password">
<FormInput name="newPassword" rules={{ required: 'Required' }}>
<PasswordField autoComplete="off" />
</FormInput>
</FormRow>
<FormRow label="Confirm password">
<FormInput
name="confirmPassword"
rules={{ required: 'Required', validate: samePassword }}
>
<PasswordField autoComplete="off" />
</FormInput>
</FormRow>
<FormButtons align="center">
<SubmitButton variant="primary">Update password</SubmitButton>
</FormButtons>
</Form>
</>
);
}

View File

@ -1,5 +1,5 @@
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { Button, Icon, Text, useToast, ModalTrigger } from 'react-basics'; import { Button, Icon, Text, useToast, ModalTrigger, Modal } from 'react-basics';
import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm'; import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm';
import { Lock } from 'components/icons'; import { Lock } from 'components/icons';
import { labels, messages } from 'components/messages'; import { labels, messages } from 'components/messages';
@ -22,7 +22,7 @@ export default function PasswordChangeButton() {
</Icon> </Icon>
<Text>{formatMessage(labels.changePassword)}</Text> <Text>{formatMessage(labels.changePassword)}</Text>
</Button> </Button>
{close => <PasswordEditForm onSave={handleSave} onClose={close} />} <Modal>{close => <PasswordEditForm onSave={handleSave} onClose={close} />}</Modal>
</ModalTrigger> </ModalTrigger>
</> </>
); );

View File

@ -14,6 +14,7 @@ export default function PasswordEditForm({ onSave, onClose }) {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(); onSave();
onClose();
}, },
}); });
}; };

View File

@ -3,7 +3,7 @@ import { useIntl } from 'react-intl';
import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting'; import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting'; import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
import LanguageSetting from 'components/pages/settings/profile/LanguageSetting'; import LanguageSetting from 'components/pages/settings/profile/LanguageSetting';
import ThemeSetting from 'components/buttons/ThemeSetting'; import ThemeSetting from 'components/pages/settings/profile/ThemeSetting';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import { labels } from 'components/messages'; import { labels } from 'components/messages';
@ -21,13 +21,13 @@ export default function ProfileDetails() {
<Form> <Form>
<FormRow label={formatMessage(labels.username)}>{username}</FormRow> <FormRow label={formatMessage(labels.username)}>{username}</FormRow>
<FormRow label={formatMessage(labels.role)}>{role}</FormRow> <FormRow label={formatMessage(labels.role)}>{role}</FormRow>
<FormRow label={formatMessage(labels.language)} inline> <FormRow label={formatMessage(labels.language)}>
<LanguageSetting /> <LanguageSetting />
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.timezone)} inline> <FormRow label={formatMessage(labels.timezone)}>
<TimezoneSetting /> <TimezoneSetting />
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.dateRange)} inline> <FormRow label={formatMessage(labels.dateRange)}>
<DateRangeSetting /> <DateRangeSetting />
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.theme)}> <FormRow label={formatMessage(labels.theme)}>

View File

@ -22,6 +22,7 @@ export default function TeamAddForm({ onSave, onClose }) {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(); onSave();
onClose();
}, },
}); });
}; };
@ -29,7 +30,7 @@ export default function TeamAddForm({ onSave, onClose }) {
return ( return (
<Form ref={ref} onSubmit={handleSubmit} error={error}> <Form ref={ref} onSubmit={handleSubmit} error={error}>
<FormRow label={formatMessage(labels.name)}> <FormRow label={formatMessage(labels.name)}>
<FormInput name="name" rules={{ required: 'Required' }}> <FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
<TextField autoComplete="off" /> <TextField autoComplete="off" />
</FormInput> </FormInput>
</FormRow> </FormRow>

View File

@ -47,7 +47,7 @@ export default function TeamEditForm({ teamId, data, onSave }) {
<TextField value={teamId} readOnly allowCopy /> <TextField value={teamId} readOnly allowCopy />
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.name)}> <FormRow label={formatMessage(labels.name)}>
<FormInput name="name" rules={{ required: 'Required' }}> <FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
<TextField /> <TextField />
</FormInput> </FormInput>
</FormRow> </FormRow>

View File

@ -11,11 +11,9 @@ import {
Flexbox, Flexbox,
Text, Text,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
import { labels } from 'components/messages'; import { labels } from 'components/messages';
import { useIntl } from 'react-intl';
const { Close } = Icons;
export default function TeamMembersTable({ data = [] }) { export default function TeamMembersTable({ data = [] }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@ -48,7 +46,7 @@ export default function TeamMembersTable({ data = [] }) {
<div> <div>
<Button> <Button>
<Icon> <Icon>
<Close /> <Icons.Close />
</Icon> </Icon>
<Text>{formatMessage(labels.remove)}</Text> <Text>{formatMessage(labels.remove)}</Text>
</Button> </Button>

View File

@ -7,7 +7,7 @@ import { messages } from 'components/messages';
export default function TeamWebsites({ teamId }) { export default function TeamWebsites({ teamId }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['team/websites', teamId], () => const { data, isLoading } = useQuery(['teams/websites', teamId], () =>
get(`/teams/${teamId}/websites`), get(`/teams/${teamId}/websites`),
); );
const hasData = data && data.length !== 0; const hasData = data && data.length !== 0;

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button, Icon, Modal, useToast, Icons, Text } from 'react-basics'; import { Button, Icon, Modal, ModalTrigger, useToast, Icons, Text } from 'react-basics';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
@ -9,58 +9,43 @@ import TeamsTable from 'components/pages/settings/teams/TeamsTable';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import { labels, messages } from 'components/messages'; import { labels, messages } from 'components/messages';
const { Plus } = Icons;
export default function TeamsList() { export default function TeamsList() {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [edit, setEdit] = useState(false);
const [update, setUpdate] = useState(0); const [update, setUpdate] = useState(0);
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['teams', update], () => get(`/teams`)); const { data, isLoading, error } = useQuery(['teams', update], () => get(`/teams`));
const hasData = data && data.length !== 0; const hasData = data && data.length !== 0;
const { toast, showToast } = useToast(); const { toast, showToast } = useToast();
const handleAdd = () => {
setEdit(true);
};
const handleSave = () => { const handleSave = () => {
setEdit(false);
setUpdate(state => state + 1); setUpdate(state => state + 1);
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
}; };
const handleClose = () => { const createButton = (
setEdit(false); <ModalTrigger>
}; <Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.createTeam)}</Text>
</Button>
<Modal title={formatMessage(labels.createTeam)}>
{close => <TeamAddForm onSave={handleSave} onClose={close} />}
</Modal>
</ModalTrigger>
);
return ( return (
<Page loading={isLoading} error={error}> <Page loading={isLoading} error={error}>
{toast} {toast}
<PageHeader title={formatMessage(labels.team)}> <PageHeader title={formatMessage(labels.team)}>{createButton}</PageHeader>
<Button variant="primary" onClick={handleAdd}>
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.createTeam)}</Text>
</Button>
</PageHeader>
{hasData && <TeamsTable data={data} />} {hasData && <TeamsTable data={data} />}
{!hasData && ( {!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noTeams)}> <EmptyPlaceholder message={formatMessage(messages.noTeams)}>
<Button variant="primary" onClick={handleAdd}> {createButton}
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.createTeam)}</Text>
</Button>
</EmptyPlaceholder> </EmptyPlaceholder>
)} )}
{edit && (
<Modal title={formatMessage(labels.createTeam)} onClose={handleClose}>
{close => <TeamAddForm onSave={handleSave} onClose={close} />}
</Modal>
)}
</Page> </Page>
); );
} }

View File

@ -15,8 +15,6 @@ import {
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { labels } from 'components/messages'; import { labels } from 'components/messages';
const { ArrowRight } = Icons;
export default function TeamsTable({ data = [] }) { export default function TeamsTable({ data = [] }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@ -46,7 +44,7 @@ export default function TeamsTable({ data = [] }) {
<a> <a>
<Button> <Button>
<Icon> <Icon>
<ArrowRight /> <Icons.ArrowRight />
</Icon> </Icon>
<Text>{formatMessage(labels.settings)}</Text> <Text>{formatMessage(labels.settings)}</Text>
</Button> </Button>

View File

@ -1,44 +1,26 @@
import { useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { Button, Icon, Text, Modal, useToast, Icons } from 'react-basics'; import { Button, Icon, Text, Modal, Icons, ModalTrigger } from 'react-basics';
import UserAddForm from './UserAddForm'; import UserAddForm from './UserAddForm';
import { labels, messages } from 'components/messages'; import { labels } from 'components/messages';
const { Plus } = Icons;
export default function UserAddButton({ onSave }) { export default function UserAddButton({ onSave }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [edit, setEdit] = useState(false);
const { toast, showToast } = useToast();
const handleSave = () => { const handleSave = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
setEdit(false);
onSave(); onSave();
}; };
const handleAdd = () => {
setEdit(true);
};
const handleClose = () => {
setEdit(false);
};
return ( return (
<> <ModalTrigger>
{toast} <Button variant="primary">
<Button variant="primary" onClick={handleAdd}>
<Icon> <Icon>
<Plus /> <Icons.Plus />
</Icon> </Icon>
<Text>{formatMessage(labels.createUser)}</Text> <Text>{formatMessage(labels.createUser)}</Text>
</Button> </Button>
{edit && ( <Modal title={formatMessage(labels.createUser)}>
<Modal title={formatMessage(labels.createUser)} onClose={handleClose}> {close => <UserAddForm onSave={handleSave} onClose={close} />}
<UserAddForm onSave={handleSave} onClose={handleClose} />
</Modal> </Modal>
)} </ModalTrigger>
</>
); );
} }

View File

@ -24,6 +24,7 @@ export default function UserAddForm({ onSave, onClose }) {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(data); onSave(data);
onClose();
}, },
}); });
}; };

View File

@ -1,7 +1,7 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { Button, Form, FormButtons, SubmitButton } from 'react-basics'; import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
import { useIntl } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import { labels, messages } from 'components/messages'; import { labels, messages } from 'components/messages';
export default function UserDeleteForm({ userId, username, onSave, onClose }) { export default function UserDeleteForm({ userId, username, onSave, onClose }) {
@ -20,9 +20,14 @@ export default function UserDeleteForm({ userId, username, onSave, onClose }) {
return ( return (
<Form onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<p>{formatMessage(messages.deleteUserWarning, { username })}</p> <p>
<FormattedMessage
{...messages.deleteUserWarning}
values={{ username: <b>{username}</b> }}
/>
</p>
<FormButtons flex> <FormButtons flex>
<SubmitButton variant="primary" disabled={isLoading}> <SubmitButton variant="danger" disabled={isLoading}>
{formatMessage(labels.delete)} {formatMessage(labels.delete)}
</SubmitButton> </SubmitButton>
<Button disabled={isLoading} onClick={onClose}> <Button disabled={isLoading} onClick={onClose}>

View File

@ -22,7 +22,7 @@ export default function UsersList() {
const handleSave = () => refetch(); const handleSave = () => refetch();
const handleDelete = () => const handleDelete = () =>
showToast({ message: formatMessage(messages.deleted), variant: 'success' }); showToast({ message: formatMessage(messages.userDeleted), variant: 'success' });
return ( return (
<Page loading={isLoading} error={error}> <Page loading={isLoading} error={error}>

View File

@ -11,18 +11,16 @@ import {
Flexbox, Flexbox,
Icons, Icons,
ModalTrigger, ModalTrigger,
Modal,
} from 'react-basics'; } from 'react-basics';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { formatDistance } from 'date-fns'; import { formatDistance } from 'date-fns';
import Link from 'next/link'; import Link from 'next/link';
import { Edit } from 'components/icons';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import UserDeleteForm from './UserDeleteForm'; import UserDeleteForm from './UserDeleteForm';
import { labels } from 'components/messages'; import { labels } from 'components/messages';
import { ROLES } from 'lib/constants'; import { ROLES } from 'lib/constants';
const { Trash } = Icons;
export default function UsersTable({ data = [], onDelete }) { export default function UsersTable({ data = [], onDelete }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { user } = useUser(); const { user } = useUser();
@ -60,7 +58,7 @@ export default function UsersTable({ data = [], onDelete }) {
<Link href={`/settings/users/${row.id}`}> <Link href={`/settings/users/${row.id}`}>
<Button> <Button>
<Icon> <Icon>
<Edit /> <Icons.Edit />
</Icon> </Icon>
<Text>{formatMessage(labels.edit)}</Text> <Text>{formatMessage(labels.edit)}</Text>
</Button> </Button>
@ -68,10 +66,11 @@ export default function UsersTable({ data = [], onDelete }) {
<ModalTrigger disabled={row.id === user.id}> <ModalTrigger disabled={row.id === user.id}>
<Button disabled={row.id === user.id}> <Button disabled={row.id === user.id}>
<Icon> <Icon>
<Trash /> <Icons.Trash />
</Icon> </Icon>
<Text>{formatMessage(labels.delete)}</Text> <Text>{formatMessage(labels.delete)}</Text>
</Button> </Button>
<Modal>
{close => ( {close => (
<UserDeleteForm <UserDeleteForm
userId={row.id} userId={row.id}
@ -80,6 +79,7 @@ export default function UsersTable({ data = [], onDelete }) {
onClose={close} onClose={close}
/> />
)} )}
</Modal>
</ModalTrigger> </ModalTrigger>
</> </>
), ),

View File

@ -25,6 +25,7 @@ export default function WebsiteAddForm({ onSave, onClose }) {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(); onSave();
onClose();
}, },
}); });
}; };

View File

@ -22,6 +22,7 @@ export default function WebsiteDeleteForm({ websiteId, onSave, onClose }) {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(); onSave();
onClose();
}, },
}); });
}; };

View File

@ -1,6 +1,4 @@
import { useRouter } from 'next/router'; import { Button, Form, FormRow, Modal, ModalTrigger } from 'react-basics';
import { useState } from 'react';
import { Button, Form, FormRow, Modal } from 'react-basics';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm'; import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm';
import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm'; import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm';
@ -8,43 +6,39 @@ import { labels, messages } from 'components/messages';
export default function WebsiteReset({ websiteId, onSave }) { export default function WebsiteReset({ websiteId, onSave }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [modal, setModal] = useState(null);
const router = useRouter();
const handleReset = async () => { const handleReset = async () => {
setModal(null); onSave('reset');
onSave();
}; };
const handleDelete = async () => { const handleDelete = async () => {
onSave(); onSave('delete');
await router.push('/websites');
}; };
const handleClose = () => setModal(null);
return ( return (
<Form> <Form>
<FormRow label={formatMessage(labels.resetWebsite)}> <FormRow label={formatMessage(labels.resetWebsite)}>
<p>{formatMessage(messages.resetWebsiteWarning)}</p> <p>{formatMessage(messages.resetWebsiteWarning)}</p>
<Button onClick={() => setModal('reset')}>{formatMessage(labels.reset)}</Button> <ModalTrigger>
<Button>{formatMessage(labels.reset)}</Button>
<Modal title={formatMessage(labels.resetWebsite)}>
{close => (
<WebsiteResetForm websiteId={websiteId} onSave={handleReset} onClose={close} />
)}
</Modal>
</ModalTrigger>
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.deleteWebsite)}> <FormRow label={formatMessage(labels.deleteWebsite)}>
<p>{formatMessage(messages.deleteWebsiteWarning)}</p> <p>{formatMessage(messages.deleteWebsiteWarning)}</p>
<Button onClick={() => setModal('delete')}>Delete</Button> <ModalTrigger>
</FormRow> <Button>Delete</Button>
{modal === 'reset' && ( <Modal title={formatMessage(labels.deleteWebsite)}>
<Modal title={formatMessage(labels.resetWebsite)} onClose={handleClose}>
{close => <WebsiteResetForm websiteId={websiteId} onSave={handleReset} onClose={close} />}
</Modal>
)}
{modal === 'delete' && (
<Modal title={formatMessage(labels.deleteWebsite)} onClose={handleClose}>
{close => ( {close => (
<WebsiteDeleteForm websiteId={websiteId} onSave={handleDelete} onClose={close} /> <WebsiteDeleteForm websiteId={websiteId} onSave={handleDelete} onClose={close} />
)} )}
</Modal> </Modal>
)} </ModalTrigger>
</FormRow>
</Form> </Form>
); );
} }

View File

@ -22,6 +22,7 @@ export default function WebsiteResetForm({ websiteId, onSave, onClose }) {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
onSave(); onSave();
onClose();
}, },
}); });
}; };

View File

@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Breadcrumbs, Item, Tabs, useToast, Button, Text, Icon, Icons } from 'react-basics'; import { Breadcrumbs, Item, Tabs, useToast, Button, Text, Icon, Icons } from 'react-basics';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
@ -11,9 +12,8 @@ import ShareUrl from 'components/pages/settings/websites/ShareUrl';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { labels, messages } from 'components/messages'; import { labels, messages } from 'components/messages';
const { External } = Icons;
export default function WebsiteSettings({ websiteId }) { export default function WebsiteSettings({ websiteId }) {
const router = useRouter();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [values, setValues] = useState(null); const [values, setValues] = useState(null);
const [tab, setTab] = useState('details'); const [tab, setTab] = useState('details');
@ -34,6 +34,12 @@ export default function WebsiteSettings({ websiteId }) {
setValues(state => ({ ...state, ...data })); setValues(state => ({ ...state, ...data }));
}; };
const handleReset = async value => {
if (value === 'delete') {
await router.push('/websites');
}
};
useEffect(() => { useEffect(() => {
if (data) { if (data) {
setValues(data); setValues(data);
@ -54,7 +60,7 @@ export default function WebsiteSettings({ websiteId }) {
<a> <a>
<Button variant="primary"> <Button variant="primary">
<Icon> <Icon>
<External /> <Icons.External />
</Icon> </Icon>
<Text>{formatMessage(labels.view)}</Text> <Text>{formatMessage(labels.view)}</Text>
</Button> </Button>
@ -72,7 +78,7 @@ export default function WebsiteSettings({ websiteId }) {
)} )}
{tab === 'tracking' && <TrackingCode websiteId={websiteId} data={values} />} {tab === 'tracking' && <TrackingCode websiteId={websiteId} data={values} />}
{tab === 'share' && <ShareUrl websiteId={websiteId} data={values} onSave={handleSave} />} {tab === 'share' && <ShareUrl websiteId={websiteId} data={values} onSave={handleSave} />}
{tab === 'data' && <WebsiteReset websiteId={websiteId} onSave={handleSave} />} {tab === 'data' && <WebsiteReset websiteId={websiteId} onSave={handleReset} />}
</Page> </Page>
); );
} }

View File

@ -1,5 +1,4 @@
import { useState } from 'react'; import { Button, Icon, Text, Modal, ModalTrigger, useToast, Icons } from 'react-basics';
import { Button, Icon, Text, Modal, useToast, Icons } from 'react-basics';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
@ -10,10 +9,7 @@ import useApi from 'hooks/useApi';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import { labels, messages } from 'components/messages'; import { labels, messages } from 'components/messages';
const { Plus } = Icons;
export default function WebsitesList() { export default function WebsitesList() {
const [edit, setEdit] = useState(false);
const { user } = useUser(); const { user } = useUser();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, error, refetch } = useQuery( const { data, isLoading, error, refetch } = useQuery(
@ -27,21 +23,21 @@ export default function WebsitesList() {
const handleSave = async () => { const handleSave = async () => {
await refetch(); await refetch();
setEdit(false);
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
}; };
const handleAdd = () => setEdit(true);
const handleClose = () => setEdit(false);
const addButton = ( const addButton = (
<Button variant="primary" onClick={handleAdd}> <ModalTrigger>
<Button variant="primary">
<Icon> <Icon>
<Plus /> <Icons.Plus />
</Icon> </Icon>
<Text>{formatMessage(labels.addWebsite)}</Text> <Text>{formatMessage(labels.addWebsite)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.addWebsite)}>
{close => <WebsiteAddForm onSave={handleSave} onClose={close} />}
</Modal>
</ModalTrigger>
); );
return ( return (
@ -54,11 +50,6 @@ export default function WebsitesList() {
{addButton} {addButton}
</EmptyPlaceholder> </EmptyPlaceholder>
)} )}
{edit && (
<Modal title={formatMessage(labels.addWebsite)} onClose={handleClose}>
{close => <WebsiteAddForm onSave={handleSave} onClose={close} />}
</Modal>
)}
</Page> </Page>
); );
} }

View File

@ -13,8 +13,6 @@ import {
} from 'react-basics'; } from 'react-basics';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
const { ArrowRight, External } = Icons;
const messages = defineMessages({ const messages = defineMessages({
name: { id: 'label.name', defaultMessage: 'Name' }, name: { id: 'label.name', defaultMessage: 'Name' },
domain: { id: 'label.domain', defaultMessage: 'Domain' }, domain: { id: 'label.domain', defaultMessage: 'Domain' },
@ -50,7 +48,7 @@ export default function WebsitesTable({ data = [] }) {
<a> <a>
<Button> <Button>
<Icon> <Icon>
<ArrowRight /> <Icons.ArrowRight />
</Icon> </Icon>
Settings Settings
</Button> </Button>
@ -60,7 +58,7 @@ export default function WebsitesTable({ data = [] }) {
<a> <a>
<Button> <Button>
<Icon> <Icon>
<External /> <Icons.External />
</Icon> </Icon>
View View
</Button> </Button>

View File

@ -1,28 +1,11 @@
import { defineMessages, useIntl } from 'react-intl';
import Link from 'components/common/Link';
import WebsiteChart from 'components/metrics/WebsiteChart'; import WebsiteChart from 'components/metrics/WebsiteChart';
import Page from 'components/layout/Page';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteList.module.css'; import styles from './WebsiteList.module.css';
import useDashboard from 'store/dashboard'; import useDashboard from 'store/dashboard';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { firstBy } from 'thenby'; import { firstBy } from 'thenby';
const messages = defineMessages({
noWebsites: {
id: 'message.no-websites-configured',
defaultMessage: "You don't have any websites configured.",
},
goToSettngs: {
id: 'message.go-to-buttons',
defaultMessage: 'Go to buttons',
},
});
export default function WebsiteList({ websites, showCharts, limit }) { export default function WebsiteList({ websites, showCharts, limit }) {
const { websiteOrder } = useDashboard(); const { websiteOrder } = useDashboard();
const { formatMessage } = useIntl();
const ordered = useMemo( const ordered = useMemo(
() => () =>
@ -32,18 +15,6 @@ export default function WebsiteList({ websites, showCharts, limit }) {
[websites, websiteOrder], [websites, websiteOrder],
); );
if (websites.length === 0) {
return (
<Page>
<EmptyPlaceholder msg={formatMessage(messages.noWebsites)}>
<Link href="/websites" icon={<Arrow />} iconRight>
{formatMessage(messages.goToSettngs)}
</Link>
</EmptyPlaceholder>
</Page>
);
}
return ( return (
<div> <div>
{ordered.map(({ id, name, domain }, index) => {ordered.map(({ id, name, domain }, index) =>

View File

@ -94,7 +94,7 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-basics": "^0.62.0", "react-basics": "^0.63.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-intl": "^5.24.7", "react-intl": "^5.24.7",
@ -103,7 +103,7 @@
"react-tooltip": "^4.2.21", "react-tooltip": "^4.2.21",
"react-use-measure": "^2.0.4", "react-use-measure": "^2.0.4",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"redis": "^4.5.0", "redis": "^4.6.2",
"request-ip": "^3.3.0", "request-ip": "^3.3.0",
"semver": "^7.3.6", "semver": "^7.3.6",
"thenby": "^1.3.4", "thenby": "^1.3.4",

121
yarn.lock
View File

@ -1627,9 +1627,9 @@
unstorage "^1.0.0" unstorage "^1.0.0"
"@netlify/plugin-nextjs@^4.27.3": "@netlify/plugin-nextjs@^4.27.3":
version "4.30.0" version "4.30.2"
resolved "https://registry.yarnpkg.com/@netlify/plugin-nextjs/-/plugin-nextjs-4.30.0.tgz#1e4330ff1094057fc337d3b3504cf4a87b1cdbab" resolved "https://registry.yarnpkg.com/@netlify/plugin-nextjs/-/plugin-nextjs-4.30.2.tgz#c783ecb0eb080a1f124fc331360c6403378fca68"
integrity sha512-+MyfV6biKGVs/jQQUKGi2vN5aFFyRUTeEAItDiSvNhAUi0mOXEkEmu5JN7ZAQtL2oRIKKnxflsZDmtP3DdAahg== integrity sha512-hqvdHlQEMfpTXh+fM0jwvtKla/gUS4gVKEZeQEkJlCzAJO+8XT2bTFAGrusNHLQ53L081nLXVXx1c+HUo0LFfQ==
dependencies: dependencies:
"@netlify/esbuild" "0.14.39" "@netlify/esbuild" "0.14.39"
"@netlify/functions" "^1.4.0" "@netlify/functions" "^1.4.0"
@ -1946,6 +1946,11 @@
resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.1.0.tgz#64e310ddee72010676e14296076329e594a1f6c7" resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.1.0.tgz#64e310ddee72010676e14296076329e594a1f6c7"
integrity sha512-9QovlxmpRtvxVbN0UBcv8WfdSMudNZZTFqCsnBszcQXqaZb/TVe30ScgGEO7u1EAIacTPAo7/oCYjYAxiHLanQ== integrity sha512-9QovlxmpRtvxVbN0UBcv8WfdSMudNZZTFqCsnBszcQXqaZb/TVe30ScgGEO7u1EAIacTPAo7/oCYjYAxiHLanQ==
"@redis/bloom@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71"
integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==
"@redis/client@1.4.2": "@redis/client@1.4.2":
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.4.2.tgz#2a3f5e98bc33b7b979390442e6e08f96e57fabdd" resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.4.2.tgz#2a3f5e98bc33b7b979390442e6e08f96e57fabdd"
@ -1955,6 +1960,15 @@
generic-pool "3.9.0" generic-pool "3.9.0"
yallist "4.0.0" yallist "4.0.0"
"@redis/client@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.3.tgz#2295ca770c9c40dcc59a96da9d05f4403c32e847"
integrity sha512-kPad3QmWyRcmFj1gnb+SkzjXBV7oPpyTJmasVA+ocgNClxqZaTJjLFReqxm9cZQiCtqZK9vrcTISNrgzQXFpLg==
dependencies:
cluster-key-slot "1.1.2"
generic-pool "3.9.0"
yallist "4.0.0"
"@redis/graph@1.1.0": "@redis/graph@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.0.tgz#cc2b82e5141a29ada2cce7d267a6b74baa6dd519" resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.0.tgz#cc2b82e5141a29ada2cce7d267a6b74baa6dd519"
@ -1970,6 +1984,11 @@
resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.0.tgz#7abb18d431f27ceafe6bcb4dd83a3fa67e9ab4df" resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.0.tgz#7abb18d431f27ceafe6bcb4dd83a3fa67e9ab4df"
integrity sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ== integrity sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==
"@redis/search@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.1.tgz#f547b76b74f267831d3b368e3d7bba3a6a9e32bd"
integrity sha512-pqCXTc5e7wJJgUuJiC3hBgfoFRoPxYzwn0BEfKgejTM7M/9zP3IpUcqcjgfp8hF+LoV8rHZzcNTz7V+pEIY7LQ==
"@redis/time-series@1.0.4": "@redis/time-series@1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.4.tgz#af85eb080f6934580e4d3b58046026b6c2b18717" resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.4.tgz#af85eb080f6934580e4d3b58046026b6c2b18717"
@ -2119,17 +2138,17 @@
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"
"@tanstack/query-core@4.22.0": "@tanstack/query-core@4.22.4":
version "4.22.0" version "4.22.4"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.22.0.tgz#7a786fcea64e229ed5d4308093dd644cdfaa895e" resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.22.4.tgz#aca622d2f8800a147ece5520d956a076ab92f0ea"
integrity sha512-OeLyBKBQoT265f5G9biReijeP8mBxNFwY7ZUu1dKL+YzqpG5q5z7J/N1eT8aWyKuhyDTiUHuKm5l+oIVzbtrjw== integrity sha512-t79CMwlbBnj+yL82tEcmRN93bL4U3pae2ota4t5NN2z3cIeWw74pzdWrKRwOfTvLcd+b30tC+ciDlfYOKFPGUw==
"@tanstack/react-query@^4.16.1": "@tanstack/react-query@^4.16.1":
version "4.22.0" version "4.23.0"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.22.0.tgz#aaa4b41a6d306be6958018c74a8a3bb3e9f1924c" resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.23.0.tgz#0b9e14269a48cf5a4ffe46c8525cdb9df2ebd9cf"
integrity sha512-P9o+HjG42uB/xHR6dMsJaPhtZydSe4v0xdG5G/cEj1oHZAXelMlm67/rYJNQGKgBamKElKogj+HYGF+NY2yHYg== integrity sha512-cfQsrecZQjYYueiow4WcK8ItokXJnv+b2OrK8Lf5kF7lM9uCo1ilyygFB8wo4MfxchUBVM6Cs8wq4Ed7fouwkA==
dependencies: dependencies:
"@tanstack/query-core" "4.22.0" "@tanstack/query-core" "4.22.4"
use-sync-external-store "^1.2.0" use-sync-external-store "^1.2.0"
"@trysound/sax@0.2.0": "@trysound/sax@0.2.0":
@ -3050,7 +3069,7 @@ cluster-key-slot@1.1.1:
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz#10ccb9ded0729464b6d2e7d714b100a2d1259d43" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz#10ccb9ded0729464b6d2e7d714b100a2d1259d43"
integrity sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw== integrity sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==
cluster-key-slot@^1.1.0: cluster-key-slot@1.1.2, cluster-key-slot@^1.1.0:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
@ -3417,7 +3436,7 @@ date-fns-tz@^1.1.4:
resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.7.tgz#e8e9d2aaceba5f1cc0e677631563081fdcb0e69a" resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.7.tgz#e8e9d2aaceba5f1cc0e677631563081fdcb0e69a"
integrity sha512-1t1b8zyJo+UI8aR+g3iqr5fkUHWpd58VBx8J/ZSQ+w7YrGlw80Ag4sA86qkfCXRBLmMc4I2US+aPMd4uKvwj5g== integrity sha512-1t1b8zyJo+UI8aR+g3iqr5fkUHWpd58VBx8J/ZSQ+w7YrGlw80Ag4sA86qkfCXRBLmMc4I2US+aPMd4uKvwj5g==
date-fns@^2.23.0: date-fns@^2.23.0, date-fns@^2.29.3:
version "2.29.3" version "2.29.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
@ -3501,10 +3520,10 @@ define-properties@^1.1.3, define-properties@^1.1.4:
has-property-descriptors "^1.0.0" has-property-descriptors "^1.0.0"
object-keys "^1.1.1" object-keys "^1.1.1"
defu@^6.0.0, defu@^6.1.0, defu@^6.1.1: defu@^6.0.0, defu@^6.1.0, defu@^6.1.2:
version "6.1.1" version "6.1.2"
resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.1.tgz#a12c712349197c545dc61d3cd3b607b4cc7ef0c1" resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.2.tgz#1217cba167410a1765ba93893c6dbac9ed9d9e5c"
integrity sha512-aA964RUCsBt0FGoNIlA3uFgo2hO+WWC0fiC6DBps/0SFzkKcYoM/3CzVLIa5xSsrFjdioMdYgAIbwo80qp2MoA== integrity sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==
del@^6.0.0: del@^6.0.0:
version "6.1.1" version "6.1.1"
@ -3525,7 +3544,7 @@ delayed-stream@~1.0.0:
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
denque@^2.0.1: denque@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
@ -4360,6 +4379,11 @@ get-port-please@^2.6.1:
dependencies: dependencies:
fs-memo "^1.2.0" fs-memo "^1.2.0"
get-port-please@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/get-port-please/-/get-port-please-3.0.1.tgz#a24953a41dc249f76869ac25e81d6623e61ab010"
integrity sha512-R5pcVO8Z1+pVDu8Ml3xaJCEkBiiy1VQN9za0YqH8GIi1nIqD4IzQhzY6dDzMRtdS1lyiGlucRzm8IN8wtLIXng==
get-stream@^6.0.0: get-stream@^6.0.0:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
@ -4496,9 +4520,9 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
h3@^1.0.1: h3@^1.0.1:
version "1.0.2" version "1.1.0"
resolved "https://registry.yarnpkg.com/h3/-/h3-1.0.2.tgz#3a2992921a62de697bd4a3f449401f7600116df6" resolved "https://registry.yarnpkg.com/h3/-/h3-1.1.0.tgz#ff10d590005711dfb41034b9b1496d165507b1ea"
integrity sha512-25QqjQMz8pX1NI2rZ/ziNT9B8Aog7jmu2a0o8Qm9kKoH3zOhE+2icVs069h6DEp0g1Dst1+zKfRdRYcK0MogJA== integrity sha512-kx3u+RMzY963fU8NNT2ePWgsryAn9DNztPqbHia/M7HgA+rtXKjHjED9/uidcYPmImNwAfJsCachCzh2T3QH2A==
dependencies: dependencies:
cookie-es "^0.5.0" cookie-es "^0.5.0"
destr "^1.2.2" destr "^1.2.2"
@ -4638,9 +4662,9 @@ image-meta@^0.1.1:
integrity sha512-+oXiHwOEPr1IE5zY0tcBLED/CYcre15J4nwL50x3o0jxWqEkyjrusiKP3YSU+tr9fvJp33ZcP5Gpj2295g3aEw== integrity sha512-+oXiHwOEPr1IE5zY0tcBLED/CYcre15J4nwL50x3o0jxWqEkyjrusiKP3YSU+tr9fvJp33ZcP5Gpj2295g3aEw==
immer@^9.0.12: immer@^9.0.12:
version "9.0.18" version "9.0.19"
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.18.tgz#d2faee58fd0e34f017f329b98cdab37826fa31b8" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.19.tgz#67fb97310555690b5f9cd8380d38fc0aabb6b38b"
integrity sha512-eAPNpsj7Ax1q6Y/3lm2PmlwRcFzpON7HSNQ3ru5WQH1/PSpnyed/HpNOELl2CxLKoj4r+bAHgdyKqW5gc2Se1A== integrity sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==
import-fresh@^3.0.0, import-fresh@^3.2.1: import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0" version "3.3.0"
@ -4723,14 +4747,14 @@ intl-messageformat@9.13.0:
tslib "^2.1.0" tslib "^2.1.0"
ioredis@^5.2.4: ioredis@^5.2.4:
version "5.2.5" version "5.3.0"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.2.5.tgz#c62dc3945ad2a8f0323fbb2765b934a84a68cde0" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.0.tgz#b5469f0fd374648ef074840c00c1d8eed42fca3f"
integrity sha512-7HKo/ClM2DGLRXdFq8ruS3Uuadensz4A76wPOU0adqlOqd1qkhoLPDaBhmVhUhNGpB+J65/bhLmNB8DDY99HJQ== integrity sha512-Id9jKHhsILuIZpHc61QkagfVdUj2Rag5GzG1TGEvRNeM7dtTOjICgjC+tvqYxi//PuX2wjQ+Xjva2ONBuf92Pw==
dependencies: dependencies:
"@ioredis/commands" "^1.1.1" "@ioredis/commands" "^1.1.1"
cluster-key-slot "^1.1.0" cluster-key-slot "^1.1.0"
debug "^4.3.4" debug "^4.3.4"
denque "^2.0.1" denque "^2.1.0"
lodash.defaults "^4.2.0" lodash.defaults "^4.2.0"
lodash.isarguments "^3.1.0" lodash.isarguments "^3.1.0"
redis-errors "^1.2.0" redis-errors "^1.2.0"
@ -5239,18 +5263,18 @@ listhen@^0.2.15:
ufo "^0.8.5" ufo "^0.8.5"
listhen@^1.0.0: listhen@^1.0.0:
version "1.0.1" version "1.0.2"
resolved "https://registry.yarnpkg.com/listhen/-/listhen-1.0.1.tgz#31054d08d850ad21473768085a50a8b34a635070" resolved "https://registry.yarnpkg.com/listhen/-/listhen-1.0.2.tgz#3332af0cf77dd914e12d125c70a9c6aed9537033"
integrity sha512-RBzBGHMCc5wP8J5Vf8WgF4CAJH8dWHi9LaKB7vfzZt54CiH/0dp01rudy2hFD9wCrTM+UfxFVnn5wTIiY+Qhiw== integrity sha512-yXz0NIYfVJDBQK2vlCpD/OjSzYkur2mR44boUtlg0eES4holn7oYZf439y5JxP55EOzFtClZ8eZlMJ8a++FwlQ==
dependencies: dependencies:
clipboardy "^3.0.0" clipboardy "^3.0.0"
colorette "^2.0.19" colorette "^2.0.19"
defu "^6.1.1" defu "^6.1.2"
get-port-please "^2.6.1" get-port-please "^3.0.1"
http-shutdown "^1.2.2" http-shutdown "^1.2.2"
ip-regex "^5.0.0" ip-regex "^5.0.0"
node-forge "^1.3.1" node-forge "^1.3.1"
ufo "^1.0.0" ufo "^1.0.1"
listr2@^3.12.2: listr2@^3.12.2:
version "3.14.0" version "3.14.0"
@ -6562,12 +6586,13 @@ rc@^1.2.7:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-basics@^0.62.0: react-basics@^0.63.0:
version "0.62.0" version "0.63.0"
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.62.0.tgz#7d4890632d9a0086e54f7472b9e59db0c973dd86" resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.63.0.tgz#1b203c701f6936076633994ed8175234bde93694"
integrity sha512-/m9LXHwRCX3uqT8exJp+D0PsDqQBAfg4BXpqRuXmQBMmEYjixQo+qBLHcbnJ0/6qLrsyb/5y4VBrorWQ2HtKiQ== integrity sha512-G6+1Z921kC/TyjZCABrDlNeB22YkN6q7V70xREGSiO55OXU73MLT+Kk96GDWYfKa5lB1zI5rCbbvGz3ELuU+mA==
dependencies: dependencies:
classnames "^2.3.1" classnames "^2.3.1"
date-fns "^2.29.3"
react "^18.2.0" react "^18.2.0"
react-dom "^18.2.0" react-dom "^18.2.0"
react-hook-form "^7.34.2" react-hook-form "^7.34.2"
@ -6786,7 +6811,7 @@ redis-parser@^3.0.0:
dependencies: dependencies:
redis-errors "^1.0.0" redis-errors "^1.0.0"
redis@^4.5.0, redis@^4.5.1: redis@^4.5.1:
version "4.5.1" version "4.5.1"
resolved "https://registry.yarnpkg.com/redis/-/redis-4.5.1.tgz#f5a818970bb2dc5d60540bab41308640604c7d33" resolved "https://registry.yarnpkg.com/redis/-/redis-4.5.1.tgz#f5a818970bb2dc5d60540bab41308640604c7d33"
integrity sha512-oxXSoIqMJCQVBTfxP6BNTCtDMyh9G6Vi5wjdPdV/sRKkufyZslDqCScSGcOr6XGR/reAWZefz7E4leM31RgdBA== integrity sha512-oxXSoIqMJCQVBTfxP6BNTCtDMyh9G6Vi5wjdPdV/sRKkufyZslDqCScSGcOr6XGR/reAWZefz7E4leM31RgdBA==
@ -6798,6 +6823,18 @@ redis@^4.5.0, redis@^4.5.1:
"@redis/search" "1.1.0" "@redis/search" "1.1.0"
"@redis/time-series" "1.0.4" "@redis/time-series" "1.0.4"
redis@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.2.tgz#5592db7b95c1b6652cde463e58a8029a1f7890bb"
integrity sha512-Xoh7UyU6YnT458xA8svaZAJu6ZunKeW7Z/7GXrLWGGwhVLTsDX6pr3u7ENAoV+DHBPO+9LwIu45ClwUwpIjAxw==
dependencies:
"@redis/bloom" "1.2.0"
"@redis/client" "1.5.3"
"@redis/graph" "1.1.0"
"@redis/json" "1.0.4"
"@redis/search" "1.1.1"
"@redis/time-series" "1.0.4"
redux@^4.0.0, redux@^4.0.4: redux@^4.0.0, redux@^4.0.4:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
@ -7867,9 +7904,9 @@ unbox-primitive@^1.0.2:
which-boxed-primitive "^1.0.2" which-boxed-primitive "^1.0.2"
undici@^5.12.0: undici@^5.12.0:
version "5.15.1" version "5.16.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.15.1.tgz#5292454b1441da486a80c0f3ada1e88f1765ff8d" resolved "https://registry.yarnpkg.com/undici/-/undici-5.16.0.tgz#6b64f9b890de85489ac6332bd45ca67e4f7d9943"
integrity sha512-XLk8g0WAngdvFqTI+VKfBtM4YWXgdxkf1WezC771Es0Dd+Pm1KmNx8t93WTC+Hh9tnghmVxkclU1HN+j+CvIUA== integrity sha512-KWBOXNv6VX+oJQhchXieUznEmnJMqgXMbs0xxH2t8q/FUAWSJvOSr/rMaZKnX5RIVq7JDn0JbP4BOnKG2SGXLQ==
dependencies: dependencies:
busboy "^1.6.0" busboy "^1.6.0"