diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03d..fd36f949 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js index cc3cde7c..cf7dce7f 100644 --- a/next.config.js +++ b/next.config.js @@ -6,7 +6,7 @@ const pkg = require('./package.json'); const contentSecurityPolicy = ` default-src 'self'; img-src *; - script-src 'self' 'unsafe-eval'; + script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' api.umami.is; frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS}; @@ -74,16 +74,23 @@ if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN }); } +const basePath = process.env.BASE_PATH; + +/** @type {import('next').NextConfig} */ const config = { + reactStrictMode: false, env: { - cloudMode: process.env.CLOUD_MODE, + basePath: basePath || '', + cloudMode: !!process.env.CLOUD_MODE, cloudUrl: process.env.CLOUD_URL, configUrl: '/config', currentVersion: pkg.version, defaultLocale: process.env.DEFAULT_LOCALE, + disableLogin: process.env.DISABLE_LOGIN, + disableUI: process.env.DISABLE_UI, isProduction: process.env.NODE_ENV === 'production', }, - basePath: process.env.BASE_PATH, + basePath, output: 'standalone', eslint: { ignoreDuringBuilds: true, @@ -92,11 +99,23 @@ const config = { ignoreBuildErrors: true, }, webpack(config) { - config.module.rules.push({ - test: /\.svg$/, - issuer: /\.{js|jsx|ts|tsx}$/, - use: ['@svgr/webpack'], - }); + const fileLoaderRule = config.module.rules.find(rule => rule.test?.test?.('.svg')); + + config.module.rules.push( + { + ...fileLoaderRule, + test: /\.svg$/i, + resourceQuery: /url/, + }, + { + test: /\.svg$/i, + issuer: fileLoaderRule.issuer, + resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, + use: ['@svgr/webpack'], + }, + ); + + fileLoaderRule.exclude = /\.svg$/i; config.resolve.alias['public'] = path.resolve('./public'); diff --git a/package.json b/package.json index 6fc413d4..5b2ef173 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,9 @@ "@clickhouse/client": "^0.2.2", "@fontsource/inter": "^4.5.15", "@prisma/client": "5.3.1", + "@react-spring/web": "^9.7.3", "@tanstack/react-query": "^4.33.0", - "@umami/prisma-client": "^0.2.0", + "@umami/prisma-client": "^0.3.0", "@umami/redis-client": "^0.15.0", "chalk": "^4.1.1", "chart.js": "^4.2.1", @@ -91,7 +92,7 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "13.5.2", + "next": "13.5.3", "next-basics": "^0.36.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", @@ -100,9 +101,8 @@ "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", - "react-intl": "^5.24.7", + "react-intl": "^6.4.7", "react-simple-maps": "^2.3.0", - "react-spring": "^9.4.4", "react-use-measure": "^2.0.4", "react-window": "^1.8.6", "request-ip": "^3.3.0", @@ -123,12 +123,12 @@ "@rollup/plugin-node-resolve": "^15.2.0", "@rollup/plugin-replace": "^5.0.2", "@svgr/rollup": "^8.1.0", - "@svgr/webpack": "^6.2.1", + "@svgr/webpack": "^8.1.0", "@types/node": "^18.11.9", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.8", - "@typescript-eslint/eslint-plugin": "^5.50.0", - "@typescript-eslint/parser": "^5.50.0", + "@typescript-eslint/eslint-plugin": "^6.7.3", + "@typescript-eslint/parser": "^6.7.3", "cross-env": "^7.0.3", "esbuild": "^0.17.17", "eslint": "^8.33.0", @@ -138,8 +138,8 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.0.0", "extract-react-intl-messages": "^4.1.1", - "husky": "^7.0.0", - "lint-staged": "^11.0.0", + "husky": "^8.0.3", + "lint-staged": "^14.0.1", "postcss": "^8.4.21", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^15.1.0", diff --git a/src/app/(app)/NavBar.js b/src/app/(app)/NavBar.js new file mode 100644 index 00000000..211adf5f --- /dev/null +++ b/src/app/(app)/NavBar.js @@ -0,0 +1,58 @@ +'use client'; +import { Icon, Text } from 'react-basics'; +import Link from 'next/link'; +import classNames from 'classnames'; +import Icons from 'components/icons'; +import ThemeButton from 'components/input/ThemeButton'; +import LanguageButton from 'components/input/LanguageButton'; +import ProfileButton from 'components/input/ProfileButton'; +import useMessages from 'components/hooks/useMessages'; +import HamburgerButton from 'components/common/HamburgerButton'; +import { usePathname } from 'next/navigation'; +import styles from './NavBar.module.css'; + +export function NavBar() { + const pathname = usePathname(); + const { formatMessage, labels } = useMessages(); + + const links = [ + { label: formatMessage(labels.dashboard), url: '/dashboard' }, + { label: formatMessage(labels.websites), url: '/websites' }, + { label: formatMessage(labels.reports), url: '/reports' }, + { label: formatMessage(labels.settings), url: '/settings' }, + ].filter(n => n); + + return ( +
+
+ + + + umami +
+
+ {links.map(({ url, label }) => { + return ( + + {label} + + ); + })} +
+
+ + + +
+
+ +
+
+ ); +} + +export default NavBar; diff --git a/src/components/layout/NavBar.module.css b/src/app/(app)/NavBar.module.css similarity index 75% rename from src/components/layout/NavBar.module.css rename to src/app/(app)/NavBar.module.css index dd5085a0..fd022eca 100644 --- a/src/components/layout/NavBar.module.css +++ b/src/app/(app)/NavBar.module.css @@ -1,7 +1,7 @@ .navbar { + display: grid; + grid-template-columns: max-content 1fr 1fr; position: relative; - display: flex; - flex-direction: row; align-items: center; height: 60px; background: var(--base75); @@ -9,17 +9,6 @@ padding: 0 20px; } -.left, -.right { - display: flex; - flex-direction: row; - align-items: center; -} - -.right { - justify-content: flex-end; -} - .logo { display: flex; flex-direction: row; @@ -35,29 +24,24 @@ flex-direction: row; gap: 30px; padding: 0 40px; - flex: 1; font-weight: 700; + max-height: 60px; } -.links a { - display: flex; - align-items: center; - gap: 10px; - line-height: 60px; +.links a, +.links a:active, +.links a:visited { color: var(--font-color200); + line-height: 60px; border-bottom: 2px solid transparent; } -.links span { - white-space: nowrap; -} - .links a:hover { color: var(--font-color100); border-bottom: 2px solid var(--primary400); } -.links .selected { +.links a.selected { color: var(--font-color100); border-bottom: 2px solid var(--primary400); } @@ -68,7 +52,6 @@ flex-direction: row; align-items: center; justify-content: flex-end; - min-width: 0; } .mobile { @@ -76,6 +59,10 @@ } @media only screen and (max-width: 768px) { + .navbar { + grid-template-columns: repeat(2, 1fr); + } + .links, .actions { display: none; diff --git a/src/app/(app)/Shell.tsx b/src/app/(app)/Shell.tsx new file mode 100644 index 00000000..980abb62 --- /dev/null +++ b/src/app/(app)/Shell.tsx @@ -0,0 +1,27 @@ +'use client'; +import Script from 'next/script'; +import { usePathname } from 'next/navigation'; +import UpdateNotice from 'components/common/UpdateNotice'; +import { useRequireLogin, useConfig } from 'components/hooks'; + +export function Shell({ children }) { + const { user } = useRequireLogin(); + const config = useConfig(); + const pathname = usePathname(); + + if (!user || !config) { + return null; + } + + return ( + <> + {children} + + {process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && ( + `; diff --git a/src/components/pages/settings/websites/WebsiteData.js b/src/app/(app)/settings/websites/[id]/WebsiteData.js similarity index 89% rename from src/components/pages/settings/websites/WebsiteData.js rename to src/app/(app)/settings/websites/[id]/WebsiteData.js index 08d6702e..07dc9257 100644 --- a/src/components/pages/settings/websites/WebsiteData.js +++ b/src/app/(app)/settings/websites/[id]/WebsiteData.js @@ -1,6 +1,6 @@ import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics'; -import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm'; -import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm'; +import WebsiteDeleteForm from './WebsiteDeleteForm'; +import WebsiteResetForm from './WebsiteResetForm'; import useMessages from 'components/hooks/useMessages'; export function WebsiteData({ websiteId, onSave }) { diff --git a/src/components/pages/settings/websites/WebsiteDeleteForm.js b/src/app/(app)/settings/websites/[id]/WebsiteDeleteForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteDeleteForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteDeleteForm.js diff --git a/src/components/pages/settings/websites/WebsiteEditForm.js b/src/app/(app)/settings/websites/[id]/WebsiteEditForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteEditForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteEditForm.js diff --git a/src/components/pages/settings/websites/WebsiteResetForm.js b/src/app/(app)/settings/websites/[id]/WebsiteResetForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteResetForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteResetForm.js diff --git a/src/app/(app)/settings/websites/[id]/page.js b/src/app/(app)/settings/websites/[id]/page.js new file mode 100644 index 00000000..bdf3b076 --- /dev/null +++ b/src/app/(app)/settings/websites/[id]/page.js @@ -0,0 +1,15 @@ +import WebsiteSettings from '../WebsiteSettings'; + +async function getDisabled() { + return !!process.env.CLOUD_MODE; +} + +export default async function WebsiteSettingsPage({ params }) { + const disabled = await getDisabled(); + + if (!params.id || disabled) { + return null; + } + + return ; +} diff --git a/src/app/(app)/settings/websites/page.js b/src/app/(app)/settings/websites/page.js new file mode 100644 index 00000000..3cd5afa7 --- /dev/null +++ b/src/app/(app)/settings/websites/page.js @@ -0,0 +1,9 @@ +import Websites from './Websites'; + +export default function () { + if (process.env.cloudMode) { + return null; + } + + return ; +} diff --git a/src/components/pages/websites/WebsiteTableView.js b/src/app/(app)/websites/WebsiteTableView.js similarity index 100% rename from src/components/pages/websites/WebsiteTableView.js rename to src/app/(app)/websites/WebsiteTableView.js diff --git a/src/components/pages/websites/WebsiteTableView.module.css b/src/app/(app)/websites/WebsiteTableView.module.css similarity index 100% rename from src/components/pages/websites/WebsiteTableView.module.css rename to src/app/(app)/websites/WebsiteTableView.module.css diff --git a/src/app/(app)/websites/WebsitesBrowse.js b/src/app/(app)/websites/WebsitesBrowse.js new file mode 100644 index 00000000..96a25743 --- /dev/null +++ b/src/app/(app)/websites/WebsitesBrowse.js @@ -0,0 +1,30 @@ +'use client'; +import WebsiteList from '../settings/websites/Websites'; +import { useMessages } from 'components/hooks'; +import { useState } from 'react'; +import { Item, Tabs } from 'react-basics'; + +const TABS = { + myWebsites: 'my-websites', + teamWebsites: 'team-websites', +}; + +export function WebsitesBrowse() { + const { formatMessage, labels } = useMessages(); + const [tab, setTab] = useState(TABS.myWebsites); + + return ( + <> + + {formatMessage(labels.myWebsites)} + {formatMessage(labels.teamWebsites)} + + {tab === TABS.myWebsites && } + {tab === TABS.teamWebsites && ( + + )} + + ); +} + +export default WebsitesBrowse; diff --git a/src/components/pages/websites/WebsiteChart.js b/src/app/(app)/websites/[id]/WebsiteChart.js similarity index 100% rename from src/components/pages/websites/WebsiteChart.js rename to src/app/(app)/websites/[id]/WebsiteChart.js diff --git a/src/components/pages/websites/WebsiteChart.module.css b/src/app/(app)/websites/[id]/WebsiteChart.module.css similarity index 100% rename from src/components/pages/websites/WebsiteChart.module.css rename to src/app/(app)/websites/[id]/WebsiteChart.module.css diff --git a/src/components/pages/websites/WebsiteChartList.js b/src/app/(app)/websites/[id]/WebsiteChartList.js similarity index 90% rename from src/components/pages/websites/WebsiteChartList.js rename to src/app/(app)/websites/[id]/WebsiteChartList.js index 56cbe157..23764dbb 100644 --- a/src/components/pages/websites/WebsiteChartList.js +++ b/src/app/(app)/websites/[id]/WebsiteChartList.js @@ -2,9 +2,8 @@ import { Button, Text, Icon } from 'react-basics'; import { useMemo } from 'react'; import { firstBy } from 'thenby'; import Link from 'next/link'; -import WebsiteChart from 'components/pages/websites/WebsiteChart'; +import WebsiteChart from './WebsiteChart'; import useDashboard from 'store/dashboard'; -import styles from './WebsiteList.module.css'; import WebsiteHeader from './WebsiteHeader'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; import { useMessages, useLocale } from 'components/hooks'; @@ -27,7 +26,7 @@ export default function WebsiteChartList({ websites, showCharts, limit }) {
{ordered.map(({ id }, index) => { return index < limit ? ( -
+
- - {formatMessage(labels.pageOf, { current: page, total: maxPage })} - - - +
+
{formatMessage(labels.numberOfRecords, { x: count })}
+
+ +
+ {formatMessage(labels.pageOf, { current: page, total: maxPage })} +
+ +
+
+
); } diff --git a/src/components/common/Pager.module.css b/src/components/common/Pager.module.css index 99eb70ce..0ed5e1f4 100644 --- a/src/components/common/Pager.module.css +++ b/src/components/common/Pager.module.css @@ -1,7 +1,27 @@ -.container { - margin-top: 20px; +.pager { + display: grid; + grid-template-columns: repeat(3, 1fr); + align-items: center; +} + +.nav { + display: flex; + align-items: center; + justify-content: center; } .text { + font-size: var(--font-size-md); margin: 0 16px; + justify-content: center; +} + +@media only screen and (max-width: 992px) { + .pager { + grid-template-columns: repeat(2, 1fr); + } + + .nav { + justify-content: end; + } } diff --git a/src/components/common/SettingsTable.js b/src/components/common/SettingsTable.js deleted file mode 100644 index 701dbe13..00000000 --- a/src/components/common/SettingsTable.js +++ /dev/null @@ -1,100 +0,0 @@ -import Empty from 'components/common/Empty'; -import useMessages from 'components/hooks/useMessages'; -import { useState } from 'react'; -import { - SearchField, - Table, - TableBody, - TableCell, - TableColumn, - TableHeader, - TableRow, -} from 'react-basics'; -import styles from './SettingsTable.module.css'; -import Pager from 'components/common/Pager'; - -export function SettingsTable({ - columns = [], - data, - children, - cellRender, - showSearch, - showPaging, - onFilterChange, - onPageChange, - onPageSizeChange, - filterValue, -}) { - const { formatMessage, labels, messages } = useMessages(); - const [filter, setFilter] = useState(filterValue); - const { data: value, page, count, pageSize } = data; - - const handleFilterChange = value => { - setFilter(value); - onFilterChange(value); - }; - - return ( - <> - {showSearch && (value.length > 0 || filterValue) && ( - - )} - {value.length === 0 && filterValue && ( - - )} - {value.length > 0 && ( - - - {(column, index) => { - return ( - - {column.label} - - ); - }} - - - {(row, keys, rowIndex) => { - row.action = children(row, keys, rowIndex); - - return ( - - {(data, key, colIndex) => { - return ( - - - {cellRender ? cellRender(row, data, key, colIndex) : data[key]} - - ); - }} - - ); - }} - - {showPaging && ( - - )} -
- )} - - ); -} - -export default SettingsTable; diff --git a/src/components/common/SettingsTable.module.css b/src/components/common/SettingsTable.module.css deleted file mode 100644 index fd6cddfa..00000000 --- a/src/components/common/SettingsTable.module.css +++ /dev/null @@ -1,44 +0,0 @@ -.cell { - align-items: center; -} - -.row .cell:last-child { - gap: 10px; - justify-content: flex-end; -} - -.label { - display: none; - font-weight: 700; -} - -@media screen and (max-width: 992px) { - .header .cell { - display: none; - } - - .label { - display: block; - min-width: 100px; - } - - .row .cell { - padding-left: 0; - flex-basis: 100%; - } -} - -@media screen and (max-width: 1200px) { - .row { - flex-wrap: wrap; - } - - .header .cell:last-child { - display: none; - } - - .row .cell:last-child { - padding-left: 0; - flex-basis: 100%; - } -} diff --git a/src/components/common/UpdateNotice.js b/src/components/common/UpdateNotice.js index 23907948..95eb350f 100644 --- a/src/components/common/UpdateNotice.js +++ b/src/components/common/UpdateNotice.js @@ -1,3 +1,4 @@ +'use client'; import { useEffect, useCallback, useState } from 'react'; import { createPortal } from 'react-dom'; import { Button, Row, Column } from 'react-basics'; @@ -6,12 +7,12 @@ import useStore, { checkVersion } from 'store/version'; import { REPO_URL, VERSION_CHECK } from 'lib/constants'; import styles from './UpdateNotice.module.css'; import useMessages from 'components/hooks/useMessages'; -import { useRouter } from 'next/router'; +import { usePathname } from 'next/navigation'; export function UpdateNotice({ user, config }) { const { formatMessage, labels, messages } = useMessages(); const { latest, checked, hasUpdate, releaseUrl } = useStore(); - const { pathname } = useRouter(); + const pathname = usePathname(); const [dismissed, setDismissed] = useState(checked); const allowUpdate = user?.isAdmin && diff --git a/src/components/common/WorldMap.js b/src/components/common/WorldMap.js index 6ae84677..ff34d5f2 100644 --- a/src/components/common/WorldMap.js +++ b/src/components/common/WorldMap.js @@ -1,5 +1,4 @@ import { useState, useMemo } from 'react'; -import { useRouter } from 'next/router'; import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps'; import classNames from 'classnames'; import { colord } from 'colord'; @@ -14,7 +13,6 @@ import { percentFilter } from 'lib/filters'; import styles from './WorldMap.module.css'; export function WorldMap({ data, className }) { - const { basePath } = useRouter(); const [tooltip, setTooltipPopup] = useState(); const { theme, colors } = useTheme(); const { locale } = useLocale(); @@ -54,7 +52,7 @@ export function WorldMap({ data, className }) { > - + {({ geographies }) => { return geographies.map(geo => { const code = ISO_COUNTRIES[geo.id]; diff --git a/src/components/hooks/useApi.ts b/src/components/hooks/useApi.ts index f41547a9..75a928d5 100644 --- a/src/components/hooks/useApi.ts +++ b/src/components/hooks/useApi.ts @@ -1,4 +1,3 @@ -import { useRouter } from 'next/router'; import * as reactQuery from '@tanstack/react-query'; import { useApi as nextUseApi } from 'next-basics'; import { getClientAuthToken } from 'lib/client'; @@ -8,12 +7,11 @@ import useStore from 'store/app'; const selector = state => state.shareToken; export function useApi() { - const { basePath } = useRouter(); const shareToken = useStore(selector); const { get, post, put, del } = nextUseApi( { authorization: `Bearer ${getClientAuthToken()}`, [SHARE_TOKEN_HEADER]: shareToken?.token }, - basePath, + process.env.basePath, ); return { get, post, put, del, ...reactQuery }; diff --git a/src/components/hooks/useCountryNames.js b/src/components/hooks/useCountryNames.js index 51cabf34..40611865 100644 --- a/src/components/hooks/useCountryNames.js +++ b/src/components/hooks/useCountryNames.js @@ -1,5 +1,4 @@ import { useState, useEffect } from 'react'; -import { useRouter } from 'next/router'; import { httpGet } from 'next-basics'; import enUS from 'public/intl/country/en-US.json'; @@ -9,10 +8,9 @@ const countryNames = { export function useCountryNames(locale) { const [list, setList] = useState(countryNames[locale] || enUS); - const { basePath } = useRouter(); async function loadData(locale) { - const { data } = await httpGet(`${basePath}/intl/country/${locale}.json`); + const { data } = await httpGet(`${process.env.basePath}/intl/country/${locale}.json`); if (data) { countryNames[locale] = data; diff --git a/src/components/hooks/useFilterQuery.ts b/src/components/hooks/useFilterQuery.ts new file mode 100644 index 00000000..987d3c44 --- /dev/null +++ b/src/components/hooks/useFilterQuery.ts @@ -0,0 +1,26 @@ +import { useCallback, useState } from 'react'; +import { useApi } from 'components/hooks/useApi'; + +export function useFilterQuery(key: any[], fn, options?: any) { + const [params, setParams] = useState({ + query: '', + page: 1, + }); + const { useQuery } = useApi(); + + const result = useQuery<{ + page: number; + pageSize: number; + count: number; + data: any[]; + }>([...key, params], fn.bind(null, params), options); + + const getProps = useCallback(() => { + const { data, isLoading, error } = result; + return { result: data, isLoading, error, params, setParams }; + }, [result, params, setParams]); + + return { ...result, getProps }; +} + +export default useFilterQuery; diff --git a/src/components/hooks/useLanguageNames.js b/src/components/hooks/useLanguageNames.js index ff59e93d..3823a26b 100644 --- a/src/components/hooks/useLanguageNames.js +++ b/src/components/hooks/useLanguageNames.js @@ -1,5 +1,4 @@ import { useState, useEffect } from 'react'; -import { useRouter } from 'next/router'; import { httpGet } from 'next-basics'; import enUS from 'public/intl/language/en-US.json'; @@ -9,10 +8,9 @@ const languageNames = { export function useLanguageNames(locale) { const [list, setList] = useState(languageNames[locale] || enUS); - const { basePath } = useRouter(); async function loadData(locale) { - const { data } = await httpGet(`${basePath}/intl/language/${locale}.json`); + const { data } = await httpGet(`${process.env.basePath}/intl/language/${locale}.json`); if (data) { languageNames[locale] = data; diff --git a/src/components/hooks/useLocale.js b/src/components/hooks/useLocale.js index 1374af81..71574d86 100644 --- a/src/components/hooks/useLocale.js +++ b/src/components/hooks/useLocale.js @@ -1,5 +1,4 @@ import { useEffect } from 'react'; -import { useRouter } from 'next/router'; import { httpGet, setItem } from 'next-basics'; import { LOCALE_CONFIG } from 'lib/constants'; import { getDateLocale, getTextDirection } from 'lib/lang'; @@ -15,13 +14,12 @@ const selector = state => state.locale; export function useLocale() { const locale = useStore(selector); - const { basePath } = useRouter(); const forceUpdate = useForceUpdate(); const dir = getTextDirection(locale); const dateLocale = getDateLocale(locale); async function loadMessages(locale) { - const { ok, data } = await httpGet(`${basePath}/intl/messages/${locale}.json`); + const { ok, data } = await httpGet(`${process.env.basePath}/intl/messages/${locale}.json`); if (ok) { messages[locale] = data; diff --git a/src/components/hooks/usePageQuery.js b/src/components/hooks/usePageQuery.js index b275d580..de87e247 100644 --- a/src/components/hooks/usePageQuery.js +++ b/src/components/hooks/usePageQuery.js @@ -1,30 +1,24 @@ import { useMemo } from 'react'; -import { useRouter } from 'next/router'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { buildUrl } from 'next-basics'; export function usePageQuery() { const router = useRouter(); - const { pathname, search } = location; - const { asPath } = router; + const pathname = usePathname(); + const params = useSearchParams(); const query = useMemo(() => { - if (!search) { - return {}; + const obj = {}; + + for (const [key, value] of params.entries()) { + obj[key] = decodeURIComponent(value); } - const params = search.substring(1).split('&'); - - return params.reduce((obj, item) => { - const [key, value] = item.split('='); - - obj[key] = decodeURIComponent(value); - - return obj; - }, {}); - }, [search]); + return obj; + }, [params]); function resolveUrl(params, reset) { - return buildUrl(asPath.split('?')[0], { ...(reset ? {} : query), ...params }); + return buildUrl(pathname, { ...(reset ? {} : query) }); } return { pathname, query, resolveUrl, router }; diff --git a/src/components/hooks/useRequireLogin.ts b/src/components/hooks/useRequireLogin.ts index d2f540d4..b0d8029d 100644 --- a/src/components/hooks/useRequireLogin.ts +++ b/src/components/hooks/useRequireLogin.ts @@ -1,10 +1,8 @@ import { useEffect } from 'react'; -import { useRouter } from 'next/router'; import useApi from 'components/hooks/useApi'; import useUser from 'components/hooks/useUser'; -export function useRequireLogin(handler: (data?: object) => void) { - const { basePath } = useRouter(); +export function useRequireLogin(handler?: (data?: object) => void) { const { get } = useApi(); const { user, setUser } = useUser(); @@ -15,7 +13,7 @@ export function useRequireLogin(handler: (data?: object) => void) { setUser(typeof handler === 'function' ? handler(data) : (data as any)?.user); } catch { - location.href = `${basePath}/login`; + location.href = `${process.env.basePath}/login`; } } diff --git a/src/components/input/LogoutButton.js b/src/components/input/LogoutButton.js index 2b04a78a..6ca358a1 100644 --- a/src/components/input/LogoutButton.js +++ b/src/components/input/LogoutButton.js @@ -5,7 +5,7 @@ import useMessages from 'components/hooks/useMessages'; export function LogoutButton({ tooltipPosition = 'top' }) { const { formatMessage, labels } = useMessages(); return ( - + - - - - {hasData && ( - - )} - {!hasData && } - - ); -} - -export default ReportsPage; diff --git a/src/components/pages/reports/ReportsTable.js b/src/components/pages/reports/ReportsTable.js deleted file mode 100644 index 52488c11..00000000 --- a/src/components/pages/reports/ReportsTable.js +++ /dev/null @@ -1,97 +0,0 @@ -import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm'; -import LinkButton from 'components/common/LinkButton'; -import SettingsTable from 'components/common/SettingsTable'; -import { useMessages } from 'components/hooks'; -import useUser from 'components/hooks/useUser'; -import { useState } from 'react'; -import { Button, Flexbox, Icon, Icons, Modal, Text } from 'react-basics'; -import { REPORT_TYPES } from 'lib/constants'; - -export function ReportsTable({ - data = [], - onDelete = () => {}, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, - showDomain, -}) { - const [report, setReport] = useState(null); - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - - const domainColumn = [ - { - name: 'domain', - label: formatMessage(labels.domain), - }, - ]; - - const columns = [ - { name: 'name', label: formatMessage(labels.name) }, - { name: 'description', label: formatMessage(labels.description) }, - { name: 'type', label: formatMessage(labels.type) }, - ...(showDomain ? domainColumn : []), - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'type') { - return formatMessage( - labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)], - ); - } - return data[key]; - }; - - const handleConfirm = () => { - onDelete(report.id); - }; - - return ( - <> - - {row => { - const { id, userId: reportOwnerId, website } = row; - if (showDomain) { - row.domain = website.domain; - } - - return ( - - {formatMessage(labels.view)} - {!showDomain || user.id === reportOwnerId || user.id === website?.userId} - - - ); - }} - - {report && ( - - setReport(null)} - /> - - )} - - ); -} - -export default ReportsTable; diff --git a/src/components/pages/settings/profile/ProfileSettings.js b/src/components/pages/settings/profile/ProfileSettings.js deleted file mode 100644 index a217e52c..00000000 --- a/src/components/pages/settings/profile/ProfileSettings.js +++ /dev/null @@ -1,17 +0,0 @@ -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import ProfileDetails from './ProfileDetails'; -import useMessages from 'components/hooks/useMessages'; - -export function ProfileSettings() { - const { formatMessage, labels } = useMessages(); - - return ( - - - - - ); -} - -export default ProfileSettings; diff --git a/src/components/pages/settings/teams/TeamMembers.js b/src/components/pages/settings/teams/TeamMembers.js deleted file mode 100644 index 207ad72d..00000000 --- a/src/components/pages/settings/teams/TeamMembers.js +++ /dev/null @@ -1,48 +0,0 @@ -import { Loading, useToasts } from 'react-basics'; -import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable'; -import useApi from 'components/hooks/useApi'; -import useMessages from 'components/hooks/useMessages'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function TeamMembers({ teamId, readOnly }) { - const { showToast } = useToasts(); - const { formatMessage, messages } = useMessages(); - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - const { get, useQuery } = useApi(); - const { data, isLoading, refetch } = useQuery( - ['teams:users', teamId, filter, page, pageSize], - () => - get(`/teams/${teamId}/users`, { - filter, - page, - pageSize, - }), - ); - - if (isLoading) { - return ; - } - - const handleSave = async () => { - await refetch(); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - return ( - <> - - - ); -} - -export default TeamMembers; diff --git a/src/components/pages/settings/teams/TeamMembersTable.js b/src/components/pages/settings/teams/TeamMembersTable.js deleted file mode 100644 index 0755c193..00000000 --- a/src/components/pages/settings/teams/TeamMembersTable.js +++ /dev/null @@ -1,68 +0,0 @@ -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import TeamMemberRemoveButton from './TeamMemberRemoveButton'; -import SettingsTable from 'components/common/SettingsTable'; - -export function TeamMembersTable({ - data = [], - teamId, - onSave, - readOnly, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - - const columns = [ - { name: 'username', label: formatMessage(labels.username) }, - { name: 'role', label: formatMessage(labels.role) }, - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'username') { - return row?.username; - } - if (key === 'role') { - return formatMessage( - labels[ - Object.keys(ROLES).find(key => ROLES[key] === row?.teamUser[0]?.role) || labels.unknown - ], - ); - } - return data[key]; - }; - - return ( - - {row => { - return ( - !readOnly && ( - - ) - ); - }} - - ); -} - -export default TeamMembersTable; diff --git a/src/components/pages/settings/teams/TeamWebsites.js b/src/components/pages/settings/teams/TeamWebsites.js deleted file mode 100644 index 27fe2f85..00000000 --- a/src/components/pages/settings/teams/TeamWebsites.js +++ /dev/null @@ -1,76 +0,0 @@ -import { - ActionForm, - Button, - Icon, - Icons, - Loading, - Modal, - ModalTrigger, - Text, - useToasts, -} from 'react-basics'; -import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable'; -import TeamAddWebsiteForm from 'components/pages/settings/teams/TeamAddWebsiteForm'; -import useApi from 'components/hooks/useApi'; -import useMessages from 'components/hooks/useMessages'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function TeamWebsites({ teamId }) { - const { showToast } = useToasts(); - const { formatMessage, labels, messages } = useMessages(); - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - const { get, useQuery } = useApi(); - const { data, isLoading, refetch } = useQuery( - ['teams:websites', teamId, filter, page, pageSize], - () => - get(`/teams/${teamId}/websites`, { - filter, - page, - pageSize, - }), - ); - const hasData = data && data.length !== 0; - - if (isLoading) { - return ; - } - - const handleSave = async () => { - await refetch(); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const addButton = ( - - - - {close => } - - - ); - - return ( -
- {addButton} - {hasData && ( - - )} -
- ); -} - -export default TeamWebsites; diff --git a/src/components/pages/settings/teams/TeamWebsitesTable.js b/src/components/pages/settings/teams/TeamWebsitesTable.js deleted file mode 100644 index 5ce08f35..00000000 --- a/src/components/pages/settings/teams/TeamWebsitesTable.js +++ /dev/null @@ -1,66 +0,0 @@ -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import Link from 'next/link'; -import { Button, Icon, Icons, Text } from 'react-basics'; -import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton'; -import SettingsTable from 'components/common/SettingsTable'; - -export function TeamWebsitesTable({ - data = [], - onSave, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, - openExternal = false, -}) { - const { formatMessage, labels } = useMessages(); - - const { user } = useUser(); - const columns = [ - { name: 'name', label: formatMessage(labels.name) }, - { name: 'domain', label: formatMessage(labels.domain) }, - { name: 'action', label: ' ' }, - ]; - - return ( - - {row => { - const { id: teamId, teamUser } = row.teamWebsite[0].team; - const { id: websiteId, name, domain, userId } = row; - const owner = teamUser[0]; - const canRemove = user.id === userId || user.id === owner.userId; - - row.name = name; - row.domain = domain; - - return ( - <> - - - - {canRemove && ( - - )} - - ); - }} - - ); -} - -export default TeamWebsitesTable; diff --git a/src/components/pages/settings/teams/TeamsList.js b/src/components/pages/settings/teams/TeamsList.js deleted file mode 100644 index 76a87b0c..00000000 --- a/src/components/pages/settings/teams/TeamsList.js +++ /dev/null @@ -1,118 +0,0 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import Icons from 'components/icons'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import TeamAddForm from 'components/pages/settings/teams/TeamAddForm'; -import TeamsTable from 'components/pages/settings/teams/TeamsTable'; -import useApi from 'components/hooks/useApi'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { useState } from 'react'; -import { Button, Flexbox, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import TeamJoinForm from './TeamJoinForm'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function TeamsList() { - const { user } = useUser(); - const { formatMessage, labels, messages } = useMessages(); - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - const [update, setUpdate] = useState(0); - - const { get, useQuery } = useApi(); - const { data, isLoading, error } = useQuery(['teams', update, filter, page, pageSize], () => { - return get(`/teams`, { - filter, - page, - pageSize, - }); - }); - - const hasData = data && data?.data.length !== 0; - const isFiltered = filter; - - const { showToast } = useToasts(); - - const handleSave = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const handleJoin = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const handleDelete = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const joinButton = ( - - - - {close => } - - - ); - - const createButton = ( - <> - {user.role !== ROLES.viewOnly && ( - - - - {close => } - - - )} - - ); - - return ( - - - {(hasData || isFiltered) && ( - - {joinButton} - {createButton} - - )} - - - {(hasData || isFiltered) && ( - - )} - - {!hasData && !isFiltered && ( - - - {joinButton} - {createButton} - - - )} - - ); -} - -export default TeamsList; diff --git a/src/components/pages/settings/teams/TeamsTable.js b/src/components/pages/settings/teams/TeamsTable.js deleted file mode 100644 index e1710783..00000000 --- a/src/components/pages/settings/teams/TeamsTable.js +++ /dev/null @@ -1,111 +0,0 @@ -import SettingsTable from 'components/common/SettingsTable'; -import useLocale from 'components/hooks/useLocale'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import Link from 'next/link'; -import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import TeamDeleteForm from './TeamDeleteForm'; -import TeamLeaveForm from './TeamLeaveForm'; - -export function TeamsTable({ - data = { data: [] }, - onDelete, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - const { dir } = useLocale(); - - const columns = [ - { name: 'name', label: formatMessage(labels.name) }, - { name: 'owner', label: formatMessage(labels.owner) }, - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'owner') { - return row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username; - } - return data[key]; - }; - - return ( - - {row => { - const { id, teamUser } = row; - const owner = teamUser.find(({ role }) => role === ROLES.teamOwner); - const showDelete = user.id === owner?.userId; - - return ( - <> - - - - {showDelete && ( - - - - {close => ( - - )} - - - )} - {!showDelete && ( - - - - {close => ( - - )} - - - )} - - ); - }} - - ); -} - -export default TeamsTable; diff --git a/src/components/pages/settings/users/UsersList.js b/src/components/pages/settings/users/UsersList.js deleted file mode 100644 index 0bc8612e..00000000 --- a/src/components/pages/settings/users/UsersList.js +++ /dev/null @@ -1,68 +0,0 @@ -import { useToasts } from 'react-basics'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import UsersTable from './UsersTable'; -import UserAddButton from './UserAddButton'; -import useApi from 'components/hooks/useApi'; -import useUser from 'components/hooks/useUser'; -import useMessages from 'components/hooks/useMessages'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function UsersList() { - const { formatMessage, labels, messages } = useMessages(); - const { user } = useUser(); - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - - const { get, useQuery } = useApi(); - const { data, isLoading, error, refetch } = useQuery( - ['user', filter, page, pageSize], - () => - get(`/users`, { - filter, - page, - pageSize, - }), - { - enabled: !!user, - }, - ); - const { showToast } = useToasts(); - const hasData = data && data.length !== 0; - - const handleSave = () => { - refetch().then(() => showToast({ message: formatMessage(messages.saved), variant: 'success' })); - }; - - const handleDelete = () => { - refetch().then(() => - showToast({ message: formatMessage(messages.userDeleted), variant: 'success' }), - ); - }; - - return ( - - - - - {(hasData || filter) && ( - - )} - {!hasData && !filter && ( - - - - )} - - ); -} - -export default UsersList; diff --git a/src/components/pages/settings/users/UsersTable.js b/src/components/pages/settings/users/UsersTable.js deleted file mode 100644 index 1a93710d..00000000 --- a/src/components/pages/settings/users/UsersTable.js +++ /dev/null @@ -1,93 +0,0 @@ -import { Button, Text, Icon, Icons, ModalTrigger, Modal } from 'react-basics'; -import { formatDistance } from 'date-fns'; -import Link from 'next/link'; -import useUser from 'components/hooks/useUser'; -import UserDeleteForm from './UserDeleteForm'; -import { ROLES } from 'lib/constants'; -import useMessages from 'components/hooks/useMessages'; -import SettingsTable from 'components/common/SettingsTable'; -import useLocale from 'components/hooks/useLocale'; - -export function UsersTable({ - data = { data: [] }, - onDelete, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - const { dateLocale } = useLocale(); - - const columns = [ - { name: 'username', label: formatMessage(labels.username) }, - { name: 'role', label: formatMessage(labels.role) }, - { name: 'created', label: formatMessage(labels.created) }, - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'created') { - return formatDistance(new Date(row.createdAt), new Date(), { - addSuffix: true, - locale: dateLocale, - }); - } - if (key === 'role') { - return formatMessage( - labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown, - ); - } - return data[key]; - }; - - return ( - - {row => { - return ( - <> - - - - - - - {close => ( - - )} - - - - ); - }} - - ); -} - -export default UsersTable; diff --git a/src/components/pages/settings/websites/WebsitesList.js b/src/components/pages/settings/websites/WebsitesList.js deleted file mode 100644 index 0cec3832..00000000 --- a/src/components/pages/settings/websites/WebsitesList.js +++ /dev/null @@ -1,79 +0,0 @@ -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; -import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; -import useApi from 'components/hooks/useApi'; -import useApiFilter from 'components/hooks/useApiFilter'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; - -export function WebsitesList({ - showTeam, - showEditButton = true, - showHeader = true, - includeTeams, - onlyTeams, - fetch, -}) { - const { formatMessage, labels, messages } = useMessages(); - const { user } = useUser(); - - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - const { get, useQuery } = useApi(); - const { data, isLoading, error, refetch } = useQuery( - ['websites', fetch, user?.id, filter, page, pageSize, includeTeams, onlyTeams], - () => - get(`/users/${user?.id}/websites`, { - filter, - page, - pageSize, - includeTeams, - onlyTeams, - }), - { enabled: !!user }, - ); - const { showToast } = useToasts(); - - const handleSave = async () => { - await refetch(); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const addButton = ( - <> - {user.role !== ROLES.viewOnly && ( - - - - {close => } - - - )} - - ); - - return ( - - {showHeader && {addButton}} - - - ); -} - -export default WebsitesList; diff --git a/src/components/pages/settings/websites/WebsitesTable.js b/src/components/pages/settings/websites/WebsitesTable.js deleted file mode 100644 index 7fa50716..00000000 --- a/src/components/pages/settings/websites/WebsitesTable.js +++ /dev/null @@ -1,89 +0,0 @@ -import Link from 'next/link'; -import { Button, Text, Icon, Icons } from 'react-basics'; -import SettingsTable from 'components/common/SettingsTable'; -import Empty from 'components/common/Empty'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; - -export function WebsitesTable({ - data = [], - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, - showTeam, - showEditButton, - openExternal = false, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - - const showTable = data && (filterValue || data?.data?.length !== 0); - - const teamColumns = [ - { name: 'teamName', label: formatMessage(labels.teamName) }, - { name: 'owner', label: formatMessage(labels.owner) }, - ]; - - const columns = [ - { name: 'name', label: formatMessage(labels.name) }, - { name: 'domain', label: formatMessage(labels.domain) }, - ...(showTeam ? teamColumns : []), - { name: 'action', label: ' ' }, - ]; - - return ( - <> - {showTable && ( - - {row => { - const { - id, - teamWebsite, - user: { username, id: ownerId }, - } = row; - if (showTeam) { - row.teamName = teamWebsite[0]?.team.name; - row.owner = username; - } - - return ( - <> - {showEditButton && (!showTeam || ownerId === user.id) && ( - - - - )} - - - - - ); - }} - - )} - {!showTable && } - - ); -} - -export default WebsitesTable; diff --git a/src/components/pages/websites/WebsiteEventDataPage.js b/src/components/pages/websites/WebsiteEventDataPage.js deleted file mode 100644 index 08acafb5..00000000 --- a/src/components/pages/websites/WebsiteEventDataPage.js +++ /dev/null @@ -1,12 +0,0 @@ -import Page from 'components/layout/Page'; -import WebsiteHeader from './WebsiteHeader'; -import WebsiteEventData from './WebsiteEventData'; - -export default function WebsiteEventDataPage({ websiteId }) { - return ( - - - - - ); -} diff --git a/src/components/pages/websites/WebsitesPage.js b/src/components/pages/websites/WebsitesPage.js deleted file mode 100644 index 61c29448..00000000 --- a/src/components/pages/websites/WebsitesPage.js +++ /dev/null @@ -1,76 +0,0 @@ -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; -import WebsiteList from 'components/pages/settings/websites/WebsitesList'; -import { useMessages } from 'components/hooks'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { useState } from 'react'; -import { - Button, - Icon, - Icons, - Item, - Modal, - ModalTrigger, - Tabs, - Text, - useToasts, -} from 'react-basics'; - -export function WebsitesPage() { - const { formatMessage, labels, messages } = useMessages(); - const [tab, setTab] = useState('my-websites'); - const [fetch, setFetch] = useState(1); - const { user } = useUser(); - const { showToast } = useToasts(); - const cloudMode = Boolean(process.env.cloudMode); - - const handleSave = async () => { - setFetch(fetch + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const addButton = ( - <> - {user.role !== ROLES.viewOnly && ( - - - - {close => } - - - )} - - ); - - return ( - - {!cloudMode && addButton} - - {formatMessage(labels.myWebsites)} - {formatMessage(labels.teamWebsites)} - - - {tab === 'my-websites' && ( - - )} - {tab === 'team-webaites' && ( - - )} - - ); -} - -export default WebsitesPage; diff --git a/src/index.ts b/src/index.ts index f2ef13ca..aeaf318a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,6 @@ export * from 'components/common/HoverTooltip'; export * from 'components/common/LinkButton'; export * from 'components/common/MobileMenu'; export * from 'components/common/Pager'; -export * from 'components/common/SettingsTable'; export * from 'components/common/UpdateNotice'; export * from 'components/common/WorldMap'; @@ -89,29 +88,29 @@ export * from 'components/hooks/useUser'; export * from 'components/hooks/useWebsite'; export * from 'components/hooks/useWebsiteReports'; -export * from 'components/pages/settings/teams/TeamAddForm'; -export * from 'components/pages/settings/teams/TeamAddWebsiteForm'; -export * from 'components/pages/settings/teams/TeamDeleteForm'; -export * from 'components/pages/settings/teams/TeamEditForm'; -export * from 'components/pages/settings/teams/TeamJoinForm'; -export * from 'components/pages/settings/teams/TeamLeaveForm'; -export * from 'components/pages/settings/teams/TeamMemberRemoveButton'; -export * from 'components/pages/settings/teams/TeamMembers'; -export * from 'components/pages/settings/teams/TeamMembersTable'; -export * from 'components/pages/settings/teams/TeamSettings'; -export * from 'components/pages/settings/teams/TeamsList'; -export * from 'components/pages/settings/teams/TeamsTable'; -export * from 'components/pages/settings/teams/TeamWebsiteRemoveButton'; -export * from 'components/pages/settings/teams/TeamWebsites'; -export * from 'components/pages/settings/teams/TeamWebsitesTable'; -export * from 'components/pages/settings/teams/WebsiteTags'; +export * from 'app/(app)/settings/teams/TeamAddForm'; +export * from 'app/(app)/settings/teams/[id]/TeamAddWebsiteForm'; +export * from 'app/(app)/settings/teams/TeamDeleteForm'; +export * from 'app/(app)/settings/teams/[id]/TeamEditForm'; +export * from 'app/(app)/settings/teams/TeamJoinForm'; +export * from 'app/(app)/settings/teams/TeamLeaveForm'; +export * from 'app/(app)/settings/teams/[id]/TeamMemberRemoveButton'; +export * from 'app/(app)/settings/teams/[id]/TeamMembers'; +export * from 'app/(app)/settings/teams/[id]/TeamMembersTable'; +export * from 'app/(app)/settings/teams/[id]/TeamSettings'; +export * from 'app/(app)/settings/teams/TeamsList'; +export * from 'app/(app)/settings/teams/TeamsTable'; +export * from 'app/(app)/settings/teams/TeamWebsiteRemoveButton'; +export * from 'app/(app)/settings/teams/[id]/TeamWebsites'; +export * from 'app/(app)/settings/teams/[id]/TeamWebsitesTable'; +export * from 'app/(app)/settings/teams/WebsiteTags'; -export * from 'components/pages/settings/websites/ShareUrl'; -export * from 'components/pages/settings/websites/TrackingCode'; -export * from 'components/pages/settings/websites/WebsiteAddForm'; -export * from 'components/pages/settings/websites/WebsiteDeleteForm'; -export * from 'components/pages/settings/websites/WebsiteEditForm'; -export * from 'components/pages/settings/websites/WebsiteResetForm'; -export * from 'components/pages/settings/websites/WebsiteSettings'; -export * from 'components/pages/settings/websites/WebsitesList'; -export * from 'components/pages/settings/websites/WebsitesTable'; +export * from 'app/(app)/settings/websites/[id]/ShareUrl'; +export * from 'app/(app)/settings/websites/[id]/TrackingCode'; +export * from 'app/(app)/settings/websites/WebsiteAddForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteDeleteForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteEditForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteResetForm'; +export * from 'app/(app)/settings/websites/WebsiteSettings'; +export * from './app/(app)/settings/websites/Websites'; +export * from 'app/(app)/settings/websites/WebsitesTable'; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 888c1484..9d7a3da8 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -19,6 +19,7 @@ export const DEFAULT_ANIMATION_DURATION = 300; export const DEFAULT_DATE_RANGE = '24hour'; export const DEFAULT_WEBSITE_LIMIT = 10; export const DEFAULT_RESET_DATE = '2000-01-01'; +export const DEFAULT_PAGE_SIZE = 10; export const REALTIME_RANGE = 30; export const REALTIME_INTERVAL = 5000; @@ -29,23 +30,7 @@ export const FILTER_DAY = 'filter-day'; export const FILTER_RANGE = 'filter-range'; export const FILTER_REFERRERS = 'filter-referrers'; export const FILTER_PAGES = 'filter-pages'; - -export const USER_FILTER_TYPES = { - all: 'All', - username: 'Username', -} as const; -export const WEBSITE_FILTER_TYPES = { all: 'All', name: 'Name', domain: 'Domain' } as const; -export const TEAM_FILTER_TYPES = { all: 'All', name: 'Name', 'user:username': 'Owner' } as const; -export const REPORT_FILTER_TYPES = { - all: 'All', - name: 'Name', - description: 'Description', - type: 'Type', - 'user:username': 'Username', - 'website:name': 'Website Name', - 'website:domain': 'Website Domain', -} as const; - +export const UNIT_TYPES = ['year', 'month', 'hour', 'day']; export const EVENT_COLUMNS = ['url', 'referrer', 'title', 'query', 'event']; export const SESSION_COLUMNS = [ diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 4be958b6..e1e2a38b 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -14,7 +14,6 @@ import { } from 'next-basics'; import { NextApiRequestCollect } from 'pages/api/send'; import { getUserById } from '../queries'; -import { NextApiRequestQueryBody } from './types'; const log = debug('umami:middleware'); @@ -83,14 +82,18 @@ export const useAuth = createMiddleware(async (req, res, next) => { next(); }); -export const useValidate = createMiddleware(async (req: any, res, next) => { - try { - const { yup } = req as NextApiRequestQueryBody; +export const useValidate = async (schema, req, res) => { + return createMiddleware(async (req: any, res, next) => { + try { + const rules = schema[req.method]; - yup[req.method] && yup[req.method].validateSync({ ...req.query, ...req.body }); - } catch (e: any) { - return badRequest(res, e.message); - } + if (rules) { + rules.validateSync(req.method === 'GET' ? { ...req.query } : { ...req.body }); + } + } catch (e: any) { + return badRequest(res, e.message); + } - next(); -}); + next(); + })(req, res); +}; diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 59638dbd..4b910f00 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,11 +1,11 @@ +import { Prisma } from '@prisma/client'; import prisma from '@umami/prisma-client'; import moment from 'moment-timezone'; import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; -import { FILTER_COLUMNS, SESSION_COLUMNS, OPERATORS } from './constants'; +import { FILTER_COLUMNS, SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants'; import { loadWebsite } from './load'; import { maxDate } from './date'; import { QueryFilters, QueryOptions, SearchFilter } from './types'; -import { Prisma } from '@prisma/client'; const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%d %H:%i:00', @@ -171,7 +171,7 @@ async function rawQuery(sql: string, data: object): Promise { return prisma.rawQuery(query, params); } -function getPageFilters(filters: SearchFilter): [ +function getPageFilters(filters: SearchFilter): [ { orderBy: { [x: string]: string; @@ -185,20 +185,20 @@ function getPageFilters(filters: SearchFilter): [ orderBy: string; }, ] { - const { pageSize = 10, page = 1, orderBy } = filters || {}; + const { page = 1, pageSize = DEFAULT_PAGE_SIZE, orderBy, sortDescending = false } = filters || {}; return [ { - ...(pageSize > 0 && { take: pageSize, skip: pageSize * (page - 1) }), + ...(pageSize > 0 && { take: +pageSize, skip: +pageSize * (page - 1) }), ...(orderBy && { orderBy: [ { - [orderBy]: 'asc', + [orderBy]: sortDescending ? 'desc' : 'asc', }, ], }), }, - { pageSize, page: +page, orderBy }, + { page: +page, pageSize, orderBy }, ]; } diff --git a/src/lib/schema.ts b/src/lib/schema.ts new file mode 100644 index 00000000..739128b3 --- /dev/null +++ b/src/lib/schema.ts @@ -0,0 +1,13 @@ +import * as yup from 'yup'; + +export const dateRange = { + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), +}; + +export const pageInfo = { + query: yup.string(), + page: yup.number().integer().positive(), + pageSize: yup.number().integer().positive().max(200), + orderBy: yup.string(), +}; diff --git a/src/lib/types.ts b/src/lib/types.ts index 3685753e..98fbc29b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -5,12 +5,8 @@ import { EVENT_TYPE, KAFKA_TOPIC, PERMISSIONS, - REPORT_FILTER_TYPES, REPORT_TYPES, ROLES, - TEAM_FILTER_TYPES, - USER_FILTER_TYPES, - WEBSITE_FILTER_TYPES, } from './constants'; import * as yup from 'yup'; import { TIME_UNIT } from './date'; @@ -27,46 +23,42 @@ export type DynamicDataType = ObjectValues; export type KafkaTopic = ObjectValues; export type ReportType = ObjectValues; -export type ReportSearchFilterType = ObjectValues; -export type UserSearchFilterType = ObjectValues; -export type WebsiteSearchFilterType = ObjectValues; -export type TeamSearchFilterType = ObjectValues; - -export interface WebsiteSearchFilter extends SearchFilter { +export interface WebsiteSearchFilter extends SearchFilter { userId?: string; teamId?: string; includeTeams?: boolean; onlyTeams?: boolean; } -export interface UserSearchFilter extends SearchFilter { +export interface UserSearchFilter extends SearchFilter { teamId?: string; } -export interface TeamSearchFilter extends SearchFilter { +export interface TeamSearchFilter extends SearchFilter { userId?: string; } -export interface ReportSearchFilter extends SearchFilter { +export interface ReportSearchFilter extends SearchFilter { userId?: string; websiteId?: string; includeTeams?: boolean; } -export interface SearchFilter { - filter?: string; - filterType?: T; - pageSize: number; - page: number; +export interface SearchFilter { + query?: string; + page?: number; + pageSize?: number; orderBy?: string; + sortDescending?: boolean; } export interface FilterResult { data: T; count: number; - pageSize: number; page: number; + pageSize: number; orderBy?: string; + sortDescending?: boolean; } export interface DynamicData { diff --git a/src/lib/yup.ts b/src/lib/yup.ts index a9d21028..2407f7ad 100644 --- a/src/lib/yup.ts +++ b/src/lib/yup.ts @@ -1,19 +1,15 @@ +import moment from 'moment'; import * as yup from 'yup'; +import { UNIT_TYPES } from './constants'; -export function getDateRangeValidation() { - return { - startAt: yup.number().integer().required(), - endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), - }; -} +export const TimezoneTest = yup.string().test( + 'timezone', + () => `Invalid timezone`, + value => moment.tz.zone(value) !== null, +); -// ex: /funnel|insights|retention/i -export function getFilterValidation(matchRegex) { - return { - filter: yup.string(), - filterType: yup.string().matches(matchRegex), - pageSize: yup.number().integer().positive().max(200), - page: yup.number().integer().positive(), - orderBy: yup.string(), - }; -} +export const UnitTypeTest = yup.string().test( + 'unit', + () => `Invalid unit`, + value => UNIT_TYPES.includes(value), +); diff --git a/src/pages/404.js b/src/pages/404.js deleted file mode 100644 index 8fa13a9c..00000000 --- a/src/pages/404.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Row, Column, Flexbox } from 'react-basics'; -import AppLayout from 'components/layout/AppLayout'; -import useMessages from 'components/hooks/useMessages'; - -export default function Custom404() { - const { formatMessage, labels } = useMessages(); - - return ( - - - - -

{formatMessage(labels.pageNotFound)}

-
-
-
-
- ); -} diff --git a/src/pages/_app.js b/src/pages/_app.js deleted file mode 100644 index 7022772c..00000000 --- a/src/pages/_app.js +++ /dev/null @@ -1,69 +0,0 @@ -import { IntlProvider } from 'react-intl'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ReactBasicsProvider } from 'react-basics'; -import Head from 'next/head'; -import Script from 'next/script'; -import { useRouter } from 'next/router'; -import ErrorBoundary from 'components/common/ErrorBoundary'; -import useLocale from 'components/hooks/useLocale'; -import '@fontsource/inter/400.css'; -import '@fontsource/inter/700.css'; -import 'react-basics/dist/styles.css'; -import 'styles/variables.css'; -import 'styles/locale.css'; -import 'styles/index.css'; -import 'chartjs-adapter-date-fns'; - -const client = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - refetchOnWindowFocus: false, - }, - }, -}); - -export default function App({ Component, pageProps }) { - const { locale, messages } = useLocale(); - const { basePath, pathname } = useRouter(); - - return ( - - null}> - - - - - - - - - - - - - - - - - {!pathname.includes('/share/') &&