mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-01 12:29:35 +01:00
commit
c37a8c343c
107
README.md
107
README.md
@ -1,69 +1,93 @@
|
||||
# umami
|
||||
<p align="center">
|
||||
<img src="https://umami.is/images/umami-logo.png" alt="Umami Logo" width="100">
|
||||
</p>
|
||||
|
||||
Umami is a simple, fast, privacy-focused alternative to Google Analytics.
|
||||
<h1 align="center">Umami</h1>
|
||||
|
||||
## Getting started
|
||||
<p align="center">
|
||||
<i>Umami is a simple, fast, privacy-focused alternative to Google Analytics.</i>
|
||||
</p>
|
||||
|
||||
A detailed getting started guide can be found at [https://umami.is/docs/](https://umami.is/docs/)
|
||||
<p align="center">
|
||||
<a href="https://github.com/umami-software/umami/releases">
|
||||
<img src="https://img.shields.io/github/release/umami-software/umami.svg" alt="GitHub Release" />
|
||||
</a>
|
||||
<a href="https://github.com/umami-software/umami/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/umami-software/umami.svg" alt="MIT License" />
|
||||
</a>
|
||||
<a href="https://github.com/umami-software/umami/actions">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/umami-software/umami/ci.yml" alt="Build Status" />
|
||||
</a>
|
||||
<a href="https://analytics.umami.is/share/LGazGOecbDtaIwDr/umami.is" style="text-decoration: none;">
|
||||
<img src="https://img.shields.io/badge/Try%20Demo%20Now-Click%20Here-brightgreen" alt="Umami Demo" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Installing from source
|
||||
---
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
A detailed getting started guide can be found at [umami.is/docs](https://umami.is/docs/).
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Installing from Source
|
||||
|
||||
### Requirements
|
||||
|
||||
- A server with Node.js version 16.13 or newer
|
||||
- A database. Umami supports [MySQL](https://www.mysql.com/) (minimum v8.0) and [Postgresql](https://www.postgresql.org/) (minimum v12.14) databases.
|
||||
- A database. Umami supports [MySQL](https://www.mysql.com/) (minimum v8.0) and [PostgreSQL](https://www.postgresql.org/) (minimum v12.14) databases.
|
||||
|
||||
### Install Yarn
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install -g yarn
|
||||
```
|
||||
|
||||
### Get the source code and install packages
|
||||
### Get the Source Code and Install Packages
|
||||
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/umami-software/umami.git
|
||||
cd umami
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Configure umami
|
||||
### Configure Umami
|
||||
|
||||
Create an `.env` file with the following
|
||||
Create an `.env` file with the following:
|
||||
|
||||
```
|
||||
```bash
|
||||
DATABASE_URL=connection-url
|
||||
```
|
||||
|
||||
The connection url is in the following format:
|
||||
The connection URL format:
|
||||
|
||||
```
|
||||
```bash
|
||||
postgresql://username:mypassword@localhost:5432/mydb
|
||||
|
||||
mysql://username:mypassword@localhost:3306/mydb
|
||||
```
|
||||
|
||||
### Build the application
|
||||
### Build the Application
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
The build step will also create tables in your database if you are installing for the first time. It will also create a login user with username **admin** and password **umami**.
|
||||
*The build step will create tables in your database if you are installing for the first time. It will also create a login user with username **admin** and password **umami**.*
|
||||
|
||||
### Start the application
|
||||
### Start the Application
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
By default this will launch the application on `http://localhost:3000`. You will need to either
|
||||
[proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server
|
||||
or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly.
|
||||
*By default, this will launch the application on `http://localhost:3000`. You will need to either [proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly.*
|
||||
|
||||
## Installing with Docker
|
||||
---
|
||||
|
||||
To build the umami container and start up a Postgres database, run:
|
||||
## 🐳 Installing with Docker
|
||||
|
||||
To build the Umami container and start up a Postgres database, run:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
@ -81,7 +105,9 @@ Or with MySQL support:
|
||||
docker pull docker.umami.is/umami-software/umami:mysql-latest
|
||||
```
|
||||
|
||||
## Getting updates
|
||||
---
|
||||
|
||||
## 🔄 Getting Updates
|
||||
|
||||
To get the latest features, simply do a pull, install any new dependencies, and rebuild:
|
||||
|
||||
@ -98,7 +124,36 @@ docker compose pull
|
||||
docker compose up --force-recreate
|
||||
```
|
||||
|
||||
## License
|
||||
---
|
||||
|
||||
MIT
|
||||
## 🛟 Support
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/umami-software/umami">
|
||||
<img src="https://img.shields.io/badge/GitHub--blue?style=social&logo=github" alt="GitHub" />
|
||||
</a>
|
||||
<a href="https://twitter.com/umami_software">
|
||||
<img src="https://img.shields.io/badge/Twitter--blue?style=social&logo=twitter" alt="Twitter" />
|
||||
</a>
|
||||
<a href="https://linkedin.com/company/umami-software">
|
||||
<img src="https://img.shields.io/badge/LinkedIn--blue?style=social&logo=linkedin" alt="LinkedIn" />
|
||||
</a>
|
||||
<a href="https://umami.is/discord">
|
||||
<img src="https://img.shields.io/badge/Discord--blue?style=social&logo=discord" alt="Discord" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[release-shield]: https://img.shields.io/github/release/umami-software/umami.svg
|
||||
[releases-url]: https://github.com/umami-software/umami/releases
|
||||
[license-shield]: https://img.shields.io/github/license/umami-software/umami.svg
|
||||
[license-url]: https://github.com/umami-software/umami/blob/master/LICENSE
|
||||
[build-shield]: https://img.shields.io/github/actions/workflow/status/umami-software/umami/ci.yml
|
||||
[build-url]: https://github.com/umami-software/umami/actions
|
||||
[github-shield]: https://img.shields.io/badge/GitHub--blue?style=social&logo=github
|
||||
[github-url]: https://github.com/umami-software/umami
|
||||
[twitter-shield]: https://img.shields.io/badge/Twitter--blue?style=social&logo=twitter
|
||||
[twitter-url]: https://twitter.com/umami_software
|
||||
[linkedin-shield]: https://img.shields.io/badge/LinkedIn--blue?style=social&logo=linkedin
|
||||
[linkedin-url]: https://linkedin.com/company/umami-software
|
||||
[discord-shield]: https://img.shields.io/badge/Discord--blue?style=social&logo=discord
|
||||
[discord-url]: https://discord.com/invite/4dz4zcXYrQ
|
||||
|
@ -64,9 +64,9 @@
|
||||
".next/cache"
|
||||
],
|
||||
"dependencies": {
|
||||
"@clickhouse/client": "^1.0.1",
|
||||
"@clickhouse/client": "^1.0.2",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
"@prisma/client": "5.13.0",
|
||||
"@prisma/client": "5.14.0",
|
||||
"@prisma/extension-read-replicas": "^0.3.0",
|
||||
"@react-spring/web": "^9.7.3",
|
||||
"@tanstack/react-query": "^5.28.6",
|
||||
@ -102,7 +102,7 @@
|
||||
"next-basics": "^0.39.0",
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prisma": "5.13.0",
|
||||
"prisma": "5.14.0",
|
||||
"react": "^18.2.0",
|
||||
"react-basics": "^0.123.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Average visit time"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visit duration"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "المتوسط"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "متوسط وقت الزيارة"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "متوسط وقت الزيارة"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Сярэдняя даўжыня наведвання"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Сярэдняя даўжыня наведвання"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "গড় পরিদর্শনের সময়"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "গড় পরিদর্শনের সময়"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Prosjek"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Prosječno vrijeme posjete"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Pregledi po posjeti"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Prosječno vrijeme posjete"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Mitjana"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Temps mitjà de visita"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Temps mitjà de visita"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Průměrný čas návštěvy"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Průměrný čas návštěvy"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Gennemsnitlig besøgstid"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Gennemsnitlig besøgstid"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Durchschn. Bsuechsziit"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Durchschn. Bsuechsziit"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Durchschnitt"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Durchschn. Besuchszeit"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Durchschn. Besuchszeit"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Μέσος χρόνος επίσκεψης"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Μέσος χρόνος επίσκεψης"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Average visit time"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visit duration"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Average visit time"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visit duration"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Media"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tiempo promedio de visita"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tiempo promedio de visita"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -71,7 +71,7 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tiempo promedio de visita"
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "میانگین زمان بازدید"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "میانگین زمان بازدید"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Keskimääräinen vierailuaika"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Keskimääräinen vierailuaika"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Miðal vitjurnartíð "
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Miðal vitjurnartíð "
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Moyenne"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Temps de visite moyen"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1237,6 +1231,12 @@
|
||||
"value": "Vues par visite"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Temps de visite moyen"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tempo medio de visita"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1257,6 +1251,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tempo medio de visita"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "זמן ביקור ממוצע"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1241,6 +1235,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "זמן ביקור ממוצע"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "औसत दृश्य समय"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "औसत दृश्य समय"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Average visit time"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visit duration"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Átlagos látogatási idő"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Átlagos látogatási idő"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Waktu kunjungan rata-rata"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1241,6 +1235,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Waktu kunjungan rata-rata"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tempo medio di visita"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tempo medio di visita"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "平均"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "平均滞在時間"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "平均滞在時間"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "មើលជាមធ្យម"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1241,6 +1235,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "មើលជាមធ្យម"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "평균 방문 시간"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1253,6 +1247,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "평균 방문 시간"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Vidurkis"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Vidutinė vizito trukmė"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1354,6 +1348,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Vidutinė vizito trukmė"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Дундаж"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Зочилсон дундаж хугацаа"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Зочилсон дундаж хугацаа"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Purata tempoh masa lawatan"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1241,6 +1235,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Purata tempoh masa lawatan"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "ပျမ်းမျှ"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "ဝဘက်ဘ်ဆိုဒ်တွင် ပျမ်းမျှကုန်ဆုံးချိန်"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "ဝဘက်ဘ်ဆိုဒ်တွင် ပျမ်းမျှကုန်ဆုံးချိန်"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Gjennomsnittlig besøkstid"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Gjennomsnittlig besøkstid"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Gemiddelde"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Gemiddelde bezoektijd"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Gemiddelde bezoektijd"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Średnia"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Średni czas wizyty"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Średni czas wizyty"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Média"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tempo médio de visita"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Visualizações por visita"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tempo médio de visita"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tempo médio de visita"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Tempo médio de visita"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Mediu"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Timp mediu de vizitare"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Timp mediu de vizitare"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Среднее время посещения"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Среднее время посещения"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Average visit time"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visit duration"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Priemerný čas návštevy"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Priemerný čas návštevy"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Povprečno"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Povprečni čas obiska"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Povprečni čas obiska"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Genomsnitt"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Genomsnittlig besökstid"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Genomsnittlig besökstid"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "சராசரி வருகை நேரம்"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "சராசரி வருகை நேரம்"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "ระยะเวลาเข้าชมเฉลี่ย"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1241,6 +1235,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "ระยะเวลาเข้าชมเฉลี่ย"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Ortalama"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Ortalama ziyaret süresi"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Ziyaret başına görüntüleme"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Ortalama ziyaret süresi"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Середнє"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Середній час візиту"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Перегляди за візит"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Середній час візиту"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "وزٹ کا اوسط وقت"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1249,6 +1243,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "وزٹ کا اوسط وقت"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "Average"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Thời gian truy cập trung bình"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1241,6 +1235,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Thời gian truy cập trung bình"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "平均"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "平均访问时间"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1257,6 +1251,12 @@
|
||||
"value": "每次访问的浏览量"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "平均访问时间"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -83,12 +83,6 @@
|
||||
"value": "平均"
|
||||
}
|
||||
],
|
||||
"label.average-visit-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "平均造訪時間"
|
||||
}
|
||||
],
|
||||
"label.back": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -1253,6 +1247,12 @@
|
||||
"value": "Views per visit"
|
||||
}
|
||||
],
|
||||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "平均造訪時間"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -7,11 +7,11 @@ import styles from './DateRangeSetting.module.css';
|
||||
|
||||
export function DateRangeSetting() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [dateRange, setDateRange] = useDateRange();
|
||||
const { dateRange, saveDateRange } = useDateRange();
|
||||
const { value } = dateRange;
|
||||
|
||||
const handleChange = (value: string | DateRange) => setDateRange(value);
|
||||
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
|
||||
const handleChange = (value: string | DateRange) => saveDateRange(value);
|
||||
const handleReset = () => saveDateRange(DEFAULT_DATE_RANGE);
|
||||
|
||||
return (
|
||||
<Flexbox gap={10} width={300}>
|
||||
|
@ -6,27 +6,48 @@ import styles from './GoalsAddForm.module.css';
|
||||
export function GoalsAddForm({
|
||||
type: defaultType = 'url',
|
||||
value: defaultValue = '',
|
||||
property: defaultProperty = '',
|
||||
operator: defaultAggregae = null,
|
||||
goal: defaultGoal = 10,
|
||||
onChange,
|
||||
}: {
|
||||
type?: string;
|
||||
value?: string;
|
||||
operator?: string;
|
||||
property?: string;
|
||||
goal?: number;
|
||||
onChange?: (step: { type: string; value: string; goal: number }) => void;
|
||||
onChange?: (step: {
|
||||
type: string;
|
||||
value: string;
|
||||
goal: number;
|
||||
operator?: string;
|
||||
property?: string;
|
||||
}) => void;
|
||||
}) {
|
||||
const [type, setType] = useState(defaultType);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [operator, setOperator] = useState(defaultAggregae);
|
||||
const [property, setProperty] = useState(defaultProperty);
|
||||
const [goal, setGoal] = useState(defaultGoal);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const items = [
|
||||
{ label: formatMessage(labels.url), value: 'url' },
|
||||
{ label: formatMessage(labels.event), value: 'event' },
|
||||
{ label: formatMessage(labels.eventData), value: 'event-data' },
|
||||
];
|
||||
const operators = [
|
||||
{ label: formatMessage(labels.count), value: 'count' },
|
||||
{ label: formatMessage(labels.average), value: 'average' },
|
||||
{ label: formatMessage(labels.sum), value: 'sum' },
|
||||
];
|
||||
const isDisabled = !type || !value;
|
||||
|
||||
const handleSave = () => {
|
||||
onChange({ type, value, goal });
|
||||
onChange(
|
||||
type === 'event-data' ? { type, value, goal, operator, property } : { type, value, goal },
|
||||
);
|
||||
setValue('');
|
||||
setProperty('');
|
||||
setGoal(10);
|
||||
};
|
||||
|
||||
@ -45,6 +66,10 @@ export function GoalsAddForm({
|
||||
return items.find(item => item.value === value)?.label;
|
||||
};
|
||||
|
||||
const renderoperatorValue = (value: any) => {
|
||||
return operators.find(item => item.value === value)?.label;
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox direction="column" gap={10}>
|
||||
<FormRow label={formatMessage(defaultValue ? labels.update : labels.add)}>
|
||||
@ -70,6 +95,31 @@ export function GoalsAddForm({
|
||||
/>
|
||||
</Flexbox>
|
||||
</FormRow>
|
||||
{type === 'event-data' && (
|
||||
<FormRow label={formatMessage(labels.property)}>
|
||||
<Flexbox gap={10}>
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
items={operators}
|
||||
value={operator}
|
||||
renderValue={renderoperatorValue}
|
||||
onChange={(value: any) => setOperator(value)}
|
||||
>
|
||||
{({ value, label }) => {
|
||||
return <Item key={value}>{label}</Item>;
|
||||
}}
|
||||
</Dropdown>
|
||||
<TextField
|
||||
className={styles.input}
|
||||
value={property}
|
||||
onChange={e => handleChange(e, setProperty)}
|
||||
autoFocus={true}
|
||||
autoComplete="off"
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</Flexbox>
|
||||
</FormRow>
|
||||
)}
|
||||
<FormRow label={formatMessage(labels.goal)}>
|
||||
<Flexbox gap={10}>
|
||||
<TextField
|
||||
|
@ -11,19 +11,36 @@ export function GoalsChart({ className }: { className?: string; isLoading?: bool
|
||||
|
||||
const { data } = report || {};
|
||||
|
||||
const getLabel = type => {
|
||||
let label = '';
|
||||
switch (type) {
|
||||
case 'url':
|
||||
label = labels.viewedPage;
|
||||
break;
|
||||
case 'event':
|
||||
label = labels.triggeredEvent;
|
||||
break;
|
||||
default:
|
||||
label = labels.collectedData;
|
||||
break;
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.chart, className)}>
|
||||
{data?.map(({ type, value, goal, result }, index: number) => {
|
||||
{data?.map(({ type, value, goal, result, property, operator }, index: number) => {
|
||||
const percent = result > goal ? 100 : (result / goal) * 100;
|
||||
|
||||
return (
|
||||
<div key={index} className={styles.goal}>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.label}>
|
||||
{formatMessage(type === 'url' ? labels.viewedPage : labels.triggeredEvent)}
|
||||
</span>
|
||||
<span className={styles.item}>{value}</span>
|
||||
<span className={styles.label}>{formatMessage(getLabel(type))}</span>
|
||||
<span className={styles.item}>{`${value}${
|
||||
type === 'event-data' ? `:(${operator}):${property}` : ''
|
||||
}`}</span>
|
||||
</div>
|
||||
<div className={styles.track}>
|
||||
<div
|
||||
|
@ -4,6 +4,16 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.eventData {
|
||||
color: var(--orange900);
|
||||
background-color: var(--orange100);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
padding: 2px 8px;
|
||||
border-radius: 5px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.goal {
|
||||
color: var(--blue900);
|
||||
background-color: var(--blue100);
|
||||
@ -11,4 +21,5 @@
|
||||
font-weight: 900;
|
||||
padding: 2px 8px;
|
||||
border-radius: 5px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { formatNumber } from 'lib/format';
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Flexbox,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormRow,
|
||||
@ -79,33 +80,53 @@ export function GoalsParameters() {
|
||||
<BaseParameters allowWebsiteSelect={!id} />
|
||||
<FormRow label={formatMessage(labels.goals)} action={<AddGoalsButton />}>
|
||||
<ParameterList>
|
||||
{goals.map((goal: { type: string; value: string; goal: number }, index: number) => {
|
||||
return (
|
||||
<PopupTrigger key={index}>
|
||||
<ParameterList.Item
|
||||
icon={goal.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}
|
||||
onRemove={() => handleRemoveGoals(index)}
|
||||
>
|
||||
<div className={styles.value}>{goal.value}</div>
|
||||
<div className={styles.goal}>
|
||||
{formatMessage(labels.goal)}: {formatNumber(goal.goal)}
|
||||
</div>
|
||||
</ParameterList.Item>
|
||||
<Popup alignment="start">
|
||||
{(close: () => void) => (
|
||||
<PopupForm>
|
||||
<GoalsAddForm
|
||||
type={goal.type}
|
||||
value={goal.value}
|
||||
goal={goal.goal}
|
||||
onChange={handleUpdateGoals.bind(null, close, index)}
|
||||
/>
|
||||
</PopupForm>
|
||||
)}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
})}
|
||||
{goals.map(
|
||||
(
|
||||
goal: {
|
||||
type: string;
|
||||
value: string;
|
||||
goal: number;
|
||||
operator?: string;
|
||||
property?: string;
|
||||
},
|
||||
index: number,
|
||||
) => {
|
||||
return (
|
||||
<PopupTrigger key={index}>
|
||||
<ParameterList.Item
|
||||
icon={goal.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}
|
||||
onRemove={() => handleRemoveGoals(index)}
|
||||
>
|
||||
<Flexbox direction="column" gap={5}>
|
||||
<div className={styles.value}>{goal.value}</div>
|
||||
{goal.type === 'event-data' && (
|
||||
<div className={styles.eventData}>
|
||||
{formatMessage(labels[goal.operator])}: {goal.property}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.goal}>
|
||||
{formatMessage(labels.goal)}: {formatNumber(goal.goal)}
|
||||
</div>
|
||||
</Flexbox>
|
||||
</ParameterList.Item>
|
||||
<Popup alignment="start">
|
||||
{(close: () => void) => (
|
||||
<PopupForm>
|
||||
<GoalsAddForm
|
||||
type={goal.type}
|
||||
value={goal.value}
|
||||
goal={goal.goal}
|
||||
operator={goal.operator}
|
||||
property={goal.property}
|
||||
onChange={handleUpdateGoals.bind(null, close, index)}
|
||||
/>
|
||||
</PopupForm>
|
||||
)}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</ParameterList>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
|
36
src/app/(main)/reports/journey/JourneyParameters.tsx
Normal file
36
src/app/(main)/reports/journey/JourneyParameters.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useContext } from 'react';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { Form, FormButtons, SubmitButton } from 'react-basics';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
|
||||
export function JourneyParameters() {
|
||||
const { report, runReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange } = parameters || {};
|
||||
const queryDisabled = !websiteId || !dateRange;
|
||||
|
||||
const handleSubmit = (data: any, e: any) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!queryDisabled) {
|
||||
runReport(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||
<BaseParameters showDateSelect={true} allowWebsiteSelect={!id} />
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
</SubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default JourneyParameters;
|
28
src/app/(main)/reports/journey/JourneyReport.tsx
Normal file
28
src/app/(main)/reports/journey/JourneyReport.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import JourneyParameters from './JourneyParameters';
|
||||
import JourneyView from './JourneyView';
|
||||
import Path from 'assets/path.svg';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
|
||||
const defaultParameters = {
|
||||
type: REPORT_TYPES.journey,
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
export default function JourneyReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Path />} />
|
||||
<ReportMenu>
|
||||
<JourneyParameters />
|
||||
</ReportMenu>
|
||||
<ReportBody>
|
||||
<JourneyView />
|
||||
</ReportBody>
|
||||
</Report>
|
||||
);
|
||||
}
|
5
src/app/(main)/reports/journey/JourneyReportPage.tsx
Normal file
5
src/app/(main)/reports/journey/JourneyReportPage.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import JourneyReport from './JourneyReport';
|
||||
|
||||
export default function JourneyReportPage() {
|
||||
return <JourneyReport />;
|
||||
}
|
14
src/app/(main)/reports/journey/JourneyView.module.css
Normal file
14
src/app/(main)/reports/journey/JourneyView.module.css
Normal file
@ -0,0 +1,14 @@
|
||||
.title {
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
gap: 20px;
|
||||
border-bottom: 1px solid var(--base300);
|
||||
padding-bottom: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
13
src/app/(main)/reports/journey/JourneyView.tsx
Normal file
13
src/app/(main)/reports/journey/JourneyView.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { useContext } from 'react';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
|
||||
export default function JourneyView() {
|
||||
const { report } = useContext(ReportContext);
|
||||
const { data } = report || {};
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div>{JSON.stringify(data)}</div>;
|
||||
}
|
10
src/app/(main)/reports/journey/page.tsx
Normal file
10
src/app/(main)/reports/journey/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Metadata } from 'next';
|
||||
import JourneyReportPage from './JourneyReportPage';
|
||||
|
||||
export default function () {
|
||||
return <JourneyReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Journey Report',
|
||||
};
|
@ -34,6 +34,7 @@ export default function UTMView() {
|
||||
{
|
||||
data: items.map(({ value }) => value),
|
||||
backgroundColor: CHART_COLORS,
|
||||
borderWidth: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -61,7 +61,7 @@ export function WebsiteSettings({
|
||||
</Tabs>
|
||||
{tab === 'details' && <WebsiteEditForm websiteId={websiteId} onSave={handleSave} />}
|
||||
{tab === 'tracking' && <TrackingCode websiteId={websiteId} />}
|
||||
{tab === 'share' && <ShareUrl websiteId={websiteId} onSave={handleSave} />}
|
||||
{tab === 'share' && <ShareUrl onSave={handleSave} />}
|
||||
{tab === 'data' && <WebsiteData websiteId={websiteId} onSave={handleSave} />}
|
||||
</>
|
||||
);
|
||||
|
@ -4,17 +4,41 @@ import { getDateArray } from 'lib/date';
|
||||
import useWebsitePageviews from 'components/hooks/queries/useWebsitePageviews';
|
||||
import { useDateRange } from 'components/hooks';
|
||||
|
||||
export function WebsiteChart({ websiteId }: { websiteId: string }) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
export function WebsiteChart({
|
||||
websiteId,
|
||||
compareMode = false,
|
||||
}: {
|
||||
websiteId: string;
|
||||
compareMode?: boolean;
|
||||
}) {
|
||||
const { dateRange, dateCompare } = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit } = dateRange;
|
||||
const { data, isLoading } = useWebsitePageviews(websiteId);
|
||||
const { data, isLoading } = useWebsitePageviews(websiteId, compareMode ? dateCompare : undefined);
|
||||
const { pageviews, sessions, compare } = (data || {}) as any;
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (data) {
|
||||
return {
|
||||
pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
|
||||
sessions: getDateArray(data.sessions, startDate, endDate, unit),
|
||||
const result = {
|
||||
pageviews: getDateArray(pageviews, startDate, endDate, unit),
|
||||
sessions: getDateArray(sessions, startDate, endDate, unit),
|
||||
};
|
||||
|
||||
if (compare) {
|
||||
result['compare'] = {
|
||||
pageviews: result.pageviews.map(({ x }, i) => ({
|
||||
x,
|
||||
y: compare.pageviews[i]?.y,
|
||||
d: compare.pageviews[i]?.x,
|
||||
})),
|
||||
sessions: result.sessions.map(({ x }, i) => ({
|
||||
x,
|
||||
y: compare.sessions[i]?.y,
|
||||
d: compare.sessions[i]?.x,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return { pageviews: [], sessions: [] };
|
||||
}, [data, startDate, endDate, unit]);
|
||||
|
@ -1,40 +0,0 @@
|
||||
'use client';
|
||||
import { Loading } from 'react-basics';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Page from 'components/layout/Page';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import { useNavigation, useWebsite } from 'components/hooks';
|
||||
import WebsiteChart from './WebsiteChart';
|
||||
import WebsiteExpandedView from './WebsiteExpandedView';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
import WebsiteMetricsBar from './WebsiteMetricsBar';
|
||||
import WebsiteTableView from './WebsiteTableView';
|
||||
|
||||
export default function WebsiteDetails({ websiteId }: { websiteId: string }) {
|
||||
const { data: website, isLoading, error } = useWebsite(websiteId);
|
||||
const pathname = usePathname();
|
||||
const { query } = useNavigation();
|
||||
|
||||
if (isLoading || error) {
|
||||
return <Page isLoading={isLoading} error={error} />;
|
||||
}
|
||||
|
||||
const showLinks = !pathname.includes('/share/');
|
||||
const { view, ...params } = query;
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} showLinks={showLinks} />
|
||||
<FilterTags websiteId={websiteId} params={params} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} sticky={true} />
|
||||
<WebsiteChart websiteId={websiteId} />
|
||||
{!website && <Loading icon="dots" style={{ minHeight: 300 }} />}
|
||||
{website && (
|
||||
<>
|
||||
{!view && <WebsiteTableView websiteId={websiteId} domainName={website.domain} />}
|
||||
{view && <WebsiteExpandedView websiteId={websiteId} domainName={website.domain} />}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
37
src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx
Normal file
37
src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import { useNavigation } from 'components/hooks';
|
||||
import WebsiteChart from './WebsiteChart';
|
||||
import WebsiteExpandedView from './WebsiteExpandedView';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
import WebsiteMetricsBar from './WebsiteMetricsBar';
|
||||
import WebsiteTableView from './WebsiteTableView';
|
||||
import WebsiteProvider from './WebsiteProvider';
|
||||
import { FILTER_COLUMNS } from 'lib/constants';
|
||||
|
||||
export default function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
|
||||
const pathname = usePathname();
|
||||
const { query } = useNavigation();
|
||||
|
||||
const showLinks = !pathname.includes('/share/');
|
||||
const { view } = query;
|
||||
|
||||
const params = Object.keys(query).reduce((obj, key) => {
|
||||
if (FILTER_COLUMNS[key]) {
|
||||
obj[key] = query[key];
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<WebsiteHeader websiteId={websiteId} showLinks={showLinks} />
|
||||
<FilterTags websiteId={websiteId} params={params} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} showChange={true} sticky={true} />
|
||||
<WebsiteChart websiteId={websiteId} />
|
||||
{!view && <WebsiteTableView websiteId={websiteId} />}
|
||||
{view && <WebsiteExpandedView websiteId={websiteId} />}
|
||||
</WebsiteProvider>
|
||||
);
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import classNames from 'classnames';
|
||||
import { Button, Icon, Icons, Popup, PopupTrigger, Text } from 'react-basics';
|
||||
import PopupForm from 'app/(main)/reports/[reportId]/PopupForm';
|
||||
import FilterSelectForm from 'app/(main)/reports/[reportId]/FilterSelectForm';
|
||||
@ -9,14 +8,22 @@ import styles from './WebsiteFilterButton.module.css';
|
||||
export function WebsiteFilterButton({
|
||||
websiteId,
|
||||
className,
|
||||
position = 'bottom',
|
||||
alignment = 'end',
|
||||
showText = true,
|
||||
}: {
|
||||
websiteId: string;
|
||||
className?: string;
|
||||
position?: 'bottom' | 'top' | 'left' | 'right';
|
||||
alignment?: 'end' | 'center' | 'start';
|
||||
showText?: boolean;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { renderUrl, router } = useNavigation();
|
||||
const { fields } = useFields();
|
||||
const [{ startDate, endDate }] = useDateRange(websiteId);
|
||||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange(websiteId);
|
||||
|
||||
const handleAddFilter = ({ name, operator, value }) => {
|
||||
const prefix = OPERATOR_PREFIXES[operator];
|
||||
@ -25,14 +32,14 @@ export function WebsiteFilterButton({
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<Button className={classNames(className, styles.button)} variant="quiet">
|
||||
<PopupTrigger className={className}>
|
||||
<Button className={styles.button} variant="quiet">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.filter)}</Text>
|
||||
{showText && <Text>{formatMessage(labels.filter)}</Text>}
|
||||
</Button>
|
||||
<Popup position="bottom" alignment="end">
|
||||
<Popup position={position} alignment={alignment}>
|
||||
{(close: () => void) => {
|
||||
return (
|
||||
<PopupForm>
|
||||
|
@ -30,6 +30,11 @@ export function WebsiteHeader({
|
||||
icon: <Icons.Overview />,
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.compare),
|
||||
icon: <Icons.Compare />,
|
||||
path: '/compare',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.realtime),
|
||||
icon: <Icons.Clock />,
|
||||
|
@ -1,6 +1,6 @@
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--base50);
|
||||
@ -11,10 +11,22 @@
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.vs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-basis: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
@ -38,9 +50,3 @@
|
||||
border-bottom: 1px solid var(--base300);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -3,94 +3,127 @@ import { useMessages, 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 { formatShortTime, formatLongNumber } from 'lib/format';
|
||||
import WebsiteFilterButton from './WebsiteFilterButton';
|
||||
import styles from './WebsiteMetricsBar.module.css';
|
||||
import useWebsiteStats from 'components/hooks/queries/useWebsiteStats';
|
||||
import styles from './WebsiteMetricsBar.module.css';
|
||||
import { Dropdown, Item } from 'react-basics';
|
||||
import useStore, { setWebsiteDateCompare } from 'store/websites';
|
||||
|
||||
export function WebsiteMetricsBar({
|
||||
websiteId,
|
||||
showFilter = true,
|
||||
sticky,
|
||||
showChange = false,
|
||||
compareMode = false,
|
||||
}: {
|
||||
websiteId: string;
|
||||
showFilter?: boolean;
|
||||
sticky?: boolean;
|
||||
showChange?: boolean;
|
||||
compareMode?: boolean;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const dateCompare = useStore(state => state[websiteId]?.dateCompare);
|
||||
const { ref, isSticky } = useSticky({ enabled: sticky });
|
||||
const { data, isLoading, isFetched, error } = useWebsiteStats(websiteId);
|
||||
const { data, isLoading, isFetched, error } = useWebsiteStats(
|
||||
websiteId,
|
||||
compareMode && dateCompare,
|
||||
);
|
||||
|
||||
const { pageviews, visitors, visits, bounces, totaltime } = data || {};
|
||||
const num = Math.min(data && visitors.value, data && bounces.value);
|
||||
const diffs = data && {
|
||||
pageviews: pageviews.value - pageviews.change,
|
||||
visitors: visitors.value - visitors.change,
|
||||
visits: visits.value - visits.change,
|
||||
bounces: bounces.value - bounces.change,
|
||||
totaltime: totaltime.value - totaltime.change,
|
||||
};
|
||||
|
||||
const metrics = data
|
||||
? [
|
||||
{
|
||||
...pageviews,
|
||||
label: formatMessage(labels.views),
|
||||
change: pageviews.value - pageviews.prev,
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
...visits,
|
||||
label: formatMessage(labels.visits),
|
||||
change: visits.value - visits.prev,
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
...visitors,
|
||||
label: formatMessage(labels.visitors),
|
||||
change: visitors.value - visitors.prev,
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.bounceRate),
|
||||
value: (Math.min(visitors.value, bounces.value) / visitors.value) * 100,
|
||||
prev: (Math.min(visitors.prev, bounces.prev) / visitors.prev) * 100,
|
||||
change:
|
||||
(Math.min(visitors.value, bounces.value) / visitors.value) * 100 -
|
||||
(Math.min(visitors.prev, bounces.prev) / visitors.prev) * 100,
|
||||
formatValue: n => Number(n).toFixed(0) + '%',
|
||||
reverseColors: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.visitDuration),
|
||||
value: totaltime.value / visits.value,
|
||||
prev: totaltime.prev / visits.prev,
|
||||
change: totaltime.value / visits.value - totaltime.prev / visits.prev,
|
||||
formatValue: n =>
|
||||
`${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const items = [
|
||||
{ label: formatMessage(labels.previousPeriod), value: 'prev' },
|
||||
{ label: formatMessage(labels.previousYear), value: 'yoy' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(styles.container, {
|
||||
[styles.sticky]: sticky,
|
||||
[styles.isSticky]: isSticky,
|
||||
[styles.isSticky]: sticky && isSticky,
|
||||
})}
|
||||
>
|
||||
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
|
||||
{pageviews && visitors && (
|
||||
<>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.views)}
|
||||
value={pageviews.value}
|
||||
change={pageviews.change}
|
||||
/>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.visits)}
|
||||
value={visits.value}
|
||||
change={visits.change}
|
||||
/>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.visitors)}
|
||||
value={visitors.value}
|
||||
change={visitors.change}
|
||||
/>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.bounceRate)}
|
||||
value={visitors.value ? (num / visitors.value) * 100 : 0}
|
||||
change={
|
||||
visitors.value && visitors.change
|
||||
? (num / visitors.value) * 100 -
|
||||
(Math.min(diffs.visitors, diffs.bounces) / diffs.visitors) * 100 || 0
|
||||
: 0
|
||||
}
|
||||
format={n => Number(n).toFixed(0) + '%'}
|
||||
reverseColors
|
||||
/>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.averageVisitTime)}
|
||||
value={
|
||||
totaltime.value && pageviews.value
|
||||
? totaltime.value / (pageviews.value - bounces.value)
|
||||
: 0
|
||||
}
|
||||
change={
|
||||
totaltime.value && pageviews.value
|
||||
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) -
|
||||
totaltime.value / (pageviews.value - bounces.value)) *
|
||||
-1 || 0
|
||||
: 0
|
||||
}
|
||||
format={n => `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</MetricsBar>
|
||||
<div>
|
||||
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
|
||||
{metrics.map(({ label, value, prev, change, formatValue, reverseColors }) => {
|
||||
return (
|
||||
<MetricCard
|
||||
key={label}
|
||||
value={value}
|
||||
previousValue={prev}
|
||||
label={label}
|
||||
change={change}
|
||||
formatValue={formatValue}
|
||||
reverseColors={reverseColors}
|
||||
showChange={compareMode || showChange}
|
||||
showPrevious={compareMode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MetricsBar>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
{showFilter && <WebsiteFilterButton websiteId={websiteId} className={styles.button} />}
|
||||
<WebsiteFilterButton websiteId={websiteId} />
|
||||
<WebsiteDateFilter websiteId={websiteId} />
|
||||
{compareMode && (
|
||||
<div className={styles.vs}>
|
||||
<b>VS</b>
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
items={items}
|
||||
value={dateCompare || 'prev'}
|
||||
renderValue={value => items.find(i => i.value === value)?.label}
|
||||
alignment="end"
|
||||
onChange={(value: any) => setWebsiteDateCompare(websiteId, value)}
|
||||
>
|
||||
{items.map(({ label, value }) => (
|
||||
<Item key={value}>{label}</Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -10,17 +10,10 @@ import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
|
||||
export default function WebsiteTableView({
|
||||
websiteId,
|
||||
domainName,
|
||||
}: {
|
||||
websiteId: string;
|
||||
domainName: string;
|
||||
}) {
|
||||
export default function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
||||
const [countryData, setCountryData] = useState();
|
||||
const tableProps = {
|
||||
websiteId,
|
||||
domainName,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
import WebsiteHeader from '../WebsiteHeader';
|
||||
import WebsiteMetricsBar from '../WebsiteMetricsBar';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import { useNavigation } from 'components/hooks';
|
||||
import { FILTER_COLUMNS } from 'lib/constants';
|
||||
import WebsiteChart from '../WebsiteChart';
|
||||
import WebsiteCompareTables from './WebsiteCompareTables';
|
||||
import WebsiteProvider from '../WebsiteProvider';
|
||||
|
||||
export function WebsiteComparePage({ websiteId }) {
|
||||
const { query } = useNavigation();
|
||||
|
||||
const params = Object.keys(query).reduce((obj, key) => {
|
||||
if (FILTER_COLUMNS[key]) {
|
||||
obj[key] = query[key];
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<FilterTags websiteId={websiteId} params={params} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} />
|
||||
<WebsiteChart websiteId={websiteId} compareMode={true} />
|
||||
<WebsiteCompareTables websiteId={websiteId} />
|
||||
</WebsiteProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteComparePage;
|
@ -0,0 +1,14 @@
|
||||
.container {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 200px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--base800);
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
import { useState } from 'react';
|
||||
import SideNav from 'components/layout/SideNav';
|
||||
import { useDateRange, useMessages, useNavigation } from 'components/hooks';
|
||||
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 ScreenTable from 'components/metrics/ScreenTable';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import RegionsTable from 'components/metrics/RegionsTable';
|
||||
import CitiesTable from 'components/metrics/CitiesTable';
|
||||
import LanguagesTable from 'components/metrics/LanguagesTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import QueryParametersTable from 'components/metrics/QueryParametersTable';
|
||||
import { Grid, GridRow } from 'components/layout/Grid';
|
||||
import MetricsTable from 'components/metrics/MetricsTable';
|
||||
import useStore from 'store/websites';
|
||||
import { getCompareDate } from 'lib/date';
|
||||
import { formatNumber } from 'lib/format';
|
||||
import ChangeLabel from 'components/metrics/ChangeLabel';
|
||||
import styles from './WebsiteCompareTables.module.css';
|
||||
|
||||
const views = {
|
||||
url: PagesTable,
|
||||
title: PagesTable,
|
||||
referrer: ReferrersTable,
|
||||
browser: BrowsersTable,
|
||||
os: OSTable,
|
||||
device: DevicesTable,
|
||||
screen: ScreenTable,
|
||||
country: CountriesTable,
|
||||
region: RegionsTable,
|
||||
city: CitiesTable,
|
||||
language: LanguagesTable,
|
||||
event: EventsTable,
|
||||
query: QueryParametersTable,
|
||||
};
|
||||
|
||||
export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
|
||||
const [data, setData] = useState([]);
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const dateCompare = useStore(state => state[websiteId]?.dateCompare);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const {
|
||||
renderUrl,
|
||||
query: { view },
|
||||
} = useNavigation();
|
||||
const Component: typeof MetricsTable = views[view || 'url'] || (() => null);
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'url',
|
||||
label: formatMessage(labels.pages),
|
||||
url: renderUrl({ view: 'url' }),
|
||||
},
|
||||
{
|
||||
key: 'referrer',
|
||||
label: formatMessage(labels.referrers),
|
||||
url: renderUrl({ view: 'referrer' }),
|
||||
},
|
||||
{
|
||||
key: 'browser',
|
||||
label: formatMessage(labels.browsers),
|
||||
url: renderUrl({ view: 'browser' }),
|
||||
},
|
||||
{
|
||||
key: 'os',
|
||||
label: formatMessage(labels.os),
|
||||
url: renderUrl({ view: 'os' }),
|
||||
},
|
||||
{
|
||||
key: 'device',
|
||||
label: formatMessage(labels.devices),
|
||||
url: renderUrl({ view: 'device' }),
|
||||
},
|
||||
{
|
||||
key: 'country',
|
||||
label: formatMessage(labels.countries),
|
||||
url: renderUrl({ view: 'country' }),
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: formatMessage(labels.regions),
|
||||
url: renderUrl({ view: 'region' }),
|
||||
},
|
||||
{
|
||||
key: 'city',
|
||||
label: formatMessage(labels.cities),
|
||||
url: renderUrl({ view: 'city' }),
|
||||
},
|
||||
{
|
||||
key: 'language',
|
||||
label: formatMessage(labels.languages),
|
||||
url: renderUrl({ view: 'language' }),
|
||||
},
|
||||
{
|
||||
key: 'screen',
|
||||
label: formatMessage(labels.screens),
|
||||
url: renderUrl({ view: 'screen' }),
|
||||
},
|
||||
{
|
||||
key: 'event',
|
||||
label: formatMessage(labels.events),
|
||||
url: renderUrl({ view: 'event' }),
|
||||
},
|
||||
{
|
||||
key: 'query',
|
||||
label: formatMessage(labels.queryParameters),
|
||||
url: renderUrl({ view: 'query' }),
|
||||
},
|
||||
];
|
||||
|
||||
const renderChange = ({ x, y }) => {
|
||||
const prev = data.find(d => d.x === x)?.y;
|
||||
const value = y - prev;
|
||||
const change = Math.abs(((y - prev) / prev) * 100);
|
||||
|
||||
return !isNaN(change) && <ChangeLabel value={value}>{formatNumber(change)}%</ChangeLabel>;
|
||||
};
|
||||
|
||||
const { startDate, endDate } = getCompareDate(
|
||||
dateCompare,
|
||||
dateRange.startDate,
|
||||
dateRange.endDate,
|
||||
);
|
||||
|
||||
const params = {
|
||||
startAt: startDate.getTime(),
|
||||
endAt: endDate.getTime(),
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid className={styles.container}>
|
||||
<GridRow columns="compare">
|
||||
<SideNav className={styles.nav} items={items} selectedKey={view} shallow={true} />
|
||||
<div>
|
||||
<div className={styles.title}>{formatMessage(labels.previous)}</div>
|
||||
<Component
|
||||
websiteId={websiteId}
|
||||
limit={20}
|
||||
showMore={false}
|
||||
onDataLoad={setData}
|
||||
params={params}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.title}> {formatMessage(labels.current)}</div>
|
||||
<Component
|
||||
websiteId={websiteId}
|
||||
limit={20}
|
||||
showMore={false}
|
||||
renderChange={renderChange}
|
||||
/>
|
||||
</div>
|
||||
</GridRow>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteCompareTables;
|
10
src/app/(main)/websites/[websiteId]/compare/page.tsx
Normal file
10
src/app/(main)/websites/[websiteId]/compare/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import WebsiteComparePage from './WebsiteComparePage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function ({ params: { websiteId } }) {
|
||||
return <WebsiteComparePage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Website Comparison',
|
||||
};
|
@ -7,7 +7,7 @@ import styles from './EventDataMetricsBar.module.css';
|
||||
export function EventDataMetricsBar({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get, useQuery } = useApi();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
|
||||
const { data, error, isLoading, isFetched } = useQuery({
|
||||
|
@ -6,7 +6,7 @@ import { useDateRange, useApi, useNavigation } from 'components/hooks';
|
||||
import styles from './WebsiteEventData.module.css';
|
||||
|
||||
function useData(websiteId: string, event: string) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery({
|
||||
|
@ -1,8 +1,8 @@
|
||||
import WebsiteDetails from './WebsiteDetails';
|
||||
import WebsiteDetailsPage from './WebsiteDetailsPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function WebsitePage({ params: { websiteId } }) {
|
||||
return <WebsiteDetails websiteId={websiteId} />;
|
||||
return <WebsiteDetailsPage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
@ -14,25 +14,25 @@ export function RealtimeHeader({ data }: { data: RealtimeData }) {
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.views)}
|
||||
value={pageviews?.length}
|
||||
hideComparison
|
||||
showChange
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.visitors)}
|
||||
value={visitors?.length}
|
||||
hideComparison
|
||||
showChange
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.events)}
|
||||
value={events?.length}
|
||||
hideComparison
|
||||
showChange
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.countries)}
|
||||
value={countries?.length}
|
||||
hideComparison
|
||||
showChange
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import WebsiteDetails from 'app/(main)/websites/[websiteId]/WebsiteDetails';
|
||||
import WebsiteDetailsPage from '../../(main)/websites/[websiteId]/WebsiteDetailsPage';
|
||||
import { useShareToken } from 'components/hooks';
|
||||
import Page from 'components/layout/Page';
|
||||
import Header from './Header';
|
||||
@ -17,7 +17,7 @@ export default function SharePage({ shareId }) {
|
||||
<div className={styles.container}>
|
||||
<Page>
|
||||
<Header />
|
||||
<WebsiteDetails websiteId={shareToken.websiteId} />
|
||||
<WebsiteDetailsPage websiteId={shareToken.websiteId} />
|
||||
<Footer />
|
||||
</Page>
|
||||
</div>
|
||||
|
1
src/assets/compare.svg
Normal file
1
src/assets/compare.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M6 22a1 1 0 0 1-.71-.29l-4-4a1 1 0 0 1 0-1.42l4-4a1 1 0 0 1 1.42 1.42L4.41 16H22a1 1 0 0 1 0 2H4.41l2.3 2.29a1 1 0 0 1 0 1.42A1 1 0 0 1 6 22zm12-10a1 1 0 0 1-.71-.29 1 1 0 0 1 0-1.42L19.59 8H2a1 1 0 0 1 0-2h17.59l-2.3-2.29a1 1 0 0 1 1.42-1.42l4 4a1 1 0 0 1 0 1.42l-4 4A1 1 0 0 1 18 12z"/></svg>
|
After Width: | Height: | Size: 388 B |
4
src/assets/logo-white.svg
Normal file
4
src/assets/logo-white.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 428 389.11">
|
||||
<circle cx="214.15" cy="181" r="171" fill="none" stroke="white" stroke-miterlimit="10" stroke-width="20"/>
|
||||
<path d="M413 134.11H15.29a15 15 0 0 0-15 15v15.3C.12 168 0 171.52 0 175.11c0 118.19 95.81 214 214 214 116.4 0 211.1-92.94 213.93-208.67 0-.44.07-.88.07-1.33v-30a15 15 0 0 0-15-15Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 404 B |
1
src/assets/path.svg
Normal file
1
src/assets/path.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none" viewBox="0 0 64 64"><path fill="#000" d="m56.4 47.6-6-6c-.8-.8-2-.8-2.8 0s-.8 2 0 2.8l2.6 2.6H18.5c-3.6 0-6.5-2.9-6.5-6.5s2.9-6.5 6.5-6.5h27C51.3 34 56 29.3 56 23.5S51.3 13 45.5 13H22.7c-.9-3.4-4-6-7.7-6-4.4 0-8 3.6-8 8s3.6 8 8 8c3.7 0 6.8-2.6 7.7-6h22.8c3.6 0 6.5 2.9 6.5 6.5S49.1 30 45.5 30h-27C12.7 30 8 34.7 8 40.5S12.7 51 18.5 51h31.7l-2.6 2.6c-.8.8-.8 2 0 2.8.4.4.9.6 1.4.6s1-.2 1.4-.6l6-6c.8-.8.8-2 0-2.8M15 19c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4"/></svg>
|
After Width: | Height: | Size: 550 B |
@ -1 +1 @@
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" height="512" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><g id="Icon"><path d="m19.393 10.825c-.097-.403.151-.808.553-.905.402-.098.808.15.905.553.181.75.277 1.533.277 2.338 0 5.485-4.453 9.939-9.939 9.939-5.485 0-9.939-4.454-9.939-9.939 0-5.486 4.454-9.939 9.939-9.939.805 0 1.588.096 2.338.277.403.097.651.503.553.905-.097.402-.502.65-.905.553-.637-.154-1.302-.235-1.986-.235-4.658 0-8.439 3.781-8.439 8.439s3.781 8.439 8.439 8.439 8.439-3.781 8.439-8.439c0-.684-.081-1.349-.235-1.986z"/><path d="m14.764 12.811c0-.414.336-.75.75-.75.413 0 .75.336.75.75 0 2.8-2.274 5.074-5.075 5.074-2.8 0-5.074-2.274-5.074-5.074 0-2.801 2.274-5.075 5.074-5.075.414 0 .75.337.75.75 0 .414-.336.75-.75.75-1.973 0-3.574 1.602-3.574 3.575s1.601 3.574 3.574 3.574 3.575-1.601 3.575-3.574z"/><path d="m22.53 5.588-3.057 3.058c-.141.141-.332.22-.531.22h-3.058c-.414 0-.75-.336-.75-.75v-3.058c0-.199.079-.39.22-.531l3.058-3.057c.184-.184.45-.26.703-.2s.457.246.539.493l.646 1.937 1.937.646c.247.082.433.286.493.539s-.016.519-.2.703zm-1.918-.202-1.142-.381c-.224-.075-.4-.251-.475-.475l-.381-1.142-1.98 1.98v1.998h1.998z"/><path d="m15.354 7.585c.293-.293.768-.293 1.061 0s.293.768 0 1.061l-4.587 4.586c-.293.293-.768.293-1.06 0-.293-.292-.293-.767 0-1.06z"/></g></svg>
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" height="512" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M19.393 10.825a.75.75 0 0 1 1.458-.352c.181.75.277 1.533.277 2.338 0 5.485-4.453 9.939-9.939 9.939-5.485 0-9.939-4.454-9.939-9.939 0-5.486 4.454-9.939 9.939-9.939.805 0 1.588.096 2.338.277a.75.75 0 1 1-.352 1.458A8.442 8.442 0 0 0 2.75 12.811a8.442 8.442 0 0 0 8.439 8.439 8.442 8.442 0 0 0 8.204-10.425z"/><path d="M14.764 12.811a.75.75 0 0 1 1.5 0c0 2.8-2.274 5.074-5.075 5.074a5.077 5.077 0 0 1-5.074-5.074 5.077 5.077 0 0 1 5.074-5.075.75.75 0 0 1 0 1.5 3.575 3.575 0 1 0 3.575 3.575zm7.766-7.223-3.057 3.058a.75.75 0 0 1-.531.22h-3.058a.75.75 0 0 1-.75-.75V5.058a.75.75 0 0 1 .22-.531l3.058-3.057a.75.75 0 0 1 1.242.293L20.3 3.7l1.937.646a.75.75 0 0 1 .293 1.242zm-1.918-.202-1.142-.381a.753.753 0 0 1-.475-.475l-.381-1.142-1.98 1.98v1.998h1.998z"/><path d="M15.354 7.585a.75.75 0 1 1 1.061 1.061l-4.587 4.586a.749.749 0 1 1-1.06-1.06z"/></svg>
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
@ -26,7 +26,7 @@ export function BarChart(props: BarChartProps) {
|
||||
stacked = false,
|
||||
} = props;
|
||||
|
||||
const options = useMemo(() => {
|
||||
const options: any = useMemo(() => {
|
||||
return {
|
||||
scales: {
|
||||
x: {
|
||||
|
@ -21,7 +21,9 @@ export default function BarChartTooltip({ tooltip, unit }) {
|
||||
|
||||
return (
|
||||
<Flexbox direction="column" gap={10}>
|
||||
<div>{formatDate(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
|
||||
<div>
|
||||
{formatDate(new Date(dataPoints[0].raw.d || dataPoints[0].raw.x), formats[unit], locale)}
|
||||
</div>
|
||||
<div>
|
||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useState, useRef, useEffect, useMemo, ReactNode } from 'react';
|
||||
import { Loading } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import ChartJS, { LegendItem } from 'chart.js/auto';
|
||||
import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto';
|
||||
import HoverTooltip from 'components/common/HoverTooltip';
|
||||
import Legend from 'components/metrics/Legend';
|
||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
@ -17,7 +17,7 @@ export interface ChartProps {
|
||||
onUpdate?: (chart: any) => void;
|
||||
onTooltip?: (model: any) => void;
|
||||
className?: string;
|
||||
chartOptions?: { [key: string]: any };
|
||||
chartOptions?: ChartOptions;
|
||||
tooltip?: ReactNode;
|
||||
}
|
||||
|
||||
@ -79,24 +79,28 @@ export function Chart({
|
||||
};
|
||||
|
||||
const updateChart = (data: any) => {
|
||||
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
|
||||
if (data?.datasets[index]) {
|
||||
dataset.data = data?.datasets[index]?.data;
|
||||
if (data.datasets.length === chart.current.data.datasets.length) {
|
||||
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
|
||||
if (data?.datasets[index]) {
|
||||
dataset.data = data?.datasets[index]?.data;
|
||||
|
||||
if (chart.current.legend.legendItems[index]) {
|
||||
chart.current.legend.legendItems[index].text = data?.datasets[index]?.label;
|
||||
if (chart.current.legend.legendItems[index]) {
|
||||
chart.current.legend.legendItems[index].text = data?.datasets[index]?.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
chart.current.data.datasets = data.datasets;
|
||||
}
|
||||
|
||||
chart.current.options = options;
|
||||
|
||||
// Allow config changes before update
|
||||
onUpdate?.(chart.current);
|
||||
|
||||
setLegendItems(chart.current.legend.legendItems);
|
||||
|
||||
chart.current.update(updateMode);
|
||||
|
||||
setLegendItems(chart.current.legend.legendItems);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -4,7 +4,7 @@ import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useWebsiteMetrics(
|
||||
websiteId: string,
|
||||
query: { type: string; limit: number; search: string },
|
||||
queryParams: { type: string; limit: number; search: string; startAt?: number; endAt?: number },
|
||||
options?: Omit<UseQueryOptions & { onDataLoad?: (data: any) => void }, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
@ -16,17 +16,17 @@ export function useWebsiteMetrics(
|
||||
{
|
||||
websiteId,
|
||||
...params,
|
||||
...query,
|
||||
...queryParams,
|
||||
},
|
||||
],
|
||||
queryFn: async () => {
|
||||
const filters = { ...params };
|
||||
|
||||
filters[query.type] = undefined;
|
||||
filters[queryParams.type] = undefined;
|
||||
|
||||
const data = await get(`/websites/${websiteId}/metrics`, {
|
||||
...filters,
|
||||
...query,
|
||||
...queryParams,
|
||||
});
|
||||
|
||||
options?.onDataLoad?.(data);
|
||||
|
@ -4,14 +4,15 @@ import { useFilterParams } from '..//useFilterParams';
|
||||
|
||||
export function useWebsitePageviews(
|
||||
websiteId: string,
|
||||
compare?: string,
|
||||
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['websites:pageviews', { websiteId, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/pageviews`, params),
|
||||
queryKey: ['websites:pageviews', { websiteId, ...params, compare }],
|
||||
queryFn: () => get(`/websites/${websiteId}/pageviews`, { ...params, compare }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { useApi } from './useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useWebsiteStats(websiteId: string, options?: { [key: string]: string }) {
|
||||
export function useWebsiteStats(
|
||||
websiteId: string,
|
||||
compare?: string,
|
||||
options?: { [key: string]: string },
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['websites:stats', { websiteId, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/stats`, params),
|
||||
queryKey: ['websites:stats', { websiteId, ...params, compare }],
|
||||
queryFn: () => get(`/websites/${websiteId}/stats`, { ...params, compare }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
@ -1,19 +1,25 @@
|
||||
import { getMinimumUnit, parseDateRange } from 'lib/date';
|
||||
import { setItem } from 'next-basics';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import websiteStore, { setWebsiteDateRange } from 'store/websites';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import websiteStore, { setWebsiteDateRange, setWebsiteDateCompare } from 'store/websites';
|
||||
import appStore, { setDateRange } from 'store/app';
|
||||
import { DateRange } from 'lib/types';
|
||||
import { useLocale } from './useLocale';
|
||||
import { useApi } from './queries/useApi';
|
||||
|
||||
export function useDateRange(websiteId?: string): [DateRange, (value: string | DateRange) => void] {
|
||||
export function useDateRange(websiteId?: string): {
|
||||
dateRange: DateRange;
|
||||
saveDateRange: (value: string | DateRange) => void;
|
||||
dateCompare: string;
|
||||
saveDateCompare: (value: string) => void;
|
||||
} {
|
||||
const { get } = useApi();
|
||||
const { locale } = useLocale();
|
||||
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
|
||||
const defaultConfig = DEFAULT_DATE_RANGE;
|
||||
const globalConfig = appStore(state => state.dateRange);
|
||||
const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale);
|
||||
const dateCompare = websiteStore(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE);
|
||||
|
||||
const saveDateRange = async (value: DateRange | string) => {
|
||||
if (websiteId) {
|
||||
@ -45,7 +51,11 @@ export function useDateRange(websiteId?: string): [DateRange, (value: string | D
|
||||
}
|
||||
};
|
||||
|
||||
return [dateRange, saveDateRange];
|
||||
const saveDateCompare = (value: string) => {
|
||||
setWebsiteDateCompare(websiteId, value);
|
||||
};
|
||||
|
||||
return { dateRange, saveDateRange, dateCompare, saveDateCompare };
|
||||
}
|
||||
|
||||
export default useDateRange;
|
||||
|
@ -4,7 +4,7 @@ import { useTimezone } from './useTimezone';
|
||||
import { zonedTimeToUtc } from 'date-fns-tz';
|
||||
|
||||
export function useFilterParams(websiteId: string) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit } = dateRange;
|
||||
const { timezone } = useTimezone();
|
||||
const {
|
||||
|
@ -6,6 +6,7 @@ import Bolt from 'assets/bolt.svg';
|
||||
import Calendar from 'assets/calendar.svg';
|
||||
import Change from 'assets/change.svg';
|
||||
import Clock from 'assets/clock.svg';
|
||||
import Compare from 'assets/compare.svg';
|
||||
import Dashboard from 'assets/dashboard.svg';
|
||||
import Eye from 'assets/eye.svg';
|
||||
import Gear from 'assets/gear.svg';
|
||||
@ -32,6 +33,7 @@ const icons = {
|
||||
Calendar,
|
||||
Change,
|
||||
Clock,
|
||||
Compare,
|
||||
Dashboard,
|
||||
Eye,
|
||||
Gear,
|
||||
|
@ -12,7 +12,7 @@ export function RefreshButton({
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
|
||||
function handleClick() {
|
||||
if (!isLoading && dateRange) {
|
||||
|
@ -8,21 +8,30 @@ import { DateRange } from 'lib/types';
|
||||
|
||||
export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
|
||||
const { dir } = useLocale();
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { dateRange, saveDateRange } = useDateRange(websiteId);
|
||||
const { value, startDate, endDate, offset } = dateRange;
|
||||
const disableForward =
|
||||
value === 'all' || isAfter(getOffsetDateRange(dateRange, 1).startDate, new Date());
|
||||
|
||||
const handleChange = (value: string | DateRange) => {
|
||||
setDateRange(value);
|
||||
saveDateRange(value);
|
||||
};
|
||||
|
||||
const handleIncrement = (increment: number) => {
|
||||
setDateRange(getOffsetDateRange(dateRange, increment));
|
||||
saveDateRange(getOffsetDateRange(dateRange, increment));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<DateFilter
|
||||
className={styles.dropdown}
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
offset={offset}
|
||||
onChange={handleChange}
|
||||
showAllTime={true}
|
||||
/>
|
||||
{value !== 'all' && !value.startsWith('range') && (
|
||||
<div className={styles.buttons}>
|
||||
<Button onClick={() => handleIncrement(-1)}>
|
||||
@ -37,15 +46,6 @@ export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<DateFilter
|
||||
className={styles.dropdown}
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
offset={offset}
|
||||
onChange={handleChange}
|
||||
showAllTime={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,10 @@
|
||||
border-top: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.row.compare {
|
||||
grid-template-columns: max-content 1fr 1fr;
|
||||
}
|
||||
|
||||
.col {
|
||||
padding: 20px;
|
||||
min-height: 430px;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user