More refactoring, cleaned up icons, nav buttons, add messages.
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
22
components/buttons/LogoutButton.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -51,3 +51,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.minimized .buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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({
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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't have any API keys.</Text>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -14,6 +14,7 @@ export default function PasswordEditForm({ onSave, onClose }) {
|
|||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
onSave();
|
onSave();
|
||||||
|
onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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)}>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ export default function UserAddForm({ onSave, onClose }) {
|
|||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
onSave(data);
|
onSave(data);
|
||||||
|
onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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}>
|
||||||
|
@ -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}>
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
@ -25,6 +25,7 @@ export default function WebsiteAddForm({ onSave, onClose }) {
|
|||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
onSave();
|
onSave();
|
||||||
|
onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -22,6 +22,7 @@ export default function WebsiteDeleteForm({ websiteId, onSave, onClose }) {
|
|||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
onSave();
|
onSave();
|
||||||
|
onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ export default function WebsiteResetForm({ websiteId, onSave, onClose }) {
|
|||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
onSave();
|
onSave();
|
||||||
|
onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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) =>
|
||||||
|
@ -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
@ -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"
|
||||||
|
|
||||||
|