Merge pull request #2758 from umami-software/dev

Merge analytics
This commit is contained in:
Mike Cao 2024-05-26 21:59:18 -07:00 committed by GitHub
commit c37a8c343c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
187 changed files with 2071 additions and 952 deletions

107
README.md
View File

@ -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

View File

@ -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",

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -71,7 +71,7 @@
"value": "Average"
}
],
"label.average-visit-time": [
"label.visit-duration": [
{
"type": 0,
"value": "Tiempo promedio de visita"

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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}>

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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>

View 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;

View 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>
);
}

View File

@ -0,0 +1,5 @@
import JourneyReport from './JourneyReport';
export default function JourneyReportPage() {
return <JourneyReport />;
}

View 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;
}

View 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>;
}

View 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',
};

View File

@ -34,6 +34,7 @@ export default function UTMView() {
{
data: items.map(({ value }) => value),
backgroundColor: CHART_COLORS,
borderWidth: 0,
},
],
};

View File

@ -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} />}
</>
);

View File

@ -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]);

View File

@ -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} />}
</>
)}
</>
);
}

View 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>
);
}

View File

@ -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>

View File

@ -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 />,

View File

@ -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;
}
}

View File

@ -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>
);

View File

@ -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,
};

View File

@ -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;

View File

@ -0,0 +1,14 @@
.container {
margin-bottom: 60px;
}
.nav {
width: 200px;
margin-top: 40px;
}
.title {
color: var(--base800);
text-align: center;
font-weight: 700;
}

View File

@ -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;

View 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',
};

View File

@ -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({

View File

@ -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({

View File

@ -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 = {

View File

@ -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>

View File

@ -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
View 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

View 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
View 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

View File

@ -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

View File

@ -26,7 +26,7 @@ export function BarChart(props: BarChartProps) {
stacked = false,
} = props;
const options = useMemo(() => {
const options: any = useMemo(() => {
return {
scales: {
x: {

View File

@ -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}

View File

@ -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(() => {

View File

@ -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);

View File

@ -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,
});

View File

@ -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,
});

View File

@ -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;

View File

@ -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 {

View File

@ -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,

View File

@ -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) {

View File

@ -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>
);
}

View File

@ -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