diff --git a/.eslintrc.json b/.eslintrc.json index 6ea8791e..99c7e132 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,12 +4,7 @@ "es2020": true, "node": true }, - "extends": [ - "eslint:recommended", - "plugin:prettier/recommended", - "plugin:import/recommended", - "next" - ], + "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaFeatures": { "jsx": true @@ -17,6 +12,15 @@ "ecmaVersion": 11, "sourceType": "module" }, + "extends": [ + "eslint:recommended", + "plugin:prettier/recommended", + "plugin:import/recommended", + "plugin:@typescript-eslint/recommended", + "next" + ], + + "plugins": ["@typescript-eslint", "prettier"], "settings": { "import/resolver": { "alias": { @@ -32,7 +36,7 @@ ["store", "./store"], ["styles", "./styles"] ], - "extensions": [".ts", ".js", ".jsx", ".json"] + "extensions": [".ts", ".tsx", ".js", ".jsx", ".json"] } } }, @@ -42,7 +46,10 @@ "react/react-in-jsx-scope": "off", "react/prop-types": "off", "import/no-anonymous-default-export": "off", - "@next/next/no-img-element": "off" + "@next/next/no-img-element": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off" }, "globals": { "React": "writable" diff --git a/.gitignore b/.gitignore index 54410324..84ac5e60 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules -/.pnp +node_modules +.pnp .pnp.js # testing @@ -15,15 +15,15 @@ # production /build -/public/umami.js -/public/geo +/public/script.js +/geo # misc .DS_Store .idea *.iml *.log -/.vscode/ +.vscode # debug npm-debug.log* diff --git a/LICENSE b/LICENSE index 51277655..eff41369 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Mike Cao +Copyright (c) 2022 Umami Software, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1ac76c66..19935ed5 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ DATABASE_URL=connection-url ``` The connection url is in the following format: + ``` postgresql://username:mypassword@localhost:5432/mydb @@ -48,7 +49,7 @@ mysql://username:mypassword@localhost:3306/mydb yarn build ``` -The build step will also create tables in your database if you ae installing for the first time. It will also create a login account with username **admin** and password **umami**. +The build step will also create tables in your database if you ae installing for the first time. It will also create a login user with username **admin** and password **umami**. ### Start the application @@ -69,11 +70,13 @@ docker compose up -d ``` Alternatively, to pull just the Umami Docker image with PostgreSQL support: + ```bash docker pull docker.umami.dev/umami-software/umami:postgresql-latest ``` Or with MySQL support: + ```bash docker pull docker.umami.dev/umami-software/umami:mysql-latest ``` diff --git a/app.json b/app.json index ba8b8c45..cec5fda1 100644 --- a/app.json +++ b/app.json @@ -6,7 +6,7 @@ "repository": "https://github.com/umami-software/umami", "addons": ["heroku-postgresql"], "env": { - "HASH_SALT": { + "APP_SECRET": { "description": "Used to generate unique values for your installation", "required": true, "generator": "secret" diff --git a/assets/add-user.svg b/assets/add-user.svg new file mode 100644 index 00000000..9d0544c6 --- /dev/null +++ b/assets/add-user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/arrow-right.svg b/assets/arrow-right.svg deleted file mode 100644 index 6fc93909..00000000 --- a/assets/arrow-right.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/arrow-up-right-from-square.svg b/assets/arrow-up-right-from-square.svg deleted file mode 100644 index 90ad457f..00000000 --- a/assets/arrow-up-right-from-square.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/bars.svg b/assets/bars.svg index fdb2d6e4..ba383fa4 100644 --- a/assets/bars.svg +++ b/assets/bars.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/calendar-alt.svg b/assets/calendar.svg similarity index 100% rename from assets/calendar-alt.svg rename to assets/calendar.svg diff --git a/assets/chart-bar.svg b/assets/chart-bar.svg deleted file mode 100644 index d1d72fdc..00000000 --- a/assets/chart-bar.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/check.svg b/assets/check.svg deleted file mode 100644 index 1a7abdce..00000000 --- a/assets/check.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/chevron-down.svg b/assets/chevron-down.svg deleted file mode 100644 index cb9d8fe1..00000000 --- a/assets/chevron-down.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/clock.svg b/assets/clock.svg new file mode 100644 index 00000000..9c2a9a41 --- /dev/null +++ b/assets/clock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/code.svg b/assets/code.svg deleted file mode 100644 index cd29765e..00000000 --- a/assets/code.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/dashboard.svg b/assets/dashboard.svg new file mode 100644 index 00000000..11859d28 --- /dev/null +++ b/assets/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ellipsis-h.svg b/assets/ellipsis-h.svg deleted file mode 100644 index 5bb08359..00000000 --- a/assets/ellipsis-h.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/exclamation-triangle.svg b/assets/exclamation-triangle.svg deleted file mode 100644 index 46bef5bc..00000000 --- a/assets/exclamation-triangle.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/external-link.svg b/assets/external-link.svg deleted file mode 100644 index ed09306f..00000000 --- a/assets/external-link.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/gear.svg b/assets/gear.svg index ab97a693..47805d46 100644 --- a/assets/gear.svg +++ b/assets/gear.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/list-ul.svg b/assets/list-ul.svg deleted file mode 100644 index 5e632126..00000000 --- a/assets/list-ul.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/lock.svg b/assets/lock.svg new file mode 100644 index 00000000..c13fb7c7 --- /dev/null +++ b/assets/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/logo.svg b/assets/logo.svg index f0e52261..d2c71326 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,2 +1 @@ - - + \ No newline at end of file diff --git a/assets/moon.svg b/assets/moon.svg index 6c8955ae..638286fd 100644 --- a/assets/moon.svg +++ b/assets/moon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/pen.svg b/assets/pen.svg deleted file mode 100644 index 426c520c..00000000 --- a/assets/pen.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/plus.svg b/assets/plus.svg deleted file mode 100644 index e4774d87..00000000 --- a/assets/plus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/profile.svg b/assets/profile.svg new file mode 100644 index 00000000..133b1bc1 --- /dev/null +++ b/assets/profile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/sun.svg b/assets/sun.svg index ebc20eb2..c9654776 100644 --- a/assets/sun.svg +++ b/assets/sun.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/times.svg b/assets/times.svg deleted file mode 100644 index c528bcdd..00000000 --- a/assets/times.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/trash.svg b/assets/trash.svg deleted file mode 100644 index 2f525c8f..00000000 --- a/assets/trash.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/user.svg b/assets/user.svg index c0094666..a75cbb8d 100644 --- a/assets/user.svg +++ b/assets/user.svg @@ -1 +1 @@ -Asset 1 \ No newline at end of file + \ No newline at end of file diff --git a/assets/users.svg b/assets/users.svg new file mode 100644 index 00000000..f775ea91 --- /dev/null +++ b/assets/users.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/website.svg b/assets/website.svg new file mode 100644 index 00000000..cfa9e565 --- /dev/null +++ b/assets/website.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/xmark.svg b/assets/xmark.svg deleted file mode 100644 index 340f479e..00000000 --- a/assets/xmark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/components/common/Button.js b/components/common/Button.js deleted file mode 100644 index d3b7d2ad..00000000 --- a/components/common/Button.js +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ReactTooltip from 'react-tooltip'; -import classNames from 'classnames'; -import Icon from './Icon'; -import styles from './Button.module.css'; - -function Button({ - type = 'button', - icon, - size, - variant, - children, - className, - tooltip, - tooltipId, - disabled, - iconRight, - onClick = () => {}, - ...props -}) { - return ( - - ); -} - -Button.propTypes = { - type: PropTypes.oneOf(['button', 'submit', 'reset']), - icon: PropTypes.node, - size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']), - variant: PropTypes.oneOf(['action', 'danger', 'light']), - children: PropTypes.node, - className: PropTypes.string, - tooltip: PropTypes.node, - tooltipId: PropTypes.string, - disabled: PropTypes.bool, - iconRight: PropTypes.bool, - onClick: PropTypes.func, -}; - -export default Button; diff --git a/components/common/Button.module.css b/components/common/Button.module.css deleted file mode 100644 index b911095f..00000000 --- a/components/common/Button.module.css +++ /dev/null @@ -1,102 +0,0 @@ -.button { - display: flex; - justify-content: center; - align-items: center; - font-size: var(--font-size-normal); - color: var(--gray900); - background: var(--gray100); - padding: 8px 16px; - border-radius: 4px; - border: 0; - outline: none; - cursor: pointer; - position: relative; -} - -.button:hover { - background: var(--gray200); -} - -.button:active { - color: var(--gray900); -} - -.label { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - max-width: 300px; -} - -.large { - font-size: var(--font-size-large); -} - -.small { - font-size: var(--font-size-small); -} - -.xsmall { - font-size: var(--font-size-xsmall); -} - -.action, -.action:active { - color: var(--gray50); - background: var(--gray900); -} - -.action:hover { - background: var(--gray800); -} - -.danger, -.danger:active { - color: var(--gray50); - background: var(--red500); -} - -.danger:hover { - background: var(--red400); -} - -.light, -.light:active { - color: var(--gray900); - background: transparent; -} - -.light:hover { - background: inherit; -} - -.button .icon + * { - margin-left: 10px; -} - -.button.iconRight .icon { - order: 1; - margin-left: 10px; -} - -.button.iconRight .icon + * { - margin: 0; -} - -.button:disabled { - cursor: default; - color: var(--gray500); - background: var(--gray75); -} - -.button:disabled:active { - color: var(--gray500); -} - -.button:disabled:hover { - background: var(--gray75); -} - -.button.light:disabled { - background: var(--gray50); -} diff --git a/components/common/ButtonGroup.js b/components/common/ButtonGroup.js deleted file mode 100644 index 353ce690..00000000 --- a/components/common/ButtonGroup.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import Button from './Button'; -import styles from './ButtonGroup.module.css'; - -function ButtonGroup({ items = [], selectedItem, className, size, icon, onClick = () => {} }) { - return ( -
- {items.map(item => { - const { label, value } = item; - return ( - - ); - })} -
- ); -} - -ButtonGroup.propTypes = { - items: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.node, - value: PropTypes.any.isRequired, - }), - ), - selectedItem: PropTypes.any, - className: PropTypes.string, - size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']), - icon: PropTypes.node, - onClick: PropTypes.func, -}; - -export default ButtonGroup; diff --git a/components/common/ButtonGroup.module.css b/components/common/ButtonGroup.module.css deleted file mode 100644 index bc60f8d3..00000000 --- a/components/common/ButtonGroup.module.css +++ /dev/null @@ -1,31 +0,0 @@ -.group { - display: inline-flex; - border-radius: 4px; - overflow: hidden; - border: 1px solid var(--gray500); -} - -.group .button { - border-radius: 0; - color: var(--gray800); - background: var(--gray50); - border-left: 1px solid var(--gray500); - padding: 4px 8px; -} - -.group .button:first-child { - border: 0; -} - -.group .button:hover { - background: var(--gray100); -} - -.group .button + .button { - margin: 0; -} - -.group .button.selected { - color: var(--gray900); - font-weight: 600; -} diff --git a/components/common/Calendar.js b/components/common/Calendar.js deleted file mode 100644 index bee65abf..00000000 --- a/components/common/Calendar.js +++ /dev/null @@ -1,273 +0,0 @@ -import React, { useState } from 'react'; -import classNames from 'classnames'; -import { - startOfWeek, - startOfMonth, - startOfYear, - endOfMonth, - addDays, - subDays, - addYears, - subYears, - addMonths, - setMonth, - setYear, - isSameDay, - isBefore, - isAfter, -} from 'date-fns'; -import Button from './Button'; -import useLocale from 'hooks/useLocale'; -import { dateFormat } from 'lib/date'; -import { chunk } from 'lib/array'; -import { getDateLocale } from 'lib/lang'; -import Chevron from 'assets/chevron-down.svg'; -import Cross from 'assets/times.svg'; -import styles from './Calendar.module.css'; -import Icon from './Icon'; - -export default function Calendar({ date, minDate, maxDate, onChange }) { - const { locale } = useLocale(); - const [selectMonth, setSelectMonth] = useState(false); - const [selectYear, setSelectYear] = useState(false); - - const month = dateFormat(date, 'MMMM', locale); - const year = date.getFullYear(); - - function toggleMonthSelect() { - setSelectYear(false); - setSelectMonth(state => !state); - } - - function toggleYearSelect() { - setSelectMonth(false); - setSelectYear(state => !state); - } - - function handleChange(value) { - setSelectMonth(false); - setSelectYear(false); - if (value) { - onChange(value); - } - } - - return ( -
-
-
{date.getDate()}
-
- {month} - : } size="small" /> -
-
- {year} - : } size="small" /> -
-
-
- {!selectMonth && !selectYear && ( - - )} - {selectMonth && ( - - )} - {selectYear && ( - - )} -
-
- ); -} - -const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => { - const dateLocale = getDateLocale(locale); - const weekStartsOn = dateLocale?.options?.weekStartsOn || 0; - const startWeek = startOfWeek(date, { - locale: dateLocale, - weekStartsOn, - }); - const startMonth = startOfMonth(date); - const startDay = subDays(startMonth, startMonth.getDay() - weekStartsOn); - const month = date.getMonth(); - const year = date.getFullYear(); - - const daysOfWeek = []; - for (let i = 0; i < 7; i++) { - daysOfWeek.push(addDays(startWeek, i)); - } - - const days = []; - for (let i = 0; i < 42; i++) { - days.push(addDays(startDay, i)); - } - - return ( - - - - {daysOfWeek.map((day, i) => ( - - ))} - - - - {chunk(days, 7).map((week, i) => ( - - {week.map((day, j) => { - const disabled = isBefore(day, minDate) || isAfter(day, maxDate); - return ( - - ); - })} - - ))} - -
- {dateFormat(day, 'EEE', locale)} -
onSelect(day) : null} - > - {day.getDate()} -
- ); -}; - -const MonthSelector = ({ date, minDate, maxDate, locale, onSelect }) => { - const start = startOfYear(date); - const months = []; - for (let i = 0; i < 12; i++) { - months.push(addMonths(start, i)); - } - - function handleSelect(value) { - onSelect(setMonth(date, value)); - } - - return ( - - - {chunk(months, 3).map((row, i) => ( - - {row.map((month, j) => { - const disabled = - isBefore(endOfMonth(month), minDate) || isAfter(startOfMonth(month), maxDate); - return ( - - ); - })} - - ))} - -
handleSelect(month.getMonth()) : null} - > - {dateFormat(month, 'MMMM', locale)} -
- ); -}; - -const YearSelector = ({ date, minDate, maxDate, onSelect }) => { - const [currentDate, setCurrentDate] = useState(date); - const year = date.getFullYear(); - const currentYear = currentDate.getFullYear(); - const minYear = minDate.getFullYear(); - const maxYear = maxDate.getFullYear(); - const years = []; - for (let i = 0; i < 15; i++) { - years.push(currentYear - 7 + i); - } - - function handleSelect(value) { - onSelect(setYear(date, value)); - } - - function handlePrevClick() { - setCurrentDate(state => subYears(state, 15)); - } - - function handleNextClick() { - setCurrentDate(state => addYears(state, 15)); - } - - return ( -
-
-
-
- - - {chunk(years, 5).map((row, i) => ( - - {row.map((n, j) => ( - - ))} - - ))} - -
maxYear, - })} - onClick={() => (n < minYear || n > maxYear ? null : handleSelect(n))} - > - {n} -
-
-
-
-
- ); -}; diff --git a/components/common/Calendar.module.css b/components/common/Calendar.module.css deleted file mode 100644 index 9751cf25..00000000 --- a/components/common/Calendar.module.css +++ /dev/null @@ -1,111 +0,0 @@ -.calendar { - display: flex; - flex-direction: column; - font-size: var(--font-size-small); - flex: 1; - min-height: 306px; -} - -.calendar table { - width: 100%; - border-spacing: 5px; -} - -.calendar td { - color: var(--gray800); - cursor: pointer; - text-align: center; - vertical-align: center; - height: 40px; - width: 40px; - border-radius: 5px; - border: 1px solid transparent; -} - -.calendar td:hover { - border: 1px solid var(--gray300); - background: var(--gray75); -} - -.calendar td.faded { - color: var(--gray500); -} - -.calendar td.selected { - font-weight: 600; - border: 1px solid var(--gray600); -} - -.calendar td.selected:hover { - background: transparent; -} - -.calendar td.disabled { - color: var(--gray400); - background: var(--gray75); -} - -.calendar td.disabled:hover { - cursor: default; - background: var(--gray75); - border-color: transparent; -} - -.calendar td.faded.disabled { - background: var(--gray100); -} - -.header { - display: flex; - justify-content: space-evenly; - align-items: center; - font-weight: 700; - line-height: 40px; - font-size: var(--font-size-normal); -} - -.body { - display: flex; -} - -.selector { - cursor: pointer; -} - -.pager { - display: flex; - flex: 1; -} - -.pager button { - align-self: center; -} - -.middle { - flex: 1; -} - -.left, -.right { - display: flex; - justify-content: center; - align-items: center; -} - -.left svg { - transform: rotate(90deg); -} - -.right svg { - transform: rotate(-90deg); -} - -.icon { - margin-left: 10px; -} - -@media only screen and (max-width: 992px) { - .calendar table { - max-width: calc(100vw - 30px); - } -} diff --git a/components/common/Checkbox.js b/components/common/Checkbox.js deleted file mode 100644 index 0cd0dcad..00000000 --- a/components/common/Checkbox.js +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useRef } from 'react'; -import PropTypes from 'prop-types'; -import Icon from 'components/common/Icon'; -import Check from 'assets/check.svg'; -import styles from './Checkbox.module.css'; - -function Checkbox({ name, value, label, onChange }) { - const ref = useRef(); - - const onClick = () => ref.current.click(); - - return ( -
-
- {value && } size="small" />} -
- - -
- ); -} - -Checkbox.propTypes = { - name: PropTypes.string, - value: PropTypes.any, - label: PropTypes.node, - onChange: PropTypes.func, -}; - -export default Checkbox; diff --git a/components/common/Checkbox.module.css b/components/common/Checkbox.module.css deleted file mode 100644 index c9a01eac..00000000 --- a/components/common/Checkbox.module.css +++ /dev/null @@ -1,30 +0,0 @@ -.container { - display: flex; - align-items: center; - position: relative; - overflow: hidden; -} - -.checkbox { - display: flex; - justify-content: center; - align-items: center; - width: 20px; - height: 20px; - border: 1px solid var(--gray500); - border-radius: 4px; -} - -.label { - margin-left: 10px; - user-select: none; /* disable text selection when clicking to toggle the checkbox */ -} - -.input { - position: absolute; - visibility: hidden; - height: 0; - width: 0; - bottom: 100%; - right: 100%; -} diff --git a/components/common/CopyButton.js b/components/common/CopyButton.js deleted file mode 100644 index b300ef31..00000000 --- a/components/common/CopyButton.js +++ /dev/null @@ -1,37 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import Button from './Button'; -import { FormattedMessage } from 'react-intl'; - -const defaultText = ( - -); - -function CopyButton({ element, ...props }) { - const [text, setText] = useState(defaultText); - - function handleClick() { - if (element?.current) { - element.current.select(); - document.execCommand('copy'); - setText(); - window.getSelection().removeAllRanges(); - } - } - - return ( - - ); -} - -CopyButton.propTypes = { - element: PropTypes.shape({ - current: PropTypes.shape({ - select: PropTypes.func.isRequired, - }), - }), -}; - -export default CopyButton; diff --git a/components/common/DateFilter.js b/components/common/DateFilter.js deleted file mode 100644 index d568a889..00000000 --- a/components/common/DateFilter.js +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import { endOfYear, isSameDay } from 'date-fns'; -import Modal from './Modal'; -import DropDown from './DropDown'; -import DatePickerForm from 'components/forms/DatePickerForm'; -import useLocale from 'hooks/useLocale'; -import { dateFormat } from 'lib/date'; -import Calendar from 'assets/calendar-alt.svg'; -import Icon from './Icon'; - -export const filterOptions = [ - { label: , value: '1day' }, - { - label: ( - - ), - value: '24hour', - }, - { - label: , - value: '-1day', - }, - { - label: , - value: '1week', - divider: true, - }, - { - label: ( - - ), - value: '7day', - }, - { - label: , - value: '1month', - divider: true, - }, - { - label: ( - - ), - value: '30day', - }, - { - label: ( - - ), - value: '90day', - }, - { label: , value: '1year' }, - { - label: , - value: 'all', - divider: true, - }, - { - label: , - value: 'custom', - divider: true, - }, -]; - -function DateFilter({ value, startDate, endDate, onChange, className, options }) { - const [showPicker, setShowPicker] = useState(false); - const displayValue = - value === 'custom' ? ( - handleChange('custom')} /> - ) : ( - value - ); - - async function handleChange(value) { - if (value === 'custom') { - setShowPicker(true); - return; - } - onChange(value); - } - - function handlePickerChange(value) { - setShowPicker(false); - onChange(value); - } - - return ( - <> - - {showPicker && ( - - setShowPicker(false)} - /> - - )} - - ); -} - -const CustomRange = ({ startDate, endDate, onClick }) => { - const { locale } = useLocale(); - - function handleClick(e) { - e.stopPropagation(); - - onClick(); - } - - return ( - <> - } className="mr-2" onClick={handleClick} /> - {dateFormat(startDate, 'd LLL y', locale)} - {!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`} - - ); -}; - -DateFilter.propTypes = { - value: PropTypes.string, - startDate: PropTypes.instanceOf(Date), - endDate: PropTypes.instanceOf(Date), - onChange: PropTypes.func, - className: PropTypes.string, -}; - -export default DateFilter; diff --git a/components/common/Dot.js b/components/common/Dot.js deleted file mode 100644 index 81454c48..00000000 --- a/components/common/Dot.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import styles from './Dot.module.css'; - -function Dot({ color, size, className }) { - return ( -
-
-
- ); -} - -Dot.propTypes = { - color: PropTypes.string, - size: PropTypes.oneOf(['small', 'large']), - className: PropTypes.string, -}; - -export default Dot; diff --git a/components/common/Dot.module.css b/components/common/Dot.module.css deleted file mode 100644 index 258d6e87..00000000 --- a/components/common/Dot.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.wrapper { - background: var(--gray50); - margin-right: 10px; - border-radius: 100%; -} - -.dot { - background: var(--green400); - width: 10px; - height: 10px; - border-radius: 100%; -} - -.dot.small { - width: 8px; - height: 8px; -} - -.dot.large { - width: 16px; - height: 16px; -} diff --git a/components/common/DropDown.js b/components/common/DropDown.js deleted file mode 100644 index 00d20e34..00000000 --- a/components/common/DropDown.js +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useState, useRef } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import Menu from './Menu'; -import useDocumentClick from 'hooks/useDocumentClick'; -import Chevron from 'assets/chevron-down.svg'; -import styles from './Dropdown.module.css'; -import Icon from './Icon'; - -function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) { - const [showMenu, setShowMenu] = useState(false); - const ref = useRef(); - const selectedOption = options.find(e => e.value === value); - - function handleShowMenu() { - setShowMenu(state => !state); - } - - function handleSelect(selected, e) { - e.stopPropagation(); - setShowMenu(false); - - onChange(selected); - } - - useDocumentClick(e => { - if (!ref.current?.contains(e.target)) { - setShowMenu(false); - } - }); - - return ( -
-
-
{options.find(e => e.value === value)?.label || value}
- } className={styles.icon} size="small" /> -
- {showMenu && ( - - )} -
- ); -} - -DropDown.propTypes = { - value: PropTypes.any, - className: PropTypes.string, - menuClassName: PropTypes.string, - options: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.any.isRequired, - label: PropTypes.node, - }), - ), - onChange: PropTypes.func, -}; - -export default DropDown; diff --git a/components/common/Dropdown.module.css b/components/common/Dropdown.module.css deleted file mode 100644 index 9738b007..00000000 --- a/components/common/Dropdown.module.css +++ /dev/null @@ -1,28 +0,0 @@ -.dropdown { - position: relative; - display: flex; - justify-content: space-between; - align-items: center; - border: 1px solid var(--gray500); - border-radius: 4px; - cursor: pointer; -} - -.value { - flex: 1; - display: flex; - justify-content: space-between; - font-size: var(--font-size-small); - flex-wrap: nowrap; - white-space: nowrap; - padding: 4px 16px; - min-width: 160px; -} - -.text { - flex: 1; -} - -.icon { - padding-left: 20px; -} diff --git a/components/common/EmptyPlaceholder.js b/components/common/EmptyPlaceholder.js index b5394e85..fa773a01 100644 --- a/components/common/EmptyPlaceholder.js +++ b/components/common/EmptyPlaceholder.js @@ -1,22 +1,16 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Icon from 'components/common/Icon'; +import { Icon, Text, Flexbox } from 'react-basics'; import Logo from 'assets/logo.svg'; -import styles from './EmptyPlaceholder.module.css'; -function EmptyPlaceholder({ msg, children }) { +function EmptyPlaceholder({ message, children }) { return ( -
- } size="xlarge" /> -

{msg}

- {children} -
+ + + + + {message} +
{children}
+
); } -EmptyPlaceholder.propTypes = { - msg: PropTypes.node, - children: PropTypes.node, -}; - export default EmptyPlaceholder; diff --git a/components/common/EmptyPlaceholder.module.css b/components/common/EmptyPlaceholder.module.css deleted file mode 100644 index a9231836..00000000 --- a/components/common/EmptyPlaceholder.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.placeholder { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - min-height: 600px; -} - -.icon { - margin-bottom: 30px; -} - -.msg { - margin-bottom: 15px; -} diff --git a/components/common/ErrorMessage.js b/components/common/ErrorMessage.js index 5747f226..b5d44bb4 100644 --- a/components/common/ErrorMessage.js +++ b/components/common/ErrorMessage.js @@ -1,14 +1,16 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import Icon from './Icon'; -import Exclamation from 'assets/exclamation-triangle.svg'; +import { Icon, Icons, Text } from 'react-basics'; import styles from './ErrorMessage.module.css'; +import useMessages from 'hooks/useMessages'; export default function ErrorMessage() { + const { formatMessage, messages } = useMessages(); + return (
- } className={styles.icon} size="large" /> - + + + + {formatMessage(messages.error)}
); } diff --git a/components/common/ErrorMessage.module.css b/components/common/ErrorMessage.module.css index 88769cf5..391190dc 100644 --- a/components/common/ErrorMessage.module.css +++ b/components/common/ErrorMessage.module.css @@ -5,9 +5,9 @@ transform: translate(-50%, -50%); margin: auto; display: flex; - z-index: 1; - background-color: var(--gray50); + background-color: var(--base50); padding: 10px; + z-index: 1; } .icon { diff --git a/components/common/EventDataButton.js b/components/common/EventDataButton.js deleted file mode 100644 index adf0d972..00000000 --- a/components/common/EventDataButton.js +++ /dev/null @@ -1,48 +0,0 @@ -import List from 'assets/list-ul.svg'; -import Modal from 'components/common/Modal'; -import PropTypes from 'prop-types'; -import { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import Button from './Button'; -import EventDataForm from 'components/forms/EventDataForm'; -import styles from './EventDataButton.module.css'; - -function EventDataButton({ websiteId }) { - const [showEventData, setShowEventData] = useState(false); - - function handleClick() { - if (!showEventData) { - setShowEventData(true); - } - } - - function handleClose() { - setShowEventData(false); - } - - return ( - <> - - {showEventData && ( - }> - - - )} - - ); -} - -EventDataButton.propTypes = { - websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), -}; - -export default EventDataButton; diff --git a/components/common/EventDataButton.module.css b/components/common/EventDataButton.module.css deleted file mode 100644 index cd2a2ed6..00000000 --- a/components/common/EventDataButton.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.button { - width: fit-content; -} diff --git a/components/common/Favicon.js b/components/common/Favicon.js index d72cd3c7..2127d482 100644 --- a/components/common/Favicon.js +++ b/components/common/Favicon.js @@ -1,5 +1,3 @@ -import React from 'react'; -import PropTypes from 'prop-types'; import styles from './Favicon.module.css'; function getHostName(url) { @@ -21,8 +19,4 @@ function Favicon({ domain, ...props }) { ) : null; } -Favicon.propTypes = { - domain: PropTypes.string, -}; - export default Favicon; diff --git a/components/common/FilterButtons.js b/components/common/FilterButtons.js index ea811216..4d08a132 100644 --- a/components/common/FilterButtons.js +++ b/components/common/FilterButtons.js @@ -1,25 +1,11 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ButtonLayout from 'components/layout/ButtonLayout'; -import ButtonGroup from './ButtonGroup'; +import { ButtonGroup, Button, Flexbox } from 'react-basics'; -function FilterButtons({ buttons, selected, onClick }) { +export default function FilterButtons({ items, selectedKey, onSelect }) { return ( - - - + + + {({ key, label }) => } + + ); } - -FilterButtons.propTypes = { - buttons: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.node, - value: PropTypes.any.isRequired, - }), - ), - selected: PropTypes.any, - onClick: PropTypes.func, -}; - -export default FilterButtons; diff --git a/components/common/FilterLink.js b/components/common/FilterLink.js index f16258f1..322b5499 100644 --- a/components/common/FilterLink.js +++ b/components/common/FilterLink.js @@ -1,32 +1,37 @@ -import React from 'react'; +import { Icon, Icons } from 'react-basics'; import classNames from 'classnames'; import Link from 'next/link'; import { safeDecodeURI } from 'next-basics'; import usePageQuery from 'hooks/usePageQuery'; -import External from 'assets/arrow-up-right-from-square.svg'; -import Icon from './Icon'; +import useMessages from 'hooks/useMessages'; import styles from './FilterLink.module.css'; export default function FilterLink({ id, value, label, externalUrl }) { - const { resolve, query } = usePageQuery(); + const { formatMessage, labels } = useMessages(); + const { resolveUrl, query } = usePageQuery(); const active = query[id] !== undefined; const selected = query[id] === value; return ( diff --git a/components/common/FilterLink.module.css b/components/common/FilterLink.module.css index 45b049da..ce3ffa5f 100644 --- a/components/common/FilterLink.module.css +++ b/components/common/FilterLink.module.css @@ -4,11 +4,11 @@ } .row .inactive { - color: var(--gray500); + color: var(--base500); } .row .active { - color: var(--gray900); + color: var(--base900); font-weight: 600; } diff --git a/components/common/HamburgerButton.js b/components/common/HamburgerButton.js index 501b8c95..b3e0b54f 100644 --- a/components/common/HamburgerButton.js +++ b/components/common/HamburgerButton.js @@ -1,43 +1,58 @@ -import Button from 'components/common/Button'; -import XMark from 'assets/xmark.svg'; -import Bars from 'assets/bars.svg'; +import { Button, Icon } from 'react-basics'; import { useState } from 'react'; -import styles from './HamburgerButton.module.css'; import MobileMenu from './MobileMenu'; -import { FormattedMessage } from 'react-intl'; - -const menuItems = [ - { - label: , - value: '/dashboard', - }, - { label: , value: '/realtime' }, - { label: , value: '/settings' }, - { - label: , - value: '/settings/profile', - }, - { label: , value: '/logout' }, -]; +import Icons from 'components/icons'; +import useMessages from 'hooks/useMessages'; +import useConfig from 'hooks/useConfig'; export default function HamburgerButton() { + const { formatMessage, labels } = useMessages(); const [active, setActive] = useState(false); + const { cloudMode } = useConfig(); - function handleClick() { - setActive(state => !state); - } + const menuItems = [ + { + label: formatMessage(labels.dashboard), + url: '/dashboard', + }, + { label: formatMessage(labels.realtime), url: '/realtime' }, + !cloudMode && { + label: formatMessage(labels.settings), + url: '/settings', + children: [ + { + label: formatMessage(labels.websites), + url: '/settings/websites', + }, + { + label: formatMessage(labels.teams), + url: '/settings/teams', + }, + { + label: formatMessage(labels.users), + url: '/settings/users', + }, + { + label: formatMessage(labels.profile), + url: '/settings/profile', + }, + ], + }, + cloudMode && { + label: formatMessage(labels.profile), + url: '/settings/profile', + }, + !cloudMode && { label: formatMessage(labels.logout), url: '/logout' }, + ].filter(n => n); - function handleClose() { - setActive(false); - } + const handleClick = () => setActive(state => !state); + const handleClose = () => setActive(false); return ( <> - {active && } ); diff --git a/components/common/HoverTooltip.js b/components/common/HoverTooltip.js new file mode 100644 index 00000000..3b885b1d --- /dev/null +++ b/components/common/HoverTooltip.js @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; +import { Tooltip } from 'react-basics'; +import styles from './HoverTooltip.module.css'; + +export default function HoverTooltip({ tooltip }) { + const [position, setPosition] = useState({ x: -1000, y: -1000 }); + + useEffect(() => { + const handler = e => { + setPosition({ x: e.clientX, y: e.clientY }); + }; + + document.addEventListener('mousemove', handler); + + return () => { + document.removeEventListener('mousemove', handler); + }; + }, []); + + return ( +
+ +
+ ); +} diff --git a/components/metrics/ChartTooltip.module.css b/components/common/HoverTooltip.module.css similarity index 75% rename from components/metrics/ChartTooltip.module.css rename to components/common/HoverTooltip.module.css index cd26d3af..ec1abc1c 100644 --- a/components/metrics/ChartTooltip.module.css +++ b/components/common/HoverTooltip.module.css @@ -3,9 +3,9 @@ } .tooltip { - color: var(--msgColor); + position: fixed; pointer-events: none; - z-index: 1; + z-index: var(--z-index-popup); } .content { @@ -17,7 +17,7 @@ } .title { - font-size: var(--font-size-xsmall); + font-size: var(--font-size-xs); font-weight: 600; } @@ -25,7 +25,7 @@ display: flex; justify-content: center; align-items: center; - font-size: var(--font-size-small); + font-size: var(--font-size-sm); font-weight: 600; } @@ -34,7 +34,7 @@ overflow: hidden; border-radius: 100%; margin-right: 8px; - background: var(--gray50); + background: var(--base50); } .color { diff --git a/components/common/Icon.js b/components/common/Icon.js deleted file mode 100644 index e9d96eb5..00000000 --- a/components/common/Icon.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import styles from './Icon.module.css'; - -function Icon({ icon, className, size = 'medium', ...props }) { - return ( -
- {icon} -
- ); -} - -Icon.propTypes = { - className: PropTypes.string, - icon: PropTypes.node.isRequired, - size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']), -}; - -export default Icon; diff --git a/components/common/Icon.module.css b/components/common/Icon.module.css deleted file mode 100644 index 5b431668..00000000 --- a/components/common/Icon.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.icon { - display: inline-flex; - justify-content: center; - align-items: center; - vertical-align: middle; -} - -.icon svg { - fill: currentColor; -} - -.xlarge > svg { - width: 48px; - height: 48px; -} - -.large > svg { - width: 24px; - height: 24px; -} - -.medium > svg { - width: 16px; - height: 16px; -} - -.small > svg { - width: 12px; - height: 12px; -} - -.xsmall > svg { - width: 10px; - height: 10px; -} diff --git a/components/common/Link.js b/components/common/Link.js deleted file mode 100644 index f683f5df..00000000 --- a/components/common/Link.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import NextLink from 'next/link'; -import Icon from './Icon'; -import styles from './Link.module.css'; - -function Link({ className, icon, children, size, iconRight, onClick, ...props }) { - return ( - - - {icon && } - {children} - - - ); -} - -Link.propTypes = { - className: PropTypes.string, - icon: PropTypes.node, - children: PropTypes.node, - size: PropTypes.oneOf(['large', 'small', 'xsmall']), - iconRight: PropTypes.bool, -}; - -export default Link; diff --git a/components/common/Link.module.css b/components/common/Link.module.css deleted file mode 100644 index dd2f92f9..00000000 --- a/components/common/Link.module.css +++ /dev/null @@ -1,42 +0,0 @@ -a.link, -a.link:active, -a.link:visited { - position: relative; - color: var(--gray900); - text-decoration: none; - display: inline-flex; - align-items: center; -} - -a.link span { - border-bottom: 2px solid transparent; -} - -a.link:hover span { - border-bottom: 2px solid var(--primary400); -} - -a.link.large { - font-size: var(--font-size-large); -} - -a.link.small { - font-size: var(--font-size-small); -} - -a.link.xsmall { - font-size: var(--font-size-xsmall); -} - -a.link .icon + * { - margin-left: 10px; -} - -a.link.iconRight .icon { - order: 1; - margin-left: 10px; -} - -a.link.iconRight .icon + * { - margin: 0; -} diff --git a/components/common/Loading.js b/components/common/Loading.js deleted file mode 100644 index 81c8a77e..00000000 --- a/components/common/Loading.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import styles from './Loading.module.css'; - -function Loading({ className, overlay = false }) { - return ( -
-
-
-
-
- ); -} - -Loading.propTypes = { - className: PropTypes.string, - overlay: PropTypes.bool, -}; - -export default Loading; diff --git a/components/common/Loading.module.css b/components/common/Loading.module.css deleted file mode 100644 index a69559e3..00000000 --- a/components/common/Loading.module.css +++ /dev/null @@ -1,55 +0,0 @@ -@keyframes blink { - 0% { - opacity: 0.2; - } - 20% { - opacity: 1; - } - 100% { - opacity: 0.2; - } -} - -.loading { - display: flex; - justify-content: center; - align-items: center; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - margin: 0; -} - -.loading.overlay { - height: 100%; - width: 100%; - z-index: 10; - background: var(--gray400); - opacity: 0.4; -} - -.loading div { - width: 10px; - height: 10px; - border-radius: 100%; - background: var(--gray400); - animation: blink 1.4s infinite; - animation-fill-mode: both; -} - -.loading.overlay div { - background: var(--gray900); -} - -.loading div + div { - margin-left: 10px; -} - -.loading div:nth-child(2) { - animation-delay: 0.2s; -} - -.loading div:nth-child(3) { - animation-delay: 0.4s; -} diff --git a/components/common/Menu.js b/components/common/Menu.js deleted file mode 100644 index 91eeee97..00000000 --- a/components/common/Menu.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import styles from './Menu.module.css'; - -function Menu({ - options = [], - selectedOption, - className, - float, - align = 'left', - optionClassName, - selectedClassName, - onSelect = () => {}, -}) { - return ( -
- {options - .filter(({ hidden }) => !hidden) - .map(option => { - const { label, value, className: customClassName, render, divider } = option; - - return render ? ( - render(option) - ) : ( -
onSelect(value, e)} - > - {label} -
- ); - })} -
- ); -} - -Menu.propTypes = { - options: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.node, - value: PropTypes.any, - className: PropTypes.string, - render: PropTypes.func, - divider: PropTypes.bool, - }), - ), - selectedOption: PropTypes.any, - className: PropTypes.string, - float: PropTypes.oneOf(['top', 'bottom']), - align: PropTypes.oneOf(['left', 'right']), - optionClassName: PropTypes.string, - selectedClassName: PropTypes.string, - onSelect: PropTypes.func, -}; - -export default Menu; diff --git a/components/common/Menu.module.css b/components/common/Menu.module.css deleted file mode 100644 index d2ad2cc4..00000000 --- a/components/common/Menu.module.css +++ /dev/null @@ -1,51 +0,0 @@ -.menu { - background: var(--gray50); - border: 1px solid var(--gray500); - border-radius: 4px; - overflow: hidden; - z-index: 100; -} - -.option { - font-size: var(--font-size-small); - font-weight: normal; - background: var(--gray50); - padding: 4px 16px; - cursor: pointer; - white-space: nowrap; -} - -.option:hover { - background: var(--gray100); -} - -.float { - position: absolute; - min-width: 100px; -} - -.top { - bottom: 100%; - margin-bottom: 5px; -} - -.bottom { - top: 100%; - margin-top: 5px; -} - -.left { - left: 0; -} - -.right { - right: 0; -} - -.divider { - border-top: 1px solid var(--gray300); -} - -.selected { - font-weight: 600; -} diff --git a/components/common/MenuButton.js b/components/common/MenuButton.js deleted file mode 100644 index efe150f0..00000000 --- a/components/common/MenuButton.js +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useState, useRef } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import Menu from 'components/common/Menu'; -import Button from 'components/common/Button'; -import useDocumentClick from 'hooks/useDocumentClick'; -import styles from './MenuButton.module.css'; - -function MenuButton({ - icon, - value, - options, - buttonClassName, - buttonVariant, - menuClassName, - menuPosition = 'bottom', - menuAlign = 'right', - onSelect, - renderValue, - hideLabel, -}) { - const [showMenu, setShowMenu] = useState(false); - const ref = useRef(); - const selectedOption = options.find(e => e.value === value); - - function handleSelect(value) { - onSelect(value); - setShowMenu(false); - } - - function toggleMenu() { - setShowMenu(state => !state); - } - - useDocumentClick(e => { - if (!ref.current?.contains(e.target)) { - setShowMenu(false); - } - }); - - return ( -
- - {showMenu && ( - - )} -
- ); -} - -MenuButton.propTypes = { - icon: PropTypes.node, - value: PropTypes.any, - options: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.node, - value: PropTypes.any, - className: PropTypes.string, - render: PropTypes.func, - divider: PropTypes.bool, - }), - ), - buttonClassName: PropTypes.string, - menuClassName: PropTypes.string, - menuPosition: PropTypes.oneOf(['top', 'bottom']), - menuAlign: PropTypes.oneOf(['left', 'right']), - onSelect: PropTypes.func, - renderValue: PropTypes.func, -}; - -export default MenuButton; diff --git a/components/common/MenuButton.module.css b/components/common/MenuButton.module.css deleted file mode 100644 index 7e9dd7e1..00000000 --- a/components/common/MenuButton.module.css +++ /dev/null @@ -1,20 +0,0 @@ -.container { - display: flex; - position: relative; - cursor: pointer; -} - -.button { - border: 1px solid transparent; - border-radius: 4px; -} - -.text { - font-size: var(--font-size-small); -} - -.open, -.open:hover { - background: var(--gray50); - border: 1px solid var(--gray500); -} diff --git a/components/common/MobileMenu.js b/components/common/MobileMenu.js index 1241b1e6..44c8da84 100644 --- a/components/common/MobileMenu.js +++ b/components/common/MobileMenu.js @@ -1,22 +1,36 @@ import classNames from 'classnames'; -import Link from './Link'; -import Button from './Button'; -import XMark from 'assets/xmark.svg'; +import { useRouter } from 'next/router'; +import Link from 'next/link'; import styles from './MobileMenu.module.css'; export default function MobileMenu({ items = [], onClose }) { + const { pathname } = useRouter(); + + const Items = ({ items, className }) => ( +
+ {items.map(({ label, url, children }) => { + const selected = pathname.startsWith(url); + + return ( + <> + + {label} + + {children && } + + ); + })} +
+ ); + return ( -
-
-
-
- {items.map(({ label, value }) => ( - - {label} - - ))} -
+
+
); } diff --git a/components/common/MobileMenu.module.css b/components/common/MobileMenu.module.css index 78a440d9..cfe6cf37 100644 --- a/components/common/MobileMenu.module.css +++ b/components/common/MobileMenu.module.css @@ -1,41 +1,39 @@ .menu { position: fixed; - top: 0; + top: 60px; left: 0; right: 0; bottom: 0; margin: auto; - z-index: 100; display: flex; flex-direction: column; - background-color: var(--gray50); + background-color: var(--base50); + z-index: var(--z-index-popup); overflow: auto; } .items { - flex: 1; display: flex; flex-direction: column; - justify-content: center; - align-items: center; } .item { - font-size: var(--font-size-large); + font-size: var(--font-size-lg); + font-weight: 700; + line-height: 80px; + padding: 0 40px; } -.item + .item { - margin-top: 20px; +a.item { + color: var(--base600); } -.item:last-child { - margin-top: 60px; +a.item.selected, +.submenu a.item.selected { + color: var(--base900); } -.header { - display: flex; - justify-content: flex-end; - align-items: center; - height: 100px; - padding: 0 30px; +.submenu a.item { + color: var(--base600); + margin-left: 40px; } diff --git a/components/common/Modal.js b/components/common/Modal.js deleted file mode 100644 index 694fba60..00000000 --- a/components/common/Modal.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; -import { useSpring, animated } from 'react-spring'; -import styles from './Modal.module.css'; - -function Modal({ title, children }) { - const props = useSpring({ opacity: 1, from: { opacity: 0 } }); - - return ReactDOM.createPortal( - -
- {title &&
{title}
} -
{children}
-
-
, - document.getElementById('__modals'), - ); -} - -Modal.propTypes = { - title: PropTypes.node, - children: PropTypes.node, -}; - -export default Modal; diff --git a/components/common/Modal.module.css b/components/common/Modal.module.css deleted file mode 100644 index bf2491c7..00000000 --- a/components/common/Modal.module.css +++ /dev/null @@ -1,46 +0,0 @@ -.modal { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: auto; - z-index: 2; -} - -.modal:before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: auto; - background: #000; - opacity: 0.5; -} - -.content { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: var(--gray50); - min-width: 400px; - min-height: 100px; - max-width: 100vw; - z-index: 1; - border: 1px solid var(--gray300); - padding: 30px; - border-radius: 4px; -} - -.header { - font-weight: 600; - margin-bottom: 20px; -} - -.body { - display: flex; - flex-direction: column; -} diff --git a/components/common/NavMenu.js b/components/common/NavMenu.js deleted file mode 100644 index 82d97fff..00000000 --- a/components/common/NavMenu.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { useRouter } from 'next/router'; -import classNames from 'classnames'; -import styles from './NavMenu.module.css'; - -function NavMenu({ options = [], className, onSelect = () => {} }) { - const router = useRouter(); - - return ( -
- {options - .filter(({ hidden }) => !hidden) - .map(option => { - const { label, value, className: customClassName, render } = option; - - return render ? ( - render(option) - ) : ( -
onSelect(value, e)} - > - {label} -
- ); - })} -
- ); -} - -NavMenu.propTypes = { - options: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.node, - value: PropTypes.any, - className: PropTypes.string, - render: PropTypes.func, - }), - ), - className: PropTypes.string, - onSelect: PropTypes.func, -}; -export default NavMenu; diff --git a/components/common/NavMenu.module.css b/components/common/NavMenu.module.css deleted file mode 100644 index 7be73973..00000000 --- a/components/common/NavMenu.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.menu { - color: var(--gray800); - border: 1px solid var(--gray500); - border-radius: 4px; - overflow: hidden; - z-index: 2; -} - -.option { - padding: 8px 16px; - cursor: pointer; - border-radius: 4px; -} - -.option:hover { - background: var(--gray75); -} - -.selected { - color: var(--gray900); - font-weight: 600; -} diff --git a/components/common/NoData.js b/components/common/NoData.js index 9d523437..e278b6f3 100644 --- a/components/common/NoData.js +++ b/components/common/NoData.js @@ -1,19 +1,15 @@ -import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { FormattedMessage } from 'react-intl'; import styles from './NoData.module.css'; +import useMessages from 'hooks/useMessages'; function NoData({ className }) { + const { formatMessage, messages } = useMessages(); + return (
- + {formatMessage(messages.noDataAvailable)}
); } -NoData.propTypes = { - className: PropTypes.string, -}; - export default NoData; diff --git a/components/common/NoData.module.css b/components/common/NoData.module.css index 518fa488..29e9f8eb 100644 --- a/components/common/NoData.module.css +++ b/components/common/NoData.module.css @@ -1,6 +1,6 @@ .container { - color: var(--gray500); - font-size: var(--font-size-normal); + color: var(--base500); + font-size: var(--font-size-md); position: relative; display: flex; align-items: center; diff --git a/components/common/OverflowText.js b/components/common/OverflowText.js deleted file mode 100644 index d67c4923..00000000 --- a/components/common/OverflowText.js +++ /dev/null @@ -1,66 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import ReactTooltip from 'react-tooltip'; - -import styles from './OverflowText.module.css'; - -const OverflowText = ({ children, tooltipId }) => { - const measureEl = useRef(); - const [isOverflown, setIsOverflown] = useState(false); - - const measure = useCallback( - el => { - if (!el) return; - setIsOverflown(el.scrollWidth > el.clientWidth); - }, - [setIsOverflown], - ); - - // Do one measure on mount - useEffect(() => { - measure(measureEl.current); - }, [measure]); - - // Set up resize listener for subsequent measures - useEffect(() => { - if (!measureEl.current) return; - - // Destructure ref in case it changes out from under us - const el = measureEl.current; - - if ('ResizeObserver' in global) { - // Ideally, we have access to ResizeObservers - const observer = new ResizeObserver(() => { - measure(el); - }); - observer.observe(el); - return () => observer.unobserve(el); - } else { - // Otherwise, fall back to measuring on window resizes - const handler = () => measure(el); - - window.addEventListener('resize', handler, { passive: true }); - return () => window.removeEventListener('resize', handler, { passive: true }); - } - }); - - return ( - - {children} - {isOverflown && {children}} - - ); -}; - -OverflowText.propTypes = { - children: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - tooltipId: PropTypes.string.isRequired, -}; - -export default OverflowText; diff --git a/components/common/OverflowText.module.css b/components/common/OverflowText.module.css deleted file mode 100644 index c2066631..00000000 --- a/components/common/OverflowText.module.css +++ /dev/null @@ -1,6 +0,0 @@ -.root { - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} diff --git a/components/common/RefreshButton.js b/components/common/RefreshButton.js deleted file mode 100644 index be1a8a1d..00000000 --- a/components/common/RefreshButton.js +++ /dev/null @@ -1,47 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import useStore from 'store/queries'; -import { setDateRange } from 'store/websites'; -import Button from './Button'; -import Refresh from 'assets/redo.svg'; -import Dots from 'assets/ellipsis-h.svg'; -import useDateRange from 'hooks/useDateRange'; - -function RefreshButton({ websiteId }) { - const [dateRange] = useDateRange(websiteId); - const [loading, setLoading] = useState(false); - const selector = useCallback(state => state[`/websites/${websiteId}/stats`], [websiteId]); - const completed = useStore(selector); - - function handleClick() { - if (!loading && dateRange) { - setLoading(true); - if (/^[\d]+/.test(dateRange.value)) { - setDateRange(websiteId, dateRange.value); - } else { - setDateRange(websiteId, dateRange); - } - } - } - - useEffect(() => { - setLoading(false); - }, [completed]); - - return ( - - - -
+ + + ); } diff --git a/components/common/UpdateNotice.module.css b/components/common/UpdateNotice.module.css index 52bac611..f5e29445 100644 --- a/components/common/UpdateNotice.module.css +++ b/components/common/UpdateNotice.module.css @@ -1,18 +1,33 @@ .notice { - display: flex; - justify-content: center; - align-items: center; - padding-top: 20px; + position: absolute; + max-width: 800px; + gap: 20px; + margin: 20px auto; + justify-self: center; + background: #fff; + padding: 20px; + border: 1px solid var(--base300); + border-radius: var(--border-radius); + z-index: var(--z-index-popup); } .message { - font-size: var(--font-size-small); + display: flex; + justify-content: center; + align-items: center; font-weight: 600; - flex: 1; - text-align: center; - margin-right: 20px; } .buttons { + display: flex; + flex-direction: row; + justify-content: flex-end; + gap: 10px; flex: 0; } + +@media only screen and (max-width: 992px) { + .message { + height: 80px; + } +} diff --git a/components/common/WorldMap.js b/components/common/WorldMap.js index 0f9a1d6a..c6aa91ab 100644 --- a/components/common/WorldMap.js +++ b/components/common/WorldMap.js @@ -1,7 +1,5 @@ -import React, { useState, useMemo } from 'react'; +import { useState, useMemo } from 'react'; import { useRouter } from 'next/router'; -import PropTypes from 'prop-types'; -import ReactTooltip from 'react-tooltip'; import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps'; import classNames from 'classnames'; import { colord } from 'colord'; @@ -10,6 +8,9 @@ import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants'; import styles from './WorldMap.module.css'; import useCountryNames from 'hooks/useCountryNames'; import useLocale from 'hooks/useLocale'; +import HoverTooltip from './HoverTooltip'; +import { formatLongNumber } from 'lib/format'; +import { percentFilter } from 'lib/filters'; function WorldMap({ data, className }) { const { basePath } = useRouter(); @@ -26,10 +27,11 @@ function WorldMap({ data, className }) { ); const { locale } = useLocale(); const countryNames = useCountryNames(locale); + const metrics = useMemo(() => (data ? percentFilter(data) : []), [data]); function getFillColor(code) { if (code === 'AQ') return; - const country = data?.find(({ x }) => x === code); + const country = metrics?.find(({ x }) => x === code); if (!country) { return colors.fillColor; @@ -46,8 +48,8 @@ function WorldMap({ data, className }) { function handleHover(code) { if (code === 'AQ') return; - const country = data?.find(({ x }) => x === code); - setTooltip(`${countryNames[code]}: ${country?.y || 0} visitors`); + const country = metrics?.find(({ x }) => x === code); + setTooltip(`${countryNames[code]}: ${formatLongNumber(country?.y || 0)} visitors`); } return ( @@ -84,20 +86,9 @@ function WorldMap({ data, className }) { - {tooltip} + {tooltip && }
); } -WorldMap.propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - x: PropTypes.string, - y: PropTypes.number, - z: PropTypes.number, - }), - ), - className: PropTypes.string, -}; - export default WorldMap; diff --git a/components/declarations.d.ts b/components/declarations.d.ts new file mode 100644 index 00000000..31e44ff3 --- /dev/null +++ b/components/declarations.d.ts @@ -0,0 +1,2 @@ +declare module '*.css'; +declare module '*.svg'; diff --git a/components/forms/AccountEditForm.js b/components/forms/AccountEditForm.js deleted file mode 100644 index 70125656..00000000 --- a/components/forms/AccountEditForm.js +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Formik, Form, Field } from 'formik'; -import Button from 'components/common/Button'; -import FormLayout, { - FormButtons, - FormError, - FormMessage, - FormRow, -} from 'components/layout/FormLayout'; -import useApi from 'hooks/useApi'; - -const initialValues = { - username: '', - password: '', -}; - -const validate = ({ id, username, password }) => { - const errors = {}; - - if (!username) { - errors.username = ; - } - if (!id && !password) { - errors.password = ; - } - - return errors; -}; - -export default function AccountEditForm({ values, onSave, onClose }) { - const { post } = useApi(); - const [message, setMessage] = useState(); - - const handleSubmit = async values => { - const { id } = values; - const { ok, data } = await post(id ? `/accounts/${id}` : '/accounts', values); - - if (ok) { - onSave(); - } else { - setMessage( - data || , - ); - } - }; - - return ( - - - {() => ( -
- - -
- - -
-
- - -
- - -
-
- - - - - {message} -
- )} -
-
- ); -} diff --git a/components/forms/ChangePasswordForm.js b/components/forms/ChangePasswordForm.js deleted file mode 100644 index dcad6f17..00000000 --- a/components/forms/ChangePasswordForm.js +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Formik, Form, Field } from 'formik'; -import Button from 'components/common/Button'; -import FormLayout, { - FormButtons, - FormError, - FormMessage, - FormRow, -} from 'components/layout/FormLayout'; -import useApi from 'hooks/useApi'; -import useUser from 'hooks/useUser'; - -const initialValues = { - current_password: '', - new_password: '', - confirm_password: '', -}; - -const validate = ({ current_password, new_password, confirm_password }) => { - const errors = {}; - - if (!current_password) { - errors.current_password = ; - } - if (!new_password) { - errors.new_password = ; - } - if (!confirm_password) { - errors.confirm_password = ; - } else if (new_password !== confirm_password) { - errors.confirm_password = ( - - ); - } - - return errors; -}; - -export default function ChangePasswordForm({ values, onSave, onClose }) { - const { post } = useApi(); - const [message, setMessage] = useState(); - const { user } = useUser(); - - const handleSubmit = async values => { - const { ok, error } = await post(`/accounts/${user.accountUuid}/password`, values); - - if (ok) { - onSave(); - } else { - setMessage( - error || , - ); - } - }; - - return ( - - - {() => ( -
- - -
- - -
-
- - -
- - -
-
- - -
- - -
-
- - - - - {message} -
- )} -
-
- ); -} diff --git a/components/forms/DeleteForm.js b/components/forms/DeleteForm.js deleted file mode 100644 index 09da94d6..00000000 --- a/components/forms/DeleteForm.js +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Formik, Form, Field } from 'formik'; -import Button from 'components/common/Button'; -import FormLayout, { - FormButtons, - FormError, - FormMessage, - FormRow, -} from 'components/layout/FormLayout'; -import Loading from 'components/common/Loading'; -import useApi from 'hooks/useApi'; - -const CONFIRMATION_WORD = 'DELETE'; - -const validate = ({ confirmation }) => { - const errors = {}; - - if (confirmation !== CONFIRMATION_WORD) { - errors.confirmation = !confirmation ? ( - - ) : ( - - ); - } - - return errors; -}; - -export default function DeleteForm({ values, onSave, onClose }) { - const { del } = useApi(); - const [message, setMessage] = useState(); - const [deleting, setDeleting] = useState(false); - - const handleSubmit = async ({ type, id }) => { - setDeleting(true); - - const { ok, data } = await del(`/${type}/${id}`); - - if (ok) { - onSave(); - } else { - setMessage( - data || , - ); - - setDeleting(false); - } - }; - - return ( - - {deleting && } - - {props => ( -
-
- {values.name} }} - /> -
-
- -
-

- {CONFIRMATION_WORD} }} - /> -

- -
- - -
-
- - - - - {message} -
- )} -
-
- ); -} diff --git a/components/forms/EventDataForm.js b/components/forms/EventDataForm.js deleted file mode 100644 index 5f31df44..00000000 --- a/components/forms/EventDataForm.js +++ /dev/null @@ -1,262 +0,0 @@ -import classNames from 'classnames'; -import Button from 'components/common/Button'; -import DateFilter from 'components/common/DateFilter'; -import DropDown from 'components/common/DropDown'; -import FormLayout, { - FormButtons, - FormError, - FormMessage, - FormRow, -} from 'components/layout/FormLayout'; -import DataTable from 'components/metrics/DataTable'; -import FilterTags from 'components/metrics/FilterTags'; -import { Field, Form, Formik } from 'formik'; -import useApi from 'hooks/useApi'; -import useDateRange from 'hooks/useDateRange'; -import { useState, useEffect } from 'react'; -import { FormattedMessage } from 'react-intl'; -import styles from './EventDataForm.module.css'; -import useTimezone from 'hooks/useTimezone'; - -export const filterOptions = [ - { label: 'Count', value: 'count' }, - { label: 'Average', value: 'avg' }, - { label: 'Minimum', value: 'min' }, - { label: 'Maximum', value: 'max' }, - { label: 'Sum', value: 'sum' }, -]; - -export const dateOptions = [ - { label: , value: '1day' }, - { - label: ( - - ), - value: '24hour', - }, - { - label: , - value: '-1day', - }, - { - label: , - value: '1week', - divider: true, - }, - { - label: ( - - ), - value: '7day', - }, - { - label: , - value: '1month', - divider: true, - }, - { - label: ( - - ), - value: '30day', - }, - { - label: ( - - ), - value: '90day', - }, - { label: , value: '1year' }, - { - label: , - value: 'custom', - divider: true, - }, -]; - -export default function EventDataForm({ websiteId, onClose, className }) { - const { post } = useApi(); - const [message, setMessage] = useState(); - const [columns, setColumns] = useState({}); - const [filters, setFilters] = useState({}); - const [data, setData] = useState([]); - const [dateRange, setDateRange] = useDateRange('report'); - const { startDate, endDate, value } = dateRange; - const [timezone] = useTimezone(); - const [isValid, setIsValid] = useState(false); - - useEffect(() => { - if (Object.keys(columns).length > 0) { - setIsValid(true); - } else { - setIsValid(false); - } - }, [columns]); - - const handleAddTag = (value, list, setState, resetForm) => { - setState({ ...list, [`${value.field}`]: value.value }); - resetForm(); - }; - - const handleRemoveTag = (value, list, setState) => { - const newList = { ...list }; - - delete newList[`${value}`]; - - setState(newList); - }; - - const handleSubmit = async () => { - const params = { - website_id: websiteId, - start_at: +startDate, - end_at: +endDate, - timezone, - columns, - filters, - }; - - const { ok, data } = await post(`/websites/${websiteId}/eventdata`, params); - - if (!ok) { - setMessage(); - setData([]); - } else { - setData(data); - setMessage(null); - } - }; - - return ( - <> - {message} -
-
- -
- - - - -
-
- - handleAddTag(value, columns, setColumns, resetForm) - } - > - {({ values, setFieldValue }) => ( -
- - -
- - -
-
- - -
- setFieldValue('value', value)} - className={styles.dropdown} - name="value" - options={filterOptions} - /> - -
-
- - - -
- )} -
- handleRemoveTag(value, columns, setColumns)} - /> -
-
- - handleAddTag(value, filters, setFilters, resetForm) - } - > - {({ values }) => ( -
- - -
- - -
-
- - -
- - -
-
- - - -
- )} -
- handleRemoveTag(value, filters, setFilters)} - /> -
-
-
-
- } /> -
-
- - - - - - ); -} diff --git a/components/forms/EventDataForm.module.css b/components/forms/EventDataForm.module.css deleted file mode 100644 index 19d76f77..00000000 --- a/components/forms/EventDataForm.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.container { - display: flex; -} - -.form { - border-right: 1px solid var(--gray300); - width: 420px; -} - -.filters { - padding: 10px 5px; -} - -.filters + .filters { - border-top: 1px solid var(--gray300); - min-height: 250px; -} - -.table { - padding: 10px; - min-height: 430px; - min-width: 400px; -} - -.formButtons { - justify-content: flex-start; - margin-left: 20px; -} - -.dropdown { - min-height: 39px; - min-width: 240px; -} - -.filterTag { - flex-wrap: wrap; - margin: 10px 5px 5px 5px; -} diff --git a/components/forms/LoginForm.js b/components/forms/LoginForm.js deleted file mode 100644 index 8c8aa09e..00000000 --- a/components/forms/LoginForm.js +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Formik, Form, Field } from 'formik'; -import { setItem } from 'next-basics'; -import { useRouter } from 'next/router'; -import Button from 'components/common/Button'; -import FormLayout, { - FormButtons, - FormError, - FormMessage, - FormRow, -} from 'components/layout/FormLayout'; -import Icon from 'components/common/Icon'; -import useApi from 'hooks/useApi'; -import { AUTH_TOKEN } from 'lib/constants'; -import { setUser } from 'store/app'; -import Logo from 'assets/logo.svg'; -import styles from './LoginForm.module.css'; - -const validate = ({ username, password }) => { - const errors = {}; - - if (!username) { - errors.username = ; - } - if (!password) { - errors.password = ; - } - - return errors; -}; - -export default function LoginForm() { - const { post } = useApi(); - const router = useRouter(); - const [message, setMessage] = useState(); - - const handleSubmit = async ({ username, password }) => { - const { ok, status, data } = await post('/auth/login', { - username, - password, - }); - - if (ok) { - setItem(AUTH_TOKEN, data.token); - - setUser(data.user); - - await router.push('/'); - - return null; - } else { - setMessage( - status === 401 ? ( - - ) : ( - data - ), - ); - } - }; - - return ( - - - {() => ( -
-
- } size="xlarge" className={styles.icon} /> -

umami

-
- - -
- - -
-
- - -
- - -
-
- - - - {message} -
- )} -
-
- ); -} diff --git a/components/forms/LoginForm.module.css b/components/forms/LoginForm.module.css deleted file mode 100644 index dfd5456a..00000000 --- a/components/forms/LoginForm.module.css +++ /dev/null @@ -1,23 +0,0 @@ -.login { - display: flex; - flex-direction: column; - margin-top: 80px; -} - -.login form { - margin: 0 auto; -} - -.icon { - display: flex; - justify-content: center; - margin: 0 auto; -} - -.header { - margin-bottom: 30px; -} - -.header h1 { - margin: 12px 0; -} diff --git a/components/forms/ResetForm.js b/components/forms/ResetForm.js deleted file mode 100644 index 924aa7b1..00000000 --- a/components/forms/ResetForm.js +++ /dev/null @@ -1,98 +0,0 @@ -import React, { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Formik, Form, Field } from 'formik'; -import Button from 'components/common/Button'; -import FormLayout, { - FormButtons, - FormError, - FormMessage, - FormRow, -} from 'components/layout/FormLayout'; -import useApi from 'hooks/useApi'; - -const CONFIRMATION_WORD = 'RESET'; - -const validate = ({ confirmation }) => { - const errors = {}; - - if (confirmation !== CONFIRMATION_WORD) { - errors.confirmation = !confirmation ? ( - - ) : ( - - ); - } - - return errors; -}; - -export default function ResetForm({ values, onSave, onClose }) { - const { post } = useApi(); - const [message, setMessage] = useState(); - - const handleSubmit = async ({ type, id }) => { - const { ok, data } = await post(`/${type}/${id}/reset`); - - if (ok) { - onSave(); - } else { - setMessage( - data || , - ); - } - }; - - return ( - - - {props => ( -
-
- {values.name} }} - /> -
-
- -
-

- {CONFIRMATION_WORD} }} - /> -

- -
- - -
-
- - - - - {message} -
- )} -
-
- ); -} diff --git a/components/forms/ShareUrlForm.js b/components/forms/ShareUrlForm.js deleted file mode 100644 index 9800e54b..00000000 --- a/components/forms/ShareUrlForm.js +++ /dev/null @@ -1,42 +0,0 @@ -import React, { useRef } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { useRouter } from 'next/router'; -import Button from 'components/common/Button'; -import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout'; -import CopyButton from 'components/common/CopyButton'; - -export default function TrackingCodeForm({ values, onClose }) { - const ref = useRef(); - const { basePath } = useRouter(); - const { name, shareId } = values; - - return ( - -

- {values.name} }} - /> -

- -