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 0a69e5e3..f2a5d9b9 100644 --- a/package.json +++ b/package.json @@ -61,16 +61,17 @@ ".next/cache" ], "dependencies": { + "@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", "chartjs-adapter-date-fns": "^3.0.0", "classnames": "^2.3.1", - "clickhouse": "^2.5.0", "colord": "^2.9.2", "cors": "^2.8.5", "cross-spawn": "^7.0.3", @@ -91,18 +92,17 @@ "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", "react": "^18.2.0", - "react-basics": "^0.100.0", + "react-basics": "^0.102.0", "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.31", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^15.1.0", diff --git a/public/images/os/windows-mobile.png b/public/images/os/windows-mobile.png new file mode 100644 index 00000000..4a899a30 Binary files /dev/null and b/public/images/os/windows-mobile.png differ diff --git a/rollup.components.config.mjs b/rollup.components.config.mjs index c4481d0e..9be07390 100644 --- a/rollup.components.config.mjs +++ b/rollup.components.config.mjs @@ -19,6 +19,7 @@ const customResolver = resolve({ const aliasConfig = { entries: [ + { find: /^app/, replacement: path.resolve('./src/app') }, { find: /^components/, replacement: path.resolve('./src/components') }, { find: /^hooks/, replacement: path.resolve('./src/hooks') }, { find: /^lib/, replacement: path.resolve('./src/lib') }, diff --git a/src/app/(main)/NavBar.js b/src/app/(main)/NavBar.js new file mode 100644 index 00000000..211adf5f --- /dev/null +++ b/src/app/(main)/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/(main)/NavBar.module.css similarity index 75% rename from src/components/layout/NavBar.module.css rename to src/app/(main)/NavBar.module.css index dd5085a0..fd022eca 100644 --- a/src/components/layout/NavBar.module.css +++ b/src/app/(main)/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/(main)/Shell.tsx b/src/app/(main)/Shell.tsx new file mode 100644 index 00000000..980abb62 --- /dev/null +++ b/src/app/(main)/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/(main)/settings/websites/[id]/WebsiteData.js similarity index 89% rename from src/components/pages/settings/websites/WebsiteData.js rename to src/app/(main)/settings/websites/[id]/WebsiteData.js index 08d6702e..07dc9257 100644 --- a/src/components/pages/settings/websites/WebsiteData.js +++ b/src/app/(main)/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/(main)/settings/websites/[id]/WebsiteDeleteForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteDeleteForm.js rename to src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.js diff --git a/src/components/pages/settings/websites/WebsiteEditForm.js b/src/app/(main)/settings/websites/[id]/WebsiteEditForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteEditForm.js rename to src/app/(main)/settings/websites/[id]/WebsiteEditForm.js diff --git a/src/components/pages/settings/websites/WebsiteResetForm.js b/src/app/(main)/settings/websites/[id]/WebsiteResetForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteResetForm.js rename to src/app/(main)/settings/websites/[id]/WebsiteResetForm.js diff --git a/src/app/(main)/settings/websites/[id]/page.js b/src/app/(main)/settings/websites/[id]/page.js new file mode 100644 index 00000000..37324659 --- /dev/null +++ b/src/app/(main)/settings/websites/[id]/page.js @@ -0,0 +1,9 @@ +import WebsiteSettings from '../WebsiteSettings'; + +export default async function WebsiteSettingsPage({ params: { id } }) { + if (process.env.cloudMode) { + return null; + } + + return ; +} diff --git a/src/app/(main)/settings/websites/page.tsx b/src/app/(main)/settings/websites/page.tsx new file mode 100644 index 00000000..2c83dce0 --- /dev/null +++ b/src/app/(main)/settings/websites/page.tsx @@ -0,0 +1,16 @@ +import WebsitesDataTable from './WebsitesDataTable'; +import WebsitesHeader from './WebsitesHeader'; +import { Metadata } from 'next'; + +export default function () { + return ( + <> + + + + ); +} + +export const metadata: Metadata = { + title: 'Websites Settings | umami', +}; diff --git a/src/app/(main)/websites/WebsiteTableView.js b/src/app/(main)/websites/WebsiteTableView.js new file mode 100644 index 00000000..7c71b84b --- /dev/null +++ b/src/app/(main)/websites/WebsiteTableView.js @@ -0,0 +1,41 @@ +import { useState } from 'react'; +import { Grid, GridRow } from 'components/layout/Grid'; +import PagesTable from 'components/metrics/PagesTable'; +import ReferrersTable from 'components/metrics/ReferrersTable'; +import BrowsersTable from 'components/metrics/BrowsersTable'; +import OSTable from 'components/metrics/OSTable'; +import DevicesTable from 'components/metrics/DevicesTable'; +import WorldMap from 'components/common/WorldMap'; +import CountriesTable from 'components/metrics/CountriesTable'; +import EventsTable from 'components/metrics/EventsTable'; +import EventsChart from 'components/metrics/EventsChart'; + +export default function WebsiteTableView({ websiteId }) { + const [countryData, setCountryData] = useState(); + const tableProps = { + websiteId, + limit: 10, + }; + + return ( + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/app/(main)/websites/WebsitesBrowse.js b/src/app/(main)/websites/WebsitesBrowse.js new file mode 100644 index 00000000..f1bab7bf --- /dev/null +++ b/src/app/(main)/websites/WebsitesBrowse.js @@ -0,0 +1,31 @@ +'use client'; +import WebsitesDataTable from '../settings/websites/WebsitesDataTable'; +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); + const allowEdit = !process.env.cloudMode; + + 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/(main)/websites/[id]/WebsiteChart.js similarity index 92% rename from src/components/pages/websites/WebsiteChart.js rename to src/app/(main)/websites/[id]/WebsiteChart.js index 7e20e785..d05ff422 100644 --- a/src/components/pages/websites/WebsiteChart.js +++ b/src/app/(main)/websites/[id]/WebsiteChart.js @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import PageviewsChart from 'components/metrics/PageviewsChart'; -import { useApi, useDateRange, useTimezone, usePageQuery } from 'components/hooks'; +import { useApi, useDateRange, useTimezone, useNavigation } from 'components/hooks'; import { getDateArray } from 'lib/date'; export function WebsiteChart({ websiteId }) { @@ -9,7 +9,7 @@ export function WebsiteChart({ websiteId }) { const [timezone] = useTimezone(); const { query: { url, referrer, os, browser, device, country, region, city, title }, - } = usePageQuery(); + } = useNavigation(); const { get, useQuery } = useApi(); const { data, isLoading } = useQuery( diff --git a/src/components/pages/websites/WebsiteChart.module.css b/src/app/(main)/websites/[id]/WebsiteChart.module.css similarity index 100% rename from src/components/pages/websites/WebsiteChart.module.css rename to src/app/(main)/websites/[id]/WebsiteChart.module.css diff --git a/src/components/pages/websites/WebsiteChartList.js b/src/app/(main)/websites/[id]/WebsiteChartList.js similarity index 90% rename from src/components/pages/websites/WebsiteChartList.js rename to src/app/(main)/websites/[id]/WebsiteChartList.js index 56cbe157..23764dbb 100644 --- a/src/components/pages/websites/WebsiteChartList.js +++ b/src/app/(main)/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.back)} + - - +
+
- - +
+
); } diff --git a/src/app/(main)/websites/[id]/WebsiteMenuView.module.css b/src/app/(main)/websites/[id]/WebsiteMenuView.module.css new file mode 100644 index 00000000..5710ac73 --- /dev/null +++ b/src/app/(main)/websites/[id]/WebsiteMenuView.module.css @@ -0,0 +1,25 @@ +.layout { + display: grid; + grid-template-columns: 300px 1fr; + border-top: 1px solid var(--base300); +} + +.menu { + display: flex; + flex-direction: column; + position: relative; + padding: 20px 20px 20px 0; +} + +.back { + display: inline-flex; + align-items: center; + align-self: center; + margin-bottom: 20px; +} + +.content { + min-height: 800px; + padding: 20px 0 20px 20px; + border-left: 1px solid var(--base300); +} diff --git a/src/components/pages/websites/WebsiteMetricsBar.js b/src/app/(main)/websites/[id]/WebsiteMetricsBar.js similarity index 55% rename from src/components/pages/websites/WebsiteMetricsBar.js rename to src/app/(main)/websites/[id]/WebsiteMetricsBar.js index 7ba4a801..3fba284e 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.js +++ b/src/app/(main)/websites/[id]/WebsiteMetricsBar.js @@ -1,12 +1,12 @@ import classNames from 'classnames'; -import { useApi, useDateRange, useMessages, usePageQuery, useSticky } from 'components/hooks'; +import { useApi, useDateRange, useMessages, useNavigation, useSticky } from 'components/hooks'; import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; import MetricCard from 'components/metrics/MetricCard'; import MetricsBar from 'components/metrics/MetricsBar'; -import FilterSelectForm from 'components/pages/reports/FilterSelectForm'; -import PopupForm from 'components/pages/reports/PopupForm'; +import FilterSelectForm from 'app/(main)/reports/[id]/FilterSelectForm'; +import PopupForm from 'app/(main)/reports/[id]/PopupForm'; import { formatShortTime } from 'lib/format'; -import { Button, Column, Icon, Icons, Popup, PopupTrigger, Row } from 'react-basics'; +import { Button, Icon, Icons, Popup, PopupTrigger } from 'react-basics'; import styles from './WebsiteMetricsBar.module.css'; export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { @@ -17,10 +17,10 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { const { startDate, endDate, modified } = dateRange; const { ref, isSticky } = useSticky({ enabled: sticky }); const { - resolveUrl, + makeUrl, router, query: { url, referrer, title, os, browser, device, country, region, city }, - } = usePageQuery(); + } = useNavigation(); const { data, error, isLoading, isFetched } = useQuery( [ @@ -64,7 +64,7 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { }; const handleAddFilter = ({ name, value }) => { - router.push(resolveUrl({ [name]: value })); + router.push(makeUrl({ [name]: value })); }; const WebsiteFilterButton = () => { @@ -98,72 +98,64 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { }; return ( - - - - {!error && isFetched && ( - <> - - - Number(n).toFixed(0) + '%'} - reverseColors - /> - - `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}` - } - /> - - )} - - - -
- {showFilter && } - -
-
-
+ + {pageviews && uniques && ( + <> + + + Number(n).toFixed(0) + '%'} + reverseColors + /> + `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`} + /> + + )} + +
+ {showFilter && } + +
+
); } diff --git a/src/components/pages/websites/WebsiteMetricsBar.module.css b/src/app/(main)/websites/[id]/WebsiteMetricsBar.module.css similarity index 70% rename from src/components/pages/websites/WebsiteMetricsBar.module.css rename to src/app/(main)/websites/[id]/WebsiteMetricsBar.module.css index 52decfc6..4d642d70 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.module.css +++ b/src/app/(main)/websites/[id]/WebsiteMetricsBar.module.css @@ -1,12 +1,12 @@ .container { - display: flex; + display: grid; + grid-template-columns: 1fr max-content; justify-content: space-between; align-items: center; - padding: 10px 0; - min-height: 90px; - margin-bottom: 20px; background: var(--base50); z-index: var(--z-index-above); + min-height: 120px; + padding-bottom: 20px; } .actions { @@ -18,8 +18,12 @@ } @media only screen and (max-width: 1200px) { + .container { + grid-template-columns: 1fr; + } + .actions { - margin-top: 40px; + margin: 20px 0; } } @@ -30,6 +34,7 @@ } .isSticky { + padding: 10px 0; border-bottom: 1px solid var(--base300); } } diff --git a/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.js b/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.js new file mode 100644 index 00000000..5be19185 --- /dev/null +++ b/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.js @@ -0,0 +1,38 @@ +import { useApi, useDateRange } from 'components/hooks'; +import MetricCard from 'components/metrics/MetricCard'; +import useMessages from 'components/hooks/useMessages'; +import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; +import MetricsBar from 'components/metrics/MetricsBar'; +import styles from './EventDataMetricsBar.module.css'; + +export function EventDataMetricsBar({ websiteId }) { + const { formatMessage, labels } = useMessages(); + const { get, useQuery } = useApi(); + const [dateRange] = useDateRange(websiteId); + const { startDate, endDate, modified } = dateRange; + + const { data, error, isLoading, isFetched } = useQuery( + ['event-data:stats', { websiteId, startDate, endDate, modified }], + () => + get(`/event-data/stats`, { + websiteId, + startAt: +startDate, + endAt: +endDate, + }), + ); + + return ( +
+ + + + + +
+ +
+
+ ); +} + +export default EventDataMetricsBar; diff --git a/src/components/pages/event-data/EventDataMetricsBar.module.css b/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.module.css similarity index 52% rename from src/components/pages/event-data/EventDataMetricsBar.module.css rename to src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.module.css index 43b14580..408396c3 100644 --- a/src/components/pages/event-data/EventDataMetricsBar.module.css +++ b/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.module.css @@ -1,5 +1,6 @@ .container { - display: flex; + display: grid; + grid-template-columns: 1fr 1fr; justify-content: space-between; align-items: center; padding: 10px 0; @@ -9,12 +10,6 @@ z-index: var(--z-index-above); } -.metrics { - display: flex; - flex-direction: row; - align-items: center; -} - .actions { display: flex; flex-direction: row; @@ -23,24 +18,9 @@ flex: 1; } -.bar { - display: flex; - cursor: pointer; - min-height: 110px; - gap: 20px; - flex-wrap: wrap; -} - -.card { - justify-self: flex-start; -} - @media only screen and (max-width: 992px) { - .card { - flex-basis: calc(50% - 20px); + .container { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; } } - -.row { - border-bottom: 1px solid var(--border-color); -} diff --git a/src/components/pages/event-data/EventDataTable.js b/src/app/(main)/websites/[id]/event-data/EventDataTable.js similarity index 84% rename from src/components/pages/event-data/EventDataTable.js rename to src/app/(main)/websites/[id]/event-data/EventDataTable.js index c79916ce..fb98e7e7 100644 --- a/src/components/pages/event-data/EventDataTable.js +++ b/src/app/(main)/websites/[id]/event-data/EventDataTable.js @@ -1,12 +1,12 @@ import Link from 'next/link'; import { GridTable, GridColumn } from 'react-basics'; -import { useMessages, usePageQuery } from 'components/hooks'; +import { useMessages, useNavigation } from 'components/hooks'; import Empty from 'components/common/Empty'; import { DATA_TYPES } from 'lib/constants'; export function EventDataTable({ data = [] }) { const { formatMessage, labels } = useMessages(); - const { resolveUrl } = usePageQuery(); + const { makeUrl } = useNavigation(); if (data.length === 0) { return ; @@ -16,7 +16,7 @@ export function EventDataTable({ data = [] }) { {row => ( - + {row.eventName} )} diff --git a/src/components/pages/event-data/EventDataValueTable.js b/src/app/(main)/websites/[id]/event-data/EventDataValueTable.js similarity index 89% rename from src/components/pages/event-data/EventDataValueTable.js rename to src/app/(main)/websites/[id]/event-data/EventDataValueTable.js index 75c11e32..4e50f5b9 100644 --- a/src/components/pages/event-data/EventDataValueTable.js +++ b/src/app/(main)/websites/[id]/event-data/EventDataValueTable.js @@ -1,5 +1,5 @@ import { GridTable, GridColumn, Button, Icon, Text } from 'react-basics'; -import { useMessages, usePageQuery } from 'components/hooks'; +import { useMessages, useNavigation } from 'components/hooks'; import Link from 'next/link'; import Icons from 'components/icons'; import PageHeader from 'components/layout/PageHeader'; @@ -8,12 +8,12 @@ import { DATA_TYPES } from 'lib/constants'; export function EventDataValueTable({ data = [], event }) { const { formatMessage, labels } = useMessages(); - const { resolveUrl } = usePageQuery(); + const { makeUrl } = useNavigation(); const Title = () => { 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 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..509df95c 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 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/index.js b/src/components/hooks/index.js index 2596ba57..79c0937c 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'; 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..1879180d --- /dev/null +++ b/src/components/hooks/useFilterQuery.ts @@ -0,0 +1,26 @@ +import { 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 { 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 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/useNavigation.js b/src/components/hooks/useNavigation.js new file mode 100644 index 00000000..658e81ed --- /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 b275d580..00000000 --- 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 d2f540d4..76460a55 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 3d6b9698..5062c73e 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/input/LanguageButton.module.css b/src/components/input/LanguageButton.module.css index 3d4c0c56..cc5d649a 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 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 ( - + - +
)} - +
); } diff --git a/src/components/input/WebsiteDateFilter.module.css b/src/components/input/WebsiteDateFilter.module.css index 986f5c17..6f2e822d 100644 --- a/src/components/input/WebsiteDateFilter.module.css +++ b/src/components/input/WebsiteDateFilter.module.css @@ -1,7 +1,17 @@ +.container { + display: flex; + align-items: center; + gap: 10px; +} + .dropdown { min-width: 200px; } +.buttons { + display: flex; +} + .buttons button:first-child { border-top-right-radius: 0; border-bottom-right-radius: 0; diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js deleted file mode 100644 index 41e2ec0d..00000000 --- a/src/components/layout/AppLayout.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Container } from 'react-basics'; -import Head from 'next/head'; -import NavBar from 'components/layout/NavBar'; -import UpdateNotice from 'components/common/UpdateNotice'; -import { useRequireLogin, useConfig } from 'components/hooks'; -import styles from './AppLayout.module.css'; - -export function AppLayout({ title, children }) { - const { user } = useRequireLogin(); - const config = useConfig(); - - if (!user || !config || config?.uiDisabled) { - return null; - } - - return ( -
- - - {title ? `${title} | umami` : 'umami'} - - -
- {children} -
-
- ); -} - -export default AppLayout; diff --git a/src/components/layout/Grid.js b/src/components/layout/Grid.js index 0276063b..86b08887 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 ; +export function Grid({ className, ...otherProps }) { + return
; } -export function GridColumn(props) { - const { className, ...otherProps } = props; - return ; +export function GridRow(props) { + const { columns = 'two', className, children, ...otherProps } = props; + return ( +
+ {mapChildren(children, child => { + return
{child}
; + })} +
+ ); } diff --git a/src/components/layout/Grid.module.css b/src/components/layout/Grid.module.css index dc2e8ff6..f72a5f12 100644 --- a/src/components/layout/Grid.module.css +++ b/src/components/layout/Grid.module.css @@ -1,27 +1,52 @@ -.col { - display: flex; - flex-direction: column; - padding: 20px; +.grid { + display: grid; } .row { + display: grid; + grid-template-columns: repeat(6, 1fr); border-top: 1px solid var(--base300); - min-height: 430px; } -.row > .col { +.col { + padding: 20px; + min-height: 430px; border-inline-start: 1px solid var(--base300); } -.row > .col:first-child { +.col:first-child { border-inline-start: 0; padding-inline-start: 0; } -.row > .col:last-child { +.col:last-child { padding-inline-end: 0; } +.col.two { + grid-column: span 3; +} + +.col.three { + grid-column: span 2; +} + +.col.two-one:first-child { + grid-column: span 4; +} + +.col.two-one:last-child { + grid-column: span 2; +} + +.col.one-two:first-child { + grid-column: span 2; +} + +.col.one-two:last-child { + grid-column: span 4; +} + @media only screen and (max-width: 992px) { .row { border: 0; @@ -33,4 +58,11 @@ border-inline-end: 0; padding: 20px 0; } + + .col.two, + .col.three, + .col.one-two, + .col.two-one { + grid-column: span 6 !important; + } } diff --git a/src/components/layout/Header.js b/src/components/layout/Header.js deleted file mode 100644 index 21cdd251..00000000 --- a/src/components/layout/Header.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Column, Icon, Row, Text } from 'react-basics'; -import Link from 'next/link'; -import LanguageButton from 'components/input/LanguageButton'; -import ThemeButton from 'components/input/ThemeButton'; -import SettingsButton from 'components/input/SettingsButton'; -import Icons from 'components/icons'; -import styles from './Header.module.css'; - -export function Header() { - return ( -
- - - - - - - umami - - - - - - - - -
- ); -} - -export default Header; diff --git a/src/components/layout/NavBar.js b/src/components/layout/NavBar.js deleted file mode 100644 index 07627e2a..00000000 --- a/src/components/layout/NavBar.js +++ /dev/null @@ -1,63 +0,0 @@ -import { Icon, Text, Row, Column } from 'react-basics'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; -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 styles from './NavBar.module.css'; - -export function NavBar() { - const { pathname } = useRouter(); - 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/NavGroup.js b/src/components/layout/NavGroup.js index 94f9d8e6..361dffb5 100644 --- a/src/components/layout/NavGroup.js +++ b/src/components/layout/NavGroup.js @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Icon, Text, TooltipPopup } from 'react-basics'; import classNames from 'classnames'; -import { useRouter } from 'next/router'; +import { usePathname } from 'next/navigation'; import Link from 'next/link'; import Icons from 'components/icons'; import styles from './NavGroup.module.css'; @@ -13,7 +13,7 @@ export function NavGroup({ allowExpand = true, minimized = false, }) { - const { pathname } = useRouter(); + const pathname = usePathname(); const [expanded, setExpanded] = useState(defaultExpanded); const handleExpand = () => setExpanded(state => !state); diff --git a/src/components/layout/Page.module.css b/src/components/layout/Page.module.css index c546971b..73f21979 100644 --- a/src/components/layout/Page.module.css +++ b/src/components/layout/Page.module.css @@ -2,6 +2,10 @@ flex: 1; display: flex; flex-direction: column; - background: var(--base50); position: relative; + width: 100%; + max-width: 1320px; + min-height: calc(100vh - 60px); + margin: 0 auto; + padding: 0 20px; } diff --git a/src/components/layout/Page.js b/src/components/layout/Page.tsx similarity index 68% rename from src/components/layout/Page.js rename to src/components/layout/Page.tsx index 4f42aa55..2f702012 100644 --- a/src/components/layout/Page.js +++ b/src/components/layout/Page.tsx @@ -1,16 +1,28 @@ +'use client'; +import { ReactNode } from 'react'; import classNames from 'classnames'; import { Banner, Loading } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; import styles from './Page.module.css'; -export function Page({ className, error, loading, children }) { +export function Page({ + className, + error, + isLoading, + children, +}: { + className?: string; + error?: unknown; + isLoading?: boolean; + children?: ReactNode; +}) { const { formatMessage, messages } = useMessages(); if (error) { return {formatMessage(messages.error)}; } - if (loading) { + if (isLoading) { return ; } diff --git a/src/components/layout/PageHeader.js b/src/components/layout/PageHeader.tsx similarity index 58% rename from src/components/layout/PageHeader.js rename to src/components/layout/PageHeader.tsx index f1363140..c92a89a0 100644 --- a/src/components/layout/PageHeader.js +++ b/src/components/layout/PageHeader.tsx @@ -1,8 +1,14 @@ import classNames from 'classnames'; -import React from 'react'; +import React, { ReactNode } from 'react'; import styles from './PageHeader.module.css'; -export function PageHeader({ title, children, className }) { +export interface PageHeaderProps { + title?: string; + className?: string; + children?: ReactNode; +} + +export function PageHeader({ title, className, children }: PageHeaderProps) { return (
{title &&
{title}
} diff --git a/src/components/layout/ReportsLayout.js b/src/components/layout/ReportsLayout.js deleted file mode 100644 index 374da263..00000000 --- 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 6922665f..00000000 --- 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 08ff02aa..00000000 --- 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 c634e1b6..00000000 --- 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}
-