+ {hasData ? (typeof children === 'function' ? children(result) : children) : null}
+ {isLoading && }
+ {!isLoading && !hasData && !query && }
+ {noResults && }
+
+ {allowPaging && hasData && (
+
diff --git a/src/components/common/Pager.js b/src/components/common/Pager.js
index 7a5e7ed5ff..a21d35d98f 100644
--- a/src/components/common/Pager.js
+++ b/src/components/common/Pager.js
@@ -1,14 +1,15 @@
-import styles from './Pager.module.css';
-import { Button, Flexbox, Icon, Icons } from 'react-basics';
+import classNames from 'classnames';
+import { Button, Icon, Icons } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
+import styles from './Pager.module.css';
-export function Pager({ page, pageSize, count, onPageChange }) {
+export function Pager({ page, pageSize, count, onPageChange, className }) {
const { formatMessage, labels } = useMessages();
- const maxPage = Math.ceil(count / pageSize);
+ const maxPage = pageSize && count ? Math.ceil(count / pageSize) : 0;
const lastPage = page === maxPage;
const firstPage = page === 1;
- if (count === 0) {
+ if (count === 0 || !maxPage) {
return null;
}
@@ -24,21 +25,25 @@ export function Pager({ page, pageSize, count, onPageChange }) {
}
return (
-
-
-
- {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 99eb70ce0a..880c1b401d 100644
--- a/src/components/common/Pager.module.css
+++ b/src/components/common/Pager.module.css
@@ -1,7 +1,32 @@
-.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;
+}
+
+.count {
+ color: var(--base600);
+ font-weight: 700;
+}
+
+@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 701dbe13b6..0000000000
--- 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 fd6cddfad4..0000000000
--- 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 23907948cf..509df95ce9 100644
--- a/src/components/common/UpdateNotice.js
+++ b/src/components/common/UpdateNotice.js
@@ -1,17 +1,18 @@
+'use client';
import { useEffect, useCallback, useState } from 'react';
import { createPortal } from 'react-dom';
-import { Button, Row, Column } from 'react-basics';
+import { Button } from 'react-basics';
import { setItem } from 'next-basics';
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 &&
@@ -46,17 +47,17 @@ export function UpdateNotice({ user, config }) {
}
return createPortal(
-
-
+
+
{formatMessage(messages.newVersionAvailable, { version: `v${latest}` })}
-
-
+
+
-
- ,
+
+
,
document.body,
);
}
diff --git a/src/components/common/WorldMap.js b/src/components/common/WorldMap.js
index 6ae84677e1..ff34d5f2b0 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/index.js b/src/components/hooks/index.js
index 2596ba57ef..697d54c3f5 100644
--- a/src/components/hooks/index.js
+++ b/src/components/hooks/index.js
@@ -10,7 +10,7 @@ export * from './useFormat';
export * from './useLanguageNames';
export * from './useLocale';
export * from './useMessages';
-export * from './usePageQuery';
+export * from './useNavigation';
export * from './useReport';
export * from './useReports';
export * from './useRequireLogin';
@@ -20,4 +20,3 @@ export * from './useTheme';
export * from './useTimezone';
export * from './useUser';
export * from './useWebsite';
-export * from './useWebsiteReports';
diff --git a/src/components/hooks/useApi.ts b/src/components/hooks/useApi.ts
index f41547a9e9..75a928d535 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 51cabf34cf..40611865c6 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 0000000000..37c28b7e63
--- /dev/null
+++ b/src/components/hooks/useFilterQuery.ts
@@ -0,0 +1,27 @@
+import { useState } from 'react';
+import { useApi } from 'components/hooks/useApi';
+import { UseQueryOptions } from '@tanstack/react-query';
+
+export function useFilterQuery(key: any[], fn, options?: UseQueryOptions) {
+ const [params, setParams] = useState({
+ query: '',
+ page: 1,
+ });
+ const { useQuery } = useApi();
+
+ const { data, ...other } = useQuery([...key, params], fn.bind(null, params), options);
+
+ return {
+ result: data as {
+ page: number;
+ pageSize: number;
+ count: number;
+ data: any[];
+ },
+ ...other,
+ params,
+ setParams,
+ };
+}
+
+export default useFilterQuery;
diff --git a/src/components/hooks/useLanguageNames.js b/src/components/hooks/useLanguageNames.js
index ff59e93dcf..3823a26bd8 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 1374af81f5..71574d86b5 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/useNavigation.js b/src/components/hooks/useNavigation.js
new file mode 100644
index 0000000000..658e81ed05
--- /dev/null
+++ b/src/components/hooks/useNavigation.js
@@ -0,0 +1,27 @@
+import { useMemo } from 'react';
+import { usePathname, useRouter, useSearchParams } from 'next/navigation';
+import { buildUrl } from 'next-basics';
+
+export function useNavigation() {
+ const router = useRouter();
+ const pathname = usePathname();
+ const params = useSearchParams();
+
+ const query = useMemo(() => {
+ const obj = {};
+
+ for (const [key, value] of params.entries()) {
+ obj[key] = decodeURIComponent(value);
+ }
+
+ return obj;
+ }, [params]);
+
+ function makeUrl(params, reset) {
+ return reset ? pathname : buildUrl(pathname, { ...query, ...params });
+ }
+
+ return { pathname, query, router, makeUrl };
+}
+
+export default useNavigation;
diff --git a/src/components/hooks/usePageQuery.js b/src/components/hooks/usePageQuery.js
deleted file mode 100644
index b275d5807c..0000000000
--- a/src/components/hooks/usePageQuery.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useMemo } from 'react';
-import { useRouter } from 'next/router';
-import { buildUrl } from 'next-basics';
-
-export function usePageQuery() {
- const router = useRouter();
- const { pathname, search } = location;
- const { asPath } = router;
-
- const query = useMemo(() => {
- if (!search) {
- return {};
- }
-
- const params = search.substring(1).split('&');
-
- return params.reduce((obj, item) => {
- const [key, value] = item.split('=');
-
- obj[key] = decodeURIComponent(value);
-
- return obj;
- }, {});
- }, [search]);
-
- function resolveUrl(params, reset) {
- return buildUrl(asPath.split('?')[0], { ...(reset ? {} : query), ...params });
- }
-
- return { pathname, query, resolveUrl, router };
-}
-
-export default usePageQuery;
diff --git a/src/components/hooks/useRequireLogin.ts b/src/components/hooks/useRequireLogin.ts
index d2f540d45a..76460a5574 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/hooks/useShareToken.js b/src/components/hooks/useShareToken.js
index 3d6b9698b6..5062c73ec2 100644
--- a/src/components/hooks/useShareToken.js
+++ b/src/components/hooks/useShareToken.js
@@ -1,4 +1,3 @@
-import { useEffect } from 'react';
import useStore, { setShareToken } from 'store/app';
import useApi from './useApi';
@@ -6,23 +5,16 @@ const selector = state => state.shareToken;
export function useShareToken(shareId) {
const shareToken = useStore(selector);
- const { get } = useApi();
+ const { get, useQuery } = useApi();
+ const { isLoading, error } = useQuery(['share', shareId], async () => {
+ const data = await get(`/share/${shareId}`);
- async function loadToken(id) {
- const data = await get(`/share/${id}`);
+ setShareToken(data);
- if (data) {
- setShareToken(data);
- }
- }
+ return data;
+ });
- useEffect(() => {
- if (shareId) {
- loadToken(shareId);
- }
- }, [shareId]);
-
- return shareToken;
+ return { shareToken, isLoading, error };
}
export default useShareToken;
diff --git a/src/components/hooks/useWebsiteReports.js b/src/components/hooks/useWebsiteReports.js
deleted file mode 100644
index c637bc76a4..0000000000
--- a/src/components/hooks/useWebsiteReports.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { useState } from 'react';
-import useApi from './useApi';
-import useApiFilter from 'components/hooks/useApiFilter';
-
-export function useWebsiteReports(websiteId) {
- const [modified, setModified] = useState(Date.now());
- const { get, useQuery, del, useMutation } = useApi();
- const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
- const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
- useApiFilter();
- const { data, error, isLoading } = useQuery(
- ['reports:website', { websiteId, modified, filter, page, pageSize }],
- () => get(`/websites/${websiteId}/reports`, { websiteId, filter, page, pageSize }),
- );
-
- const deleteReport = id => {
- mutate(id, {
- onSuccess: () => {
- setModified(Date.now());
- },
- });
- };
-
- return {
- reports: data,
- error,
- isLoading,
- deleteReport,
- filter,
- page,
- pageSize,
- handleFilterChange,
- handlePageChange,
- handlePageSizeChange,
- };
-}
-
-export default useWebsiteReports;
diff --git a/src/components/input/LanguageButton.module.css b/src/components/input/LanguageButton.module.css
index 3d4c0c5697..cc5d649a10 100644
--- a/src/components/input/LanguageButton.module.css
+++ b/src/components/input/LanguageButton.module.css
@@ -1,7 +1,6 @@
.menu {
- display: flex;
- flex-flow: row wrap;
- min-width: 640px;
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
padding: 10px;
background: var(--base50);
z-index: var(--z-index-popup);
@@ -14,7 +13,7 @@
display: flex;
align-items: center;
justify-content: space-between;
- min-width: calc(100% / 3);
+ min-width: 200px;
border-radius: 5px;
padding: 5px 10px;
}
@@ -32,3 +31,15 @@
.icon {
color: var(--primary400);
}
+
+@media screen and (max-width: 992px) {
+ .menu {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media screen and (max-width: 768px) {
+ .menu {
+ transform: translateX(40px);
+ }
+}
diff --git a/src/components/input/LogoutButton.js b/src/components/input/LogoutButton.js
index 2b04a78a27..6ca358a121 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 (
-
+
- );
-}
-
-export default AppLayout;
diff --git a/src/components/layout/Grid.js b/src/components/layout/Grid.js
index 0276063b5d..86b08887b2 100644
--- a/src/components/layout/Grid.js
+++ b/src/components/layout/Grid.js
@@ -1,13 +1,18 @@
-import { Row, Column } from 'react-basics';
import classNames from 'classnames';
+import { mapChildren } from 'react-basics';
import styles from './Grid.module.css';
-export function GridRow(props) {
- const { className, ...otherProps } = props;
- return
{title &&
{title}
}
diff --git a/src/components/layout/ReportsLayout.js b/src/components/layout/ReportsLayout.js
deleted file mode 100644
index 374da26352..0000000000
--- a/src/components/layout/ReportsLayout.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Column, Row } from 'react-basics';
-import styles from './ReportsLayout.module.css';
-
-export function ReportsLayout({ children, filter, header }) {
- return (
- <>
-
{header}
-
- {filter && (
-
- Filters
- {filter}
-
- )}
-
- {children}
-
-
- >
- );
-}
-
-export default ReportsLayout;
diff --git a/src/components/layout/ReportsLayout.module.css b/src/components/layout/ReportsLayout.module.css
deleted file mode 100644
index 6922665fa2..0000000000
--- a/src/components/layout/ReportsLayout.module.css
+++ /dev/null
@@ -1,23 +0,0 @@
-.filter {
- margin-top: 30px;
- min-width: 200px;
- max-width: 100vw;
- padding: 10px;
- background: var(--base50);
- border-radius: 5px;
- border: 1px solid var(--border-color);
-}
-
-.filter h2 {
- padding-bottom: 20px;
-}
-
-.content {
- min-height: 50vh;
-}
-
-@media only screen and (max-width: 768px) {
- .menu {
- display: none;
- }
-}
diff --git a/src/components/layout/SettingsLayout.module.css b/src/components/layout/SettingsLayout.module.css
deleted file mode 100644
index 08ff02aa65..0000000000
--- a/src/components/layout/SettingsLayout.module.css
+++ /dev/null
@@ -1,20 +0,0 @@
-.menu {
- display: flex;
- flex-direction: column;
- padding-top: 40px;
- padding-right: 20px;
-}
-
-.content {
- min-height: 50vh;
-}
-
-@media only screen and (max-width: 768px) {
- .menu {
- display: none;
- }
-
- .content {
- margin-top: 20px;
- }
-}
diff --git a/src/components/layout/ShareLayout.js b/src/components/layout/ShareLayout.js
deleted file mode 100644
index c634e1b6d5..0000000000
--- a/src/components/layout/ShareLayout.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Container } from 'react-basics';
-import Header from './Header';
-import Footer from './Footer';
-
-export function ShareLayout({ children }) {
- return (
-
-
- {children}
-
-
- );
-}
-
-export default ShareLayout;
diff --git a/src/components/layout/SideNav.js b/src/components/layout/SideNav.js
index ccb6f360cd..c93881e475 100644
--- a/src/components/layout/SideNav.js
+++ b/src/components/layout/SideNav.js
@@ -1,6 +1,6 @@
import classNames from 'classnames';
import { Menu, Item } from 'react-basics';
-import { useRouter } from 'next/router';
+import { usePathname } from 'next/navigation';
import Link from 'next/link';
import styles from './SideNav.module.css';
@@ -9,15 +9,21 @@ export function SideNav({
items,
shallow = true,
scroll = false,
+ className,
onSelect = () => {},
}) {
- const { asPath } = useRouter();
+ const pathname = usePathname();
return (
-