diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03dc6..fd36f9494e 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 cc3cde7c68..cf7dce7fb1 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 79960eb234..8d80de2fd3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.7.0", + "version": "2.8.0", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Mike Cao ", "license": "MIT", @@ -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.105.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,9 +138,9 @@ "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", - "postcss": "^8.4.21", + "husky": "^8.0.3", + "lint-staged": "^14.0.1", + "postcss": "^8.4.31", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^15.1.0", "postcss-preset-env": "7.8.3", diff --git a/public/images/os/windows-mobile.png b/public/images/os/windows-mobile.png new file mode 100644 index 0000000000..4a899a30f8 Binary files /dev/null and b/public/images/os/windows-mobile.png differ diff --git a/public/intl/messages/es-ES.json b/public/intl/messages/es-ES.json index 0e1316218c..5fd90efd83 100644 --- a/public/intl/messages/es-ES.json +++ b/public/intl/messages/es-ES.json @@ -104,7 +104,7 @@ "label.browser": [ { "type": 0, - "value": "Browser" + "value": "Navegador" } ], "label.browsers": [ @@ -134,7 +134,7 @@ "label.city": [ { "type": 0, - "value": "City" + "value": "Ciudad" } ], "label.clear-all": [ @@ -176,19 +176,19 @@ "label.country": [ { "type": 0, - "value": "Country" + "value": "País" } ], "label.create": [ { "type": 0, - "value": "Create" + "value": "Crear" } ], "label.create-report": [ { "type": 0, - "value": "Crear reporte" + "value": "Crear informe" } ], "label.create-team": [ @@ -236,7 +236,7 @@ "label.date": [ { "type": 0, - "value": "Date" + "value": "Fecha" } ], "label.date-range": [ @@ -248,7 +248,7 @@ "label.day": [ { "type": 0, - "value": "Day" + "value": "Día" } ], "label.default-date-range": [ @@ -284,7 +284,7 @@ "label.description": [ { "type": 0, - "value": "Descripciones" + "value": "Descripción" } ], "label.desktop": [ @@ -302,7 +302,7 @@ "label.device": [ { "type": 0, - "value": "Device" + "value": "Dispositivo" } ], "label.devices": [ @@ -314,7 +314,7 @@ "label.dismiss": [ { "type": 0, - "value": "Ignorar" + "value": "Cerrar" } ], "label.does-not-contain": [ @@ -332,7 +332,7 @@ "label.dropoff": [ { "type": 0, - "value": "Dropoff" + "value": "Abandono" } ], "label.edit": [ @@ -374,7 +374,7 @@ "label.false": [ { "type": 0, - "value": "False" + "value": "Falso" } ], "label.field": [ @@ -392,7 +392,7 @@ "label.filter": [ { "type": 0, - "value": "Filter" + "value": "Filtro" } ], "label.filter-combined": [ @@ -422,7 +422,7 @@ "label.funnel-description": [ { "type": 0, - "value": "Understand the conversion and drop-off rate of users." + "value": "Comprender conversión y abandono de usuarios." } ], "label.greater-than": [ @@ -470,7 +470,7 @@ "label.is-set": [ { "type": 0, - "value": "Is set" + "value": "Está establecido" } ], "label.join": [ @@ -600,7 +600,7 @@ "label.my-websites": [ { "type": 0, - "value": "My websites" + "value": "Mis sitios web" } ], "label.name": [ @@ -624,7 +624,7 @@ "label.os": [ { "type": 0, - "value": "OS" + "value": "Sistema" } ], "label.overview": [ @@ -642,7 +642,7 @@ "label.page-of": [ { "type": 0, - "value": "Page " + "value": "Página " }, { "type": 1, @@ -650,7 +650,7 @@ }, { "type": 0, - "value": " of " + "value": " de " }, { "type": 1, @@ -666,7 +666,7 @@ "label.pageTitle": [ { "type": 0, - "value": "Page title" + "value": "Título de página" } ], "label.pages": [ @@ -684,7 +684,7 @@ "label.powered-by": [ { "type": 0, - "value": "Con la ayuda de " + "value": "Analíticas de " }, { "type": 1, @@ -706,7 +706,7 @@ "label.query": [ { "type": 0, - "value": "Query" + "value": "Consulta" } ], "label.query-parameters": [ @@ -724,7 +724,7 @@ "label.referrer": [ { "type": 0, - "value": "Referrer" + "value": "Referido" } ], "label.referrers": [ @@ -766,7 +766,7 @@ "label.reports": [ { "type": 0, - "value": "Reportes" + "value": "Informes" } ], "label.required": [ @@ -784,19 +784,19 @@ "label.reset-website": [ { "type": 0, - "value": "Reiniciar estadísticas" + "value": "Reiniciar analíticas" } ], "label.retention": [ { "type": 0, - "value": "Retention" + "value": "Retención" } ], "label.retention-description": [ { "type": 0, - "value": "Measure your website stickiness by tracking how often users return." + "value": "Medir la frecuencia con la que los usuarios vuelven a tu sitio web." } ], "label.role": [ @@ -826,7 +826,7 @@ "label.search": [ { "type": 0, - "value": "Search" + "value": "Buscar" } ], "label.select-date": [ @@ -850,7 +850,7 @@ "label.settings": [ { "type": 0, - "value": "Configuraciones" + "value": "Ajustes" } ], "label.share-url": [ @@ -892,7 +892,7 @@ "label.team-id": [ { "type": 0, - "value": "ID de equipo" + "value": "ID del equipo" } ], "label.team-member": [ @@ -904,7 +904,7 @@ "label.team-name": [ { "type": 0, - "value": "Team name" + "value": "Nombre del equipo" } ], "label.team-owner": [ @@ -916,7 +916,7 @@ "label.team-websites": [ { "type": 0, - "value": "Team websites" + "value": "Sitios web del equipo" } ], "label.teams": [ @@ -1288,7 +1288,7 @@ "message.new-version-available": [ { "type": 0, - "value": "A new version of Umami " + "value": "Una nueva versión de Umami " }, { "type": 1, @@ -1296,7 +1296,7 @@ }, { "type": 0, - "value": " is available!" + "value": " está disponible" } ], "message.no-data-available": [ @@ -1376,7 +1376,7 @@ "message.saved": [ { "type": 0, - "value": "Guardado." + "value": "Guardado" } ], "message.share-url": [ diff --git a/public/intl/messages/mn-MN.json b/public/intl/messages/mn-MN.json index fd7294f3bf..1a2210453e 100644 --- a/public/intl/messages/mn-MN.json +++ b/public/intl/messages/mn-MN.json @@ -20,13 +20,13 @@ "label.add": [ { "type": 0, - "value": "Add" + "value": "Нэмэх" } ], "label.add-description": [ { "type": 0, - "value": "Add description" + "value": "Тайлбар нэмэх" } ], "label.add-website": [ @@ -44,7 +44,7 @@ "label.after": [ { "type": 0, - "value": "After" + "value": "Хойно" } ], "label.all": [ @@ -68,7 +68,7 @@ "label.average": [ { "type": 0, - "value": "Average" + "value": "Дундаж" } ], "label.average-visit-time": [ @@ -86,7 +86,7 @@ "label.before": [ { "type": 0, - "value": "Before" + "value": "Өмнө" } ], "label.bounce-rate": [ @@ -98,13 +98,13 @@ "label.breakdown": [ { "type": 0, - "value": "Breakdown" + "value": "Задаргаа" } ], "label.browser": [ { "type": 0, - "value": "Browser" + "value": "Хөтөч" } ], "label.browsers": [ @@ -134,7 +134,7 @@ "label.city": [ { "type": 0, - "value": "City" + "value": "Хот" } ], "label.clear-all": [ @@ -158,7 +158,7 @@ "label.contains": [ { "type": 0, - "value": "Contains" + "value": "Агуулах" } ], "label.continue": [ @@ -176,19 +176,19 @@ "label.country": [ { "type": 0, - "value": "Country" + "value": "Улс" } ], "label.create": [ { "type": 0, - "value": "Create" + "value": "Үүсгэх" } ], "label.create-report": [ { "type": 0, - "value": "Create report" + "value": "Тайлан үүсгэх" } ], "label.create-team": [ @@ -236,7 +236,7 @@ "label.date": [ { "type": 0, - "value": "Date" + "value": "Огноо" } ], "label.date-range": [ @@ -248,7 +248,7 @@ "label.day": [ { "type": 0, - "value": "Day" + "value": "Өдөр" } ], "label.default-date-range": [ @@ -284,7 +284,7 @@ "label.description": [ { "type": 0, - "value": "Description" + "value": "Тайлбар" } ], "label.desktop": [ @@ -302,7 +302,7 @@ "label.device": [ { "type": 0, - "value": "Device" + "value": "Төхөөрөмж" } ], "label.devices": [ @@ -320,7 +320,7 @@ "label.does-not-contain": [ { "type": 0, - "value": "Does not contain" + "value": "Агуулахгүй" } ], "label.domain": [ @@ -332,7 +332,7 @@ "label.dropoff": [ { "type": 0, - "value": "Dropoff" + "value": "Уналт" } ], "label.edit": [ @@ -356,13 +356,13 @@ "label.event": [ { "type": 0, - "value": "Event" + "value": "Үйлдэл" } ], "label.event-data": [ { "type": 0, - "value": "Event data" + "value": "Үйлдлийн өгөгдөл" } ], "label.events": [ @@ -374,25 +374,25 @@ "label.false": [ { "type": 0, - "value": "False" + "value": "Худал" } ], "label.field": [ { "type": 0, - "value": "Field" + "value": "Талбар" } ], "label.fields": [ { "type": 0, - "value": "Fields" + "value": "Талбар" } ], "label.filter": [ { "type": 0, - "value": "Filter" + "value": "Шүүлтүүр" } ], "label.filter-combined": [ @@ -410,67 +410,67 @@ "label.filters": [ { "type": 0, - "value": "Filters" + "value": "Шүүлтүүр" } ], "label.funnel": [ { "type": 0, - "value": "Funnel" + "value": "Цутгал" } ], "label.funnel-description": [ { "type": 0, - "value": "Understand the conversion and drop-off rate of users." + "value": "Хэрэглэгчдийн шилжилт, уналтын хэмжээг шижнлэх." } ], "label.greater-than": [ { "type": 0, - "value": "Greater than" + "value": "Их" } ], "label.greater-than-equals": [ { "type": 0, - "value": "Greater than or equals" + "value": "Их буюу тэнцүү" } ], "label.insights": [ { "type": 0, - "value": "Insights" + "value": "Шинжлэх" } ], "label.insights-description": [ { "type": 0, - "value": "Dive deeper into your data by using segments and filters." + "value": "Өгөгдлөө хэсэгчлэн хуваах, шүүх байдлаар задлах шинжлэх." } ], "label.is": [ { "type": 0, - "value": "Is" + "value": "Бол" } ], "label.is-not": [ { "type": 0, - "value": "Is not" + "value": "Биш" } ], "label.is-not-set": [ { "type": 0, - "value": "Is not set" + "value": "Утга оноогоогүй" } ], "label.is-set": [ { "type": 0, - "value": "Is set" + "value": "Утга оноосон" } ], "label.join": [ @@ -546,13 +546,13 @@ "label.less-than": [ { "type": 0, - "value": "Less than" + "value": "Бага" } ], "label.less-than-equals": [ { "type": 0, - "value": "Less than or equals" + "value": "Бага буюу тэнцүү" } ], "label.login": [ @@ -600,7 +600,7 @@ "label.my-websites": [ { "type": 0, - "value": "My websites" + "value": "Миний вебүүд" } ], "label.name": [ @@ -630,7 +630,7 @@ "label.overview": [ { "type": 0, - "value": "Overview" + "value": "Тойм" } ], "label.owner": [ @@ -642,19 +642,19 @@ "label.page-of": [ { "type": 0, - "value": "Page " + "value": "Хуудас " }, { "type": 1, - "value": "current" + "value": "total" }, { "type": 0, - "value": " of " + "value": "-с " }, { "type": 1, - "value": "total" + "value": "current" } ], "label.page-views": [ @@ -666,7 +666,7 @@ "label.pageTitle": [ { "type": 0, - "value": "Page title" + "value": "Хуудасны гарчиг" } ], "label.pages": [ @@ -724,7 +724,7 @@ "label.referrer": [ { "type": 0, - "value": "Referrer" + "value": "Чиглүүлэгч" } ], "label.referrers": [ @@ -748,7 +748,7 @@ "label.region": [ { "type": 0, - "value": "Region" + "value": "Бүс" } ], "label.regions": [ @@ -766,7 +766,7 @@ "label.reports": [ { "type": 0, - "value": "Reports" + "value": "Тайлан" } ], "label.required": [ @@ -790,13 +790,13 @@ "label.retention": [ { "type": 0, - "value": "Retention" + "value": "Барилт" } ], "label.retention-description": [ { "type": 0, - "value": "Measure your website stickiness by tracking how often users return." + "value": "Хэрэглэгчид таны веб рүү дахин хандах буюу хэрэглэгчидээ хэр тогтоож буйг хэмжих." } ], "label.role": [ @@ -808,7 +808,7 @@ "label.run-query": [ { "type": 0, - "value": "Run query" + "value": "Query ажиллуулах" } ], "label.save": [ @@ -826,13 +826,13 @@ "label.search": [ { "type": 0, - "value": "Search" + "value": "Хайх" } ], "label.select-date": [ { "type": 0, - "value": "Select date" + "value": "Огноо сонгох" } ], "label.select-website": [ @@ -868,7 +868,7 @@ "label.sum": [ { "type": 0, - "value": "Sum" + "value": "Нийлбэр" } ], "label.tablet": [ @@ -904,7 +904,7 @@ "label.team-name": [ { "type": 0, - "value": "Team name" + "value": "Багийн нэр" } ], "label.team-owner": [ @@ -916,7 +916,7 @@ "label.team-websites": [ { "type": 0, - "value": "Team websites" + "value": "Багийн вебүүд" } ], "label.teams": [ @@ -976,13 +976,13 @@ "label.total": [ { "type": 0, - "value": "Total" + "value": "Нийт" } ], "label.total-records": [ { "type": 0, - "value": "Total records" + "value": "Нийт мөриийн тоо" } ], "label.tracking-code": [ @@ -994,13 +994,13 @@ "label.true": [ { "type": 0, - "value": "True" + "value": "Үнэн" } ], "label.type": [ { "type": 0, - "value": "Type" + "value": "Төрөл" } ], "label.unique": [ @@ -1024,7 +1024,7 @@ "label.untitled": [ { "type": 0, - "value": "Untitled" + "value": "Гарчиггүй" } ], "label.url": [ @@ -1060,7 +1060,7 @@ "label.value": [ { "type": 0, - "value": "Value" + "value": "Утга" } ], "label.view": [ @@ -1078,7 +1078,7 @@ "label.view-only": [ { "type": 0, - "value": "View only" + "value": "Зөвхөн үзэх" } ], "label.views": [ @@ -1096,7 +1096,7 @@ "label.website": [ { "type": 0, - "value": "Website" + "value": "Веб" } ], "label.website-id": [ @@ -1114,7 +1114,7 @@ "label.window": [ { "type": 0, - "value": "Window" + "value": "Цонх" } ], "label.yesterday": [ @@ -1210,7 +1210,7 @@ "message.delete-account": [ { "type": 0, - "value": "To delete this account, type " + "value": "Энэ бүртгэлийг устгахын тулд доорх хэсэгт " }, { "type": 1, @@ -1218,13 +1218,13 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " гэж бичиж баталгаажуулна уу." } ], "message.delete-website": [ { "type": 0, - "value": "To delete this website, type " + "value": "Энэ вебийг устгахын тулд доорх хэсэгт " }, { "type": 1, @@ -1232,7 +1232,7 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " гэж бичиж баталгаажуулна уу." } ], "message.delete-website-warning": [ @@ -1296,7 +1296,7 @@ "message.new-version-available": [ { "type": 0, - "value": "A new version of Umami " + "value": "Umami-н шинэ хувилбар " }, { "type": 1, @@ -1304,7 +1304,7 @@ }, { "type": 0, - "value": " is available!" + "value": " гарсан байна!" } ], "message.no-data-available": [ @@ -1316,7 +1316,7 @@ "message.no-event-data": [ { "type": 0, - "value": "No event data is available." + "value": "Үйлдлийн өгөгдөл алга." } ], "message.no-match-password": [ @@ -1328,7 +1328,7 @@ "message.no-results-found": [ { "type": 0, - "value": "No results were found." + "value": "Ямар ч үр дүн олдсонгүй." } ], "message.no-team-websites": [ diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json index acc98be29b..6441e763b3 100644 --- a/public/intl/messages/zh-CN.json +++ b/public/intl/messages/zh-CN.json @@ -182,7 +182,7 @@ "label.create": [ { "type": 0, - "value": "Create" + "value": "创建" } ], "label.create-report": [ @@ -380,19 +380,19 @@ "label.field": [ { "type": 0, - "value": "Field" + "value": "字段" } ], "label.fields": [ { "type": 0, - "value": "Fields" + "value": "字段" } ], "label.filter": [ { "type": 0, - "value": "Filter" + "value": "筛选器" } ], "label.filter-combined": [ @@ -422,19 +422,19 @@ "label.funnel-description": [ { "type": 0, - "value": "Understand the conversion and drop-off rate of users." + "value": "了解用户的转换率和退出率。" } ], "label.greater-than": [ { "type": 0, - "value": "Greater than" + "value": "大于" } ], "label.greater-than-equals": [ { "type": 0, - "value": "Greater than or equals" + "value": "大于或等于" } ], "label.insights": [ @@ -446,7 +446,7 @@ "label.insights-description": [ { "type": 0, - "value": "Dive deeper into your data by using segments and filters." + "value": "通过使用筛选器和划分时间段来更深入地研究数据。" } ], "label.is": [ @@ -804,7 +804,7 @@ "label.retention-description": [ { "type": 0, - "value": "Measure your website stickiness by tracking how often users return." + "value": "通过跟踪用户返回的频率来衡量网站的用户粘性。" } ], "label.role": [ @@ -834,7 +834,7 @@ "label.search": [ { "type": 0, - "value": "Search" + "value": "搜索" } ], "label.select-date": [ diff --git a/public/intl/messages/zh-TW.json b/public/intl/messages/zh-TW.json index 49e4382164..82f54a667f 100644 --- a/public/intl/messages/zh-TW.json +++ b/public/intl/messages/zh-TW.json @@ -182,7 +182,7 @@ "label.create": [ { "type": 0, - "value": "Create" + "value": "建立" } ], "label.create-report": [ @@ -392,7 +392,7 @@ "label.filter": [ { "type": 0, - "value": "Filter" + "value": "篩選器" } ], "label.filter-combined": [ @@ -422,7 +422,7 @@ "label.funnel-description": [ { "type": 0, - "value": "Understand the conversion and drop-off rate of users." + "value": "瞭解使用者的轉換率和退出率" } ], "label.greater-than": [ @@ -446,7 +446,7 @@ "label.insights-description": [ { "type": 0, - "value": "Dive deeper into your data by using segments and filters." + "value": "透過使用區段和篩選器來深入探索你的數據" } ], "label.is": [ @@ -800,7 +800,7 @@ "label.retention-description": [ { "type": 0, - "value": "Measure your website stickiness by tracking how often users return." + "value": "透過追蹤使用者回訪的頻率來衡量您的網站黏著度。" } ], "label.role": [ diff --git a/rollup.components.config.mjs b/rollup.components.config.mjs index c4481d0eeb..9be073908a 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/scripts/start-env.js b/scripts/start-env.js index bfaf1330f6..e9fe2a4b4e 100644 --- a/scripts/start-env.js +++ b/scripts/start-env.js @@ -1,4 +1,8 @@ require('dotenv').config(); const cli = require('next/dist/cli/next-start'); -cli.nextStart(['-p', process.env.PORT || 3000, '-H', process.env.HOSTNAME || '0.0.0.0']); +cli.nextStart({ + '--port': process.env.PORT || 3000, + '--hostname': process.env.HOSTNAME || '0.0.0.0', + _: [], +}); diff --git a/src/app/(main)/NavBar.js b/src/app/(main)/NavBar.js new file mode 100644 index 0000000000..211adf5fb0 --- /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 dd5085a03a..fd022ecab1 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 0000000000..980abb6217 --- /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 08d6702e1f..07dc925750 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 0000000000..37324659ac --- /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 0000000000..2c83dce0c6 --- /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/WebsitesBrowse.js b/src/app/(main)/websites/WebsitesBrowse.js new file mode 100644 index 0000000000..f1bab7bf2a --- /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 7e20e78542..d05ff42208 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 56cbe157b4..23764dbb85 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 ? ( -
+
+ + {close => { + return ( + + { + handleAddFilter(value); + close(); + }} + allowFilterSelect={false} + /> + + ); + }} + + + ); +} + +export default WebsiteFilterButton; diff --git a/src/components/pages/websites/WebsiteHeader.js b/src/app/(main)/websites/[id]/WebsiteHeader.js similarity index 80% rename from src/components/pages/websites/WebsiteHeader.js rename to src/app/(main)/websites/[id]/WebsiteHeader.js index fb4e098638..bf34a2530f 100644 --- a/src/components/pages/websites/WebsiteHeader.js +++ b/src/app/(main)/websites/[id]/WebsiteHeader.js @@ -1,7 +1,8 @@ +'use client'; import classNames from 'classnames'; -import { Row, Column, Text, Button, Icon } from 'react-basics'; +import { Text, Button, Icon } from 'react-basics'; import Link from 'next/link'; -import { useRouter } from 'next/router'; +import { usePathname } from 'next/navigation'; import Favicon from 'components/common/Favicon'; import ActiveUsers from 'components/metrics/ActiveUsers'; import Icons from 'components/icons'; @@ -10,7 +11,7 @@ import styles from './WebsiteHeader.module.css'; export function WebsiteHeader({ websiteId, showLinks = true, children }) { const { formatMessage, labels } = useMessages(); - const { pathname } = useRouter(); + const pathname = usePathname(); const { data: website } = useWebsite(websiteId); const { name, domain } = website || {}; @@ -38,17 +39,19 @@ export function WebsiteHeader({ websiteId, showLinks = true, children }) { ]; return ( - - +
+
{name} - - +
+
{showLinks && (
{links.map(({ label, icon, path }) => { - const selected = path ? pathname.endsWith(path) : pathname === '/websites/[id]'; + const selected = path + ? pathname.endsWith(path) + : pathname.match(/^\/websites\/[\w-]+$/); return ( @@ -67,8 +70,8 @@ export function WebsiteHeader({ websiteId, showLinks = true, children }) {
)} {children} - - +
+
); } diff --git a/src/components/pages/websites/WebsiteHeader.module.css b/src/app/(main)/websites/[id]/WebsiteHeader.module.css similarity index 83% rename from src/components/pages/websites/WebsiteHeader.module.css rename to src/app/(main)/websites/[id]/WebsiteHeader.module.css index 93e622d908..3e58c8a3eb 100644 --- a/src/components/pages/websites/WebsiteHeader.module.css +++ b/src/app/(main)/websites/[id]/WebsiteHeader.module.css @@ -1,6 +1,6 @@ .header { - display: flex; - flex-direction: row; + display: grid; + grid-template-columns: 1fr max-content; align-items: center; } @@ -35,6 +35,10 @@ } @media only screen and (max-width: 768px) { + .header { + grid-template-columns: 1fr; + } + .links { justify-content: space-evenly; flex: 1; @@ -49,7 +53,7 @@ .icon, .icon svg { - width: 30px; - height: 30px; + width: 20px; + height: 20px; } } diff --git a/src/components/pages/websites/WebsiteMenuView.js b/src/app/(main)/websites/[id]/WebsiteMenuView.js similarity index 61% rename from src/components/pages/websites/WebsiteMenuView.js rename to src/app/(main)/websites/[id]/WebsiteMenuView.js index 8c74d61515..c501645a4b 100644 --- a/src/components/pages/websites/WebsiteMenuView.js +++ b/src/app/(main)/websites/[id]/WebsiteMenuView.js @@ -1,6 +1,4 @@ -import { Icon, Button, Flexbox, Text } from 'react-basics'; -import Link from 'next/link'; -import { GridRow, GridColumn } from 'components/layout/Grid'; +import { Icons, Icon, Text, Dropdown, Item } from 'react-basics'; import BrowsersTable from 'components/metrics/BrowsersTable'; import CountriesTable from 'components/metrics/CountriesTable'; import RegionsTable from 'components/metrics/RegionsTable'; @@ -13,12 +11,11 @@ import QueryParametersTable from 'components/metrics/QueryParametersTable'; import ReferrersTable from 'components/metrics/ReferrersTable'; import ScreenTable from 'components/metrics/ScreenTable'; import EventsTable from 'components/metrics/EventsTable'; -import Icons from 'components/icons'; import SideNav from 'components/layout/SideNav'; -import usePageQuery from 'components/hooks/usePageQuery'; +import useNavigation from 'components/hooks/useNavigation'; import useMessages from 'components/hooks/useMessages'; +import LinkButton from 'components/common/LinkButton'; import styles from './WebsiteMenuView.module.css'; -import useLocale from 'components/hooks/useLocale'; const views = { url: PagesTable, @@ -38,93 +35,106 @@ const views = { export default function WebsiteMenuView({ websiteId, websiteDomain }) { const { formatMessage, labels } = useMessages(); - const { dir } = useLocale(); const { - resolveUrl, + router, + makeUrl, + pathname, query: { view }, - } = usePageQuery(); + } = useNavigation(); const items = [ { key: 'url', label: formatMessage(labels.pages), - url: resolveUrl({ view: 'url' }), + url: makeUrl({ view: 'url' }), }, { key: 'referrer', label: formatMessage(labels.referrers), - url: resolveUrl({ view: 'referrer' }), + url: makeUrl({ view: 'referrer' }), }, { key: 'browser', label: formatMessage(labels.browsers), - url: resolveUrl({ view: 'browser' }), + url: makeUrl({ view: 'browser' }), }, { key: 'os', label: formatMessage(labels.os), - url: resolveUrl({ view: 'os' }), + url: makeUrl({ view: 'os' }), }, { key: 'device', label: formatMessage(labels.devices), - url: resolveUrl({ view: 'device' }), + url: makeUrl({ view: 'device' }), }, { key: 'country', label: formatMessage(labels.countries), - url: resolveUrl({ view: 'country' }), + url: makeUrl({ view: 'country' }), }, { key: 'region', label: formatMessage(labels.regions), - url: resolveUrl({ view: 'region' }), + url: makeUrl({ view: 'region' }), }, { key: 'city', label: formatMessage(labels.cities), - url: resolveUrl({ view: 'city' }), + url: makeUrl({ view: 'city' }), }, { key: 'language', label: formatMessage(labels.languages), - url: resolveUrl({ view: 'language' }), + url: makeUrl({ view: 'language' }), }, { key: 'screen', label: formatMessage(labels.screens), - url: resolveUrl({ view: 'screen' }), + url: makeUrl({ view: 'screen' }), }, { key: 'event', label: formatMessage(labels.events), - url: resolveUrl({ view: 'event' }), + url: makeUrl({ view: 'event' }), }, { key: 'query', label: formatMessage(labels.queryParameters), - url: resolveUrl({ view: 'query' }), + url: makeUrl({ view: 'query' }), }, ]; const DetailsComponent = views[view] || (() => null); + const handleChange = view => { + router.push(makeUrl({ view })); + }; + + const renderValue = value => items.find(({ key }) => key === value)?.label; + return ( - - - - - - - - - - +
+
+ + + + + {formatMessage(labels.back)} + + + + {({ key, label }) => {label}} + +
+
- - +
+
); } 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 0000000000..b3dcb8d042 --- /dev/null +++ b/src/app/(main)/websites/[id]/WebsiteMenuView.module.css @@ -0,0 +1,63 @@ +.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); +} + +.dropdown { + display: none; +} + +@media screen and (max-width: 992px) { + .layout { + grid-template-columns: 1fr; + } + + .content { + border: 0; + } + + .back { + align-self: start; + margin: 0; + } + + .nav { + display: none; + } + + .dropdown { + display: flex; + width: 200px; + align-self: end; + } + + .menu { + display: flex; + flex-direction: row; + gap: 20px; + align-items: center; + justify-content: space-between; + padding-right: 0; + } +} diff --git a/src/app/(main)/websites/[id]/WebsiteMetricsBar.js b/src/app/(main)/websites/[id]/WebsiteMetricsBar.js new file mode 100644 index 0000000000..0dd6a4e2f1 --- /dev/null +++ b/src/app/(main)/websites/[id]/WebsiteMetricsBar.js @@ -0,0 +1,112 @@ +import classNames from 'classnames'; +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 { formatShortTime } from 'lib/format'; +import WebsiteFilterButton from './WebsiteFilterButton'; +import styles from './WebsiteMetricsBar.module.css'; + +export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { + const { formatMessage, labels } = useMessages(); + const { get, useQuery } = useApi(); + const [dateRange] = useDateRange(websiteId); + const { startDate, endDate, modified } = dateRange; + const { ref, isSticky } = useSticky({ enabled: sticky }); + const { + query: { url, referrer, title, os, browser, device, country, region, city }, + } = useNavigation(); + + const { data, error, isLoading, isFetched } = useQuery( + [ + 'websites:stats', + { websiteId, modified, url, referrer, title, os, browser, device, country, region, city }, + ], + () => + get(`/websites/${websiteId}/stats`, { + startAt: +startDate, + endAt: +endDate, + url, + referrer, + title, + os, + browser, + device, + country, + region, + city, + }), + ); + + const { pageviews, uniques, bounces, totaltime } = data || {}; + const num = Math.min(data && uniques.value, data && bounces.value); + const diffs = data && { + pageviews: pageviews.value - pageviews.change, + uniques: uniques.value - uniques.change, + bounces: bounces.value - bounces.change, + totaltime: totaltime.value - totaltime.change, + }; + + return ( +
+ + {pageviews && uniques && ( + <> + + + Number(n).toFixed(0) + '%'} + reverseColors + /> + `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`} + /> + + )} + +
+ {showFilter && } + +
+
+ ); +} + +export default WebsiteMetricsBar; diff --git a/src/app/(main)/websites/[id]/WebsiteMetricsBar.module.css b/src/app/(main)/websites/[id]/WebsiteMetricsBar.module.css new file mode 100644 index 0000000000..db48bd550f --- /dev/null +++ b/src/app/(main)/websites/[id]/WebsiteMetricsBar.module.css @@ -0,0 +1,46 @@ +.container { + display: grid; + grid-template-columns: 1fr max-content; + justify-content: space-between; + align-items: center; + background: var(--base50); + z-index: var(--z-index-above); + min-height: 120px; + padding-bottom: 20px; +} + +.actions { + display: flex; + align-items: center; + flex-direction: row; + justify-content: flex-end; + gap: 10px; +} + +@media screen and (max-width: 1200px) { + .container { + grid-template-columns: 1fr; + } + + .actions { + margin: 20px 0; + } +} + +@media screen and (min-width: 992px) { + .sticky { + position: sticky; + top: -1px; + } + + .isSticky { + padding: 10px 0; + border-bottom: 1px solid var(--base300); + } +} + +@media screen and (max-width: 768px) { + .button { + display: none; + } +} diff --git a/src/app/(main)/websites/[id]/WebsiteTableView.js b/src/app/(main)/websites/[id]/WebsiteTableView.js new file mode 100644 index 0000000000..7c71b84b95 --- /dev/null +++ b/src/app/(main)/websites/[id]/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/[id]/event-data/EventDataMetricsBar.js b/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.js new file mode 100644 index 0000000000..5be191854b --- /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/websites/WebsiteMetricsBar.module.css b/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.module.css similarity index 53% rename from src/components/pages/websites/WebsiteMetricsBar.module.css rename to src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.module.css index 52decfc65a..408396c37b 100644 --- a/src/components/pages/websites/WebsiteMetricsBar.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; @@ -11,25 +12,15 @@ .actions { display: flex; - align-items: center; flex-direction: row; + align-items: center; justify-content: flex-end; - gap: 10px; -} - -@media only screen and (max-width: 1200px) { - .actions { - margin-top: 40px; - } + flex: 1; } -@media only screen and (min-width: 992px) { - .sticky { - position: sticky; - top: -1px; - } - - .isSticky { - border-bottom: 1px solid var(--base300); +@media only screen and (max-width: 992px) { + .container { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; } } 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 c79916ce5f..fb98e7e76a 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 75c11e32b7..4e50f5b9b3 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 ( <> - + + + + + + ); +} + +export default WebsiteReports; diff --git a/src/app/(main)/websites/[id]/reports/page.tsx b/src/app/(main)/websites/[id]/reports/page.tsx new file mode 100644 index 0000000000..bf564025d6 --- /dev/null +++ b/src/app/(main)/websites/[id]/reports/page.tsx @@ -0,0 +1,9 @@ +import WebsiteReports from './WebsiteReports'; + +export default function WebsiteReportsPage({ params: { id } }) { + if (!id) { + return null; + } + + return ; +} diff --git a/src/app/(main)/websites/page.tsx b/src/app/(main)/websites/page.tsx new file mode 100644 index 0000000000..a154251013 --- /dev/null +++ b/src/app/(main)/websites/page.tsx @@ -0,0 +1,16 @@ +import WebsitesHeader from 'app/(main)/settings/websites/WebsitesHeader'; +import WebsitesBrowse from './WebsitesBrowse'; +import { Metadata } from 'next'; + +export default function WebsitesPage() { + return ( + <> + + + + ); +} + +export const metadata: Metadata = { + title: 'Websites | umami', +}; diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx new file mode 100644 index 0000000000..c3d6269939 --- /dev/null +++ b/src/app/Providers.tsx @@ -0,0 +1,39 @@ +'use client'; +import { IntlProvider } from 'react-intl'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactBasicsProvider } from 'react-basics'; +import ErrorBoundary from 'components/common/ErrorBoundary'; +import useLocale from 'components/hooks/useLocale'; +import 'chartjs-adapter-date-fns'; + +const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnWindowFocus: false, + }, + }, +}); + +function MessagesProvider({ children }) { + const { locale, messages } = useLocale(); + return ( + null}> + {children} + + ); +} + +export function Providers({ children }) { + return ( + + + + {children} + + + + ); +} + +export default Providers; diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000000..e2478a9580 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,36 @@ +import { Metadata } from 'next'; +import Providers from './Providers'; +import '@fontsource/inter/400.css'; +import '@fontsource/inter/700.css'; +import '@fontsource/inter/800.css'; +import 'react-basics/dist/styles.css'; +import 'styles/locale.css'; +import 'styles/index.css'; +import 'styles/variables.css'; + +export default function RootLayout({ children }) { + return ( + + + + + + + + + + + + + + + + {children} + + + ); +} + +export const metadata: Metadata = { + title: 'umami', +}; diff --git a/src/components/pages/login/LoginForm.js b/src/app/login/LoginForm.js similarity index 96% rename from src/components/pages/login/LoginForm.js rename to src/app/login/LoginForm.js index 797eea14d6..59d145bf35 100644 --- a/src/components/pages/login/LoginForm.js +++ b/src/app/login/LoginForm.js @@ -1,3 +1,4 @@ +'use client'; import { useMutation } from '@tanstack/react-query'; import { Form, @@ -9,7 +10,7 @@ import { SubmitButton, Icon, } from 'react-basics'; -import { useRouter } from 'next/router'; +import { useRouter } from 'next/navigation'; import useApi from 'components/hooks/useApi'; import { setUser } from 'store/app'; import { setClientAuthToken } from 'lib/client'; diff --git a/src/components/pages/login/LoginForm.module.css b/src/app/login/LoginForm.module.css similarity index 100% rename from src/components/pages/login/LoginForm.module.css rename to src/app/login/LoginForm.module.css diff --git a/src/components/pages/login/LoginLayout.module.css b/src/app/login/page.module.css similarity index 76% rename from src/components/pages/login/LoginLayout.module.css rename to src/app/login/page.module.css index d12306ea89..45115d5b6b 100644 --- a/src/components/pages/login/LoginLayout.module.css +++ b/src/app/login/page.module.css @@ -1,6 +1,5 @@ -.layout { +.page { display: flex; - flex-direction: column; align-items: center; justify-content: center; height: 100vh; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000000..2ac3f72440 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,25 @@ +import LoginForm from './LoginForm'; +import { Metadata } from 'next'; +import styles from './page.module.css'; + +async function getDisabled() { + return !!process.env.LOGIN_DISABLED; +} + +export default async function LoginPage() { + const disabled = await getDisabled(); + + if (disabled) { + return null; + } + + return ( +
+ +
+ ); +} + +export const metadata: Metadata = { + title: 'Login | umami', +}; diff --git a/src/pages/logout.js b/src/app/logout/Logout.js similarity index 68% rename from src/pages/logout.js rename to src/app/logout/Logout.js index ef89080c39..e9da0373e9 100644 --- a/src/pages/logout.js +++ b/src/app/logout/Logout.js @@ -1,10 +1,12 @@ +'use client'; import { useEffect } from 'react'; -import { useRouter } from 'next/router'; +import { useRouter } from 'next/navigation'; import useApi from 'components/hooks/useApi'; import { setUser } from 'store/app'; import { removeClientAuthToken } from 'lib/client'; -export default function ({ disabled }) { +export function Logout() { + const disabled = !!(process.env.disableLogin || process.env.cloudMode); const router = useRouter(); const { post } = useApi(); @@ -27,10 +29,4 @@ export default function ({ disabled }) { return null; } -export async function getServerSideProps() { - return { - props: { - disabled: !!(process.env.DISABLE_LOGIN || process.env.CLOUD_MODE), - }, - }; -} +export default Logout; diff --git a/src/app/logout/page.tsx b/src/app/logout/page.tsx new file mode 100644 index 0000000000..bce247364e --- /dev/null +++ b/src/app/logout/page.tsx @@ -0,0 +1,5 @@ +import Logout from './Logout'; + +export default function () { + return ; +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000000..16c5bbcbe7 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,13 @@ +'use client'; +import { Flexbox } from 'react-basics'; +import useMessages from 'components/hooks/useMessages'; + +export default function () { + const { formatMessage, labels } = useMessages(); + + return ( + +

{formatMessage(labels.pageNotFound)}

+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000000..6a146801d5 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,6 @@ +'use client'; +import { redirect } from 'next/navigation'; + +export default function RootPage() { + redirect('/dashboard'); +} diff --git a/src/components/layout/Footer.js b/src/app/share/[...id]/Footer.js similarity index 95% rename from src/components/layout/Footer.js rename to src/app/share/[...id]/Footer.js index 3a07c12a88..84d4162f71 100644 --- a/src/components/layout/Footer.js +++ b/src/app/share/[...id]/Footer.js @@ -1,3 +1,4 @@ +'use client'; import { CURRENT_VERSION, HOMEPAGE_URL } from 'lib/constants'; import styles from './Footer.module.css'; diff --git a/src/components/layout/Footer.module.css b/src/app/share/[...id]/Footer.module.css similarity index 80% rename from src/components/layout/Footer.module.css rename to src/app/share/[...id]/Footer.module.css index 348c92d888..5dc2d5841b 100644 --- a/src/components/layout/Footer.module.css +++ b/src/app/share/[...id]/Footer.module.css @@ -1,10 +1,10 @@ .footer { display: flex; flex-direction: row; + align-items: center; justify-content: flex-end; font-size: var(--font-size-sm); - line-height: 30px; - margin: 40px 0; + height: 100px; } .footer a { diff --git a/src/app/share/[...id]/Header.js b/src/app/share/[...id]/Header.js new file mode 100644 index 0000000000..41e93f52e3 --- /dev/null +++ b/src/app/share/[...id]/Header.js @@ -0,0 +1,30 @@ +'use client'; +import { Icon, 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/Header.module.css b/src/app/share/[...id]/Header.module.css similarity index 86% rename from src/components/layout/Header.module.css rename to src/app/share/[...id]/Header.module.css index 26f3055218..d353d79a17 100644 --- a/src/components/layout/Header.module.css +++ b/src/app/share/[...id]/Header.module.css @@ -2,6 +2,7 @@ display: flex; flex-direction: row; align-items: center; + justify-content: space-between; width: 100%; height: 100px; } @@ -38,10 +39,3 @@ min-width: 100%; } } - -@media only screen and (max-width: 768px) { - .buttons, - .links { - display: none; - } -} diff --git a/src/app/share/[...id]/Share.js b/src/app/share/[...id]/Share.js new file mode 100644 index 0000000000..99ba640788 --- /dev/null +++ b/src/app/share/[...id]/Share.js @@ -0,0 +1,25 @@ +'use client'; +import WebsiteDetails from 'app/(main)/websites/[id]/WebsiteDetails'; +import useShareToken from 'components/hooks/useShareToken'; +import styles from './Share.module.css'; +import Page from 'components/layout/Page'; +import Header from './Header'; +import Footer from './Footer'; + +export default function Share({ shareId }) { + const { shareToken, isLoading } = useShareToken(shareId); + + if (isLoading || !shareToken) { + return null; + } + + return ( +
+ +
+ +
+ +
+ ); +} diff --git a/src/app/share/[...id]/Share.module.css b/src/app/share/[...id]/Share.module.css new file mode 100644 index 0000000000..d985435c11 --- /dev/null +++ b/src/app/share/[...id]/Share.module.css @@ -0,0 +1,4 @@ +.container { + flex: 1; + min-height: calc(100vh - 200px); +} diff --git a/src/app/share/[...id]/page.tsx b/src/app/share/[...id]/page.tsx new file mode 100644 index 0000000000..ca1541653a --- /dev/null +++ b/src/app/share/[...id]/page.tsx @@ -0,0 +1,5 @@ +import Share from './Share'; + +export default function ({ params: { id } }) { + return ; +} diff --git a/src/pages/sso.js b/src/app/sso/page.tsx similarity index 60% rename from src/pages/sso.js rename to src/app/sso/page.tsx index 6e63520614..75ea945d4b 100644 --- a/src/pages/sso.js +++ b/src/app/sso/page.tsx @@ -1,11 +1,14 @@ +'use client'; import { useEffect } from 'react'; import { Loading } from 'react-basics'; -import { useRouter } from 'next/router'; +import { useRouter, useSearchParams } from 'next/navigation'; import { setClientAuthToken } from 'lib/client'; -export default function () { +export default function SSOPage() { const router = useRouter(); - const { token, url } = router.query; + const search = useSearchParams(); + const url = search.get('url'); + const token = search.get('token'); useEffect(() => { if (url && token) { diff --git a/src/components/common/DataTable.module.css b/src/components/common/DataTable.module.css new file mode 100644 index 0000000000..e738c89583 --- /dev/null +++ b/src/components/common/DataTable.module.css @@ -0,0 +1,52 @@ +.table { + grid-template-rows: repeat(auto-fit, max-content); +} + +.table td { + align-items: center; + max-height: max-content; +} + +.search { + max-width: 300px; + margin: 20px 0; +} + +.action { + justify-content: flex-end; + gap: 5px; +} + +.body { + display: flex; + flex-direction: column; + position: relative; + overflow-x: auto; +} + +.body td { + display: flex; + gap: 10px; + min-height: 70px; + align-items: center; + min-width: min-content; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.body > div > div > div { + display: flex; + gap: 10px; +} + +.pager { + margin: 20px 0; +} + +.status { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; +} diff --git a/src/components/common/DataTable.tsx b/src/components/common/DataTable.tsx new file mode 100644 index 0000000000..a3c63c0abb --- /dev/null +++ b/src/components/common/DataTable.tsx @@ -0,0 +1,92 @@ +import { ReactNode, Dispatch, SetStateAction } from 'react'; +import classNames from 'classnames'; +import { Banner, Loading, SearchField } from 'react-basics'; +import { useMessages } from 'components/hooks'; +import Empty from 'components/common/Empty'; +import Pager from 'components/common/Pager'; +import styles from './DataTable.module.css'; + +const DEFAULT_SEARCH_DELAY = 600; + +export interface DataTableProps { + queryResult: { + result: { + page: number; + pageSize: number; + count: number; + data: any[]; + }; + params: { + query: string; + page: number; + }; + setParams: Dispatch>; + isLoading: boolean; + error: unknown; + }; + searchDelay?: number; + allowSearch?: boolean; + allowPaging?: boolean; + children: ReactNode | ((data: any) => ReactNode); +} + +export function DataTable({ + queryResult, + searchDelay = 600, + allowSearch = true, + allowPaging = true, + children, +}: DataTableProps) { + const { formatMessage, labels, messages } = useMessages(); + const { result, error, isLoading, params, setParams } = queryResult || {}; + const { page, pageSize, count, data } = result || {}; + const { query } = params || {}; + const hasData = Boolean(!isLoading && data?.length); + const noResults = Boolean(!isLoading && query && !hasData); + + const handleSearch = query => { + setParams({ ...params, query, page: params.page ? page : 1 }); + }; + + const handlePageChange = page => { + setParams({ ...params, query, page }); + }; + + if (error) { + return {formatMessage(messages.error)}; + } + + return ( + <> + {allowSearch && (hasData || query) && ( + + )} +
+ {hasData ? (typeof children === 'function' ? children(result) : children) : null} + {isLoading && } + {!isLoading && !hasData && !query && } + {noResults && } +
+ {allowPaging && hasData && ( + + )} + + ); +} + +export default DataTable; diff --git a/src/components/common/Empty.js b/src/components/common/Empty.tsx similarity index 72% rename from src/components/common/Empty.js rename to src/components/common/Empty.tsx index c0be761a03..2c7fcd4a2f 100644 --- a/src/components/common/Empty.js +++ b/src/components/common/Empty.tsx @@ -2,7 +2,12 @@ import classNames from 'classnames'; import styles from './Empty.module.css'; import useMessages from 'components/hooks/useMessages'; -export function Empty({ message, className }) { +export interface EmptyProps { + message?: string; + className?: string; +} + +export function Empty({ message, className }: EmptyProps) { const { formatMessage, messages } = useMessages(); return ( diff --git a/src/components/common/FilterLink.js b/src/components/common/FilterLink.js index 2a95e011da..89648255a4 100644 --- a/src/components/common/FilterLink.js +++ b/src/components/common/FilterLink.js @@ -2,13 +2,13 @@ import { Icon, Icons } from 'react-basics'; import classNames from 'classnames'; import Link from 'next/link'; import { safeDecodeURI } from 'next-basics'; -import usePageQuery from 'components/hooks/usePageQuery'; +import useNavigation from 'components/hooks/useNavigation'; import useMessages from 'components/hooks/useMessages'; import styles from './FilterLink.module.css'; export function FilterLink({ id, value, label, externalUrl, children, className }) { const { formatMessage, labels } = useMessages(); - const { resolveUrl, query } = usePageQuery(); + const { makeUrl, query } = useNavigation(); const active = query[id] !== undefined; const selected = query[id] === value; @@ -22,7 +22,7 @@ export function FilterLink({ id, value, label, externalUrl, children, className {children} {!value && `(${label || formatMessage(labels.unknown)})`} {value && ( - + {safeDecodeURI(label || value)} )} diff --git a/src/components/common/LinkButton.js b/src/components/common/LinkButton.js index 54c7fa6319..a9a8562dbb 100644 --- a/src/components/common/LinkButton.js +++ b/src/components/common/LinkButton.js @@ -1,12 +1,19 @@ +import classNames from 'classnames'; import Link from 'next/link'; -import { Icon, Icons, Text } from 'react-basics'; +import { useLocale } from 'components/hooks'; import styles from './LinkButton.module.css'; -export function LinkButton({ href, icon, children }) { +export function LinkButton({ href, className, variant, scroll = true, children }) { + const { dir } = useLocale(); + return ( - - {icon || } - {children} + + {children} ); } diff --git a/src/components/common/LinkButton.module.css b/src/components/common/LinkButton.module.css index ae8a3b6272..5561f536fa 100644 --- a/src/components/common/LinkButton.module.css +++ b/src/components/common/LinkButton.module.css @@ -26,3 +26,82 @@ .button:visited { color: var(--base900); } + +.button.disabled { + color: var(--disabled-color) !important; + background-color: var(--disabled-background) !important; + border-color: transparent !important; + pointer-events: none; +} + +.button.primary { + color: var(--light50); + background: var(--primary400); +} + +.button.primary:hover { + color: var(--light50); + background: var(--primary500); +} + +.button.primary:active { + color: var(--light50); + background: var(--primary600); +} + +.button.secondary { + border: 1px solid var(--border-color); + background: var(--base50); +} + +.button.secondary:hover { + background: var(--base75); +} + +.button.secondary:active { + background: var(--base100); +} + +.button.quiet { + color: var(--base900); + background: transparent; +} + +.button.quiet:hover { + background: var(--base100); +} + +.button.quiet:active { + background: var(--base200); +} + +.button.danger { + color: var(--light50); + background: var(--red800); +} + +.button.danger:hover { + color: var(--light50); + background: var(--red900); +} + +.button.danger:active { + color: var(--light50); + background: var(--red1000); +} + +.button.size-sm { + font-size: var(--font-size-sm); + height: calc(var(--base-height) * 0.75); + padding: 0 calc(var(--size600) * 0.75); +} + +.button.size-md { + font-size: var(--font-size-md); +} + +.button.size-lg { + font-size: var(--font-size-lg); + height: calc(var(--base-height) * 1.25); + padding: 0 calc(var(--size600) * 1.25); +} diff --git a/src/components/common/MobileMenu.js b/src/components/common/MobileMenu.js index de1e9ffa8d..83a05dff37 100644 --- a/src/components/common/MobileMenu.js +++ b/src/components/common/MobileMenu.js @@ -1,11 +1,11 @@ import { createPortal } from 'react-dom'; import classNames from 'classnames'; -import { useRouter } from 'next/router'; +import { usePathname } from 'next/navigation'; import Link from 'next/link'; import styles from './MobileMenu.module.css'; export function MobileMenu({ items = [], onClose }) { - const { pathname } = useRouter(); + const pathname = usePathname(); const Items = ({ items, className }) => (
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 ( - + - +
)} - +
); } diff --git a/src/components/input/WebsiteDateFilter.module.css b/src/components/input/WebsiteDateFilter.module.css index 986f5c1784..6f2e822dc6 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/input/WebsiteSelect.js b/src/components/input/WebsiteSelect.js index 1bdc4608a5..078389d310 100644 --- a/src/components/input/WebsiteSelect.js +++ b/src/components/input/WebsiteSelect.js @@ -1,11 +1,12 @@ import { Dropdown, Item } from 'react-basics'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; +import styles from './WebsiteSelect.module.css'; export function WebsiteSelect({ websiteId, onSelect }) { const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); - const { data } = useQuery(['websites:me'], () => get('/me/websites')); + const { data } = useQuery(['websites:me'], () => get('/me/websites', { pageSize: 100 })); const renderValue = value => { return data?.data?.find(({ id }) => id === value)?.name; @@ -13,6 +14,7 @@ export function WebsiteSelect({ websiteId, onSelect }) { return ( - - - {title ? `${title} | umami` : 'umami'} - - -
- {children} -
-
- ); -} - -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 ; +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 dc2e8ff6c5..f72a5f126a 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 21cdd25139..0000000000 --- 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 07627e2a2b..0000000000 --- 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 94f9d8e663..361dffb5a1 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 c546971b6e..52893157cf 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; + margin: 0 auto; + padding: 0 20px; + min-height: calc(100vh - 60px); } 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 4f42aa55ae..2f7020128d 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 f13631409f..2261bebcb0 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?: ReactNode; + 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 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}
-