diff --git a/next-env.d.ts b/next-env.d.ts
index 4f11a03d..fd36f949 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+///
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/next.config.js b/next.config.js
index cc3cde7c..cf7dce7f 100644
--- a/next.config.js
+++ b/next.config.js
@@ -6,7 +6,7 @@ const pkg = require('./package.json');
const contentSecurityPolicy = `
default-src 'self';
img-src *;
- script-src 'self' 'unsafe-eval';
+ script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self' api.umami.is;
frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS};
@@ -74,16 +74,23 @@ if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN
});
}
+const basePath = process.env.BASE_PATH;
+
+/** @type {import('next').NextConfig} */
const config = {
+ reactStrictMode: false,
env: {
- cloudMode: process.env.CLOUD_MODE,
+ basePath: basePath || '',
+ cloudMode: !!process.env.CLOUD_MODE,
cloudUrl: process.env.CLOUD_URL,
configUrl: '/config',
currentVersion: pkg.version,
defaultLocale: process.env.DEFAULT_LOCALE,
+ disableLogin: process.env.DISABLE_LOGIN,
+ disableUI: process.env.DISABLE_UI,
isProduction: process.env.NODE_ENV === 'production',
},
- basePath: process.env.BASE_PATH,
+ basePath,
output: 'standalone',
eslint: {
ignoreDuringBuilds: true,
@@ -92,11 +99,23 @@ const config = {
ignoreBuildErrors: true,
},
webpack(config) {
- config.module.rules.push({
- test: /\.svg$/,
- issuer: /\.{js|jsx|ts|tsx}$/,
- use: ['@svgr/webpack'],
- });
+ const fileLoaderRule = config.module.rules.find(rule => rule.test?.test?.('.svg'));
+
+ config.module.rules.push(
+ {
+ ...fileLoaderRule,
+ test: /\.svg$/i,
+ resourceQuery: /url/,
+ },
+ {
+ test: /\.svg$/i,
+ issuer: fileLoaderRule.issuer,
+ resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
+ use: ['@svgr/webpack'],
+ },
+ );
+
+ fileLoaderRule.exclude = /\.svg$/i;
config.resolve.alias['public'] = path.resolve('./public');
diff --git a/package.json b/package.json
index 6fc413d4..67211f13 100644
--- a/package.json
+++ b/package.json
@@ -64,8 +64,9 @@
"@clickhouse/client": "^0.2.2",
"@fontsource/inter": "^4.5.15",
"@prisma/client": "5.3.1",
+ "@react-spring/web": "^9.7.3",
"@tanstack/react-query": "^4.33.0",
- "@umami/prisma-client": "^0.2.0",
+ "@umami/prisma-client": "^0.3.0",
"@umami/redis-client": "^0.15.0",
"chalk": "^4.1.1",
"chart.js": "^4.2.1",
@@ -91,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 00000000..4a899a30
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 0e131621..5fd90efd 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 fd7294f3..1a221045 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 "
- },
- {
- "type": 1,
- "value": "current"
- },
- {
- "type": 0,
- "value": " of "
+ "value": "Хуудас "
},
{
"type": 1,
"value": "total"
+ },
+ {
+ "type": 0,
+ "value": "-с "
+ },
+ {
+ "type": 1,
+ "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 acc98be2..6441e763 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 49e43821..82f54a66 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 c4481d0e..9be07390 100644
--- a/rollup.components.config.mjs
+++ b/rollup.components.config.mjs
@@ -19,6 +19,7 @@ const customResolver = resolve({
const aliasConfig = {
entries: [
+ { find: /^app/, replacement: path.resolve('./src/app') },
{ find: /^components/, replacement: path.resolve('./src/components') },
{ find: /^hooks/, replacement: path.resolve('./src/hooks') },
{ find: /^lib/, replacement: path.resolve('./src/lib') },
diff --git a/scripts/start-env.js b/scripts/start-env.js
index bfaf1330..e9fe2a4b 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 00000000..211adf5f
--- /dev/null
+++ b/src/app/(main)/NavBar.js
@@ -0,0 +1,58 @@
+'use client';
+import { Icon, Text } from 'react-basics';
+import Link from 'next/link';
+import classNames from 'classnames';
+import Icons from 'components/icons';
+import ThemeButton from 'components/input/ThemeButton';
+import LanguageButton from 'components/input/LanguageButton';
+import ProfileButton from 'components/input/ProfileButton';
+import useMessages from 'components/hooks/useMessages';
+import HamburgerButton from 'components/common/HamburgerButton';
+import { usePathname } from 'next/navigation';
+import styles from './NavBar.module.css';
+
+export function NavBar() {
+ const pathname = usePathname();
+ const { formatMessage, labels } = useMessages();
+
+ const links = [
+ { label: formatMessage(labels.dashboard), url: '/dashboard' },
+ { label: formatMessage(labels.websites), url: '/websites' },
+ { label: formatMessage(labels.reports), url: '/reports' },
+ { label: formatMessage(labels.settings), url: '/settings' },
+ ].filter(n => n);
+
+ return (
+
+
+
+
+
+ umami
+
+
+ {links.map(({ url, label }) => {
+ return (
+
+ {label}
+
+ );
+ })}
+
+
+
+
+
+
+ );
+}
+
+export default NavBar;
diff --git a/src/components/layout/NavBar.module.css b/src/app/(main)/NavBar.module.css
similarity index 75%
rename from src/components/layout/NavBar.module.css
rename to src/app/(main)/NavBar.module.css
index dd5085a0..fd022eca 100644
--- a/src/components/layout/NavBar.module.css
+++ b/src/app/(main)/NavBar.module.css
@@ -1,7 +1,7 @@
.navbar {
+ display: grid;
+ grid-template-columns: max-content 1fr 1fr;
position: relative;
- display: flex;
- flex-direction: row;
align-items: center;
height: 60px;
background: var(--base75);
@@ -9,17 +9,6 @@
padding: 0 20px;
}
-.left,
-.right {
- display: flex;
- flex-direction: row;
- align-items: center;
-}
-
-.right {
- justify-content: flex-end;
-}
-
.logo {
display: flex;
flex-direction: row;
@@ -35,29 +24,24 @@
flex-direction: row;
gap: 30px;
padding: 0 40px;
- flex: 1;
font-weight: 700;
+ max-height: 60px;
}
-.links a {
- display: flex;
- align-items: center;
- gap: 10px;
- line-height: 60px;
+.links a,
+.links a:active,
+.links a:visited {
color: var(--font-color200);
+ line-height: 60px;
border-bottom: 2px solid transparent;
}
-.links span {
- white-space: nowrap;
-}
-
.links a:hover {
color: var(--font-color100);
border-bottom: 2px solid var(--primary400);
}
-.links .selected {
+.links a.selected {
color: var(--font-color100);
border-bottom: 2px solid var(--primary400);
}
@@ -68,7 +52,6 @@
flex-direction: row;
align-items: center;
justify-content: flex-end;
- min-width: 0;
}
.mobile {
@@ -76,6 +59,10 @@
}
@media only screen and (max-width: 768px) {
+ .navbar {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
.links,
.actions {
display: none;
diff --git a/src/app/(main)/Shell.tsx b/src/app/(main)/Shell.tsx
new file mode 100644
index 00000000..980abb62
--- /dev/null
+++ b/src/app/(main)/Shell.tsx
@@ -0,0 +1,27 @@
+'use client';
+import Script from 'next/script';
+import { usePathname } from 'next/navigation';
+import UpdateNotice from 'components/common/UpdateNotice';
+import { useRequireLogin, useConfig } from 'components/hooks';
+
+export function Shell({ children }) {
+ const { user } = useRequireLogin();
+ const config = useConfig();
+ const pathname = usePathname();
+
+ if (!user || !config) {
+ return null;
+ }
+
+ return (
+ <>
+ {children}
+
+ {process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && (
+
+ )}
+ >
+ );
+}
+
+export default Shell;
diff --git a/src/components/pages/console/TestConsole.js b/src/app/(main)/console/TestConsole.js
similarity index 88%
rename from src/components/pages/console/TestConsole.js
rename to src/app/(main)/console/TestConsole.js
index 71eb27b4..1ae3eaf1 100644
--- a/src/components/pages/console/TestConsole.js
+++ b/src/app/(main)/console/TestConsole.js
@@ -1,14 +1,15 @@
+'use client';
import WebsiteSelect from 'components/input/WebsiteSelect';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import EventsChart from 'components/metrics/EventsChart';
-import WebsiteChart from 'components/pages/websites/WebsiteChart';
+import WebsiteChart from '../../(main)/websites/[id]/WebsiteChart';
import useApi from 'components/hooks/useApi';
import Head from 'next/head';
import Link from 'next/link';
-import { useRouter } from 'next/router';
+import { useRouter } from 'next/navigation';
import Script from 'next/script';
-import { Button, Column, Row } from 'react-basics';
+import { Button } from 'react-basics';
import styles from './TestConsole.module.css';
export function TestConsole() {
@@ -90,8 +91,8 @@ export function TestConsole() {
src={`${basePath}/script.js`}
data-cache="true"
/>
-
-
+
+
Page links
page one
@@ -114,8 +115,8 @@ export function TestConsole() {
external link (tab)
-
-
+
+
Click events
-
-
+
+
Javascript events
+
+
+
+
+
>
)}
diff --git a/src/components/pages/console/TestConsole.module.css b/src/app/(main)/console/TestConsole.module.css
similarity index 100%
rename from src/components/pages/console/TestConsole.module.css
rename to src/app/(main)/console/TestConsole.module.css
diff --git a/src/app/(main)/console/[[...id]]/page.tsx b/src/app/(main)/console/[[...id]]/page.tsx
new file mode 100644
index 00000000..d020ddf9
--- /dev/null
+++ b/src/app/(main)/console/[[...id]]/page.tsx
@@ -0,0 +1,20 @@
+import TestConsole from '../TestConsole';
+import { Metadata } from 'next';
+
+async function getEnabled() {
+ return !!process.env.ENABLE_TEST_CONSOLE;
+}
+
+export default async function ConsolePage() {
+ const enabled = await getEnabled();
+
+ if (!enabled) {
+ return null;
+ }
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Test Console | umami',
+};
diff --git a/src/components/pages/dashboard/Dashboard.js b/src/app/(main)/dashboard/Dashboard.js
similarity index 79%
rename from src/components/pages/dashboard/Dashboard.js
rename to src/app/(main)/dashboard/Dashboard.js
index 2294b8be..5fb65f23 100644
--- a/src/components/pages/dashboard/Dashboard.js
+++ b/src/app/(main)/dashboard/Dashboard.js
@@ -1,11 +1,11 @@
-import { Button, Icon, Icons, Text } from 'react-basics';
+'use client';
+import { Button, Icon, Icons, Loading, Text } from 'react-basics';
import Link from 'next/link';
-import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import Pager from 'components/common/Pager';
-import WebsiteChartList from 'components/pages/websites/WebsiteChartList';
-import DashboardSettingsButton from 'components/pages/dashboard/DashboardSettingsButton';
-import DashboardEdit from 'components/pages/dashboard/DashboardEdit';
+import WebsiteChartList from '../../(main)/websites/[id]/WebsiteChartList';
+import DashboardSettingsButton from 'app/(main)/dashboard/DashboardSettingsButton';
+import DashboardEdit from 'app/(main)/dashboard/DashboardEdit';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import useApi from 'components/hooks/useApi';
import useDashboard from 'store/dashboard';
@@ -20,18 +20,18 @@ export function Dashboard() {
const { get, useQuery } = useApi();
const { page, handlePageChange } = useApiFilter();
const pageSize = 10;
- const {
- data: result,
- isLoading,
- error,
- } = useQuery(['websites', page, pageSize], () =>
+ const { data: result, isLoading } = useQuery(['websites', page, pageSize], () =>
get('/websites', { includeTeams: 1, page, pageSize }),
);
const { data, count } = result || {};
const hasData = data && data?.length !== 0;
+ if (isLoading) {
+ return ;
+ }
+
return (
-
+ <>
{!editing && hasData && }
@@ -63,7 +63,7 @@ export function Dashboard() {
)}
>
)}
-
+ >
);
}
diff --git a/src/components/pages/dashboard/DashboardEdit.js b/src/app/(main)/dashboard/DashboardEdit.js
similarity index 93%
rename from src/components/pages/dashboard/DashboardEdit.js
rename to src/app/(main)/dashboard/DashboardEdit.js
index f628599f..3af33867 100644
--- a/src/components/pages/dashboard/DashboardEdit.js
+++ b/src/app/(main)/dashboard/DashboardEdit.js
@@ -1,3 +1,4 @@
+'use client';
import { useState, useMemo } from 'react';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import classNames from 'classnames';
@@ -7,7 +8,6 @@ import useDashboard, { saveDashboard } from 'store/dashboard';
import useMessages from 'components/hooks/useMessages';
import useApi from 'components/hooks/useApi';
import styles from './DashboardEdit.module.css';
-import Page from 'components/layout/Page';
const dragId = 'dashboard-website-ordering';
@@ -17,11 +17,7 @@ export function DashboardEdit() {
const { formatMessage, labels } = useMessages();
const [order, setOrder] = useState(websiteOrder || []);
const { get, useQuery } = useApi();
- const {
- data: result,
- isLoading,
- error,
- } = useQuery(['websites'], () => get('/websites', { includeTeams: 1 }));
+ const { data: result } = useQuery(['websites'], () => get('/websites', { includeTeams: 1 }));
const { data: websites } = result || {};
const ordered = useMemo(() => {
@@ -59,7 +55,7 @@ export function DashboardEdit() {
}
return (
-
+ <>
{formatMessage(labels.save)}
@@ -105,7 +101,7 @@ export function DashboardEdit() {
-
+ >
);
}
diff --git a/src/components/pages/dashboard/DashboardEdit.module.css b/src/app/(main)/dashboard/DashboardEdit.module.css
similarity index 100%
rename from src/components/pages/dashboard/DashboardEdit.module.css
rename to src/app/(main)/dashboard/DashboardEdit.module.css
diff --git a/src/components/pages/dashboard/DashboardSettingsButton.js b/src/app/(main)/dashboard/DashboardSettingsButton.js
similarity index 100%
rename from src/components/pages/dashboard/DashboardSettingsButton.js
rename to src/app/(main)/dashboard/DashboardSettingsButton.js
diff --git a/src/components/pages/dashboard/DashboardSettingsButton.module.css b/src/app/(main)/dashboard/DashboardSettingsButton.module.css
similarity index 100%
rename from src/components/pages/dashboard/DashboardSettingsButton.module.css
rename to src/app/(main)/dashboard/DashboardSettingsButton.module.css
diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx
new file mode 100644
index 00000000..91cc9c6e
--- /dev/null
+++ b/src/app/(main)/dashboard/page.tsx
@@ -0,0 +1,10 @@
+import Dashboard from 'app/(main)/dashboard/Dashboard';
+import { Metadata } from 'next';
+
+export default function DashboardPage() {
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Dashboard | umami',
+};
diff --git a/src/components/layout/AppLayout.module.css b/src/app/(main)/layout.module.css
similarity index 91%
rename from src/components/layout/AppLayout.module.css
rename to src/app/(main)/layout.module.css
index bcce963f..0afd11f9 100644
--- a/src/components/layout/AppLayout.module.css
+++ b/src/app/(main)/layout.module.css
@@ -10,7 +10,6 @@
width: 100vw;
grid-column: 1;
grid-row: 1 / 2;
- z-index: var(--z-index-popup);
}
.body {
diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx
new file mode 100644
index 00000000..1c9cc277
--- /dev/null
+++ b/src/app/(main)/layout.tsx
@@ -0,0 +1,19 @@
+import Shell from './Shell';
+import NavBar from './NavBar';
+import Page from 'components/layout/Page';
+import styles from './layout.module.css';
+
+export default function AppLayout({ children }) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/reports/ReportDeleteButton.js b/src/app/(main)/reports/ReportDeleteButton.js
new file mode 100644
index 00000000..35809a98
--- /dev/null
+++ b/src/app/(main)/reports/ReportDeleteButton.js
@@ -0,0 +1,42 @@
+import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
+import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
+import { useApi, useMessages } from 'components/hooks';
+import { setValue } from 'store/cache';
+
+export function ReportDeleteButton({ reportId, reportName, onDelete }) {
+ const { formatMessage, labels } = useMessages();
+ const { del, useMutation } = useApi();
+ const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
+
+ const handleConfirm = close => {
+ mutate(reportId, {
+ onSuccess: () => {
+ setValue('reports', Date.now());
+ onDelete?.();
+ close();
+ },
+ });
+ };
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.delete)}
+
+
+ {close => (
+
+ )}
+
+
+ );
+}
+
+export default ReportDeleteButton;
diff --git a/src/app/(main)/reports/ReportsDataTable.js b/src/app/(main)/reports/ReportsDataTable.js
new file mode 100644
index 00000000..0daa3d06
--- /dev/null
+++ b/src/app/(main)/reports/ReportsDataTable.js
@@ -0,0 +1,20 @@
+'use client';
+import { useApi } from 'components/hooks';
+import ReportsTable from './ReportsTable';
+import useFilterQuery from 'components/hooks/useFilterQuery';
+import DataTable from 'components/common/DataTable';
+import useCache from 'store/cache';
+
+export default function ReportsDataTable({ websiteId }) {
+ const { get } = useApi();
+ const modified = useCache(state => state?.reports);
+ const queryResult = useFilterQuery(['reports', { websiteId, modified }], params =>
+ get(websiteId ? `/websites/${websiteId}/reports` : `/reports`, params),
+ );
+
+ return (
+
+ {({ data }) => }
+
+ );
+}
diff --git a/src/app/(main)/reports/ReportsHeader.js b/src/app/(main)/reports/ReportsHeader.js
new file mode 100644
index 00000000..43cce217
--- /dev/null
+++ b/src/app/(main)/reports/ReportsHeader.js
@@ -0,0 +1,25 @@
+'use client';
+import PageHeader from 'components/layout/PageHeader';
+import { Button, Icon, Icons, Text } from 'react-basics';
+import { useMessages } from 'components/hooks';
+import { useRouter } from 'next/navigation';
+
+export function ReportsHeader() {
+ const { formatMessage, labels } = useMessages();
+ const router = useRouter();
+
+ const handleClick = () => router.push('/reports/create');
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.createReport)}
+
+
+ );
+}
+
+export default ReportsHeader;
diff --git a/src/app/(main)/reports/ReportsTable.js b/src/app/(main)/reports/ReportsTable.js
new file mode 100644
index 00000000..6b2a7932
--- /dev/null
+++ b/src/app/(main)/reports/ReportsTable.js
@@ -0,0 +1,51 @@
+import { GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from 'react-basics';
+import LinkButton from 'components/common/LinkButton';
+import { useMessages } from 'components/hooks';
+import useUser from 'components/hooks/useUser';
+import { REPORT_TYPES } from 'lib/constants';
+import ReportDeleteButton from './ReportDeleteButton';
+
+export function ReportsTable({ data = [], showDomain }) {
+ const { formatMessage, labels } = useMessages();
+ const { user } = useUser();
+ const breakpoint = useBreakpoint();
+
+ return (
+
+
+
+
+ {row => {
+ return formatMessage(
+ labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)],
+ );
+ }}
+
+ {showDomain && (
+
+ {row => row?.website?.domain}
+
+ )}
+
+ {row => {
+ const { id, name, userId, website } = row;
+ return (
+ <>
+ {(user.id === userId || user.id === website?.userId) && (
+
+ )}
+
+
+
+
+ {formatMessage(labels.view)}
+
+ >
+ );
+ }}
+
+
+ );
+}
+
+export default ReportsTable;
diff --git a/src/components/pages/reports/BaseParameters.js b/src/app/(main)/reports/[id]/BaseParameters.js
similarity index 100%
rename from src/components/pages/reports/BaseParameters.js
rename to src/app/(main)/reports/[id]/BaseParameters.js
index 44a9da5b..a54ef4f3 100644
--- a/src/components/pages/reports/BaseParameters.js
+++ b/src/app/(main)/reports/[id]/BaseParameters.js
@@ -1,10 +1,10 @@
+import { useContext } from 'react';
import { FormRow } from 'react-basics';
+import { parseDateRange } from 'lib/date';
import DateFilter from 'components/input/DateFilter';
import WebsiteSelect from 'components/input/WebsiteSelect';
-import { parseDateRange } from 'lib/date';
-import { useContext } from 'react';
-import { ReportContext } from './Report';
import { useMessages } from 'components/hooks';
+import { ReportContext } from './Report';
export function BaseParameters({
showWebsiteSelect = true,
diff --git a/src/components/pages/reports/FieldAddForm.js b/src/app/(main)/reports/[id]/FieldAddForm.js
similarity index 88%
rename from src/components/pages/reports/FieldAddForm.js
rename to src/app/(main)/reports/[id]/FieldAddForm.js
index e8831247..6923bceb 100644
--- a/src/components/pages/reports/FieldAddForm.js
+++ b/src/app/(main)/reports/[id]/FieldAddForm.js
@@ -7,7 +7,7 @@ import FieldAggregateForm from './FieldAggregateForm';
import FieldFilterForm from './FieldFilterForm';
import styles from './FieldAddForm.module.css';
-export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
+export function FieldAddForm({ fields = [], group, onAdd, onClose }) {
const [selected, setSelected] = useState();
const handleSelect = value => {
@@ -28,7 +28,7 @@ export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
};
return createPortal(
-
+
{!selected && }
{selected && group === REPORT_PARAMETERS.fields && (
diff --git a/src/components/pages/reports/FieldAddForm.module.css b/src/app/(main)/reports/[id]/FieldAddForm.module.css
similarity index 100%
rename from src/components/pages/reports/FieldAddForm.module.css
rename to src/app/(main)/reports/[id]/FieldAddForm.module.css
diff --git a/src/components/pages/reports/FieldAggregateForm.js b/src/app/(main)/reports/[id]/FieldAggregateForm.js
similarity index 100%
rename from src/components/pages/reports/FieldAggregateForm.js
rename to src/app/(main)/reports/[id]/FieldAggregateForm.js
diff --git a/src/components/pages/reports/FieldFilterForm.js b/src/app/(main)/reports/[id]/FieldFilterForm.js
similarity index 100%
rename from src/components/pages/reports/FieldFilterForm.js
rename to src/app/(main)/reports/[id]/FieldFilterForm.js
diff --git a/src/components/pages/reports/FieldFilterForm.module.css b/src/app/(main)/reports/[id]/FieldFilterForm.module.css
similarity index 100%
rename from src/components/pages/reports/FieldFilterForm.module.css
rename to src/app/(main)/reports/[id]/FieldFilterForm.module.css
diff --git a/src/components/pages/reports/FieldSelectForm.js b/src/app/(main)/reports/[id]/FieldSelectForm.js
similarity index 100%
rename from src/components/pages/reports/FieldSelectForm.js
rename to src/app/(main)/reports/[id]/FieldSelectForm.js
diff --git a/src/components/pages/reports/FieldSelectForm.module.css b/src/app/(main)/reports/[id]/FieldSelectForm.module.css
similarity index 100%
rename from src/components/pages/reports/FieldSelectForm.module.css
rename to src/app/(main)/reports/[id]/FieldSelectForm.module.css
diff --git a/src/components/pages/reports/FilterSelectForm.js b/src/app/(main)/reports/[id]/FilterSelectForm.js
similarity index 90%
rename from src/components/pages/reports/FilterSelectForm.js
rename to src/app/(main)/reports/[id]/FilterSelectForm.js
index 5265c741..9ad4cd93 100644
--- a/src/components/pages/reports/FilterSelectForm.js
+++ b/src/app/(main)/reports/[id]/FilterSelectForm.js
@@ -1,16 +1,20 @@
import { useState } from 'react';
+import { Loading } from 'react-basics';
+import { subDays } from 'date-fns';
import FieldSelectForm from './FieldSelectForm';
import FieldFilterForm from './FieldFilterForm';
import { useApi } from 'components/hooks';
-import { Loading } from 'react-basics';
function useValues(websiteId, type) {
+ const now = Date.now();
const { get, useQuery } = useApi();
const { data, error, isLoading } = useQuery(
['websites:values', websiteId, type],
() =>
get(`/websites/${websiteId}/values`, {
type,
+ startAt: +subDays(now, 90),
+ endAt: now,
}),
{ enabled: !!(websiteId && type) },
);
diff --git a/src/components/pages/reports/ParameterList.js b/src/app/(main)/reports/[id]/ParameterList.js
similarity index 100%
rename from src/components/pages/reports/ParameterList.js
rename to src/app/(main)/reports/[id]/ParameterList.js
diff --git a/src/components/pages/reports/ParameterList.module.css b/src/app/(main)/reports/[id]/ParameterList.module.css
similarity index 100%
rename from src/components/pages/reports/ParameterList.module.css
rename to src/app/(main)/reports/[id]/ParameterList.module.css
diff --git a/src/components/pages/reports/PopupForm.js b/src/app/(main)/reports/[id]/PopupForm.js
similarity index 100%
rename from src/components/pages/reports/PopupForm.js
rename to src/app/(main)/reports/[id]/PopupForm.js
diff --git a/src/components/pages/reports/PopupForm.module.css b/src/app/(main)/reports/[id]/PopupForm.module.css
similarity index 100%
rename from src/components/pages/reports/PopupForm.module.css
rename to src/app/(main)/reports/[id]/PopupForm.module.css
diff --git a/src/components/pages/reports/Report.js b/src/app/(main)/reports/[id]/Report.js
similarity index 72%
rename from src/components/pages/reports/Report.js
rename to src/app/(main)/reports/[id]/Report.js
index 538a444d..4514a60a 100644
--- a/src/components/pages/reports/Report.js
+++ b/src/app/(main)/reports/[id]/Report.js
@@ -1,18 +1,22 @@
+'use client';
import { createContext } from 'react';
-import Page from 'components/layout/Page';
-import styles from './reports.module.css';
import { useReport } from 'components/hooks';
+import styles from './Report.module.css';
export const ReportContext = createContext(null);
export function Report({ reportId, defaultParameters, children, ...props }) {
const report = useReport(reportId, defaultParameters);
+ if (!report) {
+ return null;
+ }
+
return (
-
+
{children}
-
+
);
}
diff --git a/src/app/(main)/reports/[id]/Report.module.css b/src/app/(main)/reports/[id]/Report.module.css
new file mode 100644
index 00000000..18153655
--- /dev/null
+++ b/src/app/(main)/reports/[id]/Report.module.css
@@ -0,0 +1,5 @@
+.container {
+ display: grid;
+ grid-template-rows: max-content 1fr;
+ grid-template-columns: max-content 1fr;
+}
diff --git a/src/components/pages/reports/ReportBody.js b/src/app/(main)/reports/[id]/ReportBody.js
similarity index 73%
rename from src/components/pages/reports/ReportBody.js
rename to src/app/(main)/reports/[id]/ReportBody.js
index 2310c8af..a116bf8e 100644
--- a/src/components/pages/reports/ReportBody.js
+++ b/src/app/(main)/reports/[id]/ReportBody.js
@@ -1,4 +1,4 @@
-import styles from './reports.module.css';
+import styles from './ReportBody.module.css';
export function ReportBody({ children }) {
return {children}
;
diff --git a/src/app/(main)/reports/[id]/ReportBody.module.css b/src/app/(main)/reports/[id]/ReportBody.module.css
new file mode 100644
index 00000000..79e3caa0
--- /dev/null
+++ b/src/app/(main)/reports/[id]/ReportBody.module.css
@@ -0,0 +1,5 @@
+.body {
+ padding-left: 20px;
+ grid-row: 2/3;
+ grid-column: 2 / 3;
+}
diff --git a/src/app/(main)/reports/[id]/ReportDetails.js b/src/app/(main)/reports/[id]/ReportDetails.js
new file mode 100644
index 00000000..8605ffb3
--- /dev/null
+++ b/src/app/(main)/reports/[id]/ReportDetails.js
@@ -0,0 +1,26 @@
+'use client';
+import FunnelReport from '../funnel/FunnelReport';
+import EventDataReport from '../event-data/EventDataReport';
+import InsightsReport from '../insights/InsightsReport';
+import RetentionReport from '../retention/RetentionReport';
+import { useApi } from 'components/hooks';
+
+const reports = {
+ funnel: FunnelReport,
+ 'event-data': EventDataReport,
+ insights: InsightsReport,
+ retention: RetentionReport,
+};
+
+export default function ReportDetails({ reportId }) {
+ const { get, useQuery } = useApi();
+ const { data: report } = useQuery(['reports', reportId], () => get(`/reports/${reportId}`));
+
+ if (!report) {
+ return null;
+ }
+
+ const ReportComponent = reports[report.type];
+
+ return ;
+}
diff --git a/src/components/pages/reports/ReportHeader.js b/src/app/(main)/reports/[id]/ReportHeader.js
similarity index 65%
rename from src/components/pages/reports/ReportHeader.js
rename to src/app/(main)/reports/[id]/ReportHeader.js
index 930f745b..ed3b9736 100644
--- a/src/components/pages/reports/ReportHeader.js
+++ b/src/app/(main)/reports/[id]/ReportHeader.js
@@ -1,11 +1,10 @@
import { useContext } from 'react';
-import { useRouter } from 'next/router';
+import { useRouter } from 'next/navigation';
import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics';
-import PageHeader from 'components/layout/PageHeader';
import { useMessages, useApi } from 'components/hooks';
import { ReportContext } from './Report';
import styles from './ReportHeader.module.css';
-import reportStyles from './reports.module.css';
+import { REPORT_TYPES } from 'lib/constants';
export function ReportHeader({ icon }) {
const { report, updateReport } = useContext(ReportContext);
@@ -47,24 +46,39 @@ export function ReportHeader({ icon }) {
updateReport({ description });
};
- const Title = () => {
- return (
- <>
- {icon}
-
- >
- );
- };
+ if (!report) {
+ return null;
+ }
return (
-
-
}>
+
+
+
+ {formatMessage(
+ labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === report?.type)],
+ )}
+
+
+ {icon}
+
+
+
+
+
+
+
{formatMessage(labels.save)}
-
-
-
);
diff --git a/src/app/(main)/reports/[id]/ReportHeader.module.css b/src/app/(main)/reports/[id]/ReportHeader.module.css
new file mode 100644
index 00000000..5ff26104
--- /dev/null
+++ b/src/app/(main)/reports/[id]/ReportHeader.module.css
@@ -0,0 +1,36 @@
+.header {
+ display: grid;
+ grid-template-columns: 1fr min-content;
+ align-items: center;
+ grid-row: 1 / 2;
+ grid-column: 1 / 3;
+ margin: 20px 0 40px 0;
+}
+
+.title {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ font-size: 24px;
+ font-weight: 700;
+ gap: 20px;
+ height: 60px;
+}
+
+.type {
+ font-size: 11px;
+ font-weight: 700;
+ text-transform: uppercase;
+ color: var(--base600);
+}
+
+.description {
+ color: var(--font-color300);
+ max-width: 500px;
+ height: 30px;
+}
+
+.actions {
+ display: flex;
+ align-items: center;
+}
diff --git a/src/components/pages/reports/ReportMenu.js b/src/app/(main)/reports/[id]/ReportMenu.js
similarity index 73%
rename from src/components/pages/reports/ReportMenu.js
rename to src/app/(main)/reports/[id]/ReportMenu.js
index abfea6fe..72bc197a 100644
--- a/src/components/pages/reports/ReportMenu.js
+++ b/src/app/(main)/reports/[id]/ReportMenu.js
@@ -1,4 +1,4 @@
-import styles from './reports.module.css';
+import styles from './ReportMenu.module.css';
export function ReportMenu({ children }) {
return
{children}
;
diff --git a/src/app/(main)/reports/[id]/ReportMenu.module.css b/src/app/(main)/reports/[id]/ReportMenu.module.css
new file mode 100644
index 00000000..1fc8db29
--- /dev/null
+++ b/src/app/(main)/reports/[id]/ReportMenu.module.css
@@ -0,0 +1,7 @@
+.menu {
+ width: 300px;
+ padding-right: 20px;
+ border-right: 1px solid var(--base300);
+ grid-row: 2 / 3;
+ grid-column: 1 / 2;
+}
diff --git a/src/app/(main)/reports/[id]/page.tsx b/src/app/(main)/reports/[id]/page.tsx
new file mode 100644
index 00000000..9ba87f41
--- /dev/null
+++ b/src/app/(main)/reports/[id]/page.tsx
@@ -0,0 +1,14 @@
+import ReportDetails from './ReportDetails';
+import { Metadata } from 'next';
+
+export default function ReportDetailsPage({ params: { id } }) {
+ if (!id) {
+ return null;
+ }
+
+ return
;
+}
+
+export const metadata: Metadata = {
+ title: 'Reports | umami',
+};
diff --git a/src/components/pages/reports/ReportTemplates.js b/src/app/(main)/reports/create/ReportTemplates.js
similarity index 96%
rename from src/components/pages/reports/ReportTemplates.js
rename to src/app/(main)/reports/create/ReportTemplates.js
index 59cc8b31..003cb3fc 100644
--- a/src/components/pages/reports/ReportTemplates.js
+++ b/src/app/(main)/reports/create/ReportTemplates.js
@@ -1,6 +1,6 @@
+'use client';
import Link from 'next/link';
import { Button, Icons, Text, Icon } from 'react-basics';
-import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import Funnel from 'assets/funnel.svg';
import Lightbulb from 'assets/lightbulb.svg';
@@ -57,7 +57,7 @@ export function ReportTemplates({ showHeader = true }) {
];
return (
-
+ <>
{showHeader && }
{reports.map(({ title, description, url, icon }) => {
@@ -66,7 +66,7 @@ export function ReportTemplates({ showHeader = true }) {
);
})}
-
+ >
);
}
diff --git a/src/components/pages/reports/ReportTemplates.module.css b/src/app/(main)/reports/create/ReportTemplates.module.css
similarity index 100%
rename from src/components/pages/reports/ReportTemplates.module.css
rename to src/app/(main)/reports/create/ReportTemplates.module.css
diff --git a/src/app/(main)/reports/create/page.tsx b/src/app/(main)/reports/create/page.tsx
new file mode 100644
index 00000000..a1a761bc
--- /dev/null
+++ b/src/app/(main)/reports/create/page.tsx
@@ -0,0 +1,10 @@
+import ReportTemplates from './ReportTemplates';
+import { Metadata } from 'next';
+
+export default function ReportsCreatePage() {
+ return
;
+}
+
+export const metadata: Metadata = {
+ title: 'Create Report | umami',
+};
diff --git a/src/components/pages/reports/event-data/EventDataParameters.js b/src/app/(main)/reports/event-data/EventDataParameters.js
similarity index 94%
rename from src/components/pages/reports/event-data/EventDataParameters.js
rename to src/app/(main)/reports/event-data/EventDataParameters.js
index e0fadb8b..6b9a0344 100644
--- a/src/components/pages/reports/event-data/EventDataParameters.js
+++ b/src/app/(main)/reports/event-data/EventDataParameters.js
@@ -1,13 +1,13 @@
import { useContext, useRef } from 'react';
-import { useApi, useMessages } from 'components/hooks';
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
-import { ReportContext } from 'components/pages/reports/Report';
import Empty from 'components/common/Empty';
-import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
import Icons from 'components/icons';
-import FieldAddForm from '../FieldAddForm';
-import BaseParameters from '../BaseParameters';
-import ParameterList from '../ParameterList';
+import { useApi, useMessages } from 'components/hooks';
+import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
+import { ReportContext } from '../[id]/Report';
+import FieldAddForm from '../[id]/FieldAddForm';
+import ParameterList from '../[id]/ParameterList';
+import BaseParameters from '../[id]/BaseParameters';
import styles from './EventDataParameters.module.css';
function useFields(websiteId, startDate, endDate) {
@@ -74,7 +74,7 @@ export function EventDataParameters() {
- {(close, element) => {
+ {close => {
return (
({
@@ -82,7 +82,6 @@ export function EventDataParameters() {
type: DATA_TYPES[eventDataType],
}))}
group={group}
- element={element}
onAdd={handleAdd}
onClose={close}
/>
diff --git a/src/components/pages/reports/event-data/EventDataParameters.module.css b/src/app/(main)/reports/event-data/EventDataParameters.module.css
similarity index 100%
rename from src/components/pages/reports/event-data/EventDataParameters.module.css
rename to src/app/(main)/reports/event-data/EventDataParameters.module.css
diff --git a/src/components/pages/reports/event-data/EventDataReport.js b/src/app/(main)/reports/event-data/EventDataReport.js
similarity index 76%
rename from src/components/pages/reports/event-data/EventDataReport.js
rename to src/app/(main)/reports/event-data/EventDataReport.js
index eb49a29d..e91cb4a2 100644
--- a/src/components/pages/reports/event-data/EventDataReport.js
+++ b/src/app/(main)/reports/event-data/EventDataReport.js
@@ -1,7 +1,7 @@
-import Report from '../Report';
-import ReportHeader from '../ReportHeader';
-import ReportMenu from '../ReportMenu';
-import ReportBody from '../ReportBody';
+import Report from '../[id]/Report';
+import ReportHeader from '../[id]/ReportHeader';
+import ReportMenu from '../[id]/ReportMenu';
+import ReportBody from '../[id]/ReportBody';
import EventDataParameters from './EventDataParameters';
import EventDataTable from './EventDataTable';
import Nodes from 'assets/nodes.svg';
diff --git a/src/components/pages/reports/event-data/EventDataTable.js b/src/app/(main)/reports/event-data/EventDataTable.js
similarity index 92%
rename from src/components/pages/reports/event-data/EventDataTable.js
rename to src/app/(main)/reports/event-data/EventDataTable.js
index b6450261..b709aee7 100644
--- a/src/components/pages/reports/event-data/EventDataTable.js
+++ b/src/app/(main)/reports/event-data/EventDataTable.js
@@ -1,7 +1,7 @@
import { useContext } from 'react';
import { GridTable, GridColumn } from 'react-basics';
import { useMessages } from 'components/hooks';
-import { ReportContext } from '../Report';
+import { ReportContext } from '../[id]/Report';
export function EventDataTable() {
const { report } = useContext(ReportContext);
diff --git a/src/components/pages/reports/funnel/FunnelChart.js b/src/app/(main)/reports/funnel/FunnelChart.js
similarity index 97%
rename from src/components/pages/reports/funnel/FunnelChart.js
rename to src/app/(main)/reports/funnel/FunnelChart.js
index 829a3008..7461afbc 100644
--- a/src/components/pages/reports/funnel/FunnelChart.js
+++ b/src/app/(main)/reports/funnel/FunnelChart.js
@@ -5,7 +5,7 @@ import useTheme from 'components/hooks/useTheme';
import BarChart from 'components/metrics/BarChart';
import { formatLongNumber } from 'lib/format';
import styles from './FunnelChart.module.css';
-import { ReportContext } from '../Report';
+import { ReportContext } from '../[id]/Report';
export function FunnelChart({ className, loading }) {
const { report } = useContext(ReportContext);
diff --git a/src/components/pages/reports/funnel/FunnelChart.module.css b/src/app/(main)/reports/funnel/FunnelChart.module.css
similarity index 100%
rename from src/components/pages/reports/funnel/FunnelChart.module.css
rename to src/app/(main)/reports/funnel/FunnelChart.module.css
diff --git a/src/components/pages/reports/funnel/FunnelParameters.js b/src/app/(main)/reports/funnel/FunnelParameters.js
similarity index 81%
rename from src/components/pages/reports/funnel/FunnelParameters.js
rename to src/app/(main)/reports/funnel/FunnelParameters.js
index a3fbe663..135b5db8 100644
--- a/src/components/pages/reports/funnel/FunnelParameters.js
+++ b/src/app/(main)/reports/funnel/FunnelParameters.js
@@ -13,10 +13,10 @@ import {
} from 'react-basics';
import Icons from 'components/icons';
import UrlAddForm from './UrlAddForm';
-import { ReportContext } from 'components/pages/reports/Report';
-import BaseParameters from '../BaseParameters';
-import ParameterList from '../ParameterList';
-import PopupForm from '../PopupForm';
+import { ReportContext } from '../[id]/Report';
+import BaseParameters from '../[id]/BaseParameters';
+import ParameterList from '../[id]/ParameterList';
+import PopupForm from '../[id]/PopupForm';
export function FunnelParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
@@ -52,14 +52,10 @@ export function FunnelParameters() {
-
- {(close, element) => {
- return (
-
-
-
- );
- }}
+
+
+
+
);
diff --git a/src/components/pages/reports/funnel/FunnelReport.js b/src/app/(main)/reports/funnel/FunnelReport.js
similarity index 77%
rename from src/components/pages/reports/funnel/FunnelReport.js
rename to src/app/(main)/reports/funnel/FunnelReport.js
index d2971fa3..69f46091 100644
--- a/src/components/pages/reports/funnel/FunnelReport.js
+++ b/src/app/(main)/reports/funnel/FunnelReport.js
@@ -1,10 +1,11 @@
+'use client';
import FunnelChart from './FunnelChart';
import FunnelTable from './FunnelTable';
import FunnelParameters from './FunnelParameters';
-import Report from '../Report';
-import ReportHeader from '../ReportHeader';
-import ReportMenu from '../ReportMenu';
-import ReportBody from '../ReportBody';
+import Report from '../[id]/Report';
+import ReportHeader from '../[id]/ReportHeader';
+import ReportMenu from '../[id]/ReportMenu';
+import ReportBody from '../[id]/ReportBody';
import Funnel from 'assets/funnel.svg';
import { REPORT_TYPES } from 'lib/constants';
diff --git a/src/components/pages/reports/funnel/FunnelReport.module.css b/src/app/(main)/reports/funnel/FunnelReport.module.css
similarity index 100%
rename from src/components/pages/reports/funnel/FunnelReport.module.css
rename to src/app/(main)/reports/funnel/FunnelReport.module.css
diff --git a/src/components/pages/reports/funnel/FunnelTable.js b/src/app/(main)/reports/funnel/FunnelTable.js
similarity index 90%
rename from src/components/pages/reports/funnel/FunnelTable.js
rename to src/app/(main)/reports/funnel/FunnelTable.js
index 5ca2593c..4cf797f2 100644
--- a/src/components/pages/reports/funnel/FunnelTable.js
+++ b/src/app/(main)/reports/funnel/FunnelTable.js
@@ -1,7 +1,7 @@
import { useContext } from 'react';
import ListTable from 'components/metrics/ListTable';
import { useMessages } from 'components/hooks';
-import { ReportContext } from '../Report';
+import { ReportContext } from '../[id]/Report';
export function FunnelTable() {
const { report } = useContext(ReportContext);
diff --git a/src/components/pages/reports/funnel/UrlAddForm.js b/src/app/(main)/reports/funnel/UrlAddForm.js
similarity index 100%
rename from src/components/pages/reports/funnel/UrlAddForm.js
rename to src/app/(main)/reports/funnel/UrlAddForm.js
diff --git a/src/components/pages/reports/funnel/UrlAddForm.module.css b/src/app/(main)/reports/funnel/UrlAddForm.module.css
similarity index 100%
rename from src/components/pages/reports/funnel/UrlAddForm.module.css
rename to src/app/(main)/reports/funnel/UrlAddForm.module.css
diff --git a/src/app/(main)/reports/funnel/page.tsx b/src/app/(main)/reports/funnel/page.tsx
new file mode 100644
index 00000000..1ce70c75
--- /dev/null
+++ b/src/app/(main)/reports/funnel/page.tsx
@@ -0,0 +1,10 @@
+import FunnelReport from './FunnelReport';
+import { Metadata } from 'next';
+
+export default function FunnelReportPage() {
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Funnel Report | umami',
+};
diff --git a/src/components/pages/reports/insights/InsightsParameters.js b/src/app/(main)/reports/insights/InsightsParameters.js
similarity index 81%
rename from src/components/pages/reports/insights/InsightsParameters.js
rename to src/app/(main)/reports/insights/InsightsParameters.js
index 3ddc0367..91dd09f8 100644
--- a/src/components/pages/reports/insights/InsightsParameters.js
+++ b/src/app/(main)/reports/insights/InsightsParameters.js
@@ -10,14 +10,14 @@ import {
Popup,
TooltipPopup,
} from 'react-basics';
-import { ReportContext } from 'components/pages/reports/Report';
import Icons from 'components/icons';
-import BaseParameters from '../BaseParameters';
-import ParameterList from '../ParameterList';
+import BaseParameters from '../[id]/BaseParameters';
+import { ReportContext } from '../[id]/Report';
+import ParameterList from '../[id]/ParameterList';
+import FilterSelectForm from '../[id]/FilterSelectForm';
+import FieldSelectForm from '../[id]/FieldSelectForm';
+import PopupForm from '../[id]/PopupForm';
import styles from './InsightsParameters.module.css';
-import PopupForm from '../PopupForm';
-import FilterSelectForm from '../FilterSelectForm';
-import FieldSelectForm from '../FieldSelectForm';
export function InsightsParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
@@ -81,26 +81,22 @@ export function InsightsParameters() {
- {close => {
- return (
-
- {id === 'fields' && (
-
- )}
- {id === 'filters' && (
-
- )}
-
- );
- }}
+
+ {id === 'fields' && (
+
+ )}
+ {id === 'filters' && (
+
+ )}
+
);
diff --git a/src/components/pages/reports/insights/InsightsParameters.module.css b/src/app/(main)/reports/insights/InsightsParameters.module.css
similarity index 100%
rename from src/components/pages/reports/insights/InsightsParameters.module.css
rename to src/app/(main)/reports/insights/InsightsParameters.module.css
diff --git a/src/components/pages/reports/insights/InsightsReport.js b/src/app/(main)/reports/insights/InsightsReport.js
similarity index 76%
rename from src/components/pages/reports/insights/InsightsReport.js
rename to src/app/(main)/reports/insights/InsightsReport.js
index 3d855d9e..f99e187b 100644
--- a/src/components/pages/reports/insights/InsightsReport.js
+++ b/src/app/(main)/reports/insights/InsightsReport.js
@@ -1,7 +1,8 @@
-import Report from '../Report';
-import ReportHeader from '../ReportHeader';
-import ReportMenu from '../ReportMenu';
-import ReportBody from '../ReportBody';
+'use client';
+import Report from '../[id]/Report';
+import ReportHeader from '../[id]/ReportHeader';
+import ReportMenu from '../[id]/ReportMenu';
+import ReportBody from '../[id]/ReportBody';
import InsightsParameters from './InsightsParameters';
import InsightsTable from './InsightsTable';
import Lightbulb from 'assets/lightbulb.svg';
diff --git a/src/components/pages/reports/insights/InsightsTable.js b/src/app/(main)/reports/insights/InsightsTable.js
similarity index 96%
rename from src/components/pages/reports/insights/InsightsTable.js
rename to src/app/(main)/reports/insights/InsightsTable.js
index 88bd0275..05d38042 100644
--- a/src/components/pages/reports/insights/InsightsTable.js
+++ b/src/app/(main)/reports/insights/InsightsTable.js
@@ -1,7 +1,7 @@
import { useContext, useEffect, useState } from 'react';
import { GridTable, GridColumn } from 'react-basics';
import { useFormat, useMessages } from 'components/hooks';
-import { ReportContext } from '../Report';
+import { ReportContext } from '../[id]/Report';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
export function InsightsTable() {
diff --git a/src/app/(main)/reports/insights/page.tsx b/src/app/(main)/reports/insights/page.tsx
new file mode 100644
index 00000000..1f4db5c6
--- /dev/null
+++ b/src/app/(main)/reports/insights/page.tsx
@@ -0,0 +1,10 @@
+import InsightsReport from './InsightsReport';
+import { Metadata } from 'next';
+
+export default function InsightsReportPage() {
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Insights Report | umami',
+};
diff --git a/src/app/(main)/reports/page.tsx b/src/app/(main)/reports/page.tsx
new file mode 100644
index 00000000..aba59db2
--- /dev/null
+++ b/src/app/(main)/reports/page.tsx
@@ -0,0 +1,14 @@
+import ReportsHeader from './ReportsHeader';
+import ReportsDataTable from './ReportsDataTable';
+
+export default function ReportsPage() {
+ return (
+ <>
+
+
+ >
+ );
+}
+export const metadata = {
+ title: 'Reports | umami',
+};
diff --git a/src/components/pages/reports/retention/RetentionParameters.js b/src/app/(main)/reports/retention/RetentionParameters.js
similarity index 92%
rename from src/components/pages/reports/retention/RetentionParameters.js
rename to src/app/(main)/reports/retention/RetentionParameters.js
index e87108d1..762a313d 100644
--- a/src/components/pages/reports/retention/RetentionParameters.js
+++ b/src/app/(main)/reports/retention/RetentionParameters.js
@@ -1,9 +1,9 @@
import { useContext, useRef } from 'react';
import { useMessages } from 'components/hooks';
import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics';
-import { ReportContext } from 'components/pages/reports/Report';
+import { ReportContext } from '../[id]/Report';
import { MonthSelect } from 'components/input/MonthSelect';
-import BaseParameters from '../BaseParameters';
+import BaseParameters from '../[id]/BaseParameters';
import { parseDateRange } from 'lib/date';
export function RetentionParameters() {
@@ -19,6 +19,7 @@ export function RetentionParameters() {
const handleSubmit = (data, e) => {
e.stopPropagation();
e.preventDefault();
+
if (!queryDisabled) {
runReport(data);
}
diff --git a/src/components/pages/reports/retention/RetentionReport.js b/src/app/(main)/reports/retention/RetentionReport.js
similarity index 81%
rename from src/components/pages/reports/retention/RetentionReport.js
rename to src/app/(main)/reports/retention/RetentionReport.js
index a9aaeb3e..ae42e76b 100644
--- a/src/components/pages/reports/retention/RetentionReport.js
+++ b/src/app/(main)/reports/retention/RetentionReport.js
@@ -1,9 +1,10 @@
+'use client';
import RetentionTable from './RetentionTable';
import RetentionParameters from './RetentionParameters';
-import Report from '../Report';
-import ReportHeader from '../ReportHeader';
-import ReportMenu from '../ReportMenu';
-import ReportBody from '../ReportBody';
+import Report from '../[id]/Report';
+import ReportHeader from '../[id]/ReportHeader';
+import ReportMenu from '../[id]/ReportMenu';
+import ReportBody from '../[id]/ReportBody';
import Magnet from 'assets/magnet.svg';
import { REPORT_TYPES } from 'lib/constants';
import { parseDateRange } from 'lib/date';
diff --git a/src/components/pages/reports/retention/RetentionReport.module.css b/src/app/(main)/reports/retention/RetentionReport.module.css
similarity index 100%
rename from src/components/pages/reports/retention/RetentionReport.module.css
rename to src/app/(main)/reports/retention/RetentionReport.module.css
diff --git a/src/components/pages/reports/retention/RetentionTable.js b/src/app/(main)/reports/retention/RetentionTable.js
similarity index 91%
rename from src/components/pages/reports/retention/RetentionTable.js
rename to src/app/(main)/reports/retention/RetentionTable.js
index ad1eaa6f..a71fae6f 100644
--- a/src/components/pages/reports/retention/RetentionTable.js
+++ b/src/app/(main)/reports/retention/RetentionTable.js
@@ -1,13 +1,14 @@
import { useContext } from 'react';
import classNames from 'classnames';
-import { ReportContext } from '../Report';
+import { ReportContext } from '../[id]/Report';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
-import { useMessages } from 'components/hooks';
-import { useLocale } from 'components/hooks';
+import { useMessages, useLocale } from 'components/hooks';
import { formatDate } from 'lib/date';
import styles from './RetentionTable.module.css';
-export function RetentionTable() {
+const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
+
+export function RetentionTable({ days = DAYS }) {
const { formatMessage, labels } = useMessages();
const { locale } = useLocale();
const { report } = useContext(ReportContext);
@@ -17,8 +18,6 @@ export function RetentionTable() {
return ;
}
- const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
-
const rows = data.reduce((arr, row) => {
const { date, visitors, day } = row;
if (day === 0) {
diff --git a/src/components/pages/reports/retention/RetentionTable.module.css b/src/app/(main)/reports/retention/RetentionTable.module.css
similarity index 100%
rename from src/components/pages/reports/retention/RetentionTable.module.css
rename to src/app/(main)/reports/retention/RetentionTable.module.css
diff --git a/src/app/(main)/reports/retention/page.js b/src/app/(main)/reports/retention/page.js
new file mode 100644
index 00000000..7c60cee8
--- /dev/null
+++ b/src/app/(main)/reports/retention/page.js
@@ -0,0 +1,9 @@
+import RetentionReport from './RetentionReport';
+
+export default function RetentionReportPage() {
+ return ;
+}
+
+export const metadata = {
+ title: 'Create Report | umami',
+};
diff --git a/src/app/(main)/settings/layout.module.css b/src/app/(main)/settings/layout.module.css
new file mode 100644
index 00000000..19162ef5
--- /dev/null
+++ b/src/app/(main)/settings/layout.module.css
@@ -0,0 +1,31 @@
+.layout {
+ display: grid;
+ grid-template-columns: max-content 1fr;
+ gap: 20px;
+}
+
+.menu {
+ width: 240px;
+ padding-top: 34px;
+ padding-right: 20px;
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ min-height: 50vh;
+}
+
+@media only screen and (max-width: 992px) {
+ .layout {
+ grid-template-columns: 1fr;
+ }
+
+ .menu {
+ display: none;
+ }
+
+ .content {
+ margin-top: 20px;
+ }
+}
diff --git a/src/components/layout/SettingsLayout.js b/src/app/(main)/settings/layout.tsx
similarity index 57%
rename from src/components/layout/SettingsLayout.js
rename to src/app/(main)/settings/layout.tsx
index 0f4aa5d9..f738f883 100644
--- a/src/components/layout/SettingsLayout.js
+++ b/src/app/(main)/settings/layout.tsx
@@ -1,15 +1,15 @@
-import { Row, Column } from 'react-basics';
-import { useRouter } from 'next/router';
-import SideNav from './SideNav';
+'use client';
+import { usePathname } from 'next/navigation';
import useUser from 'components/hooks/useUser';
import useMessages from 'components/hooks/useMessages';
-import styles from './SettingsLayout.module.css';
+import SideNav from 'components/layout/SideNav';
+import styles from './layout.module.css';
-export function SettingsLayout({ children }) {
+export default function SettingsLayout({ children }) {
const { user } = useUser();
- const { pathname } = useRouter();
+ const pathname = usePathname();
const { formatMessage, labels } = useMessages();
- const cloudMode = Boolean(process.env.cloudMode);
+ const cloudMode = !!process.env.cloudMode;
const items = [
{ key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' },
@@ -20,18 +20,18 @@ export function SettingsLayout({ children }) {
const getKey = () => items.find(({ url }) => pathname === url)?.key;
+ if (cloudMode && pathname != '/settings/profile') {
+ return null;
+ }
+
return (
-
+
{!cloudMode && (
-
+
-
+
)}
-
- {children}
-
-
+ {children}
+
);
}
-
-export default SettingsLayout;
diff --git a/src/components/pages/settings/profile/DateRangeSetting.js b/src/app/(main)/settings/profile/DateRangeSetting.js
similarity index 100%
rename from src/components/pages/settings/profile/DateRangeSetting.js
rename to src/app/(main)/settings/profile/DateRangeSetting.js
diff --git a/src/components/pages/settings/profile/LanguageSetting.js b/src/app/(main)/settings/profile/LanguageSetting.js
similarity index 100%
rename from src/components/pages/settings/profile/LanguageSetting.js
rename to src/app/(main)/settings/profile/LanguageSetting.js
diff --git a/src/components/pages/settings/profile/PasswordChangeButton.js b/src/app/(main)/settings/profile/PasswordChangeButton.js
similarity index 91%
rename from src/components/pages/settings/profile/PasswordChangeButton.js
rename to src/app/(main)/settings/profile/PasswordChangeButton.js
index 81324eaa..29ec844a 100644
--- a/src/components/pages/settings/profile/PasswordChangeButton.js
+++ b/src/app/(main)/settings/profile/PasswordChangeButton.js
@@ -1,5 +1,5 @@
import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics';
-import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm';
+import PasswordEditForm from 'app/(main)/settings/profile/PasswordEditForm';
import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages';
diff --git a/src/components/pages/settings/profile/PasswordEditForm.js b/src/app/(main)/settings/profile/PasswordEditForm.js
similarity index 100%
rename from src/components/pages/settings/profile/PasswordEditForm.js
rename to src/app/(main)/settings/profile/PasswordEditForm.js
diff --git a/src/app/(main)/settings/profile/ProfileHeader.js b/src/app/(main)/settings/profile/ProfileHeader.js
new file mode 100644
index 00000000..35aeb0e6
--- /dev/null
+++ b/src/app/(main)/settings/profile/ProfileHeader.js
@@ -0,0 +1,11 @@
+'use client';
+import PageHeader from 'components/layout/PageHeader';
+import { useMessages } from 'components/hooks';
+
+export function ProfileHeader() {
+ const { formatMessage, labels } = useMessages();
+
+ return ;
+}
+
+export default ProfileHeader;
diff --git a/src/components/pages/settings/profile/ProfileDetails.js b/src/app/(main)/settings/profile/ProfileSettings.js
similarity index 80%
rename from src/components/pages/settings/profile/ProfileDetails.js
rename to src/app/(main)/settings/profile/ProfileSettings.js
index d4a3a7d5..89c6354c 100644
--- a/src/components/pages/settings/profile/ProfileDetails.js
+++ b/src/app/(main)/settings/profile/ProfileSettings.js
@@ -1,14 +1,15 @@
+'use client';
import { Form, FormRow } from 'react-basics';
-import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
-import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
-import LanguageSetting from 'components/pages/settings/profile/LanguageSetting';
-import ThemeSetting from 'components/pages/settings/profile/ThemeSetting';
+import TimezoneSetting from 'app/(main)/settings/profile/TimezoneSetting';
+import DateRangeSetting from 'app/(main)/settings/profile/DateRangeSetting';
+import LanguageSetting from 'app/(main)/settings/profile/LanguageSetting';
+import ThemeSetting from 'app/(main)/settings/profile/ThemeSetting';
import PasswordChangeButton from './PasswordChangeButton';
import useUser from 'components/hooks/useUser';
import useMessages from 'components/hooks/useMessages';
import { ROLES } from 'lib/constants';
-export function ProfileDetails() {
+export function ProfileSettings() {
const { user } = useUser();
const { formatMessage, labels } = useMessages();
const cloudMode = Boolean(process.env.cloudMode);
@@ -58,4 +59,4 @@ export function ProfileDetails() {
);
}
-export default ProfileDetails;
+export default ProfileSettings;
diff --git a/src/components/pages/settings/profile/ThemeSetting.js b/src/app/(main)/settings/profile/ThemeSetting.js
similarity index 100%
rename from src/components/pages/settings/profile/ThemeSetting.js
rename to src/app/(main)/settings/profile/ThemeSetting.js
diff --git a/src/components/pages/settings/profile/ThemeSetting.module.css b/src/app/(main)/settings/profile/ThemeSetting.module.css
similarity index 100%
rename from src/components/pages/settings/profile/ThemeSetting.module.css
rename to src/app/(main)/settings/profile/ThemeSetting.module.css
diff --git a/src/components/pages/settings/profile/TimezoneSetting.js b/src/app/(main)/settings/profile/TimezoneSetting.js
similarity index 100%
rename from src/components/pages/settings/profile/TimezoneSetting.js
rename to src/app/(main)/settings/profile/TimezoneSetting.js
diff --git a/src/app/(main)/settings/profile/page.tsx b/src/app/(main)/settings/profile/page.tsx
new file mode 100644
index 00000000..d7a3ad92
--- /dev/null
+++ b/src/app/(main)/settings/profile/page.tsx
@@ -0,0 +1,16 @@
+import ProfileHeader from './ProfileHeader';
+import ProfileSettings from './ProfileSettings';
+import { Metadata } from 'next';
+
+export default function () {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const metadata: Metadata = {
+ title: 'Profile Settings | umami',
+};
diff --git a/src/components/pages/settings/teams/TeamAddForm.js b/src/app/(main)/settings/teams/TeamAddForm.js
similarity index 91%
rename from src/components/pages/settings/teams/TeamAddForm.js
rename to src/app/(main)/settings/teams/TeamAddForm.js
index 7910e098..b8bb8c3a 100644
--- a/src/components/pages/settings/teams/TeamAddForm.js
+++ b/src/app/(main)/settings/teams/TeamAddForm.js
@@ -8,6 +8,7 @@ import {
Button,
SubmitButton,
} from 'react-basics';
+import { setValue } from 'store/cache';
import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
@@ -20,8 +21,9 @@ export function TeamAddForm({ onSave, onClose }) {
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
- onSave();
- onClose();
+ setValue('teams', Date.now());
+ onSave?.();
+ onClose?.();
},
});
};
diff --git a/src/app/(main)/settings/teams/TeamDeleteButton.js b/src/app/(main)/settings/teams/TeamDeleteButton.js
new file mode 100644
index 00000000..5e4a41ea
--- /dev/null
+++ b/src/app/(main)/settings/teams/TeamDeleteButton.js
@@ -0,0 +1,25 @@
+import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
+import useMessages from 'components/hooks/useMessages';
+import TeamDeleteForm from './TeamDeleteForm';
+
+export function TeamDeleteButton({ teamId, teamName, onDelete }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.delete)}
+
+
+ {close => (
+
+ )}
+
+
+ );
+}
+
+export default TeamDeleteButton;
diff --git a/src/components/pages/settings/teams/TeamDeleteForm.js b/src/app/(main)/settings/teams/TeamDeleteForm.js
similarity index 89%
rename from src/components/pages/settings/teams/TeamDeleteForm.js
rename to src/app/(main)/settings/teams/TeamDeleteForm.js
index 210c8ada..9b80668a 100644
--- a/src/components/pages/settings/teams/TeamDeleteForm.js
+++ b/src/app/(main)/settings/teams/TeamDeleteForm.js
@@ -1,6 +1,7 @@
import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
+import { setValue } from 'store/cache';
export function TeamDeleteForm({ teamId, teamName, onSave, onClose }) {
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
@@ -10,8 +11,9 @@ export function TeamDeleteForm({ teamId, teamName, onSave, onClose }) {
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
- onSave();
- onClose();
+ setValue('teams', Date.now());
+ onSave?.();
+ onClose?.();
},
});
};
diff --git a/src/components/pages/settings/teams/TeamJoinForm.js b/src/app/(main)/settings/teams/TeamJoinForm.js
similarity index 90%
rename from src/components/pages/settings/teams/TeamJoinForm.js
rename to src/app/(main)/settings/teams/TeamJoinForm.js
index 23abcf00..528e1d75 100644
--- a/src/components/pages/settings/teams/TeamJoinForm.js
+++ b/src/app/(main)/settings/teams/TeamJoinForm.js
@@ -10,6 +10,7 @@ import {
} from 'react-basics';
import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
+import { setValue } from 'store/cache';
export function TeamJoinForm({ onSave, onClose }) {
const { formatMessage, labels, getMessage } = useMessages();
@@ -20,8 +21,9 @@ export function TeamJoinForm({ onSave, onClose }) {
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
- onSave();
- onClose();
+ setValue('teams:members', Date.now());
+ onSave?.();
+ onClose?.();
},
});
};
diff --git a/src/app/(main)/settings/teams/TeamLeaveButton.js b/src/app/(main)/settings/teams/TeamLeaveButton.js
new file mode 100644
index 00000000..7b98f082
--- /dev/null
+++ b/src/app/(main)/settings/teams/TeamLeaveButton.js
@@ -0,0 +1,35 @@
+import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
+import useMessages from 'components/hooks/useMessages';
+import useLocale from 'components/hooks/useLocale';
+import useUser from 'components/hooks/useUser';
+import TeamDeleteForm from './TeamLeaveForm';
+
+export function TeamLeaveButton({ teamId, teamName, onLeave }) {
+ const { formatMessage, labels } = useMessages();
+ const { dir } = useLocale();
+ const { user } = useUser();
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.leave)}
+
+
+ {close => (
+
+ )}
+
+
+ );
+}
+
+export default TeamLeaveButton;
diff --git a/src/components/pages/settings/teams/TeamLeaveForm.js b/src/app/(main)/settings/teams/TeamLeaveForm.js
similarity index 92%
rename from src/components/pages/settings/teams/TeamLeaveForm.js
rename to src/app/(main)/settings/teams/TeamLeaveForm.js
index 8af2932d..a9b6922a 100644
--- a/src/components/pages/settings/teams/TeamLeaveForm.js
+++ b/src/app/(main)/settings/teams/TeamLeaveForm.js
@@ -1,6 +1,7 @@
import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
+import { setValue } from 'store/cache';
export function TeamLeaveForm({ teamId, userId, teamName, onSave, onClose }) {
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
@@ -12,6 +13,7 @@ export function TeamLeaveForm({ teamId, userId, teamName, onSave, onClose }) {
{},
{
onSuccess: async () => {
+ setValue('team:members', Date.now());
onSave();
onClose();
},
diff --git a/src/app/(main)/settings/teams/TeamsAddButton.js b/src/app/(main)/settings/teams/TeamsAddButton.js
new file mode 100644
index 00000000..b7850812
--- /dev/null
+++ b/src/app/(main)/settings/teams/TeamsAddButton.js
@@ -0,0 +1,24 @@
+import { Button, Icon, Modal, ModalTrigger, Text } from 'react-basics';
+import Icons from 'components/icons';
+import useMessages from 'components/hooks/useMessages';
+import TeamAddForm from './TeamAddForm';
+
+export function TeamsAddButton({ onAdd }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.createTeam)}
+
+
+ {close => }
+
+
+ );
+}
+
+export default TeamsAddButton;
diff --git a/src/app/(main)/settings/teams/TeamsDataTable.js b/src/app/(main)/settings/teams/TeamsDataTable.js
new file mode 100644
index 00000000..164838f9
--- /dev/null
+++ b/src/app/(main)/settings/teams/TeamsDataTable.js
@@ -0,0 +1,26 @@
+'use client';
+import DataTable from 'components/common/DataTable';
+import TeamsTable from 'app/(main)/settings/teams/TeamsTable';
+import useApi from 'components/hooks/useApi';
+import useFilterQuery from 'components/hooks/useFilterQuery';
+import useCache from 'store/cache';
+
+export function TeamsDataTable() {
+ const { get } = useApi();
+ const modified = useCache(state => state?.teams);
+ const queryResult = useFilterQuery(['teams', { modified }], params => {
+ return get(`/teams`, {
+ ...params,
+ });
+ });
+
+ return (
+
+ {({ data }) => {
+ return ;
+ }}
+
+ );
+}
+
+export default TeamsDataTable;
diff --git a/src/app/(main)/settings/teams/TeamsHeader.js b/src/app/(main)/settings/teams/TeamsHeader.js
new file mode 100644
index 00000000..444f8703
--- /dev/null
+++ b/src/app/(main)/settings/teams/TeamsHeader.js
@@ -0,0 +1,24 @@
+'use client';
+import { Flexbox } from 'react-basics';
+import PageHeader from 'components/layout/PageHeader';
+import { ROLES } from 'lib/constants';
+import useUser from 'components/hooks/useUser';
+import useMessages from 'components/hooks/useMessages';
+import TeamsJoinButton from './TeamsJoinButton';
+import TeamsAddButton from './TeamsAddButton';
+
+export function TeamsHeader() {
+ const { formatMessage, labels } = useMessages();
+ const { user } = useUser();
+
+ return (
+
+
+
+ {user.role !== ROLES.viewOnly && }
+
+
+ );
+}
+
+export default TeamsHeader;
diff --git a/src/app/(main)/settings/teams/TeamsJoinButton.js b/src/app/(main)/settings/teams/TeamsJoinButton.js
new file mode 100644
index 00000000..f8d2fa23
--- /dev/null
+++ b/src/app/(main)/settings/teams/TeamsJoinButton.js
@@ -0,0 +1,29 @@
+import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
+import Icons from 'components/icons';
+import useMessages from 'components/hooks/useMessages';
+import TeamJoinForm from './TeamJoinForm';
+
+export function TeamsJoinButton() {
+ const { formatMessage, labels, messages } = useMessages();
+ const { showToast } = useToasts();
+
+ const handleJoin = () => {
+ showToast({ message: formatMessage(messages.saved), variant: 'success' });
+ };
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.joinTeam)}
+
+
+ {close => }
+
+
+ );
+}
+
+export default TeamsJoinButton;
diff --git a/src/app/(main)/settings/teams/TeamsTable.js b/src/app/(main)/settings/teams/TeamsTable.js
new file mode 100644
index 00000000..1f7f1da4
--- /dev/null
+++ b/src/app/(main)/settings/teams/TeamsTable.js
@@ -0,0 +1,47 @@
+'use client';
+import useMessages from 'components/hooks/useMessages';
+import useUser from 'components/hooks/useUser';
+import { ROLES } from 'lib/constants';
+import Link from 'next/link';
+import { Button, GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from 'react-basics';
+import TeamDeleteButton from './TeamDeleteButton';
+import TeamLeaveButton from './TeamLeaveButton';
+
+export function TeamsTable({ data = [] }) {
+ const { formatMessage, labels } = useMessages();
+ const { user } = useUser();
+ const breakpoint = useBreakpoint();
+
+ return (
+
+
+
+ {row => row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username}
+
+
+ {row => {
+ const { id, name, teamUser } = row;
+ const owner = teamUser.find(({ role }) => role === ROLES.teamOwner);
+ const isOwner = user.id === owner?.userId;
+
+ return (
+ <>
+ {isOwner && }
+ {!isOwner && }
+
+
+
+
+
+ {formatMessage(isOwner ? labels.edit : labels.view)}
+
+
+ >
+ );
+ }}
+
+
+ );
+}
+
+export default TeamsTable;
diff --git a/src/components/pages/settings/teams/WebsiteTags.js b/src/app/(main)/settings/teams/WebsiteTags.js
similarity index 100%
rename from src/components/pages/settings/teams/WebsiteTags.js
rename to src/app/(main)/settings/teams/WebsiteTags.js
diff --git a/src/components/pages/settings/teams/WebsiteTags.module.css b/src/app/(main)/settings/teams/WebsiteTags.module.css
similarity index 100%
rename from src/components/pages/settings/teams/WebsiteTags.module.css
rename to src/app/(main)/settings/teams/WebsiteTags.module.css
diff --git a/src/components/pages/settings/teams/TeamEditForm.js b/src/app/(main)/settings/teams/[id]/TeamEditForm.js
similarity index 100%
rename from src/components/pages/settings/teams/TeamEditForm.js
rename to src/app/(main)/settings/teams/[id]/TeamEditForm.js
diff --git a/src/components/pages/settings/teams/TeamMemberRemoveButton.js b/src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.js
similarity index 88%
rename from src/components/pages/settings/teams/TeamMemberRemoveButton.js
rename to src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.js
index 3ec0f8b3..603adae3 100644
--- a/src/components/pages/settings/teams/TeamMemberRemoveButton.js
+++ b/src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.js
@@ -1,6 +1,7 @@
import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
import { Icon, Icons, LoadingButton, Text } from 'react-basics';
+import { setValue } from 'store/cache';
export function TeamMemberRemoveButton({ teamId, userId, disabled, onSave }) {
const { formatMessage, labels } = useMessages();
@@ -12,7 +13,8 @@ export function TeamMemberRemoveButton({ teamId, userId, disabled, onSave }) {
{},
{
onSuccess: () => {
- onSave();
+ setValue('team:members', Date.now());
+ onSave?.();
},
},
);
diff --git a/src/app/(main)/settings/teams/[id]/TeamMembers.js b/src/app/(main)/settings/teams/[id]/TeamMembers.js
new file mode 100644
index 00000000..fb31b6fa
--- /dev/null
+++ b/src/app/(main)/settings/teams/[id]/TeamMembers.js
@@ -0,0 +1,29 @@
+import useApi from 'components/hooks/useApi';
+import TeamMembersTable from './TeamMembersTable';
+import useFilterQuery from 'components/hooks/useFilterQuery';
+import DataTable from 'components/common/DataTable';
+import useCache from 'store/cache';
+
+export function TeamMembers({ teamId, readOnly }) {
+ const { get } = useApi();
+ const modified = useCache(state => state?.['team:members']);
+ const queryResult = useFilterQuery(
+ ['team:members', { teamId, modified }],
+ params => {
+ return get(`/teams/${teamId}/users`, {
+ ...params,
+ });
+ },
+ { enabled: !!teamId },
+ );
+
+ return (
+ <>
+
+ {({ data }) => }
+
+ >
+ );
+}
+
+export default TeamMembers;
diff --git a/src/app/(main)/settings/teams/[id]/TeamMembersTable.js b/src/app/(main)/settings/teams/[id]/TeamMembersTable.js
new file mode 100644
index 00000000..9a402d44
--- /dev/null
+++ b/src/app/(main)/settings/teams/[id]/TeamMembersTable.js
@@ -0,0 +1,36 @@
+import { GridColumn, GridTable, useBreakpoint } from 'react-basics';
+import useMessages from 'components/hooks/useMessages';
+import useUser from 'components/hooks/useUser';
+import { ROLES } from 'lib/constants';
+import TeamMemberRemoveButton from './TeamMemberRemoveButton';
+
+export function TeamMembersTable({ data = [], teamId, readOnly }) {
+ const { formatMessage, labels } = useMessages();
+ const { user } = useUser();
+ const breakpoint = useBreakpoint();
+
+ const roles = {
+ [ROLES.teamOwner]: formatMessage(labels.teamOwner),
+ [ROLES.teamMember]: formatMessage(labels.teamMember),
+ };
+
+ return (
+
+
+
+ {row => roles[row?.teamUser?.[0]?.role]}
+
+
+ {row => {
+ return (
+ !readOnly &&
+ row?.teamUser?.[0]?.role !== ROLES.teamOwner &&
+ user?.id !== row?.id &&
+ );
+ }}
+
+
+ );
+}
+
+export default TeamMembersTable;
diff --git a/src/components/pages/settings/teams/TeamSettings.js b/src/app/(main)/settings/teams/[id]/TeamSettings.js
similarity index 90%
rename from src/components/pages/settings/teams/TeamSettings.js
rename to src/app/(main)/settings/teams/[id]/TeamSettings.js
index 8c4fe8f4..8ec0ad85 100644
--- a/src/components/pages/settings/teams/TeamSettings.js
+++ b/src/app/(main)/settings/teams/[id]/TeamSettings.js
@@ -1,6 +1,6 @@
+'use client';
import { useEffect, useState } from 'react';
-import { Item, Tabs, useToasts } from 'react-basics';
-import Page from 'components/layout/Page';
+import { Item, Loading, Tabs, useToasts, Flexbox } from 'react-basics';
import PageHeader from 'components/layout/PageHeader';
import { ROLES } from 'lib/constants';
import useUser from 'components/hooks/useUser';
@@ -41,8 +41,12 @@ export function TeamSettings({ teamId }) {
}
}, [data]);
+ if (isLoading || !values) {
+ return ;
+ }
+
return (
-
+
- {formatMessage(labels.details)}
@@ -54,7 +58,7 @@ export function TeamSettings({ teamId }) {
)}
{tab === 'members' && }
{tab === 'websites' && }
-
+
);
}
diff --git a/src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.js b/src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.js
new file mode 100644
index 00000000..9c2ae7bd
--- /dev/null
+++ b/src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.js
@@ -0,0 +1,64 @@
+import useApi from 'components/hooks/useApi';
+import { useState } from 'react';
+import { Button, Form, FormButtons, GridColumn, Loading, SubmitButton, Toggle } from 'react-basics';
+import useMessages from 'components/hooks/useMessages';
+import WebsitesDataTable from '../../websites/WebsitesDataTable';
+import Empty from 'components/common/Empty';
+import { setValue } from 'store/cache';
+
+export function TeamWebsiteAddForm({ teamId, onSave, onClose }) {
+ const { formatMessage, labels } = useMessages();
+ const { get, post, useQuery, useMutation } = useApi();
+ const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data));
+ const { data: websites, isLoading } = useQuery(['websites'], () => get('/websites'));
+ const [selected, setSelected] = useState([]);
+ const hasData = websites && websites.data.length > 0;
+
+ const handleSubmit = () => {
+ mutate(
+ { websiteIds: selected },
+ {
+ onSuccess: async () => {
+ setValue('team:websites', Date.now());
+ onSave?.();
+ onClose?.();
+ },
+ },
+ );
+ };
+
+ const handleSelect = id => {
+ setSelected(state => (state.includes(id) ? state.filter(n => n !== id) : state.concat(id)));
+ };
+
+ return (
+ <>
+ {isLoading && !hasData && }
+ {!isLoading && !hasData && }
+ {hasData && (
+
+ )}
+ >
+ );
+}
+
+export default TeamWebsiteAddForm;
diff --git a/src/components/pages/settings/teams/TeamWebsiteRemoveButton.js b/src/app/(main)/settings/teams/[id]/TeamWebsiteRemoveButton.js
similarity index 73%
rename from src/components/pages/settings/teams/TeamWebsiteRemoveButton.js
rename to src/app/(main)/settings/teams/[id]/TeamWebsiteRemoveButton.js
index c0ddf95c..59e393e1 100644
--- a/src/components/pages/settings/teams/TeamWebsiteRemoveButton.js
+++ b/src/app/(main)/settings/teams/[id]/TeamWebsiteRemoveButton.js
@@ -7,19 +7,16 @@ export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
const { del, useMutation } = useApi();
const { mutate, isLoading } = useMutation(() => del(`/teams/${teamId}/websites/${websiteId}`));
- const handleRemoveTeamMember = () => {
- mutate(
- {},
- {
- onSuccess: () => {
- onSave();
- },
+ const handleRemoveTeamMember = async () => {
+ await mutate(null, {
+ onSuccess: () => {
+ onSave();
},
- );
+ });
};
return (
- handleRemoveTeamMember()} isLoading={isLoading}>
+ handleRemoveTeamMember()} isLoading={isLoading}>
diff --git a/src/app/(main)/settings/teams/[id]/TeamWebsites.js b/src/app/(main)/settings/teams/[id]/TeamWebsites.js
new file mode 100644
index 00000000..9e76ffab
--- /dev/null
+++ b/src/app/(main)/settings/teams/[id]/TeamWebsites.js
@@ -0,0 +1,52 @@
+import { ActionForm, Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
+import TeamWebsitesTable from './TeamWebsitesTable';
+import TeamWebsiteAddForm from './TeamWebsiteAddForm';
+import useApi from 'components/hooks/useApi';
+import useMessages from 'components/hooks/useMessages';
+import useUser from 'components/hooks/useUser';
+import useFilterQuery from 'components/hooks/useFilterQuery';
+import DataTable from 'components/common/DataTable';
+import useCache from 'store/cache';
+
+export function TeamWebsites({ teamId }) {
+ const { formatMessage, labels, messages } = useMessages();
+ const { user } = useUser();
+ const { get } = useApi();
+ const modified = useCache(state => state?.['team:websites']);
+ const queryResult = useFilterQuery(
+ ['team:websites', { teamId, modified }],
+ params => {
+ return get(`/teams/${teamId}/websites`, {
+ ...params,
+ });
+ },
+ { enabled: !!user },
+ );
+
+ const handleChange = () => {
+ queryResult.refetch();
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ {formatMessage(labels.addWebsite)}
+
+
+ {close => }
+
+
+
+
+ {({ data }) => }
+
+ >
+ );
+}
+
+export default TeamWebsites;
diff --git a/src/app/(main)/settings/teams/[id]/TeamWebsitesTable.js b/src/app/(main)/settings/teams/[id]/TeamWebsitesTable.js
new file mode 100644
index 00000000..0f802212
--- /dev/null
+++ b/src/app/(main)/settings/teams/[id]/TeamWebsitesTable.js
@@ -0,0 +1,42 @@
+import Link from 'next/link';
+import { Button, GridColumn, GridTable, Icon, Icons, Text } from 'react-basics';
+import useMessages from 'components/hooks/useMessages';
+import useUser from 'components/hooks/useUser';
+import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton';
+
+export function TeamWebsitesTable({ data = [], onRemove }) {
+ const { formatMessage, labels } = useMessages();
+ const { user } = useUser();
+
+ return (
+
+
+
+
+ {row => {
+ const { id: teamId, teamUser } = row.teamWebsite[0].team;
+ const { id: websiteId, userId } = row;
+ const owner = teamUser[0];
+ const canRemove = user.id === userId || user.id === owner.userId;
+ return (
+ <>
+ {canRemove && (
+
+ )}
+
+
+
+
+
+ {formatMessage(labels.view)}
+
+
+ >
+ );
+ }}
+
+
+ );
+}
+
+export default TeamWebsitesTable;
diff --git a/src/app/(main)/settings/teams/[id]/page.js b/src/app/(main)/settings/teams/[id]/page.js
new file mode 100644
index 00000000..652e65c1
--- /dev/null
+++ b/src/app/(main)/settings/teams/[id]/page.js
@@ -0,0 +1,9 @@
+import TeamSettings from './TeamSettings';
+
+export default function ({ params }) {
+ if (process.env.cloudMode) {
+ return null;
+ }
+
+ return ;
+}
diff --git a/src/app/(main)/settings/teams/page.tsx b/src/app/(main)/settings/teams/page.tsx
new file mode 100644
index 00000000..0cdb6f7d
--- /dev/null
+++ b/src/app/(main)/settings/teams/page.tsx
@@ -0,0 +1,20 @@
+import TeamsDataTable from './TeamsDataTable';
+import TeamsHeader from './TeamsHeader';
+import { Metadata } from 'next';
+
+export default function () {
+ if (process.env.cloudMode) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const metadata: Metadata = {
+ title: 'Teams Settings | umami',
+};
diff --git a/src/components/pages/settings/users/UserAddButton.js b/src/app/(main)/settings/users/UserAddButton.js
similarity index 100%
rename from src/components/pages/settings/users/UserAddButton.js
rename to src/app/(main)/settings/users/UserAddButton.js
diff --git a/src/components/pages/settings/users/UserAddForm.js b/src/app/(main)/settings/users/UserAddForm.js
similarity index 100%
rename from src/components/pages/settings/users/UserAddForm.js
rename to src/app/(main)/settings/users/UserAddForm.js
diff --git a/src/app/(main)/settings/users/UserDeleteButton.js b/src/app/(main)/settings/users/UserDeleteButton.js
new file mode 100644
index 00000000..22d93171
--- /dev/null
+++ b/src/app/(main)/settings/users/UserDeleteButton.js
@@ -0,0 +1,27 @@
+import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
+import useMessages from 'components/hooks/useMessages';
+import useUser from 'components/hooks/useUser';
+import UserDeleteForm from './UserDeleteForm';
+
+export function UserDeleteButton({ userId, username, onDelete }) {
+ const { formatMessage, labels } = useMessages();
+ const { user } = useUser();
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.delete)}
+
+
+ {close => (
+
+ )}
+
+
+ );
+}
+
+export default UserDeleteButton;
diff --git a/src/components/pages/settings/users/UserDeleteForm.js b/src/app/(main)/settings/users/UserDeleteForm.js
similarity index 100%
rename from src/components/pages/settings/users/UserDeleteForm.js
rename to src/app/(main)/settings/users/UserDeleteForm.js
diff --git a/src/components/pages/settings/users/UserEditForm.js b/src/app/(main)/settings/users/UserEditForm.js
similarity index 100%
rename from src/components/pages/settings/users/UserEditForm.js
rename to src/app/(main)/settings/users/UserEditForm.js
diff --git a/src/components/pages/settings/users/UserWebsites.js b/src/app/(main)/settings/users/UserWebsites.js
similarity index 90%
rename from src/components/pages/settings/users/UserWebsites.js
rename to src/app/(main)/settings/users/UserWebsites.js
index 14127275..18b5f1a7 100644
--- a/src/components/pages/settings/users/UserWebsites.js
+++ b/src/app/(main)/settings/users/UserWebsites.js
@@ -1,6 +1,6 @@
import Page from 'components/layout/Page';
import useApi from 'components/hooks/useApi';
-import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
+import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable';
import useApiFilter from 'components/hooks/useApiFilter';
export function UserWebsites({ userId }) {
@@ -22,7 +22,7 @@ export function UserWebsites({ userId }) {
{hasData && (
state?.users);
+ const queryResult = useFilterQuery(['users', { modified }], params => {
+ return get(`/users`, {
+ ...params,
+ });
+ });
+
+ return (
+ <>
+
+ {({ data }) => }
+ >
+ );
+}
+
+export default UsersDataTable;
diff --git a/src/app/(main)/settings/users/UsersHeader.js b/src/app/(main)/settings/users/UsersHeader.js
new file mode 100644
index 00000000..caf1f913
--- /dev/null
+++ b/src/app/(main)/settings/users/UsersHeader.js
@@ -0,0 +1,16 @@
+'use client';
+import PageHeader from 'components/layout/PageHeader';
+import useMessages from 'components/hooks/useMessages';
+import UserAddButton from './UserAddButton';
+
+export function UsersHeader({ onAdd }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+ );
+}
+
+export default UsersHeader;
diff --git a/src/app/(main)/settings/users/UsersTable.js b/src/app/(main)/settings/users/UsersTable.js
new file mode 100644
index 00000000..a0b5aba1
--- /dev/null
+++ b/src/app/(main)/settings/users/UsersTable.js
@@ -0,0 +1,54 @@
+import { Button, Text, Icon, Icons, GridTable, GridColumn, useBreakpoint } from 'react-basics';
+import { formatDistance } from 'date-fns';
+import Link from 'next/link';
+import { ROLES } from 'lib/constants';
+import useMessages from 'components/hooks/useMessages';
+import useLocale from 'components/hooks/useLocale';
+import UserDeleteButton from './UserDeleteButton';
+
+export function UsersTable({ data = [] }) {
+ const { formatMessage, labels } = useMessages();
+ const { dateLocale } = useLocale();
+ const breakpoint = useBreakpoint();
+
+ return (
+
+
+
+ {row =>
+ formatMessage(
+ labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown,
+ )
+ }
+
+
+ {row =>
+ formatDistance(new Date(row.createdAt), new Date(), {
+ addSuffix: true,
+ locale: dateLocale,
+ })
+ }
+
+
+ {row => {
+ const { id, username } = row;
+ return (
+ <>
+
+
+
+
+
+ {formatMessage(labels.edit)}
+
+
+
+ >
+ );
+ }}
+
+
+ );
+}
+
+export default UsersTable;
diff --git a/src/components/pages/settings/users/UserSettings.js b/src/app/(main)/settings/users/[id]/UserSettings.js
similarity index 84%
rename from src/components/pages/settings/users/UserSettings.js
rename to src/app/(main)/settings/users/[id]/UserSettings.js
index 5fadf1a1..ea340ab7 100644
--- a/src/components/pages/settings/users/UserSettings.js
+++ b/src/app/(main)/settings/users/[id]/UserSettings.js
@@ -1,10 +1,10 @@
+'use client';
import { useEffect, useState } from 'react';
-import { Item, Tabs, useToasts } from 'react-basics';
-import UserEditForm from 'components/pages/settings/users/UserEditForm';
-import Page from 'components/layout/Page';
+import { Item, Loading, Tabs, useToasts } from 'react-basics';
+import UserEditForm from '../UserEditForm';
import PageHeader from 'components/layout/PageHeader';
import useApi from 'components/hooks/useApi';
-import UserWebsites from './UserWebsites';
+import UserWebsites from '../UserWebsites';
import useMessages from 'components/hooks/useMessages';
export function UserSettings({ userId }) {
@@ -41,8 +41,12 @@ export function UserSettings({ userId }) {
}
}, [data]);
+ if (isLoading || !values) {
+ return ;
+ }
+
return (
-
+ <>
- {formatMessage(labels.details)}
@@ -50,7 +54,7 @@ export function UserSettings({ userId }) {
{tab === 'details' && }
{tab === 'websites' && }
-
+ >
);
}
diff --git a/src/app/(main)/settings/users/[id]/page.js b/src/app/(main)/settings/users/[id]/page.js
new file mode 100644
index 00000000..7a6378aa
--- /dev/null
+++ b/src/app/(main)/settings/users/[id]/page.js
@@ -0,0 +1,9 @@
+import UserSettings from './UserSettings';
+
+export default function ({ params }) {
+ if (process.env.cloudMode) {
+ return null;
+ }
+
+ return ;
+}
diff --git a/src/app/(main)/settings/users/page.tsx b/src/app/(main)/settings/users/page.tsx
new file mode 100644
index 00000000..00ebe98c
--- /dev/null
+++ b/src/app/(main)/settings/users/page.tsx
@@ -0,0 +1,9 @@
+import UsersDataTable from './UsersDataTable';
+import { Metadata } from 'next';
+
+export default function () {
+ return ;
+}
+export const metadata: Metadata = {
+ title: 'Users | umami',
+};
diff --git a/src/app/(main)/settings/websites/WebsiteAddButton.js b/src/app/(main)/settings/websites/WebsiteAddButton.js
new file mode 100644
index 00000000..b1a69429
--- /dev/null
+++ b/src/app/(main)/settings/websites/WebsiteAddButton.js
@@ -0,0 +1,31 @@
+import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
+import WebsiteAddForm from './WebsiteAddForm';
+import useMessages from 'components/hooks/useMessages';
+import { setValue } from 'store/cache';
+
+export function WebsiteAddButton({ onSave }) {
+ const { formatMessage, labels, messages } = useMessages();
+ const { showToast } = useToasts();
+
+ const handleSave = async () => {
+ showToast({ message: formatMessage(messages.saved), variant: 'success' });
+ setValue('websites', Date.now());
+ onSave?.();
+ };
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.addWebsite)}
+
+
+ {close => }
+
+
+ );
+}
+
+export default WebsiteAddButton;
diff --git a/src/components/pages/settings/websites/WebsiteAddForm.js b/src/app/(main)/settings/websites/WebsiteAddForm.js
similarity index 100%
rename from src/components/pages/settings/websites/WebsiteAddForm.js
rename to src/app/(main)/settings/websites/WebsiteAddForm.js
diff --git a/src/components/pages/settings/websites/WebsiteSettings.js b/src/app/(main)/settings/websites/WebsiteSettings.js
similarity index 80%
rename from src/components/pages/settings/websites/WebsiteSettings.js
rename to src/app/(main)/settings/websites/WebsiteSettings.js
index 63e89814..71d5fe23 100644
--- a/src/components/pages/settings/websites/WebsiteSettings.js
+++ b/src/app/(main)/settings/websites/WebsiteSettings.js
@@ -1,13 +1,13 @@
+'use client';
import { useEffect, useState } from 'react';
import { Item, Tabs, useToasts, Button, Text, Icon, Icons } from 'react-basics';
-import { useRouter } from 'next/router';
+import { useRouter } from 'next/navigation';
import Link from 'next/link';
-import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
-import WebsiteEditForm from 'components/pages/settings/websites/WebsiteEditForm';
-import WebsiteData from 'components/pages/settings/websites/WebsiteData';
-import TrackingCode from 'components/pages/settings/websites/TrackingCode';
-import ShareUrl from 'components/pages/settings/websites/ShareUrl';
+import WebsiteEditForm from './[id]/WebsiteEditForm';
+import WebsiteData from './[id]/WebsiteData';
+import TrackingCode from './[id]/TrackingCode';
+import ShareUrl from './[id]/ShareUrl';
import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
@@ -16,11 +16,10 @@ export function WebsiteSettings({ websiteId, openExternal = false, analyticsUrl
const { formatMessage, labels, messages } = useMessages();
const { get, useQuery } = useApi();
const { showToast } = useToasts();
- const { data, isLoading } = useQuery(
- ['website', websiteId],
- () => get(`/websites/${websiteId}`),
- { enabled: !!websiteId, cacheTime: 0 },
- );
+ const { data } = useQuery(['website', websiteId], () => get(`/websites/${websiteId}`), {
+ enabled: !!websiteId,
+ cacheTime: 0,
+ });
const [values, setValues] = useState(null);
const [tab, setTab] = useState('details');
@@ -48,7 +47,7 @@ export function WebsiteSettings({ websiteId, openExternal = false, analyticsUrl
}, [data]);
return (
-
+ <>
@@ -78,7 +77,7 @@ export function WebsiteSettings({ websiteId, openExternal = false, analyticsUrl
/>
)}
{tab === 'data' && }
-
+ >
);
}
diff --git a/src/components/pages/websites/WebsiteList.module.css b/src/app/(main)/settings/websites/Websites.module.css
similarity index 100%
rename from src/components/pages/websites/WebsiteList.module.css
rename to src/app/(main)/settings/websites/Websites.module.css
diff --git a/src/app/(main)/settings/websites/WebsitesDataTable.tsx b/src/app/(main)/settings/websites/WebsitesDataTable.tsx
new file mode 100644
index 00000000..441ae56d
--- /dev/null
+++ b/src/app/(main)/settings/websites/WebsitesDataTable.tsx
@@ -0,0 +1,66 @@
+'use client';
+import { ReactNode } from 'react';
+import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable';
+import useUser from 'components/hooks/useUser';
+import useApi from 'components/hooks/useApi';
+import DataTable from 'components/common/DataTable';
+import useFilterQuery from 'components/hooks/useFilterQuery';
+import useCache from 'store/cache';
+
+export interface WebsitesDataTableProps {
+ allowEdit?: boolean;
+ allowView?: boolean;
+ showActions?: boolean;
+ showTeam?: boolean;
+ includeTeams?: boolean;
+ onlyTeams?: boolean;
+ children?: ReactNode;
+}
+
+function useWebsites({ includeTeams, onlyTeams }) {
+ const { user } = useUser();
+ const { get } = useApi();
+ const modified = useCache((state: any) => state?.websites);
+
+ return useFilterQuery(
+ ['websites', { includeTeams, onlyTeams, modified }],
+ (params: any) => {
+ return get(`/users/${user?.id}/websites`, {
+ includeTeams,
+ onlyTeams,
+ ...params,
+ });
+ },
+ { enabled: !!user },
+ );
+}
+
+export function WebsitesDataTable({
+ allowEdit = true,
+ allowView = true,
+ showActions = true,
+ showTeam,
+ includeTeams,
+ onlyTeams,
+ children,
+}: WebsitesDataTableProps) {
+ const queryResult = useWebsites({ includeTeams, onlyTeams });
+
+ return (
+
+ {({ data }) => (
+
+ {children}
+
+ )}
+
+ );
+}
+
+export default WebsitesDataTable;
diff --git a/src/app/(main)/settings/websites/WebsitesHeader.js b/src/app/(main)/settings/websites/WebsitesHeader.js
new file mode 100644
index 00000000..61bfb886
--- /dev/null
+++ b/src/app/(main)/settings/websites/WebsitesHeader.js
@@ -0,0 +1,16 @@
+'use client';
+import useMessages from 'components/hooks/useMessages';
+import PageHeader from 'components/layout/PageHeader';
+import WebsiteAddButton from './WebsiteAddButton';
+
+export function WebsitesHeader({ showActions = true }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+ {!process.env.cloudMode && showActions && }
+
+ );
+}
+
+export default WebsitesHeader;
diff --git a/src/app/(main)/settings/websites/WebsitesTable.js b/src/app/(main)/settings/websites/WebsitesTable.js
new file mode 100644
index 00000000..eef3f7d4
--- /dev/null
+++ b/src/app/(main)/settings/websites/WebsitesTable.js
@@ -0,0 +1,72 @@
+import Link from 'next/link';
+import { Button, Text, Icon, Icons, GridTable, GridColumn, useBreakpoint } from 'react-basics';
+import useMessages from 'components/hooks/useMessages';
+import useUser from 'components/hooks/useUser';
+
+export function WebsitesTable({
+ data = [],
+ showTeam,
+ showActions,
+ allowEdit,
+ allowView,
+ children,
+}) {
+ const { formatMessage, labels } = useMessages();
+ const { user } = useUser();
+ const breakpoint = useBreakpoint();
+
+ return (
+
+
+
+ {showTeam && (
+
+ {row => row.teamWebsite[0]?.team.name}
+
+ )}
+ {showTeam && (
+
+ {row => row.user.username}
+
+ )}
+ {showActions && (
+
+ {row => {
+ const {
+ id,
+ user: { id: ownerId },
+ } = row;
+
+ return (
+ <>
+ {allowEdit && (!showTeam || ownerId === user.id) && (
+
+
+
+
+
+ {formatMessage(labels.edit)}
+
+
+ )}
+ {allowView && (
+
+
+
+
+
+ {formatMessage(labels.view)}
+
+
+ )}
+ >
+ );
+ }}
+
+ )}
+ {children}
+
+ );
+}
+
+export default WebsitesTable;
diff --git a/src/components/pages/settings/websites/WebsitesTable.module.css b/src/app/(main)/settings/websites/WebsitesTable.module.css
similarity index 100%
rename from src/components/pages/settings/websites/WebsitesTable.module.css
rename to src/app/(main)/settings/websites/WebsitesTable.module.css
diff --git a/src/components/pages/settings/websites/ShareUrl.js b/src/app/(main)/settings/websites/[id]/ShareUrl.js
similarity index 91%
rename from src/components/pages/settings/websites/ShareUrl.js
rename to src/app/(main)/settings/websites/[id]/ShareUrl.js
index f4569ca3..72ba217c 100644
--- a/src/components/pages/settings/websites/ShareUrl.js
+++ b/src/app/(main)/settings/websites/[id]/ShareUrl.js
@@ -10,7 +10,6 @@ import {
} from 'react-basics';
import { useEffect, useMemo, useRef, useState } from 'react';
import { getRandomChars } from 'next-basics';
-import { useRouter } from 'next/router';
import useApi from 'components/hooks/useApi';
import useMessages from 'components/hooks/useMessages';
@@ -21,14 +20,16 @@ export function ShareUrl({ websiteId, data, analyticsUrl, onSave }) {
const { name, shareId } = data;
const [id, setId] = useState(shareId);
const { post, useMutation } = useApi();
- const { basePath } = useRouter();
const { mutate, error } = useMutation(({ shareId }) =>
post(`/websites/${websiteId}`, { shareId }),
);
const ref = useRef(null);
const url = useMemo(
- () => `${analyticsUrl || location.origin}${basePath}/share/${id}/${encodeURIComponent(name)}`,
- [id, name, basePath],
+ () =>
+ `${analyticsUrl || location.origin}${process.env.basePath}/share/${id}/${encodeURIComponent(
+ name,
+ )}`,
+ [id, name],
);
const handleSubmit = async data => {
diff --git a/src/components/pages/settings/websites/TrackingCode.js b/src/app/(main)/settings/websites/[id]/TrackingCode.js
similarity index 82%
rename from src/components/pages/settings/websites/TrackingCode.js
rename to src/app/(main)/settings/websites/[id]/TrackingCode.js
index 298cd17a..368368d7 100644
--- a/src/components/pages/settings/websites/TrackingCode.js
+++ b/src/app/(main)/settings/websites/[id]/TrackingCode.js
@@ -1,11 +1,9 @@
import { TextArea } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import useConfig from 'components/hooks/useConfig';
-import { useRouter } from 'next/router';
export function TrackingCode({ websiteId, analyticsUrl }) {
const { formatMessage, messages } = useMessages();
- const { basePath } = useRouter();
const config = useConfig();
const trackerScriptName =
@@ -13,7 +11,7 @@ export function TrackingCode({ websiteId, analyticsUrl }) {
const url = trackerScriptName?.startsWith('http')
? trackerScriptName
- : `${analyticsUrl || location.origin}${basePath}/${trackerScriptName}`;
+ : `${analyticsUrl || location.origin}${process.env.basePath}/${trackerScriptName}`;
const code = ``;
diff --git a/src/components/pages/settings/websites/WebsiteData.js b/src/app/(main)/settings/websites/[id]/WebsiteData.js
similarity index 89%
rename from src/components/pages/settings/websites/WebsiteData.js
rename to src/app/(main)/settings/websites/[id]/WebsiteData.js
index 08d6702e..07dc9257 100644
--- a/src/components/pages/settings/websites/WebsiteData.js
+++ b/src/app/(main)/settings/websites/[id]/WebsiteData.js
@@ -1,6 +1,6 @@
import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics';
-import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm';
-import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm';
+import WebsiteDeleteForm from './WebsiteDeleteForm';
+import WebsiteResetForm from './WebsiteResetForm';
import useMessages from 'components/hooks/useMessages';
export function WebsiteData({ websiteId, onSave }) {
diff --git a/src/components/pages/settings/websites/WebsiteDeleteForm.js b/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.js
similarity index 100%
rename from src/components/pages/settings/websites/WebsiteDeleteForm.js
rename to src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.js
diff --git a/src/components/pages/settings/websites/WebsiteEditForm.js b/src/app/(main)/settings/websites/[id]/WebsiteEditForm.js
similarity index 100%
rename from src/components/pages/settings/websites/WebsiteEditForm.js
rename to src/app/(main)/settings/websites/[id]/WebsiteEditForm.js
diff --git a/src/components/pages/settings/websites/WebsiteResetForm.js b/src/app/(main)/settings/websites/[id]/WebsiteResetForm.js
similarity index 100%
rename from src/components/pages/settings/websites/WebsiteResetForm.js
rename to src/app/(main)/settings/websites/[id]/WebsiteResetForm.js
diff --git a/src/app/(main)/settings/websites/[id]/page.js b/src/app/(main)/settings/websites/[id]/page.js
new file mode 100644
index 00000000..37324659
--- /dev/null
+++ b/src/app/(main)/settings/websites/[id]/page.js
@@ -0,0 +1,9 @@
+import WebsiteSettings from '../WebsiteSettings';
+
+export default async function WebsiteSettingsPage({ params: { id } }) {
+ if (process.env.cloudMode) {
+ return null;
+ }
+
+ return ;
+}
diff --git a/src/app/(main)/settings/websites/page.tsx b/src/app/(main)/settings/websites/page.tsx
new file mode 100644
index 00000000..2c83dce0
--- /dev/null
+++ b/src/app/(main)/settings/websites/page.tsx
@@ -0,0 +1,16 @@
+import WebsitesDataTable from './WebsitesDataTable';
+import WebsitesHeader from './WebsitesHeader';
+import { Metadata } from 'next';
+
+export default function () {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const metadata: Metadata = {
+ title: 'Websites Settings | umami',
+};
diff --git a/src/app/(main)/websites/WebsitesBrowse.js b/src/app/(main)/websites/WebsitesBrowse.js
new file mode 100644
index 00000000..f1bab7bf
--- /dev/null
+++ b/src/app/(main)/websites/WebsitesBrowse.js
@@ -0,0 +1,31 @@
+'use client';
+import WebsitesDataTable from '../settings/websites/WebsitesDataTable';
+import { useMessages } from 'components/hooks';
+import { useState } from 'react';
+import { Item, Tabs } from 'react-basics';
+
+const TABS = {
+ myWebsites: 'my-websites',
+ teamWebsites: 'team-websites',
+};
+
+export function WebsitesBrowse() {
+ const { formatMessage, labels } = useMessages();
+ const [tab, setTab] = useState(TABS.myWebsites);
+ const allowEdit = !process.env.cloudMode;
+
+ return (
+ <>
+
+ - {formatMessage(labels.myWebsites)}
+ - {formatMessage(labels.teamWebsites)}
+
+ {tab === TABS.myWebsites && }
+ {tab === TABS.teamWebsites && (
+
+ )}
+ >
+ );
+}
+
+export default WebsitesBrowse;
diff --git a/src/components/pages/websites/WebsiteChart.js b/src/app/(main)/websites/[id]/WebsiteChart.js
similarity index 92%
rename from src/components/pages/websites/WebsiteChart.js
rename to src/app/(main)/websites/[id]/WebsiteChart.js
index 7e20e785..d05ff422 100644
--- a/src/components/pages/websites/WebsiteChart.js
+++ b/src/app/(main)/websites/[id]/WebsiteChart.js
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import PageviewsChart from 'components/metrics/PageviewsChart';
-import { useApi, useDateRange, useTimezone, usePageQuery } from 'components/hooks';
+import { useApi, useDateRange, useTimezone, useNavigation } from 'components/hooks';
import { getDateArray } from 'lib/date';
export function WebsiteChart({ websiteId }) {
@@ -9,7 +9,7 @@ export function WebsiteChart({ websiteId }) {
const [timezone] = useTimezone();
const {
query: { url, referrer, os, browser, device, country, region, city, title },
- } = usePageQuery();
+ } = useNavigation();
const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(
diff --git a/src/components/pages/websites/WebsiteChart.module.css b/src/app/(main)/websites/[id]/WebsiteChart.module.css
similarity index 100%
rename from src/components/pages/websites/WebsiteChart.module.css
rename to src/app/(main)/websites/[id]/WebsiteChart.module.css
diff --git a/src/components/pages/websites/WebsiteChartList.js b/src/app/(main)/websites/[id]/WebsiteChartList.js
similarity index 90%
rename from src/components/pages/websites/WebsiteChartList.js
rename to src/app/(main)/websites/[id]/WebsiteChartList.js
index 56cbe157..23764dbb 100644
--- a/src/components/pages/websites/WebsiteChartList.js
+++ b/src/app/(main)/websites/[id]/WebsiteChartList.js
@@ -2,9 +2,8 @@ import { Button, Text, Icon } from 'react-basics';
import { useMemo } from 'react';
import { firstBy } from 'thenby';
import Link from 'next/link';
-import WebsiteChart from 'components/pages/websites/WebsiteChart';
+import WebsiteChart from './WebsiteChart';
import useDashboard from 'store/dashboard';
-import styles from './WebsiteList.module.css';
import WebsiteHeader from './WebsiteHeader';
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
import { useMessages, useLocale } from 'components/hooks';
@@ -27,7 +26,7 @@ export default function WebsiteChartList({ websites, showCharts, limit }) {
{ordered.map(({ id }, index) => {
return index < limit ? (
-
+
diff --git a/src/components/pages/websites/WebsiteDetailsPage.js b/src/app/(main)/websites/[id]/WebsiteDetails.js
similarity index 72%
rename from src/components/pages/websites/WebsiteDetailsPage.js
rename to src/app/(main)/websites/[id]/WebsiteDetails.js
index 222d94d9..c6ad1acc 100644
--- a/src/components/pages/websites/WebsiteDetailsPage.js
+++ b/src/app/(main)/websites/[id]/WebsiteDetails.js
@@ -1,26 +1,31 @@
+'use client';
import { Loading } from 'react-basics';
-import { useRouter } from 'next/router';
+import { usePathname } from 'next/navigation';
import Page from 'components/layout/Page';
-import WebsiteChart from 'components/pages/websites/WebsiteChart';
import FilterTags from 'components/metrics/FilterTags';
-import usePageQuery from 'components/hooks/usePageQuery';
-import WebsiteTableView from './WebsiteTableView';
-import WebsiteMenuView from './WebsiteMenuView';
+import useNavigation from 'components/hooks/useNavigation';
import { useWebsite } from 'components/hooks';
+import WebsiteChart from './WebsiteChart';
+import WebsiteMenuView from './WebsiteMenuView';
import WebsiteHeader from './WebsiteHeader';
-import { WebsiteMetricsBar } from './WebsiteMetricsBar';
+import WebsiteMetricsBar from './WebsiteMetricsBar';
+import WebsiteTableView from './WebsiteTableView';
-export default function WebsiteDetailsPage({ websiteId }) {
+export default function WebsiteDetails({ websiteId }) {
const { data: website, isLoading, error } = useWebsite(websiteId);
- const { pathname } = useRouter();
+ const pathname = usePathname();
const showLinks = !pathname.includes('/share/');
const {
query: { view, url, referrer, os, browser, device, country, region, city, title },
- } = usePageQuery();
+ } = useNavigation();
+
+ if (isLoading || error) {
+ return ;
+ }
return (
-
+ <>
}
>
)}
-
+ >
);
}
diff --git a/src/app/(main)/websites/[id]/WebsiteFilterButton.js b/src/app/(main)/websites/[id]/WebsiteFilterButton.js
new file mode 100644
index 00000000..e96856f6
--- /dev/null
+++ b/src/app/(main)/websites/[id]/WebsiteFilterButton.js
@@ -0,0 +1,54 @@
+import { Button, Icon, Icons, Popup, PopupTrigger, Text } from 'react-basics';
+import PopupForm from 'app/(main)/reports/[id]/PopupForm';
+import FilterSelectForm from 'app/(main)/reports/[id]/FilterSelectForm';
+import { useMessages, useNavigation } from 'components/hooks';
+
+export function WebsiteFilterButton({ websiteId, className }) {
+ const { formatMessage, labels } = useMessages();
+ const { makeUrl, router } = useNavigation();
+
+ const fieldOptions = [
+ { name: 'url', type: 'string', label: formatMessage(labels.url) },
+ { name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
+ { name: 'browser', type: 'string', label: formatMessage(labels.browser) },
+ { name: 'os', type: 'string', label: formatMessage(labels.os) },
+ { name: 'device', type: 'string', label: formatMessage(labels.device) },
+ { name: 'country', type: 'string', label: formatMessage(labels.country) },
+ { name: 'region', type: 'string', label: formatMessage(labels.region) },
+ { name: 'city', type: 'string', label: formatMessage(labels.city) },
+ ];
+
+ const handleAddFilter = ({ name, value }) => {
+ router.push(makeUrl({ [name]: value }));
+ };
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.filter)}
+
+
+ {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 fb4e0986..bf34a253 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 (
-
-
+
+
+
{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 93e622d9..3e58c8a3 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 8c74d615..c501645a 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)}
-
-
-
-
-
-
+
+
+
+
+
+
+ {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 00000000..b3dcb8d0
--- /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 00000000..0dd6a4e2
--- /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 00000000..db48bd55
--- /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 00000000..7c71b84b
--- /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 00000000..5be19185
--- /dev/null
+++ b/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.js
@@ -0,0 +1,38 @@
+import { useApi, useDateRange } from 'components/hooks';
+import MetricCard from 'components/metrics/MetricCard';
+import useMessages from 'components/hooks/useMessages';
+import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
+import MetricsBar from 'components/metrics/MetricsBar';
+import styles from './EventDataMetricsBar.module.css';
+
+export function EventDataMetricsBar({ websiteId }) {
+ const { formatMessage, labels } = useMessages();
+ const { get, useQuery } = useApi();
+ const [dateRange] = useDateRange(websiteId);
+ const { startDate, endDate, modified } = dateRange;
+
+ const { data, error, isLoading, isFetched } = useQuery(
+ ['event-data:stats', { websiteId, startDate, endDate, modified }],
+ () =>
+ get(`/event-data/stats`, {
+ websiteId,
+ startAt: +startDate,
+ endAt: +endDate,
+ }),
+ );
+
+ return (
+
+ );
+}
+
+export default EventDataMetricsBar;
diff --git a/src/components/pages/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 52decfc6..408396c3 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;
+ flex: 1;
}
-@media only screen and (max-width: 1200px) {
- .actions {
- margin-top: 40px;
- }
-}
-
-@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 c79916ce..fb98e7e7 100644
--- a/src/components/pages/event-data/EventDataTable.js
+++ b/src/app/(main)/websites/[id]/event-data/EventDataTable.js
@@ -1,12 +1,12 @@
import Link from 'next/link';
import { GridTable, GridColumn } from 'react-basics';
-import { useMessages, usePageQuery } from 'components/hooks';
+import { useMessages, useNavigation } from 'components/hooks';
import Empty from 'components/common/Empty';
import { DATA_TYPES } from 'lib/constants';
export function EventDataTable({ data = [] }) {
const { formatMessage, labels } = useMessages();
- const { resolveUrl } = usePageQuery();
+ const { makeUrl } = useNavigation();
if (data.length === 0) {
return ;
@@ -16,7 +16,7 @@ export function EventDataTable({ data = [] }) {
{row => (
-
+
{row.eventName}
)}
diff --git a/src/components/pages/event-data/EventDataValueTable.js b/src/app/(main)/websites/[id]/event-data/EventDataValueTable.js
similarity index 89%
rename from src/components/pages/event-data/EventDataValueTable.js
rename to src/app/(main)/websites/[id]/event-data/EventDataValueTable.js
index 75c11e32..4e50f5b9 100644
--- a/src/components/pages/event-data/EventDataValueTable.js
+++ b/src/app/(main)/websites/[id]/event-data/EventDataValueTable.js
@@ -1,5 +1,5 @@
import { GridTable, GridColumn, Button, Icon, Text } from 'react-basics';
-import { useMessages, usePageQuery } from 'components/hooks';
+import { useMessages, useNavigation } from 'components/hooks';
import Link from 'next/link';
import Icons from 'components/icons';
import PageHeader from 'components/layout/PageHeader';
@@ -8,12 +8,12 @@ import { DATA_TYPES } from 'lib/constants';
export function EventDataValueTable({ data = [], event }) {
const { formatMessage, labels } = useMessages();
- const { resolveUrl } = usePageQuery();
+ const { makeUrl } = useNavigation();
const Title = () => {
return (
<>
-
+
diff --git a/src/components/pages/websites/WebsiteEventData.js b/src/app/(main)/websites/[id]/event-data/WebsiteEventData.js
similarity index 76%
rename from src/components/pages/websites/WebsiteEventData.js
rename to src/app/(main)/websites/[id]/event-data/WebsiteEventData.js
index d38ca1ad..b5982e32 100644
--- a/src/components/pages/websites/WebsiteEventData.js
+++ b/src/app/(main)/websites/[id]/event-data/WebsiteEventData.js
@@ -1,8 +1,9 @@
+'use client';
import { Flexbox, Loading } from 'react-basics';
-import EventDataTable from 'components/pages/event-data/EventDataTable';
-import EventDataValueTable from 'components/pages/event-data/EventDataValueTable';
-import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetricsBar';
-import { useDateRange, useApi, usePageQuery } from 'components/hooks';
+import EventDataTable from './EventDataTable';
+import EventDataValueTable from './EventDataValueTable';
+import { EventDataMetricsBar } from './EventDataMetricsBar';
+import { useDateRange, useApi, useNavigation } from 'components/hooks';
import styles from './WebsiteEventData.module.css';
function useData(websiteId, event) {
@@ -27,7 +28,7 @@ function useData(websiteId, event) {
export default function WebsiteEventData({ websiteId }) {
const {
query: { event },
- } = usePageQuery();
+ } = useNavigation();
const { data, isLoading } = useData(websiteId, event);
return (
diff --git a/src/components/pages/websites/WebsiteEventData.module.css b/src/app/(main)/websites/[id]/event-data/WebsiteEventData.module.css
similarity index 100%
rename from src/components/pages/websites/WebsiteEventData.module.css
rename to src/app/(main)/websites/[id]/event-data/WebsiteEventData.module.css
diff --git a/src/app/(main)/websites/[id]/event-data/page.js b/src/app/(main)/websites/[id]/event-data/page.js
new file mode 100644
index 00000000..14e878c6
--- /dev/null
+++ b/src/app/(main)/websites/[id]/event-data/page.js
@@ -0,0 +1,15 @@
+import WebsiteHeader from '../WebsiteHeader';
+import WebsiteEventData from './WebsiteEventData';
+
+export default function WebsiteEventDataPage({ params: { id } }) {
+ if (!id) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/app/(main)/websites/[id]/page.tsx b/src/app/(main)/websites/[id]/page.tsx
new file mode 100644
index 00000000..4b7d6ff3
--- /dev/null
+++ b/src/app/(main)/websites/[id]/page.tsx
@@ -0,0 +1,5 @@
+import WebsiteDetails from './WebsiteDetails';
+
+export default function WebsiteReportsPage({ params: { id } }) {
+ return ;
+}
diff --git a/src/components/pages/realtime/RealtimePage.js b/src/app/(main)/websites/[id]/realtime/Realtime.js
similarity index 75%
rename from src/components/pages/realtime/RealtimePage.js
rename to src/app/(main)/websites/[id]/realtime/Realtime.js
index f26a2869..b4219b0a 100644
--- a/src/components/pages/realtime/RealtimePage.js
+++ b/src/app/(main)/websites/[id]/realtime/Realtime.js
@@ -1,20 +1,21 @@
-import { useState, useEffect, useMemo } from 'react';
+'use client';
+import { useMemo, useState, useEffect } from 'react';
import { subMinutes, startOfMinute } from 'date-fns';
import firstBy from 'thenby';
-import { GridRow, GridColumn } from 'components/layout/Grid';
+import { Grid, GridRow } from 'components/layout/Grid';
import Page from 'components/layout/Page';
import RealtimeChart from 'components/metrics/RealtimeChart';
import WorldMap from 'components/common/WorldMap';
-import RealtimeLog from 'components/pages/realtime/RealtimeLog';
-import RealtimeHeader from 'components/pages/realtime/RealtimeHeader';
-import RealtimeUrls from 'components/pages/realtime/RealtimeUrls';
-import RealtimeCountries from 'components/pages/realtime/RealtimeCountries';
-import WebsiteHeader from 'components/pages/websites/WebsiteHeader';
+import RealtimeLog from './RealtimeLog';
+import RealtimeHeader from './RealtimeHeader';
+import RealtimeUrls from './RealtimeUrls';
+import RealtimeCountries from './RealtimeCountries';
+import WebsiteHeader from '../WebsiteHeader';
import useApi from 'components/hooks/useApi';
import { percentFilter } from 'lib/filters';
import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
-import styles from './RealtimePage.module.css';
import { useWebsite } from 'components/hooks';
+import styles from './Realtime.module.css';
function mergeData(state = [], data = [], time) {
const ids = state.map(({ __id }) => __id);
@@ -23,7 +24,7 @@ function mergeData(state = [], data = [], time) {
.filter(({ timestamp }) => timestamp >= time);
}
-export function RealtimePage({ websiteId }) {
+export function Realtime({ websiteId }) {
const [currentData, setCurrentData] = useState();
const { get, useQuery } = useApi();
const { data: website } = useWebsite(websiteId);
@@ -89,29 +90,27 @@ export function RealtimePage({ websiteId }) {
return currentData;
}, [currentData]);
+ if (isLoading || error) {
+ return ;
+ }
+
return (
-
+ <>
-
-
+
+
-
-
-
-
-
-
+
+
-
-
-
-
-
+
+
+ >
);
}
-export default RealtimePage;
+export default Realtime;
diff --git a/src/components/pages/realtime/RealtimePage.module.css b/src/app/(main)/websites/[id]/realtime/Realtime.module.css
similarity index 100%
rename from src/components/pages/realtime/RealtimePage.module.css
rename to src/app/(main)/websites/[id]/realtime/Realtime.module.css
diff --git a/src/components/pages/realtime/RealtimeCountries.js b/src/app/(main)/websites/[id]/realtime/RealtimeCountries.js
similarity index 81%
rename from src/components/pages/realtime/RealtimeCountries.js
rename to src/app/(main)/websites/[id]/realtime/RealtimeCountries.js
index 7a61651a..6f484b09 100644
--- a/src/components/pages/realtime/RealtimeCountries.js
+++ b/src/app/(main)/websites/[id]/realtime/RealtimeCountries.js
@@ -1,5 +1,4 @@
import { useCallback } from 'react';
-import { useRouter } from 'next/router';
import ListTable from 'components/metrics/ListTable';
import useLocale from 'components/hooks/useLocale';
import useCountryNames from 'components/hooks/useCountryNames';
@@ -11,16 +10,18 @@ export function RealtimeCountries({ data }) {
const { formatMessage, labels } = useMessages();
const { locale } = useLocale();
const countryNames = useCountryNames(locale);
- const { basePath } = useRouter();
const renderCountryName = useCallback(
({ x: code }) => (
-
+
{countryNames[code]}
),
- [countryNames, locale, basePath],
+ [countryNames, locale],
);
return (
diff --git a/src/components/pages/realtime/RealtimeCountries.module.css b/src/app/(main)/websites/[id]/realtime/RealtimeCountries.module.css
similarity index 100%
rename from src/components/pages/realtime/RealtimeCountries.module.css
rename to src/app/(main)/websites/[id]/realtime/RealtimeCountries.module.css
diff --git a/src/components/pages/realtime/RealtimeHeader.js b/src/app/(main)/websites/[id]/realtime/RealtimeHeader.js
similarity index 100%
rename from src/components/pages/realtime/RealtimeHeader.js
rename to src/app/(main)/websites/[id]/realtime/RealtimeHeader.js
diff --git a/src/components/pages/realtime/RealtimeHeader.module.css b/src/app/(main)/websites/[id]/realtime/RealtimeHeader.module.css
similarity index 100%
rename from src/components/pages/realtime/RealtimeHeader.module.css
rename to src/app/(main)/websites/[id]/realtime/RealtimeHeader.module.css
diff --git a/src/components/pages/realtime/RealtimeHome.js b/src/app/(main)/websites/[id]/realtime/RealtimeHome.js
similarity index 95%
rename from src/components/pages/realtime/RealtimeHome.js
rename to src/app/(main)/websites/[id]/realtime/RealtimeHome.js
index 4f2f6279..dbaeb541 100644
--- a/src/components/pages/realtime/RealtimeHome.js
+++ b/src/app/(main)/websites/[id]/realtime/RealtimeHome.js
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
-import { useRouter } from 'next/router';
+import { useRouter } from 'next/navigation';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import useApi from 'components/hooks/useApi';
diff --git a/src/components/pages/realtime/RealtimeLog.js b/src/app/(main)/websites/[id]/realtime/RealtimeLog.js
similarity index 100%
rename from src/components/pages/realtime/RealtimeLog.js
rename to src/app/(main)/websites/[id]/realtime/RealtimeLog.js
diff --git a/src/components/pages/realtime/RealtimeLog.module.css b/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css
similarity index 98%
rename from src/components/pages/realtime/RealtimeLog.module.css
rename to src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css
index dc78f818..f400cc1b 100644
--- a/src/components/pages/realtime/RealtimeLog.module.css
+++ b/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css
@@ -55,7 +55,6 @@
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
- line-clamp: 2;
-webkit-box-orient: vertical;
}
diff --git a/src/components/pages/realtime/RealtimeUrls.js b/src/app/(main)/websites/[id]/realtime/RealtimeUrls.js
similarity index 100%
rename from src/components/pages/realtime/RealtimeUrls.js
rename to src/app/(main)/websites/[id]/realtime/RealtimeUrls.js
diff --git a/src/app/(main)/websites/[id]/realtime/page.tsx b/src/app/(main)/websites/[id]/realtime/page.tsx
new file mode 100644
index 00000000..b2957fa0
--- /dev/null
+++ b/src/app/(main)/websites/[id]/realtime/page.tsx
@@ -0,0 +1,9 @@
+import Realtime from './Realtime';
+
+export default function WebsiteRealtimePage({ params: { id } }) {
+ if (!id) {
+ return null;
+ }
+
+ return ;
+}
diff --git a/src/app/(main)/websites/[id]/reports/WebsiteReports.js b/src/app/(main)/websites/[id]/reports/WebsiteReports.js
new file mode 100644
index 00000000..5ea6f614
--- /dev/null
+++ b/src/app/(main)/websites/[id]/reports/WebsiteReports.js
@@ -0,0 +1,29 @@
+'use client';
+import Link from 'next/link';
+import { Button, Flexbox, Icon, Icons, Text } from 'react-basics';
+import { useMessages } from 'components/hooks';
+import WebsiteHeader from '../WebsiteHeader';
+import ReportsDataTable from 'app/(main)/reports/ReportsDataTable';
+
+export function WebsiteReports({ websiteId }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {formatMessage(labels.createReport)}
+
+
+
+
+ >
+ );
+}
+
+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 00000000..bf564025
--- /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 00000000..a1542510
--- /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 00000000..c3d62699
--- /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 00000000..e2478a95
--- /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 797eea14..59d145bf 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 d12306ea..45115d5b 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 00000000..2ac3f724
--- /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 ef89080c..e9da0373 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 00000000..bce24736
--- /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 00000000..16c5bbcb
--- /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 00000000..6a146801
--- /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 3a07c12a..84d4162f 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 348c92d8..5dc2d584 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 00000000..41e93f52
--- /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 26f30552..d353d79a 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 00000000..99ba6407
--- /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 00000000..d985435c
--- /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 00000000..ca154165
--- /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 6e635206..75ea945d 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 00000000..e738c895
--- /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 00000000..a3c63c0a
--- /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 c0be761a..2c7fcd4a 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 2a95e011..89648255 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 54c7fa63..a9a8562d 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 ae8a3b62..5561f536 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 de1e9ffa..83a05dff 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 7a5e7ed5..a21d35d9 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 (
-
- handlePageChange(-1)} disabled={firstPage}>
-
-
-
-
-
- {formatMessage(labels.pageOf, { current: page, total: maxPage })}
-
- handlePageChange(1)} disabled={lastPage}>
-
-
-
-
-
+
+
{formatMessage(labels.numberOfRecords, { x: count })}
+
+
handlePageChange(-1)} disabled={firstPage}>
+
+
+
+
+
+ {formatMessage(labels.pageOf, { current: page, total: maxPage })}
+
+
handlePageChange(1)} disabled={lastPage}>
+
+
+
+
+
+
+
);
}
diff --git a/src/components/common/Pager.module.css b/src/components/common/Pager.module.css
index 99eb70ce..880c1b40 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 701dbe13..00000000
--- a/src/components/common/SettingsTable.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import Empty from 'components/common/Empty';
-import useMessages from 'components/hooks/useMessages';
-import { useState } from 'react';
-import {
- SearchField,
- Table,
- TableBody,
- TableCell,
- TableColumn,
- TableHeader,
- TableRow,
-} from 'react-basics';
-import styles from './SettingsTable.module.css';
-import Pager from 'components/common/Pager';
-
-export function SettingsTable({
- columns = [],
- data,
- children,
- cellRender,
- showSearch,
- showPaging,
- onFilterChange,
- onPageChange,
- onPageSizeChange,
- filterValue,
-}) {
- const { formatMessage, labels, messages } = useMessages();
- const [filter, setFilter] = useState(filterValue);
- const { data: value, page, count, pageSize } = data;
-
- const handleFilterChange = value => {
- setFilter(value);
- onFilterChange(value);
- };
-
- return (
- <>
- {showSearch && (value.length > 0 || filterValue) && (
-
- )}
- {value.length === 0 && filterValue && (
-
- )}
- {value.length > 0 && (
-
-
- {(column, index) => {
- return (
-
- {column.label}
-
- );
- }}
-
-
- {(row, keys, rowIndex) => {
- row.action = children(row, keys, rowIndex);
-
- return (
-
- {(data, key, colIndex) => {
- return (
-
-
- {cellRender ? cellRender(row, data, key, colIndex) : data[key]}
-
- );
- }}
-
- );
- }}
-
- {showPaging && (
-
- )}
-
- )}
- >
- );
-}
-
-export default SettingsTable;
diff --git a/src/components/common/SettingsTable.module.css b/src/components/common/SettingsTable.module.css
deleted file mode 100644
index fd6cddfa..00000000
--- a/src/components/common/SettingsTable.module.css
+++ /dev/null
@@ -1,44 +0,0 @@
-.cell {
- align-items: center;
-}
-
-.row .cell:last-child {
- gap: 10px;
- justify-content: flex-end;
-}
-
-.label {
- display: none;
- font-weight: 700;
-}
-
-@media screen and (max-width: 992px) {
- .header .cell {
- display: none;
- }
-
- .label {
- display: block;
- min-width: 100px;
- }
-
- .row .cell {
- padding-left: 0;
- flex-basis: 100%;
- }
-}
-
-@media screen and (max-width: 1200px) {
- .row {
- flex-wrap: wrap;
- }
-
- .header .cell:last-child {
- display: none;
- }
-
- .row .cell:last-child {
- padding-left: 0;
- flex-basis: 100%;
- }
-}
diff --git a/src/components/common/UpdateNotice.js b/src/components/common/UpdateNotice.js
index 23907948..509df95c 100644
--- a/src/components/common/UpdateNotice.js
+++ b/src/components/common/UpdateNotice.js
@@ -1,17 +1,18 @@
+'use client';
import { useEffect, useCallback, useState } from 'react';
import { createPortal } from 'react-dom';
-import { Button, Row, Column } from 'react-basics';
+import { Button } from 'react-basics';
import { setItem } from 'next-basics';
import useStore, { checkVersion } from 'store/version';
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
import styles from './UpdateNotice.module.css';
import useMessages from 'components/hooks/useMessages';
-import { useRouter } from 'next/router';
+import { usePathname } from 'next/navigation';
export function UpdateNotice({ user, config }) {
const { formatMessage, labels, messages } = useMessages();
const { latest, checked, hasUpdate, releaseUrl } = useStore();
- const { pathname } = useRouter();
+ const pathname = usePathname();
const [dismissed, setDismissed] = useState(checked);
const allowUpdate =
user?.isAdmin &&
@@ -46,17 +47,17 @@ export function UpdateNotice({ user, config }) {
}
return createPortal(
-
-
+
+
{formatMessage(messages.newVersionAvailable, { version: `v${latest}` })}
-
-
+
+
{formatMessage(labels.viewDetails)}
{formatMessage(labels.dismiss)}
-
- ,
+
+
,
document.body,
);
}
diff --git a/src/components/common/WorldMap.js b/src/components/common/WorldMap.js
index 6ae84677..ff34d5f2 100644
--- a/src/components/common/WorldMap.js
+++ b/src/components/common/WorldMap.js
@@ -1,5 +1,4 @@
import { useState, useMemo } from 'react';
-import { useRouter } from 'next/router';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import classNames from 'classnames';
import { colord } from 'colord';
@@ -14,7 +13,6 @@ import { percentFilter } from 'lib/filters';
import styles from './WorldMap.module.css';
export function WorldMap({ data, className }) {
- const { basePath } = useRouter();
const [tooltip, setTooltipPopup] = useState();
const { theme, colors } = useTheme();
const { locale } = useLocale();
@@ -54,7 +52,7 @@ export function WorldMap({ data, className }) {
>
-
+
{({ geographies }) => {
return geographies.map(geo => {
const code = ISO_COUNTRIES[geo.id];
diff --git a/src/components/hooks/index.js b/src/components/hooks/index.js
index 2596ba57..697d54c3 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 f41547a9..75a928d5 100644
--- a/src/components/hooks/useApi.ts
+++ b/src/components/hooks/useApi.ts
@@ -1,4 +1,3 @@
-import { useRouter } from 'next/router';
import * as reactQuery from '@tanstack/react-query';
import { useApi as nextUseApi } from 'next-basics';
import { getClientAuthToken } from 'lib/client';
@@ -8,12 +7,11 @@ import useStore from 'store/app';
const selector = state => state.shareToken;
export function useApi() {
- const { basePath } = useRouter();
const shareToken = useStore(selector);
const { get, post, put, del } = nextUseApi(
{ authorization: `Bearer ${getClientAuthToken()}`, [SHARE_TOKEN_HEADER]: shareToken?.token },
- basePath,
+ process.env.basePath,
);
return { get, post, put, del, ...reactQuery };
diff --git a/src/components/hooks/useCountryNames.js b/src/components/hooks/useCountryNames.js
index 51cabf34..40611865 100644
--- a/src/components/hooks/useCountryNames.js
+++ b/src/components/hooks/useCountryNames.js
@@ -1,5 +1,4 @@
import { useState, useEffect } from 'react';
-import { useRouter } from 'next/router';
import { httpGet } from 'next-basics';
import enUS from 'public/intl/country/en-US.json';
@@ -9,10 +8,9 @@ const countryNames = {
export function useCountryNames(locale) {
const [list, setList] = useState(countryNames[locale] || enUS);
- const { basePath } = useRouter();
async function loadData(locale) {
- const { data } = await httpGet(`${basePath}/intl/country/${locale}.json`);
+ const { data } = await httpGet(`${process.env.basePath}/intl/country/${locale}.json`);
if (data) {
countryNames[locale] = data;
diff --git a/src/components/hooks/useFilterQuery.ts b/src/components/hooks/useFilterQuery.ts
new file mode 100644
index 00000000..37c28b7e
--- /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 ff59e93d..3823a26b 100644
--- a/src/components/hooks/useLanguageNames.js
+++ b/src/components/hooks/useLanguageNames.js
@@ -1,5 +1,4 @@
import { useState, useEffect } from 'react';
-import { useRouter } from 'next/router';
import { httpGet } from 'next-basics';
import enUS from 'public/intl/language/en-US.json';
@@ -9,10 +8,9 @@ const languageNames = {
export function useLanguageNames(locale) {
const [list, setList] = useState(languageNames[locale] || enUS);
- const { basePath } = useRouter();
async function loadData(locale) {
- const { data } = await httpGet(`${basePath}/intl/language/${locale}.json`);
+ const { data } = await httpGet(`${process.env.basePath}/intl/language/${locale}.json`);
if (data) {
languageNames[locale] = data;
diff --git a/src/components/hooks/useLocale.js b/src/components/hooks/useLocale.js
index 1374af81..71574d86 100644
--- a/src/components/hooks/useLocale.js
+++ b/src/components/hooks/useLocale.js
@@ -1,5 +1,4 @@
import { useEffect } from 'react';
-import { useRouter } from 'next/router';
import { httpGet, setItem } from 'next-basics';
import { LOCALE_CONFIG } from 'lib/constants';
import { getDateLocale, getTextDirection } from 'lib/lang';
@@ -15,13 +14,12 @@ const selector = state => state.locale;
export function useLocale() {
const locale = useStore(selector);
- const { basePath } = useRouter();
const forceUpdate = useForceUpdate();
const dir = getTextDirection(locale);
const dateLocale = getDateLocale(locale);
async function loadMessages(locale) {
- const { ok, data } = await httpGet(`${basePath}/intl/messages/${locale}.json`);
+ const { ok, data } = await httpGet(`${process.env.basePath}/intl/messages/${locale}.json`);
if (ok) {
messages[locale] = data;
diff --git a/src/components/hooks/useNavigation.js b/src/components/hooks/useNavigation.js
new file mode 100644
index 00000000..658e81ed
--- /dev/null
+++ b/src/components/hooks/useNavigation.js
@@ -0,0 +1,27 @@
+import { useMemo } from 'react';
+import { usePathname, useRouter, useSearchParams } from 'next/navigation';
+import { buildUrl } from 'next-basics';
+
+export function useNavigation() {
+ const router = useRouter();
+ const pathname = usePathname();
+ const params = useSearchParams();
+
+ const query = useMemo(() => {
+ const obj = {};
+
+ for (const [key, value] of params.entries()) {
+ obj[key] = decodeURIComponent(value);
+ }
+
+ return obj;
+ }, [params]);
+
+ function makeUrl(params, reset) {
+ return reset ? pathname : buildUrl(pathname, { ...query, ...params });
+ }
+
+ return { pathname, query, router, makeUrl };
+}
+
+export default useNavigation;
diff --git a/src/components/hooks/usePageQuery.js b/src/components/hooks/usePageQuery.js
deleted file mode 100644
index b275d580..00000000
--- a/src/components/hooks/usePageQuery.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useMemo } from 'react';
-import { useRouter } from 'next/router';
-import { buildUrl } from 'next-basics';
-
-export function usePageQuery() {
- const router = useRouter();
- const { pathname, search } = location;
- const { asPath } = router;
-
- const query = useMemo(() => {
- if (!search) {
- return {};
- }
-
- const params = search.substring(1).split('&');
-
- return params.reduce((obj, item) => {
- const [key, value] = item.split('=');
-
- obj[key] = decodeURIComponent(value);
-
- return obj;
- }, {});
- }, [search]);
-
- function resolveUrl(params, reset) {
- return buildUrl(asPath.split('?')[0], { ...(reset ? {} : query), ...params });
- }
-
- return { pathname, query, resolveUrl, router };
-}
-
-export default usePageQuery;
diff --git a/src/components/hooks/useRequireLogin.ts b/src/components/hooks/useRequireLogin.ts
index d2f540d4..76460a55 100644
--- a/src/components/hooks/useRequireLogin.ts
+++ b/src/components/hooks/useRequireLogin.ts
@@ -1,10 +1,8 @@
import { useEffect } from 'react';
-import { useRouter } from 'next/router';
import useApi from 'components/hooks/useApi';
import useUser from 'components/hooks/useUser';
-export function useRequireLogin(handler: (data?: object) => void) {
- const { basePath } = useRouter();
+export function useRequireLogin(handler?: (data?: object) => void) {
const { get } = useApi();
const { user, setUser } = useUser();
@@ -15,7 +13,7 @@ export function useRequireLogin(handler: (data?: object) => void) {
setUser(typeof handler === 'function' ? handler(data) : (data as any)?.user);
} catch {
- location.href = `${basePath}/login`;
+ location.href = `${process.env.basePath || ''}/login`;
}
}
diff --git a/src/components/hooks/useShareToken.js b/src/components/hooks/useShareToken.js
index 3d6b9698..5062c73e 100644
--- a/src/components/hooks/useShareToken.js
+++ b/src/components/hooks/useShareToken.js
@@ -1,4 +1,3 @@
-import { useEffect } from 'react';
import useStore, { setShareToken } from 'store/app';
import useApi from './useApi';
@@ -6,23 +5,16 @@ const selector = state => state.shareToken;
export function useShareToken(shareId) {
const shareToken = useStore(selector);
- const { get } = useApi();
+ const { get, useQuery } = useApi();
+ const { isLoading, error } = useQuery(['share', shareId], async () => {
+ const data = await get(`/share/${shareId}`);
- async function loadToken(id) {
- const data = await get(`/share/${id}`);
+ setShareToken(data);
- if (data) {
- setShareToken(data);
- }
- }
+ return data;
+ });
- useEffect(() => {
- if (shareId) {
- loadToken(shareId);
- }
- }, [shareId]);
-
- return shareToken;
+ return { shareToken, isLoading, error };
}
export default useShareToken;
diff --git a/src/components/hooks/useWebsiteReports.js b/src/components/hooks/useWebsiteReports.js
deleted file mode 100644
index c637bc76..00000000
--- 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 3d4c0c56..cc5d649a 100644
--- a/src/components/input/LanguageButton.module.css
+++ b/src/components/input/LanguageButton.module.css
@@ -1,7 +1,6 @@
.menu {
- display: flex;
- flex-flow: row wrap;
- min-width: 640px;
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
padding: 10px;
background: var(--base50);
z-index: var(--z-index-popup);
@@ -14,7 +13,7 @@
display: flex;
align-items: center;
justify-content: space-between;
- min-width: calc(100% / 3);
+ min-width: 200px;
border-radius: 5px;
padding: 5px 10px;
}
@@ -32,3 +31,15 @@
.icon {
color: var(--primary400);
}
+
+@media screen and (max-width: 992px) {
+ .menu {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media screen and (max-width: 768px) {
+ .menu {
+ transform: translateX(40px);
+ }
+}
diff --git a/src/components/input/LogoutButton.js b/src/components/input/LogoutButton.js
index 2b04a78a..6ca358a1 100644
--- a/src/components/input/LogoutButton.js
+++ b/src/components/input/LogoutButton.js
@@ -5,7 +5,7 @@ import useMessages from 'components/hooks/useMessages';
export function LogoutButton({ tooltipPosition = 'top' }) {
const { formatMessage, labels } = useMessages();
return (
-
+
diff --git a/src/components/input/ProfileButton.js b/src/components/input/ProfileButton.js
index 35b0eb45..2c3f8629 100644
--- a/src/components/input/ProfileButton.js
+++ b/src/components/input/ProfileButton.js
@@ -1,5 +1,5 @@
import { Icon, Button, PopupTrigger, Popup, Menu, Item, Text } from 'react-basics';
-import { useRouter } from 'next/router';
+import { useRouter } from 'next/navigation';
import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser';
diff --git a/src/components/input/SettingsButton.js b/src/components/input/SettingsButton.js
index a6d72a2b..46c72597 100644
--- a/src/components/input/SettingsButton.js
+++ b/src/components/input/SettingsButton.js
@@ -1,6 +1,6 @@
import { Button, Icon, PopupTrigger, Popup, Form, FormRow } from 'react-basics';
-import TimezoneSetting from 'components/pages/settings/profile/TimezoneSetting';
-import DateRangeSetting from 'components/pages/settings/profile/DateRangeSetting';
+import TimezoneSetting from 'app/(main)/settings/profile/TimezoneSetting';
+import DateRangeSetting from 'app/(main)/settings/profile/DateRangeSetting';
import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages';
import styles from './SettingsButton.module.css';
diff --git a/src/components/input/ThemeButton.js b/src/components/input/ThemeButton.js
index 3a6a9d14..76d1b370 100644
--- a/src/components/input/ThemeButton.js
+++ b/src/components/input/ThemeButton.js
@@ -1,4 +1,4 @@
-import { useTransition, animated } from 'react-spring';
+import { useTransition, animated } from '@react-spring/web';
import { Button, Icon } from 'react-basics';
import useTheme from 'components/hooks/useTheme';
import Icons from 'components/icons';
diff --git a/src/components/input/WebsiteDateFilter.js b/src/components/input/WebsiteDateFilter.js
index 6903a708..1725ca3b 100644
--- a/src/components/input/WebsiteDateFilter.js
+++ b/src/components/input/WebsiteDateFilter.js
@@ -1,7 +1,7 @@
import useDateRange from 'components/hooks/useDateRange';
import { isAfter } from 'date-fns';
import { incrementDateRange } from 'lib/date';
-import { Button, Flexbox, Icon, Icons } from 'react-basics';
+import { Button, Icon, Icons } from 'react-basics';
import DateFilter from './DateFilter';
import styles from './WebsiteDateFilter.module.css';
@@ -22,9 +22,9 @@ export function WebsiteDateFilter({ websiteId }) {
};
return (
-
+
{value !== 'all' && selectedUnit && (
-
+
handleIncrement(1)}>
@@ -35,7 +35,7 @@ export function WebsiteDateFilter({ websiteId }) {
-
+
)}
-
+
);
}
diff --git a/src/components/input/WebsiteDateFilter.module.css b/src/components/input/WebsiteDateFilter.module.css
index 986f5c17..6f2e822d 100644
--- a/src/components/input/WebsiteDateFilter.module.css
+++ b/src/components/input/WebsiteDateFilter.module.css
@@ -1,7 +1,17 @@
+.container {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
.dropdown {
min-width: 200px;
}
+.buttons {
+ display: flex;
+}
+
.buttons button:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
diff --git a/src/components/input/WebsiteSelect.js b/src/components/input/WebsiteSelect.js
index 1bdc4608..078389d3 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 0276063b..86b08887 100644
--- a/src/components/layout/Grid.js
+++ b/src/components/layout/Grid.js
@@ -1,13 +1,18 @@
-import { Row, Column } from 'react-basics';
import classNames from 'classnames';
+import { mapChildren } from 'react-basics';
import styles from './Grid.module.css';
-export function GridRow(props) {
- const { className, ...otherProps } = props;
- return
;
+export function Grid({ className, ...otherProps }) {
+ return ;
}
-export function GridColumn(props) {
- const { className, ...otherProps } = props;
- return ;
+export function GridRow(props) {
+ const { columns = 'two', className, children, ...otherProps } = props;
+ return (
+
+ {mapChildren(children, child => {
+ return
{child}
;
+ })}
+
+ );
}
diff --git a/src/components/layout/Grid.module.css b/src/components/layout/Grid.module.css
index dc2e8ff6..f72a5f12 100644
--- a/src/components/layout/Grid.module.css
+++ b/src/components/layout/Grid.module.css
@@ -1,27 +1,52 @@
-.col {
- display: flex;
- flex-direction: column;
- padding: 20px;
+.grid {
+ display: grid;
}
.row {
+ display: grid;
+ grid-template-columns: repeat(6, 1fr);
border-top: 1px solid var(--base300);
- min-height: 430px;
}
-.row > .col {
+.col {
+ padding: 20px;
+ min-height: 430px;
border-inline-start: 1px solid var(--base300);
}
-.row > .col:first-child {
+.col:first-child {
border-inline-start: 0;
padding-inline-start: 0;
}
-.row > .col:last-child {
+.col:last-child {
padding-inline-end: 0;
}
+.col.two {
+ grid-column: span 3;
+}
+
+.col.three {
+ grid-column: span 2;
+}
+
+.col.two-one:first-child {
+ grid-column: span 4;
+}
+
+.col.two-one:last-child {
+ grid-column: span 2;
+}
+
+.col.one-two:first-child {
+ grid-column: span 2;
+}
+
+.col.one-two:last-child {
+ grid-column: span 4;
+}
+
@media only screen and (max-width: 992px) {
.row {
border: 0;
@@ -33,4 +58,11 @@
border-inline-end: 0;
padding: 20px 0;
}
+
+ .col.two,
+ .col.three,
+ .col.one-two,
+ .col.two-one {
+ grid-column: span 6 !important;
+ }
}
diff --git a/src/components/layout/Header.js b/src/components/layout/Header.js
deleted file mode 100644
index 21cdd251..00000000
--- a/src/components/layout/Header.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Column, Icon, Row, Text } from 'react-basics';
-import Link from 'next/link';
-import LanguageButton from 'components/input/LanguageButton';
-import ThemeButton from 'components/input/ThemeButton';
-import SettingsButton from 'components/input/SettingsButton';
-import Icons from 'components/icons';
-import styles from './Header.module.css';
-
-export function Header() {
- return (
-
-
-
-
-
-
-
- umami
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default Header;
diff --git a/src/components/layout/NavBar.js b/src/components/layout/NavBar.js
deleted file mode 100644
index 07627e2a..00000000
--- a/src/components/layout/NavBar.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { Icon, Text, Row, Column } from 'react-basics';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import classNames from 'classnames';
-import Icons from 'components/icons';
-import ThemeButton from 'components/input/ThemeButton';
-import LanguageButton from 'components/input/LanguageButton';
-import ProfileButton from 'components/input/ProfileButton';
-import useMessages from 'components/hooks/useMessages';
-import HamburgerButton from 'components/common/HamburgerButton';
-import styles from './NavBar.module.css';
-
-export function NavBar() {
- const { pathname } = useRouter();
- const { formatMessage, labels } = useMessages();
-
- const links = [
- { label: formatMessage(labels.dashboard), url: '/dashboard' },
- { label: formatMessage(labels.websites), url: '/websites' },
- { label: formatMessage(labels.reports), url: '/reports' },
- { label: formatMessage(labels.settings), url: '/settings' },
- ].filter(n => n);
-
- return (
-
-
-
-
-
-
-
- umami
-
-
- {links.map(({ url, label }) => {
- return (
-
- {label}
-
- );
- })}
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default NavBar;
diff --git a/src/components/layout/NavGroup.js b/src/components/layout/NavGroup.js
index 94f9d8e6..361dffb5 100644
--- a/src/components/layout/NavGroup.js
+++ b/src/components/layout/NavGroup.js
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Icon, Text, TooltipPopup } from 'react-basics';
import classNames from 'classnames';
-import { useRouter } from 'next/router';
+import { usePathname } from 'next/navigation';
import Link from 'next/link';
import Icons from 'components/icons';
import styles from './NavGroup.module.css';
@@ -13,7 +13,7 @@ export function NavGroup({
allowExpand = true,
minimized = false,
}) {
- const { pathname } = useRouter();
+ const pathname = usePathname();
const [expanded, setExpanded] = useState(defaultExpanded);
const handleExpand = () => setExpanded(state => !state);
diff --git a/src/components/layout/Page.module.css b/src/components/layout/Page.module.css
index c546971b..52893157 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 4f42aa55..2f702012 100644
--- a/src/components/layout/Page.js
+++ b/src/components/layout/Page.tsx
@@ -1,16 +1,28 @@
+'use client';
+import { ReactNode } from 'react';
import classNames from 'classnames';
import { Banner, Loading } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import styles from './Page.module.css';
-export function Page({ className, error, loading, children }) {
+export function Page({
+ className,
+ error,
+ isLoading,
+ children,
+}: {
+ className?: string;
+ error?: unknown;
+ isLoading?: boolean;
+ children?: ReactNode;
+}) {
const { formatMessage, messages } = useMessages();
if (error) {
return {formatMessage(messages.error)};
}
- if (loading) {
+ if (isLoading) {
return ;
}
diff --git a/src/components/layout/PageHeader.js b/src/components/layout/PageHeader.tsx
similarity index 58%
rename from src/components/layout/PageHeader.js
rename to src/components/layout/PageHeader.tsx
index f1363140..2261bebc 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 374da263..00000000
--- a/src/components/layout/ReportsLayout.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Column, Row } from 'react-basics';
-import styles from './ReportsLayout.module.css';
-
-export function ReportsLayout({ children, filter, header }) {
- return (
- <>
-
{header}
-
- {filter && (
-
- Filters
- {filter}
-
- )}
-
- {children}
-
-
- >
- );
-}
-
-export default ReportsLayout;
diff --git a/src/components/layout/ReportsLayout.module.css b/src/components/layout/ReportsLayout.module.css
deleted file mode 100644
index 6922665f..00000000
--- a/src/components/layout/ReportsLayout.module.css
+++ /dev/null
@@ -1,23 +0,0 @@
-.filter {
- margin-top: 30px;
- min-width: 200px;
- max-width: 100vw;
- padding: 10px;
- background: var(--base50);
- border-radius: 5px;
- border: 1px solid var(--border-color);
-}
-
-.filter h2 {
- padding-bottom: 20px;
-}
-
-.content {
- min-height: 50vh;
-}
-
-@media only screen and (max-width: 768px) {
- .menu {
- display: none;
- }
-}
diff --git a/src/components/layout/SettingsLayout.module.css b/src/components/layout/SettingsLayout.module.css
deleted file mode 100644
index 08ff02aa..00000000
--- a/src/components/layout/SettingsLayout.module.css
+++ /dev/null
@@ -1,20 +0,0 @@
-.menu {
- display: flex;
- flex-direction: column;
- padding-top: 40px;
- padding-right: 20px;
-}
-
-.content {
- min-height: 50vh;
-}
-
-@media only screen and (max-width: 768px) {
- .menu {
- display: none;
- }
-
- .content {
- margin-top: 20px;
- }
-}
diff --git a/src/components/layout/ShareLayout.js b/src/components/layout/ShareLayout.js
deleted file mode 100644
index c634e1b6..00000000
--- a/src/components/layout/ShareLayout.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Container } from 'react-basics';
-import Header from './Header';
-import Footer from './Footer';
-
-export function ShareLayout({ children }) {
- return (
-
-
- {children}
-
-
- );
-}
-
-export default ShareLayout;
diff --git a/src/components/layout/SideNav.js b/src/components/layout/SideNav.js
index ccb6f360..c93881e4 100644
--- a/src/components/layout/SideNav.js
+++ b/src/components/layout/SideNav.js
@@ -1,6 +1,6 @@
import classNames from 'classnames';
import { Menu, Item } from 'react-basics';
-import { useRouter } from 'next/router';
+import { usePathname } from 'next/navigation';
import Link from 'next/link';
import styles from './SideNav.module.css';
@@ -9,15 +9,21 @@ export function SideNav({
items,
shallow = true,
scroll = false,
+ className,
onSelect = () => {},
}) {
- const { asPath } = useRouter();
+ const pathname = usePathname();
return (
-