Compare commits

...

166 Commits

Author SHA1 Message Date
Iván Eixarch 732fc73fe6 Commit con #3 2023-06-05 18:29:26 +02:00
Mike Cao 586529a5ca
Merge pull request #2009 from franciscao633/master
Github Issues Organizational Templates
2023-04-28 12:49:29 -07:00
Francis Cao 22e2f79bff re order required fields 2023-04-28 11:35:01 -07:00
Francis Cao 6f4cc9e84c typo correction 2023-04-28 11:34:03 -07:00
Francis Cao e77e030f2c update bug report 2023-04-28 11:31:54 -07:00
Francis Cao a43bbc51f6 update feature name 2023-04-28 11:24:31 -07:00
Francis Cao bf26874965 clean up templates 2023-04-28 11:18:53 -07:00
Francis Cao 6cdf9a68d1 fix config and clean up feature request 2023-04-28 11:06:25 -07:00
Francis Cao 402acf563e fix bug report template 2023-04-28 11:01:40 -07:00
Francis Cao 2d6077fe16 clean-up template 2023-04-28 10:59:35 -07:00
Francis Cao c9ec055996 introduce templates 2023-04-28 10:04:23 -07:00
Francis Cao 3f0815855c issues template 2023-04-28 09:52:42 -07:00
Mike Cao 46615fe7eb
Merge pull request #1994 from umami-software/dev
v2.2.0
2023-04-25 21:49:42 -07:00
Mike Cao 9345c9ac9e Apply CSS to flag icons. 2023-04-25 21:18:03 -07:00
Mike Cao 938047ce45 Revert tracker logic. Bump search depth. 2023-04-25 21:03:26 -07:00
Mike Cao 34757e7b2a Bump version v2.2.0. 2023-04-25 20:52:05 -07:00
Mike Cao 01fe8b44a3 Updated packages. 2023-04-25 20:34:56 -07:00
Mike Cao ae85bf696b Allow tracker to traverse all levels to find anchor. 2023-04-25 19:12:17 -07:00
Mike Cao f450fc35fb Fixed city showing undefined. 2023-04-25 16:56:31 -07:00
Mike Cao 4c544361fd Fixed vercel again. 2023-04-25 16:51:42 -07:00
Mike Cao 4ea3eec98c Fixed vercel region issues. 2023-04-25 16:29:47 -07:00
Mike Cao 5864aee043 Fixed vercel issues. 2023-04-25 15:58:52 -07:00
Mike Cao c49e2c6974 Fixed Vercel geolocation. 2023-04-25 14:52:29 -07:00
Mike Cao 02f031bde1 Separate CLOUD_MODE and DISABLE_LOGIN. 2023-04-25 14:48:50 -07:00
Mike Cao 3f1ed750f0 Added error boundary. Fixed #1976. 2023-04-25 14:41:54 -07:00
Mike Cao d73def80c7 Update message bundles. 2023-04-25 12:46:19 -07:00
Mike Cao 4b72918a91 Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-25 12:45:12 -07:00
Mike Cao 44a1c7fa10
Merge pull request #1963 from RikoDEV/patch-1
i18n: update pl-PL translations
2023-04-25 12:44:51 -07:00
Mike Cao c47d246a79
Merge pull request #1974 from atmonshi/update-lang-ar
update AR phrases
2023-04-25 12:44:01 -07:00
Mike Cao 89d37e0045
Merge pull request #1975 from atmonshi/refresh-after-delete-user
small improve to Users List
2023-04-25 12:43:30 -07:00
Mike Cao 4045cfd039
Merge pull request #1980 from juangacovas/patch-3
Update es-MX.json
2023-04-25 12:42:25 -07:00
Mike Cao c3beb10e3b
Merge pull request #1992 from Truimo/dev
fix for #1940: check if there is no parent element
2023-04-25 12:36:26 -07:00
Mike Cao b6f3f57455 Added fallback in case country code not found. 2023-04-25 12:35:09 -07:00
Truimo 4876684461 fix: added check if there is no parent element 2023-04-26 00:10:33 +08:00
Mike Cao 9e11866ddd Remove password from logging. 2023-04-24 22:00:40 -07:00
Mike Cao 1e9a4ad08f Added login debugging. 2023-04-24 21:35:09 -07:00
Mike Cao 92ab391ef8 Use Vercel headers for location. 2023-04-24 19:29:31 -07:00
Mike Cao c23373d164 Check page title exists first. 2023-04-24 18:07:27 -07:00
Juanga Covas 1b8c2c4dde
Update es-MX.json
Update missing translations
2023-04-24 21:27:53 +02:00
Mike Cao 26a9cb67d8 Merge remote-tracking branch 'origin/dev' 2023-04-23 19:55:35 -07:00
Mike Cao 4a51a5db3f Added country flags. 2023-04-23 19:52:44 -07:00
Ash Monsh 8bc0fb92ba Update UsersList.js
show toast after creating user and refresh the list after delete
2023-04-23 05:43:09 +03:00
Ash Monsh ff9a6335d1 update AR phrases 2023-04-23 05:39:35 +03:00
Mike Cao c5bffb97cc Improved error handling for useSession middleware. 2023-04-22 14:17:57 -07:00
RikoDEV 2e3d2924e7
Update pl-PL.json 2023-04-22 10:10:50 +02:00
Mike Cao 8666965930 Updated lang files. 2023-04-22 00:55:55 -07:00
Mike Cao 5027d9d945 Merge branch 'dev' 2023-04-21 22:05:38 -07:00
Mike Cao 6cb2429ee1 Bump version v2.1.1. 2023-04-21 22:05:06 -07:00
Mike Cao ef59e93adc Merge branch 'dev' 2023-04-21 21:29:17 -07:00
Mike Cao 47f63d80c4 Fix tracker. 2023-04-21 21:28:02 -07:00
Mike Cao 36b767af75
Merge pull request #1955 from umami-software/dev
v2.1.0
2023-04-21 18:20:48 -07:00
Mike Cao 00d0421623 Make title optional. 2023-04-21 17:59:03 -07:00
Mike Cao 987e3aabbd Fix tracking code form. 2023-04-21 17:21:44 -07:00
Mike Cao a2bce7ae2c Revert next for vercel build. 2023-04-21 17:01:31 -07:00
Mike Cao 6469dd296d Revert tsconfig.json. 2023-04-21 16:06:49 -07:00
Mike Cao 71fcad26a5 Updated .track method functionality. 2023-04-21 15:55:52 -07:00
Mike Cao 80438bf84c Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-21 15:16:39 -07:00
Mike Cao eaaf3a0a5c Merge branch 'master' into dev 2023-04-21 15:16:26 -07:00
Mike Cao a7e6c32460 Updated page titles. 2023-04-21 15:14:30 -07:00
Mike Cao 39f630400f Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-21 15:02:25 -07:00
Mike Cao 3ba75a6737
Merge pull request #1944 from atmonshi/page-titles
add page titles
2023-04-21 15:02:18 -07:00
Mike Cao 71628bec90
Merge pull request #1945 from yumusb/patch-1
Fix "x-umami-cache: undefined"
2023-04-21 14:59:39 -07:00
Mike Cao 6f80908160 Updated message bundles. 2023-04-21 14:58:37 -07:00
Mike Cao 21c4ec068d
Merge pull request #1948 from Maxime-J/dev
restore login localization
2023-04-21 14:57:16 -07:00
Mike Cao 115a2560a7
Merge branch 'dev' into dev 2023-04-21 14:57:04 -07:00
Mike Cao f246183094
Merge pull request #1951 from ym-project/master
update ru-RU locale
2023-04-21 14:55:20 -07:00
Mike Cao edce62eede
Merge pull request #1953 from winterrific/master
Update lang de-DE
2023-04-21 14:54:43 -07:00
Mike Cao 223f0051ed Only display first tracker script name. 2023-04-21 14:45:06 -07:00
Mike Cao 6c09dd1e23 Merge remote-tracking branch 'origin/dev' into dev 2023-04-21 14:39:15 -07:00
Mike Cao 5b4bd67455 Updated packages. 2023-04-21 14:39:06 -07:00
Mike Cao e6c5a9b17d Added country to regions display. 2023-04-21 14:35:00 -07:00
Mike Cao 69b9458975 Removed rollup for components. 2023-04-21 13:16:54 -07:00
Ruben 65c07f5034 Update lang de-DE 2023-04-21 22:10:14 +02:00
Mike Cao b9a0f0442e Updated next to 13.3.0. 2023-04-21 12:52:09 -07:00
Mike Cao a0894866b9 POC for exporting components. 2023-04-21 12:43:37 -07:00
ym-project 39cafafd76
fix `label.data` translation 2023-04-21 19:32:04 +03:00
ym-project 31df0f7f07
update ru-RU locale 2023-04-21 19:27:09 +03:00
Mike Cao 8bddc666b4 Refactored exports. 2023-04-21 08:00:42 -07:00
Maxime-J b2d8423dcb restore login localization 2023-04-21 13:14:14 +00:00
榆木 5d8fb2f791
Update index.js
Do not send the x-umami-cache request header when the cache is not defined (the first request sent to the umami server when the visitor opens the page)
2023-04-21 12:04:30 +08:00
Ash Monsh 3039ad9315 add page titles 2023-04-21 05:58:16 +03:00
Mike Cao 1baf4d5571 Code style fix. 2023-04-20 15:19:09 -07:00
Francis Cao 322990b03b userstable css fix for edit button 2023-04-20 12:10:33 -07:00
Francis Cao 7aac95a410 Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-20 12:07:10 -07:00
Francis Cao cce966c48c Fix realtime countries / visitors 2023-04-20 12:07:04 -07:00
Mike Cao 5bfbc71e7f Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-20 11:53:45 -07:00
Mike Cao 1d639c77a2
Merge pull request #1931 from atmonshi/more-rtl-ui
rotate arrows in rtl
2023-04-20 11:53:28 -07:00
php coder 80aad4ef55
Update WebsiteMenuView.js 2023-04-20 21:49:42 +03:00
Mike Cao c4d3d6fb09 Updated language bundle. 2023-04-20 11:36:30 -07:00
Mike Cao 288f77d6ff
Merge pull request #1940 from MexHigh/master
Adjusted tracking script to find parent a-tag for click tracking
2023-04-20 11:34:18 -07:00
Mike Cao 4bd8b44420
Merge pull request #1942 from atmonshi/rtl-css
fix some css for RTL
2023-04-20 11:25:59 -07:00
Mike Cao 5f30cbd789
Merge pull request #1938 from MBRjun/dev
Bug Fix & Translation Updates
2023-04-20 11:24:59 -07:00
Ash Monsh 5cd4e9fd8d fix some css for RTL 2023-04-20 21:08:56 +03:00
Francis Cao fa3659fcf6 fix user password edit and fix prisma schema for updated_at columns 2023-04-20 10:46:58 -07:00
Leon Schmidt a84d2181e7
Adjusted tracking script to find parent a-tag for click tracking 2023-04-20 17:14:41 +02:00
MBRjun 981abd5966
fix: get tracking code correctly 2023-04-20 22:41:25 +08:00
MBRjun 136f962218
i18n: update zh-CN translations 2023-04-20 21:29:58 +08:00
Ash Monsh aba523cbee rotate arrows in rtl 2023-04-20 12:41:06 +03:00
Mike Cao d2c9c45461 Allow slashes in tracker names. 2023-04-20 01:52:49 -07:00
Mike Cao dbc19e4ffd Allow multiple tracker names. 2023-04-20 01:12:43 -07:00
Mike Cao f0d13efc7a Fix typo. 2023-04-19 22:11:00 -07:00
Mike Cao 03ba225587 More resetDate conversion. 2023-04-19 21:16:56 -07:00
Mike Cao e63591e35d resetDate should be a Date object. 2023-04-19 21:08:52 -07:00
Mike Cao 98028f1756 Add version info. 2023-04-19 19:33:24 -07:00
Mike Cao 3b6c6846b1 Fixed lang references. 2023-04-19 18:42:29 -07:00
Mike Cao ae49317c93 Merge remote-tracking branch 'origin/dev' into dev 2023-04-19 18:38:46 -07:00
Mike Cao e33a8e054b Updated how datasets are loaded, preventing chart redraw. 2023-04-19 18:38:08 -07:00
Mike Cao 1cfebc8c78
Merge pull request #1896 from lozanoSergio/develop
fix typo prisma mysql schema
2023-04-19 17:09:31 -07:00
Mike Cao 06d19e8594
Merge branch 'dev' into develop 2023-04-19 17:09:21 -07:00
Mike Cao eb7a83b404
Merge pull request #1879 from kaisteinke/master
Add missing locales
2023-04-19 17:07:03 -07:00
Mike Cao afde1377dc
Merge branch 'dev' into master 2023-04-19 17:06:46 -07:00
Mike Cao b1856c7eb3 Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-19 17:03:11 -07:00
Mike Cao f2c0b4253a
Merge pull request #1917 from atmonshi/ui-rtl
UI rtl
2023-04-19 17:02:25 -07:00
php coder 1520f21b82
Merge branch 'umami-software:master' into ui-rtl 2023-04-20 03:01:20 +03:00
Mike Cao 1d9cb0d7c7 Fix message. 2023-04-19 16:42:42 -07:00
Mike Cao dbafc98142 Update checkout action. 2023-04-19 16:20:45 -07:00
Mike Cao b39ef68372 Update language bundle. 2023-04-19 13:18:47 -07:00
Mike Cao b95208afcd
Merge pull request #1921 from MBRjun/dev
i18n: update zh-CN translations
2023-04-19 13:15:44 -07:00
Mike Cao 374555aff3 Updated prisma to 4.13.0. 2023-04-19 12:51:42 -07:00
MBRjun af14ccb60e
i18n: update zh-CN translations 2023-04-20 03:03:33 +08:00
Mike Cao 8bd289ad01 Fixed realtime metrics display. Closes #1899. 2023-04-19 11:46:41 -07:00
Mike Cao 9c2242012e Merge remote-tracking branch 'origin/dev' into dev 2023-04-19 11:41:40 -07:00
Mike Cao cc20f898b1 Localize x axis labels. Closes #1913 2023-04-19 11:41:31 -07:00
Francis Cao 2cd1f5541b add event type to getWebsiteStats 2023-04-19 09:13:16 -07:00
Mike Cao 083a9ffc2c Merge remote-tracking branch 'origin/dev' into dev 2023-04-19 08:49:47 -07:00
Mike Cao ac8d8bbd1e Added middleware for docker. 2023-04-19 08:49:16 -07:00
Ash Monsh ab48a0882a rotate the arrow icon for rtl language
rotate the arrow icon for rtl language
2023-04-19 07:31:25 +03:00
Ash Monsh aca612c833 Update ProfileButton.js
set alignment base on the page dir for User profile menu
2023-04-19 07:30:43 +03:00
Ash Monsh a52bfaa629 Update LanguageButton.js
set alignment base on the page dir for Language menu
2023-04-19 07:30:09 +03:00
Francis Cao 0889542f3b fix minimum password length message 2023-04-18 16:00:33 -07:00
Francis Cao 16ece9ad64 Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-18 12:45:08 -07:00
Francis Cao e8ca23c390 update mysql min versino to 5.7 2023-04-18 11:27:55 -07:00
Mike Cao 3eabe9b958 Merge branch 'dev' 2023-04-18 10:56:05 -07:00
Mike Cao 78030eb4a0 Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-18 10:55:55 -07:00
Mike Cao db31810e68 Updated messages. 2023-04-18 10:04:49 -07:00
Mike Cao 81bc5d730e
Merge pull request #1897 from atmonshi/update-lang
update missing translation for arabic lang
2023-04-18 09:53:26 -07:00
Mike Cao 64d2cf3805
Merge pull request #1904 from screendriver/noindex
Prevent indexing content by search engines
2023-04-18 09:51:46 -07:00
Mike Cao 3d64985f1a
Merge pull request #1910 from jonaskuske/patch-1
fix: use new script name in update-tracker.js
2023-04-18 09:51:15 -07:00
Mike Cao 03f7f910f2
Merge pull request #1911 from Maxime-J/localization
ensure some translations
2023-04-18 09:50:37 -07:00
Mike Cao ab0066517d
Merge pull request #1912 from Maxime-J/fr
update fr-FR
2023-04-18 09:49:28 -07:00
Francis Cao 939f4752bb Fix mysql prisma schema teamuser 2023-04-18 09:43:58 -07:00
Mike Cao 56ad1c1602
Merge pull request #1908 from dngferreira/master
fix version sql on check_db.js
2023-04-18 09:37:29 -07:00
Maxime-J c548ac9ca3 update fr-FR 2023-04-18 14:51:04 +00:00
Maxime-J b3c1eb1437 ensure some translations 2023-04-18 14:35:45 +00:00
Jonas e19d612db2
fix: use new script name in update-tracker.js 2023-04-18 16:29:30 +02:00
Duarte Ferreira 94df42a306 fix version sql on check_db.js 2023-04-18 15:17:34 +01:00
Christian Rackerseder 904f9408d8
Prevent indexing content by search engines 2023-04-18 14:05:43 +02:00
Ash Monsh 10a4921942 update missing translation for arabic lang 2023-04-18 10:27:17 +03:00
Sergio a7daae2d20 fix typo prisma mysql schema 2023-04-18 09:07:59 +02:00
Mike Cao bac8edd113 Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-17 11:13:08 -07:00
Mike Cao 54b018f7ba Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-13 23:37:50 -07:00
Mike Cao 5997efc276 Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-12 20:03:16 -07:00
Kai Steinke 0926a4c310
feat: add missing locales and sort alphabetically 2023-04-11 10:53:43 +02:00
Mike Cao da79e4c19e Merge branch 'dev' of https://github.com/umami-software/umami into dev 2023-04-10 07:29:15 -07:00
Francis Cao 6ee846d102
Merge pull request #1875 from umami-software/dev
add referrer_domain to getEvent
2023-04-07 14:40:16 -07:00
Francis Cao 0f6cfca522
Merge pull request #1874 from umami-software/dev
fix urlPath clickhouse
2023-04-07 14:27:04 -07:00
Mike Cao a33706585b Merge branch 'dev' into analytics 2023-04-07 11:49:31 -07:00
Mike Cao 459fbc03ce Merge branch 'dev' into analytics 2023-04-07 11:39:52 -07:00
Mike Cao b1fbb20967 Merge branch 'dev' into analytics 2023-04-06 20:41:24 -07:00
Mike Cao 0217f19d99 Merge branch 'dev' into analytics 2023-04-04 23:41:58 -07:00
Mike Cao dea4b4766c Merge branch 'dev' into analytics 2023-04-04 00:36:11 -07:00
Brian Cao c0e79f5e9d Merge branch 'dev' into analytics 2023-04-04 00:24:10 -07:00
Brian Cao 4c92da6fc6 Merge branch 'dev' into analytics 2023-04-03 23:22:25 -07:00
Brian Cao 329df22a5d Fix realtime. 2023-04-03 23:21:29 -07:00
Mike Cao b4f6dfedda Merge branch 'dev' into analytics 2023-04-03 20:32:12 -07:00
Mike Cao 005b14b629
Merge pull request #1864 from umami-software/dev
Dev into analytics
2023-04-03 19:40:20 -07:00
521 changed files with 7442 additions and 4851 deletions

View File

@ -4,3 +4,4 @@ Dockerfile
.gitignore .gitignore
.DS_Store .DS_Store
node_modules node_modules
.idea

View File

@ -46,6 +46,7 @@
"react/react-in-jsx-scope": "off", "react/react-in-jsx-scope": "off",
"react/prop-types": "off", "react/prop-types": "off",
"import/no-anonymous-default-export": "off", "import/no-anonymous-default-export": "off",
"import/no-named-as-default": "off",
"@next/next/no-img-element": "off", "@next/next/no-img-element": "off",
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",

32
.github/ISSUE_TEMPLATE/1.bug_report.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: "🐛 Bug Report"
description: Create a bug report for Umami.
body:
- type: textarea
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: dropdown
attributes:
label: Database
description: What database are you using?
options:
- PostgreSQL
- MySQL
- Umami Cloud
validations:
required: true
- type: textarea
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: input
attributes:
label: Which browser are you using? (if relevant)
description: 'For example: Chrome, Edge, Firefox, etc'
- type: input
attributes:
label: How are you deploying your application? (if relevant)
description: 'For example: Vercel, Railway, Docker, etc'

View File

@ -0,0 +1,10 @@
name: "✨ Feature Request"
description: Create a feature or enhancement request for Umami.
labels: ['enhancement']
body:
- type: textarea
attributes:
label: Describe the feature or enhancement
description: A clear and concise description of what the feature or enhancement is.
validations:
required: true

6
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,6 @@
blank_issues_enabled: false
contact_links:
- name: "🤔 Ask a question"
url: https://github.com/umami-software/umami/discussions
about: Ask questions and discuss with other community members.

View File

@ -18,7 +18,7 @@ jobs:
db-type: [postgresql, mysql] db-type: [postgresql, mysql]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: mr-smithers-excellent/docker-build-push@v6 - uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image for ${{ matrix.db-type }} name: Build & push Docker image for ${{ matrix.db-type }}

View File

@ -3,7 +3,6 @@ name: Create docker images
on: [create] on: [create]
jobs: jobs:
build: build:
name: Build, push, and deploy name: Build, push, and deploy
if: ${{ startsWith(github.ref, 'refs/tags/v') }} if: ${{ startsWith(github.ref, 'refs/tags/v') }}
@ -14,7 +13,7 @@ jobs:
db-type: [postgresql, mysql] db-type: [postgresql, mysql]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set env - name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV

1
.gitignore vendored
View File

@ -17,6 +17,7 @@ node_modules
/build /build
/public/script.js /public/script.js
/geo /geo
/dist
# misc # misc
.DS_Store .DS_Store

View File

@ -1 +1 @@
/public/ /public/script.js

View File

@ -12,6 +12,7 @@ RUN yarn install --frozen-lockfile
FROM node:18-alpine AS builder FROM node:18-alpine AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY docker/middleware.js .
COPY . . COPY . .
ARG DATABASE_TYPE ARG DATABASE_TYPE

View File

@ -1,6 +1,6 @@
# umami # umami
Umami is a simple, fast, privacy-focused alternative to Google Analytics. Umami MAMI is a simple, fast, privacy-focused alternative to Google Analytics.
## Getting started ## Getting started

View File

@ -1,7 +1,7 @@
import { Icon, Text, Flexbox } from 'react-basics'; import { Icon, Text, Flexbox } from 'react-basics';
import Logo from 'assets/logo.svg'; import Logo from 'assets/logo.svg';
function EmptyPlaceholder({ message, children }) { export function EmptyPlaceholder({ message, children }) {
return ( return (
<Flexbox direction="column" alignItems="center" justifyContent="center" gap={60} height={600}> <Flexbox direction="column" alignItems="center" justifyContent="center" gap={60} height={600}>
<Icon size="xl"> <Icon size="xl">

View File

@ -0,0 +1,33 @@
/* eslint-disable no-console */
import { ErrorBoundary as Boundary } from 'react-error-boundary';
import { Button } from 'react-basics';
import useMessages from 'hooks/useMessages';
import styles from './ErrorBoundry.module.css';
const logError = (error, info) => {
console.error(error, info.componentStack);
};
export function ErrorBoundary({ children }) {
const { formatMessage, messages } = useMessages();
const fallbackRender = ({ error, resetErrorBoundary }) => {
console.log({ error });
return (
<div className={styles.error} role="alert">
<h1>{formatMessage(messages.error)}</h1>
<h3>{error.message}</h3>
<pre>{error.stack}</pre>
<Button onClick={resetErrorBoundary}>OK</Button>
</div>
);
};
return (
<Boundary fallbackRender={fallbackRender} onError={logError}>
{children}
</Boundary>
);
}
export default ErrorBoundary;

View File

@ -0,0 +1,19 @@
.error {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
z-index: var(--z-index-overlay);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 600px;
gap: 20px;
}
.error button {
align-self: center;
}

View File

@ -2,7 +2,7 @@ import { Icon, Icons, Text } from 'react-basics';
import styles from './ErrorMessage.module.css'; import styles from './ErrorMessage.module.css';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function ErrorMessage() { export function ErrorMessage() {
const { formatMessage, messages } = useMessages(); const { formatMessage, messages } = useMessages();
return ( return (
@ -14,3 +14,5 @@ export default function ErrorMessage() {
</div> </div>
); );
} }
export default ErrorMessage;

View File

@ -5,7 +5,7 @@ function getHostName(url) {
return match && match.length > 1 ? match[1] : null; return match && match.length > 1 ? match[1] : null;
} }
function Favicon({ domain, ...props }) { export function Favicon({ domain, ...props }) {
const hostName = domain ? getHostName(domain) : null; const hostName = domain ? getHostName(domain) : null;
return hostName ? ( return hostName ? (

View File

@ -1,3 +1,3 @@
.favicon { .favicon {
margin-right: 8px; margin-inline-end: 8px;
} }

View File

@ -1,6 +1,6 @@
import { ButtonGroup, Button, Flexbox } from 'react-basics'; import { ButtonGroup, Button, Flexbox } from 'react-basics';
export default function FilterButtons({ items, selectedKey, onSelect }) { export function FilterButtons({ items, selectedKey, onSelect }) {
return ( return (
<Flexbox justifyContent="center"> <Flexbox justifyContent="center">
<ButtonGroup items={items} selectedKey={selectedKey} onSelect={onSelect}> <ButtonGroup items={items} selectedKey={selectedKey} onSelect={onSelect}>
@ -9,3 +9,5 @@ export default function FilterButtons({ items, selectedKey, onSelect }) {
</Flexbox> </Flexbox>
); );
} }
export default FilterButtons;

View File

@ -6,24 +6,23 @@ import usePageQuery from 'hooks/usePageQuery';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './FilterLink.module.css'; import styles from './FilterLink.module.css';
export default function FilterLink({ id, value, label, externalUrl }) { export function FilterLink({ id, value, label, externalUrl, children, className }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { resolveUrl, query } = usePageQuery(); const { resolveUrl, query } = usePageQuery();
const active = query[id] !== undefined; const active = query[id] !== undefined;
const selected = query[id] === value; const selected = query[id] === value;
return ( return (
<div className={styles.row}> <div
className={classNames(styles.row, className, {
[styles.inactive]: active && !selected,
[styles.active]: active && selected,
})}
>
{children}
{!value && `(${label || formatMessage(labels.unknown)})`} {!value && `(${label || formatMessage(labels.unknown)})`}
{value && ( {value && (
<Link <Link href={resolveUrl({ [id]: value })} className={styles.label} replace>
href={resolveUrl({ [id]: value })}
className={classNames(styles.label, {
[styles.inactive]: active && !selected,
[styles.active]: active && selected,
})}
replace
>
{safeDecodeURI(label || value)} {safeDecodeURI(label || value)}
</Link> </Link>
)} )}
@ -37,3 +36,5 @@ export default function FilterLink({ id, value, label, externalUrl }) {
</div> </div>
); );
} }
export default FilterLink;

View File

@ -1,20 +1,25 @@
.row { .row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px;
} }
.row .inactive { .row.inactive {
color: var(--base500); color: var(--base500);
} }
.row .active { .row.inactive img {
opacity: 0.35;
}
.row.active {
color: var(--base900); color: var(--base900);
font-weight: 600; font-weight: 600;
} }
.row .link { .row .link {
display: none; display: none;
margin-left: 20px; margin-inline-start: 20px;
} }
.row .label { .row .label {

View File

@ -5,7 +5,7 @@ import Icons from 'components/icons';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import useConfig from 'hooks/useConfig'; import useConfig from 'hooks/useConfig';
export default function HamburgerButton() { export function HamburgerButton() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const { cloudMode } = useConfig(); const { cloudMode } = useConfig();
@ -57,3 +57,5 @@ export default function HamburgerButton() {
</> </>
); );
} }
export default HamburgerButton;

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { Tooltip } from 'react-basics'; import { Tooltip } from 'react-basics';
import styles from './HoverTooltip.module.css'; import styles from './HoverTooltip.module.css';
export default function HoverTooltip({ tooltip }) { export function HoverTooltip({ tooltip }) {
const [position, setPosition] = useState({ x: -1000, y: -1000 }); const [position, setPosition] = useState({ x: -1000, y: -1000 });
useEffect(() => { useEffect(() => {
@ -23,3 +23,5 @@ export default function HoverTooltip({ tooltip }) {
</div> </div>
); );
} }
export default HoverTooltip;

View File

@ -3,7 +3,7 @@ import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import styles from './MobileMenu.module.css'; import styles from './MobileMenu.module.css';
export default function MobileMenu({ items = [], onClose }) { export function MobileMenu({ items = [], onClose }) {
const { pathname } = useRouter(); const { pathname } = useRouter();
const Items = ({ items, className }) => ( const Items = ({ items, className }) => (
@ -34,3 +34,5 @@ export default function MobileMenu({ items = [], onClose }) {
</div> </div>
); );
} }
export default MobileMenu;

View File

@ -2,7 +2,7 @@ import classNames from 'classnames';
import styles from './NoData.module.css'; import styles from './NoData.module.css';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
function NoData({ className }) { export function NoData({ className }) {
const { formatMessage, messages } = useMessages(); const { formatMessage, messages } = useMessages();
return ( return (

View File

@ -1,7 +1,7 @@
import { Table, TableHeader, TableBody, TableRow, TableCell, TableColumn } from 'react-basics'; import { Table, TableHeader, TableBody, TableRow, TableCell, TableColumn } from 'react-basics';
import styles from './SettingsTable.module.css'; import styles from './SettingsTable.module.css';
export default function SettingsTable({ columns = [], data = [], children, cellRender }) { export function SettingsTable({ columns = [], data = [], children, cellRender }) {
return ( return (
<Table columns={columns} rows={data}> <Table columns={columns} rows={data}>
<TableHeader className={styles.header}> <TableHeader className={styles.header}>
@ -34,3 +34,5 @@ export default function SettingsTable({ columns = [], data = [], children, cellR
</Table> </Table>
); );
} }
export default SettingsTable;

View File

@ -6,7 +6,7 @@ import { REPO_URL, VERSION_CHECK } from 'lib/constants';
import styles from './UpdateNotice.module.css'; import styles from './UpdateNotice.module.css';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function UpdateNotice() { export function UpdateNotice() {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { latest, checked, hasUpdate, releaseUrl } = useStore(); const { latest, checked, hasUpdate, releaseUrl } = useStore();
const [dismissed, setDismissed] = useState(false); const [dismissed, setDismissed] = useState(false);
@ -50,3 +50,5 @@ export default function UpdateNotice() {
</Row> </Row>
); );
} }
export default UpdateNotice;

View File

@ -3,16 +3,16 @@ import { useRouter } from 'next/router';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps'; import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import classNames from 'classnames'; import classNames from 'classnames';
import { colord } from 'colord'; import { colord } from 'colord';
import useTheme from 'hooks/useTheme'; import HoverTooltip from 'components/common/HoverTooltip';
import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants'; import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants';
import styles from './WorldMap.module.css'; import useTheme from 'hooks/useTheme';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import HoverTooltip from './HoverTooltip';
import { formatLongNumber } from 'lib/format'; import { formatLongNumber } from 'lib/format';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import styles from './WorldMap.module.css';
function WorldMap({ data, className }) { export function WorldMap({ data, className }) {
const { basePath } = useRouter(); const { basePath } = useRouter();
const [tooltip, setTooltip] = useState(); const [tooltip, setTooltip] = useState();
const [theme] = useTheme(); const [theme] = useTheme();

View File

@ -9,7 +9,7 @@ import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
function DateFilter({ websiteId, value, className }) { export function DateFilter({ websiteId, value, className }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { get } = useApi(); const { get } = useApi();
const [dateRange, setDateRange] = useDateRange(websiteId); const [dateRange, setDateRange] = useDateRange(websiteId);
@ -23,7 +23,7 @@ function DateFilter({ websiteId, value, className }) {
if (data) { if (data) {
setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }); setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) });
} }
} else { } else if (value !== 'all') {
setDateRange(value); setDateRange(value);
} }
} }
@ -61,7 +61,7 @@ function DateFilter({ websiteId, value, className }) {
value: '90day', value: '90day',
}, },
{ label: formatMessage(labels.thisYear), value: '1year' }, { label: formatMessage(labels.thisYear), value: '1year' },
{ websiteId && {
label: formatMessage(labels.allTime), label: formatMessage(labels.allTime),
value: 'all', value: 'all',
divider: true, divider: true,
@ -71,7 +71,7 @@ function DateFilter({ websiteId, value, className }) {
value: 'custom', value: 'custom',
divider: true, divider: true,
}, },
]; ].filter(n => n);
const renderValue = value => { const renderValue = value => {
return value === 'custom' ? ( return value === 'custom' ? (

View File

@ -5,8 +5,8 @@ import useLocale from 'hooks/useLocale';
import Icons from 'components/icons'; import Icons from 'components/icons';
import styles from './LanguageButton.module.css'; import styles from './LanguageButton.module.css';
export default function LanguageButton() { export function LanguageButton() {
const { locale, saveLocale } = useLocale(); const { locale, saveLocale, dir } = useLocale();
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key })); const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
function handleSelect(value) { function handleSelect(value) {
@ -20,7 +20,7 @@ export default function LanguageButton() {
<Icons.Globe /> <Icons.Globe />
</Icon> </Icon>
</Button> </Button>
<Popup position="bottom" alignment="end"> <Popup position="bottom" alignment={dir === 'rtl' ? 'start' : 'end'}>
<div className={styles.menu}> <div className={styles.menu}>
{items.map(({ value, label }) => { {items.map(({ value, label }) => {
return ( return (
@ -43,3 +43,5 @@ export default function LanguageButton() {
</PopupTrigger> </PopupTrigger>
); );
} }
export default LanguageButton;

View File

@ -2,7 +2,7 @@ import { Button, Icon, Icons, Tooltip } from 'react-basics';
import Link from 'next/link'; import Link from 'next/link';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function LogoutButton({ tooltipPosition = 'top' }) { export function LogoutButton({ tooltipPosition = 'top' }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
<Link href="/logout"> <Link href="/logout">
@ -16,3 +16,5 @@ export default function LogoutButton({ tooltipPosition = 'top' }) {
</Link> </Link>
); );
} }
export default LogoutButton;

View File

@ -5,12 +5,14 @@ import useMessages from 'hooks/useMessages';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import useConfig from 'hooks/useConfig'; import useConfig from 'hooks/useConfig';
import styles from './ProfileButton.module.css'; import styles from './ProfileButton.module.css';
import useLocale from 'hooks/useLocale';
export default function ProfileButton() { export function ProfileButton() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { user } = useUser(); const { user } = useUser();
const { cloudMode } = useConfig(); const { cloudMode } = useConfig();
const router = useRouter(); const router = useRouter();
const { dir } = useLocale();
const handleSelect = key => { const handleSelect = key => {
if (key === 'profile') { if (key === 'profile') {
@ -31,7 +33,7 @@ export default function ProfileButton() {
<Icons.ChevronDown /> <Icons.ChevronDown />
</Icon> </Icon>
</Button> </Button>
<Popup position="bottom" alignment="end"> <Popup position="bottom" alignment={dir === 'rtl' ? 'start' : 'end'}>
<Menu variant="popup" onSelect={handleSelect} className={styles.menu}> <Menu variant="popup" onSelect={handleSelect} className={styles.menu}>
<Item key="user" className={styles.item}> <Item key="user" className={styles.item}>
<Text>{user.username}</Text> <Text>{user.username}</Text>
@ -55,3 +57,5 @@ export default function ProfileButton() {
</PopupTrigger> </PopupTrigger>
); );
} }
export default ProfileButton;

View File

@ -4,7 +4,7 @@ import useDateRange from 'hooks/useDateRange';
import Icons from 'components/icons'; import Icons from 'components/icons';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
function RefreshButton({ websiteId, isLoading }) { export function RefreshButton({ websiteId, isLoading }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);

View File

@ -5,7 +5,7 @@ import Icons from 'components/icons';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './SettingsButton.module.css'; import styles from './SettingsButton.module.css';
export default function SettingsButton() { export function SettingsButton() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
@ -33,3 +33,5 @@ export default function SettingsButton() {
</PopupTrigger> </PopupTrigger>
); );
} }
export default SettingsButton;

View File

@ -4,7 +4,7 @@ import useTheme from 'hooks/useTheme';
import Icons from 'components/icons'; import Icons from 'components/icons';
import styles from './ThemeButton.module.css'; import styles from './ThemeButton.module.css';
export default function ThemeButton() { export function ThemeButton() {
const [theme, setTheme] = useTheme(); const [theme, setTheme] = useTheme();
const transitions = useTransition(theme, { const transitions = useTransition(theme, {
@ -34,3 +34,5 @@ export default function ThemeButton() {
</Button> </Button>
); );
} }
export default ThemeButton;

View File

@ -2,7 +2,7 @@ import { Dropdown, Item } from 'react-basics';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function WebsiteSelect({ websiteId, onSelect }) { export function WebsiteSelect({ websiteId, onSelect }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data } = useQuery(['websites:me'], () => get('/me/websites')); const { data } = useQuery(['websites:me'], () => get('/me/websites'));
@ -25,3 +25,5 @@ export default function WebsiteSelect({ websiteId, onSelect }) {
</Dropdown> </Dropdown>
); );
} }
export default WebsiteSelect;

View File

@ -5,9 +5,10 @@ import NavBar from 'components/layout/NavBar';
import UpdateNotice from 'components/common/UpdateNotice'; import UpdateNotice from 'components/common/UpdateNotice';
import useRequireLogin from 'hooks/useRequireLogin'; import useRequireLogin from 'hooks/useRequireLogin';
import useConfig from 'hooks/useConfig'; import useConfig from 'hooks/useConfig';
import { CURRENT_VERSION } from 'lib/constants';
import styles from './AppLayout.module.css'; import styles from './AppLayout.module.css';
export default function AppLayout({ title, children }) { export function AppLayout({ title, children }) {
const { user } = useRequireLogin(); const { user } = useRequireLogin();
const config = useConfig(); const config = useConfig();
const { pathname } = useRouter(); const { pathname } = useRouter();
@ -19,7 +20,7 @@ export default function AppLayout({ title, children }) {
const allowUpdate = user?.isAdmin && !config?.updatesDisabled && !pathname.includes('/share/'); const allowUpdate = user?.isAdmin && !config?.updatesDisabled && !pathname.includes('/share/');
return ( return (
<div className={styles.layout}> <div className={styles.layout} data-app-version={CURRENT_VERSION}>
{allowUpdate && <UpdateNotice />} {allowUpdate && <UpdateNotice />}
<Head> <Head>
<title>{title ? `${title} | umami` : 'umami'}</title> <title>{title ? `${title} | umami` : 'umami'}</title>
@ -33,3 +34,5 @@ export default function AppLayout({ title, children }) {
</div> </div>
); );
} }
export default AppLayout;

View File

@ -4,7 +4,7 @@ import { CURRENT_VERSION, HOMEPAGE_URL, REPO_URL } from 'lib/constants';
import { labels } from 'components/messages'; import { labels } from 'components/messages';
import styles from './Footer.module.css'; import styles from './Footer.module.css';
export default function Footer() { export function Footer() {
return ( return (
<footer className={styles.footer}> <footer className={styles.footer}>
<Row> <Row>
@ -29,3 +29,5 @@ export default function Footer() {
</footer> </footer>
); );
} }
export default Footer;

View File

@ -10,16 +10,16 @@
} }
.row > .col { .row > .col {
border-left: 1px solid var(--base300); border-inline-start: 1px solid var(--base300);
} }
.row > .col:first-child { .row > .col:first-child {
border-left: 0; border-inline-start: 0;
padding-left: 0; padding-inline-start: 0;
} }
.row > .col:last-child { .row > .col:last-child {
padding-right: 0; padding-inline-end: 0;
} }
@media only screen and (max-width: 992px) { @media only screen and (max-width: 992px) {
@ -29,7 +29,7 @@
.row > .col { .row > .col {
border-top: 1px solid var(--base300); border-top: 1px solid var(--base300);
border-left: 0; border-inline-end: 0;
padding: 20px 0; padding: 20px 0;
} }
} }

View File

@ -6,7 +6,7 @@ import SettingsButton from 'components/input/SettingsButton';
import Icons from 'components/icons'; import Icons from 'components/icons';
import styles from './Header.module.css'; import styles from './Header.module.css';
export default function Header() { export function Header() {
return ( return (
<header className={styles.header}> <header className={styles.header}>
<Row> <Row>
@ -27,3 +27,5 @@ export default function Header() {
</header> </header>
); );
} }
export default Header;

View File

@ -1,4 +1,3 @@
import { useState } from 'react';
import { Icon, Text, Row, Column } from 'react-basics'; import { Icon, Text, Row, Column } from 'react-basics';
import Link from 'next/link'; import Link from 'next/link';
import classNames from 'classnames'; import classNames from 'classnames';
@ -12,7 +11,7 @@ import useMessages from 'hooks/useMessages';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import HamburgerButton from '../common/HamburgerButton'; import HamburgerButton from '../common/HamburgerButton';
export default function NavBar() { export function NavBar() {
const { pathname } = useRouter(); const { pathname } = useRouter();
const { cloudMode } = useConfig(); const { cloudMode } = useConfig();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -61,3 +60,5 @@ export default function NavBar() {
</div> </div>
); );
} }
export default NavBar;

View File

@ -6,7 +6,7 @@ import Link from 'next/link';
import Icons from 'components/icons'; import Icons from 'components/icons';
import styles from './NavGroup.module.css'; import styles from './NavGroup.module.css';
export default function NavGroup({ export function NavGroup({
title, title,
items, items,
defaultExpanded = true, defaultExpanded = true,
@ -54,3 +54,5 @@ export default function NavGroup({
</div> </div>
); );
} }
export default NavGroup;

View File

@ -3,7 +3,7 @@ import { Banner, Loading } from 'react-basics';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './Page.module.css'; import styles from './Page.module.css';
export default function Page({ className, error, loading, children }) { export function Page({ className, error, loading, children }) {
const { formatMessage, messages } = useMessages(); const { formatMessage, messages } = useMessages();
if (error) { if (error) {
@ -16,3 +16,5 @@ export default function Page({ className, error, loading, children }) {
return <div className={classNames(styles.page, className)}>{children}</div>; return <div className={classNames(styles.page, className)}>{children}</div>;
} }
export default Page;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import styles from './PageHeader.module.css'; import styles from './PageHeader.module.css';
export default function PageHeader({ title, children }) { export function PageHeader({ title, children }) {
return ( return (
<div className={styles.header}> <div className={styles.header}>
<div className={styles.title}>{title}</div> <div className={styles.title}>{title}</div>
@ -9,3 +9,5 @@ export default function PageHeader({ title, children }) {
</div> </div>
); );
} }
export default PageHeader;

View File

@ -1,5 +1,4 @@
import { Row, Column } from 'react-basics'; import { Row, Column } from 'react-basics';
import classNames from 'classnames';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import SideNav from './SideNav'; import SideNav from './SideNav';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
@ -7,7 +6,7 @@ import useMessages from 'hooks/useMessages';
import useConfig from 'hooks/useConfig'; import useConfig from 'hooks/useConfig';
import styles from './SettingsLayout.module.css'; import styles from './SettingsLayout.module.css';
export default function SettingsLayout({ children }) { export function SettingsLayout({ children }) {
const { user } = useUser(); const { user } = useUser();
const { pathname } = useRouter(); const { pathname } = useRouter();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -35,3 +34,5 @@ export default function SettingsLayout({ children }) {
</Row> </Row>
); );
} }
export default SettingsLayout;

View File

@ -2,7 +2,7 @@ import { Container } from 'react-basics';
import Header from './Header'; import Header from './Header';
import Footer from './Footer'; import Footer from './Footer';
export default function ShareLayout({ children }) { export function ShareLayout({ children }) {
return ( return (
<Container> <Container>
<Header /> <Header />
@ -11,3 +11,5 @@ export default function ShareLayout({ children }) {
</Container> </Container>
); );
} }
export default ShareLayout;

View File

@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import styles from './SideNav.module.css'; import styles from './SideNav.module.css';
export default function SideNav({ selectedKey, items, shallow, onSelect = () => {} }) { export function SideNav({ selectedKey, items, shallow, onSelect = () => {} }) {
const { asPath } = useRouter(); const { asPath } = useRouter();
return ( return (
<Menu items={items} selectedKey={selectedKey} className={styles.menu} onSelect={onSelect}> <Menu items={items} selectedKey={selectedKey} className={styles.menu} onSelect={onSelect}>
@ -21,3 +21,5 @@ export default function SideNav({ selectedKey, items, shallow, onSelect = () =>
</Menu> </Menu>
); );
} }
export default SideNav;

View File

@ -62,6 +62,7 @@ export const labels = defineMessages({
queries: { id: 'label.queries', defaultMessage: 'Queries' }, queries: { id: 'label.queries', defaultMessage: 'Queries' },
teams: { id: 'label.teams', defaultMessage: 'Teams' }, teams: { id: 'label.teams', defaultMessage: 'Teams' },
analytics: { id: 'label.analytics', defaultMessage: 'Analytics' }, analytics: { id: 'label.analytics', defaultMessage: 'Analytics' },
login: { id: 'label.login', defaultMessage: 'Login' },
logout: { id: 'label.logout', defaultMessage: 'Logout' }, logout: { id: 'label.logout', defaultMessage: 'Logout' },
singleDay: { id: 'label.single-day', defaultMessage: 'Single day' }, singleDay: { id: 'label.single-day', defaultMessage: 'Single day' },
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' }, dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
@ -94,12 +95,12 @@ export const labels = defineMessages({
thisMonth: { id: 'label.this-month', defaultMessage: 'This month' }, thisMonth: { id: 'label.this-month', defaultMessage: 'This month' },
thisYear: { id: 'label.this-year', defaultMessage: 'This year' }, thisYear: { id: 'label.this-year', defaultMessage: 'This year' },
allTime: { id: 'label.all-time', defaultMessage: 'All time' }, allTime: { id: 'label.all-time', defaultMessage: 'All time' },
customRange: { id: 'label.custom-range', defaultMessage: 'Custom-range' }, customRange: { id: 'label.custom-range', defaultMessage: 'Custom range' },
selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' }, selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' },
all: { id: 'label.all', defaultMessage: 'All' }, all: { id: 'label.all', defaultMessage: 'All' },
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' }, sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' }, pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' },
logs: { id: 'label.activity-log', defaultMessage: 'Activity log' }, activityLog: { id: 'label.activity-log', defaultMessage: 'Activity log' },
dismiss: { id: 'label.dismiss', defaultMessage: 'Dismiss' }, dismiss: { id: 'label.dismiss', defaultMessage: 'Dismiss' },
poweredBy: { id: 'label.powered-by', defaultMessage: 'Powered by {name}' }, poweredBy: { id: 'label.powered-by', defaultMessage: 'Powered by {name}' },
pageViews: { id: 'label.page-views', defaultMessage: 'Page views' }, pageViews: { id: 'label.page-views', defaultMessage: 'Page views' },

View File

@ -4,7 +4,7 @@ import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './ActiveUsers.module.css'; import styles from './ActiveUsers.module.css';
export default function ActiveUsers({ websiteId, value, refetchInterval = 60000 }) { export function ActiveUsers({ websiteId, value, refetchInterval = 60000 }) {
const { formatMessage, messages } = useMessages(); const { formatMessage, messages } = useMessages();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data } = useQuery( const { data } = useQuery(
@ -34,3 +34,5 @@ export default function ActiveUsers({ websiteId, value, refetchInterval = 60000
</StatusLight> </StatusLight>
); );
} }
export default ActiveUsers;

View File

@ -1,5 +1,5 @@
import { useState, useRef, useEffect, useMemo, useCallback } from 'react'; import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { StatusLight } from 'react-basics'; import { StatusLight, Loading } from 'react-basics';
import classNames from 'classnames'; import classNames from 'classnames';
import Chart from 'chart.js/auto'; import Chart from 'chart.js/auto';
import HoverTooltip from 'components/common/HoverTooltip'; import HoverTooltip from 'components/common/HoverTooltip';
@ -11,7 +11,7 @@ import useTheme from 'hooks/useTheme';
import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants'; import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
import styles from './BarChart.module.css'; import styles from './BarChart.module.css';
export default function BarChart({ export function BarChart({
datasets, datasets,
unit, unit,
animationDuration = DEFAULT_ANIMATION_DURATION, animationDuration = DEFAULT_ANIMATION_DURATION,
@ -39,6 +39,26 @@ export default function BarChart({
return +label > 1000 ? formatLongNumber(label) : label; return +label > 1000 ? formatLongNumber(label) : label;
}; };
const renderXLabel = useCallback(
(label, index, values) => {
const d = new Date(values[index].value);
switch (unit) {
case 'minute':
return dateFormat(d, 'h:mm', locale);
case 'hour':
return dateFormat(d, 'p', locale);
case 'day':
return dateFormat(d, 'MMM d', locale);
case 'month':
return dateFormat(d, 'MMM', locale);
default:
return label;
}
},
[locale, unit],
);
const renderTooltip = useCallback( const renderTooltip = useCallback(
model => { model => {
const { opacity, labelColors, dataPoints } = model.tooltip; const { opacity, labelColors, dataPoints } = model.tooltip;
@ -115,6 +135,7 @@ export default function BarChart({
color: colors.text, color: colors.text,
autoSkip: false, autoSkip: false,
maxRotation: 0, maxRotation: 0,
callback: renderXLabel,
}, },
}, },
y: { y: {
@ -135,7 +156,7 @@ export default function BarChart({
}, },
}, },
}; };
}, [animationDuration, renderTooltip, stacked, colors, unit]); }, [animationDuration, renderTooltip, renderXLabel, stacked, colors, unit, locale]);
const createChart = () => { const createChart = () => {
Chart.defaults.font.family = 'Inter'; Chart.defaults.font.family = 'Inter';
@ -156,13 +177,16 @@ export default function BarChart({
const updateChart = () => { const updateChart = () => {
setTooltip(null); setTooltip(null);
datasets.forEach((dataset, index) => {
chart.current.data.datasets[index].data = dataset.data;
chart.current.data.datasets[index].label = dataset.label;
});
chart.current.options = getOptions(); chart.current.options = getOptions();
chart.current.data.datasets = datasets; onUpdate(chart.current);
chart.current.update(); chart.current.update();
onUpdate(chart.current);
}; };
useEffect(() => { useEffect(() => {
@ -173,11 +197,12 @@ export default function BarChart({
updateChart(); updateChart();
} }
} }
}, [datasets, unit, theme, animationDuration, locale, loading]); }, [datasets, unit, theme, animationDuration, locale]);
return ( return (
<> <>
<div className={classNames(styles.chart, className)}> <div className={classNames(styles.chart, className)}>
{loading && <Loading position="page" icon="dots" />}
<canvas ref={canvas} /> <canvas ref={canvas} />
</div> </div>
<Legend chart={chart.current} /> <Legend chart={chart.current} />
@ -185,3 +210,5 @@ export default function BarChart({
</> </>
); );
} }
export default BarChart;

View File

@ -3,7 +3,7 @@ import MetricsTable from 'components/metrics/MetricsTable';
import { BROWSERS } from 'lib/constants'; import { BROWSERS } from 'lib/constants';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function BrowsersTable({ websiteId, ...props }) { export function BrowsersTable({ websiteId, ...props }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
function renderLink({ x: browser }) { function renderLink({ x: browser }) {
@ -21,3 +21,5 @@ export default function BrowsersTable({ websiteId, ...props }) {
/> />
); );
} }
export default BrowsersTable;

View File

@ -4,7 +4,7 @@ import FilterLink from 'components/common/FilterLink';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function CitiesTable({ websiteId, ...props }) { export function CitiesTable({ websiteId, ...props }) {
const { locale } = useLocale(); const { locale } = useLocale();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -28,3 +28,5 @@ export default function CitiesTable({ websiteId, ...props }) {
/> />
); );
} }
export default CitiesTable;

View File

@ -4,16 +4,21 @@ import useCountryNames from 'hooks/useCountryNames';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function CountriesTable({ websiteId, ...props }) { export function CountriesTable({ websiteId, ...props }) {
const { locale } = useLocale(); const { locale } = useLocale();
const countryNames = useCountryNames(locale); const countryNames = useCountryNames(locale);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
function renderLink({ x: code }) { function renderLink({ x: code }) {
return ( return (
<div className={locale}> <FilterLink
<FilterLink id="country" value={countryNames[code] && code} label={countryNames[code]} /> id="country"
</div> className={locale}
value={countryNames[code] && code}
label={countryNames[code]}
>
<img src={`/images/flags/${code?.toLowerCase() || 'xx'}.png`} alt={code} />
</FilterLink>
); );
} }
@ -28,3 +33,5 @@ export default function CountriesTable({ websiteId, ...props }) {
/> />
); );
} }
export default CountriesTable;

View File

@ -3,13 +3,12 @@ import useMeasure from 'react-use-measure';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import { useSpring, animated, config } from 'react-spring'; import { useSpring, animated, config } from 'react-spring';
import classNames from 'classnames'; import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import NoData from 'components/common/NoData'; import NoData from 'components/common/NoData';
import { formatNumber, formatLongNumber } from 'lib/format'; import { formatNumber, formatLongNumber } from 'lib/format';
import styles from './DataTable.module.css'; import styles from './DataTable.module.css';
import useMessages from '../../hooks/useMessages'; import useMessages from '../../hooks/useMessages';
export default function DataTable({ export function DataTable({
data = [], data = [],
title, title,
metric, metric,
@ -102,3 +101,5 @@ const AnimatedRow = ({
</div> </div>
); );
}; };
export default DataTable;

View File

@ -68,8 +68,8 @@
.value { .value {
width: 50px; width: 50px;
text-align: right; text-align: end;
margin-right: 10px; margin-inline-end: 10px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
} }
@ -79,7 +79,7 @@
width: 50px; width: 50px;
color: var(--base600); color: var(--base600);
border-left: 1px solid var(--base600); border-left: 1px solid var(--base600);
padding-left: 10px; padding-inline-start: 10px;
z-index: 1; z-index: 1;
} }

View File

@ -8,7 +8,7 @@ import { FILTER_DAY, FILTER_RANGE } from 'lib/constants';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './DatePickerForm.module.css'; import styles from './DatePickerForm.module.css';
export default function DatePickerForm({ export function DatePickerForm({
startDate: defaultStartDate, startDate: defaultStartDate,
endDate: defaultEndDate, endDate: defaultEndDate,
minDate, minDate,
@ -78,3 +78,5 @@ export default function DatePickerForm({
</div> </div>
); );
} }
export default DatePickerForm;

View File

@ -2,7 +2,7 @@ import MetricsTable from './MetricsTable';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function DevicesTable({ websiteId, ...props }) { export function DevicesTable({ websiteId, ...props }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
function renderLink({ x: device }) { function renderLink({ x: device }) {
@ -26,3 +26,5 @@ export default function DevicesTable({ websiteId, ...props }) {
/> />
); );
} }
export default DevicesTable;

View File

@ -9,7 +9,7 @@ import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import { EVENT_COLORS } from 'lib/constants'; import { EVENT_COLORS } from 'lib/constants';
export default function EventsChart({ websiteId, className, token }) { export function EventsChart({ websiteId, className, token }) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId); const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId);
const [timezone] = useTimezone(); const [timezone] = useTimezone();
@ -76,3 +76,5 @@ export default function EventsChart({ websiteId, className, token }) {
/> />
); );
} }
export default EventsChart;

View File

@ -1,7 +1,7 @@
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function EventsTable({ websiteId, ...props }) { export function EventsTable({ websiteId, ...props }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
function handleDataLoad(data) { function handleDataLoad(data) {
@ -19,3 +19,5 @@ export default function EventsTable({ websiteId, ...props }) {
/> />
); );
} }
export default EventsTable;

View File

@ -4,7 +4,7 @@ import usePageQuery from 'hooks/usePageQuery';
import styles from './FilterTags.module.css'; import styles from './FilterTags.module.css';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function FilterTags({ params }) { export function FilterTags({ params }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { const {
router, router,
@ -50,3 +50,5 @@ export default function FilterTags({ params }) {
</div> </div>
); );
} }
export default FilterTags;

View File

@ -4,7 +4,7 @@ import useLanguageNames from 'hooks/useLanguageNames';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function LanguagesTable({ websiteId, onDataLoad, ...props }) { export function LanguagesTable({ websiteId, onDataLoad, ...props }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { locale } = useLocale(); const { locale } = useLocale();
const languageNames = useLanguageNames(locale); const languageNames = useLanguageNames(locale);
@ -25,3 +25,5 @@ export default function LanguagesTable({ websiteId, onDataLoad, ...props }) {
/> />
); );
} }
export default LanguagesTable;

View File

@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { StatusLight } from 'react-basics'; import { StatusLight } from 'react-basics';
import { colord } from 'colord'; import { colord } from 'colord';
import classNames from 'classnames'; import classNames from 'classnames';
@ -5,11 +6,11 @@ import useLocale from 'hooks/useLocale';
import useForceUpdate from 'hooks/useForceUpdate'; import useForceUpdate from 'hooks/useForceUpdate';
import styles from './Legend.module.css'; import styles from './Legend.module.css';
export default function Legend({ chart }) { export function Legend({ chart }) {
const { locale } = useLocale(); const { locale } = useLocale();
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
function handleClick(index) { const handleClick = index => {
const meta = chart.getDatasetMeta(index); const meta = chart.getDatasetMeta(index);
meta.hidden = meta.hidden === null ? !chart.data.datasets[index].hidden : null; meta.hidden = meta.hidden === null ? !chart.data.datasets[index].hidden : null;
@ -17,7 +18,11 @@ export default function Legend({ chart }) {
chart.update(); chart.update();
forceUpdate(); forceUpdate();
} };
useEffect(() => {
forceUpdate();
}, [locale]);
if (!chart?.legend?.legendItems.find(({ text }) => text)) { if (!chart?.legend?.legendItems.find(({ text }) => text)) {
return null; return null;
@ -43,3 +48,5 @@ export default function Legend({ chart }) {
</div> </div>
); );
} }
export default Legend;

View File

@ -13,7 +13,7 @@
} }
.label + .label { .label + .label {
margin-left: 20px; margin-inline-start: 20px;
} }
.hidden { .hidden {

View File

@ -3,7 +3,7 @@ import { useSpring, animated } from 'react-spring';
import { formatNumber } from 'lib/format'; import { formatNumber } from 'lib/format';
import styles from './MetricCard.module.css'; import styles from './MetricCard.module.css';
const MetricCard = ({ export const MetricCard = ({
value = 0, value = 0,
change = 0, change = 0,
label, label,

View File

@ -9,7 +9,7 @@ import MetricCard from './MetricCard';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './MetricsBar.module.css'; import styles from './MetricsBar.module.css';
export default function MetricsBar({ websiteId }) { export function MetricsBar({ websiteId }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);
@ -111,3 +111,5 @@ export default function MetricsBar({ websiteId }) {
</div> </div>
); );
} }
export default MetricsBar;

View File

@ -13,8 +13,9 @@ import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import Icons from 'components/icons'; import Icons from 'components/icons';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './MetricsTable.module.css'; import styles from './MetricsTable.module.css';
import useLocale from 'hooks/useLocale';
export default function MetricsTable({ export function MetricsTable({
websiteId, websiteId,
type, type,
className, className,
@ -69,6 +70,7 @@ export default function MetricsTable({
} }
return []; return [];
}, [data, error, dataFilter, filterOptions]); }, [data, error, dataFilter, filterOptions]);
const { dir } = useLocale();
return ( return (
<div className={classNames(styles.container, className)}> <div className={classNames(styles.container, className)}>
@ -80,7 +82,7 @@ export default function MetricsTable({
<Link href={router.pathname} as={resolveUrl({ view: type })}> <Link href={router.pathname} as={resolveUrl({ view: type })}>
<Button variant="quiet"> <Button variant="quiet">
<Text>{formatMessage(labels.more)}</Text> <Text>{formatMessage(labels.more)}</Text>
<Icon size="sm"> <Icon size="sm" rotate={dir === 'rtl' ? 180 : 0}>
<Icons.ArrowRight /> <Icons.ArrowRight />
</Icon> </Icon>
</Button> </Button>
@ -90,3 +92,5 @@ export default function MetricsTable({
</div> </div>
); );
} }
export default MetricsTable;

View File

@ -2,7 +2,7 @@ import MetricsTable from './MetricsTable';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function OSTable({ websiteId, ...props }) { export function OSTable({ websiteId, ...props }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
function renderLink({ x: os }) { function renderLink({ x: os }) {
@ -20,3 +20,5 @@ export default function OSTable({ websiteId, ...props }) {
/> />
); );
} }
export default OSTable;

View File

@ -5,7 +5,7 @@ import useMessages from 'hooks/useMessages';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import { emptyFilter } from 'lib/filters'; import { emptyFilter } from 'lib/filters';
export default function PagesTable({ websiteId, showFilters, ...props }) { export function PagesTable({ websiteId, showFilters, ...props }) {
const { const {
router, router,
resolveUrl, resolveUrl,
@ -47,3 +47,5 @@ export default function PagesTable({ websiteId, showFilters, ...props }) {
</> </>
); );
} }
export default PagesTable;

View File

@ -4,18 +4,12 @@ import BarChart from './BarChart';
import { THEME_COLORS } from 'lib/constants'; import { THEME_COLORS } from 'lib/constants';
import useTheme from 'hooks/useTheme'; import useTheme from 'hooks/useTheme';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import useLocale from 'hooks/useLocale';
export default function PageviewsChart({ export function PageviewsChart({ websiteId, data, unit, records, className, loading, ...props }) {
websiteId,
data,
unit,
records,
className,
loading,
...props
}) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const [theme] = useTheme(); const [theme] = useTheme();
const { locale } = useLocale();
const colors = useMemo(() => { const colors = useMemo(() => {
const primaryColor = colord(THEME_COLORS[theme].primary); const primaryColor = colord(THEME_COLORS[theme].primary);
@ -52,7 +46,7 @@ export default function PageviewsChart({
...colors.views, ...colors.views,
}, },
]; ];
}, [data]); }, [data, locale, colors]);
return ( return (
<BarChart <BarChart
@ -66,3 +60,5 @@ export default function PageviewsChart({
/> />
); );
} }
export default PageviewsChart;

View File

@ -12,7 +12,7 @@ const filters = {
[FILTER_COMBINED]: paramFilter, [FILTER_COMBINED]: paramFilter,
}; };
export default function QueryParametersTable({ websiteId, showFilters, ...props }) { export function QueryParametersTable({ websiteId, showFilters, ...props }) {
const [filter, setFilter] = useState(FILTER_COMBINED); const [filter, setFilter] = useState(FILTER_COMBINED);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -49,3 +49,5 @@ export default function QueryParametersTable({ websiteId, showFilters, ...props
</> </>
); );
} }
export default QueryParametersTable;

View File

@ -23,7 +23,7 @@ function mapData(data) {
return arr; return arr;
} }
export default function RealtimeChart({ data, unit, ...props }) { export function RealtimeChart({ data, unit, ...props }) {
const endDate = startOfMinute(new Date()); const endDate = startOfMinute(new Date());
const startDate = subMinutes(endDate, REALTIME_RANGE); const startDate = subMinutes(endDate, REALTIME_RANGE);
const prevEndDate = useRef(endDate); const prevEndDate = useRef(endDate);
@ -58,3 +58,5 @@ export default function RealtimeChart({ data, unit, ...props }) {
/> />
); );
} }
export default RealtimeChart;

View File

@ -2,7 +2,7 @@ import MetricsTable from './MetricsTable';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function ReferrersTable({ websiteId, ...props }) { export function ReferrersTable({ websiteId, ...props }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const renderLink = ({ x: referrer }) => { const renderLink = ({ x: referrer }) => {
@ -29,3 +29,5 @@ export default function ReferrersTable({ websiteId, ...props }) {
</> </>
); );
} }
export default ReferrersTable;

View File

@ -3,19 +3,25 @@ import { emptyFilter } from 'lib/filters';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import useCountryNames from 'hooks/useCountryNames';
import regions from 'public/iso-3166-2.json'; import regions from 'public/iso-3166-2.json';
export default function RegionsTable({ websiteId, ...props }) { export function RegionsTable({ websiteId, ...props }) {
const { locale } = useLocale(); const { locale } = useLocale();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const countryNames = useCountryNames(locale);
function renderLink({ x }) { const renderLabel = x => {
return regions[x] ? `${regions[x]}, ${countryNames[x.split('-')[0]]}` : x;
};
const renderLink = ({ x: code }) => {
return ( return (
<div className={locale}> <FilterLink id="region" className={locale} value={code} label={renderLabel(code)}>
<FilterLink id="region" value={x} label={regions[x] || x} /> <img src={`/images/flags/${code?.split('-')?.[0]?.toLowerCase() || 'xx'}.png`} alt={code} />
</div> </FilterLink>
); );
} };
return ( return (
<MetricsTable <MetricsTable
@ -29,3 +35,5 @@ export default function RegionsTable({ websiteId, ...props }) {
/> />
); );
} }
export default RegionsTable;

View File

@ -1,7 +1,7 @@
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function ScreenTable({ websiteId, ...props }) { export function ScreenTable({ websiteId, ...props }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
@ -14,3 +14,5 @@ export default function ScreenTable({ websiteId, ...props }) {
/> />
); );
} }
export default ScreenTable;

View File

@ -18,8 +18,9 @@ import Icons from 'components/icons';
import useSticky from 'hooks/useSticky'; import useSticky from 'hooks/useSticky';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './WebsiteChart.module.css'; import styles from './WebsiteChart.module.css';
import useLocale from 'hooks/useLocale';
export default function WebsiteChart({ export function WebsiteChart({
websiteId, websiteId,
name, name,
domain, domain,
@ -72,6 +73,7 @@ export default function WebsiteChart({
return { pageviews: [], sessions: [] }; return { pageviews: [], sessions: [] };
}, [data, modified]); }, [data, modified]);
const { dir } = useLocale();
return ( return (
<> <>
<WebsiteHeader websiteId={websiteId} name={name} domain={domain}> <WebsiteHeader websiteId={websiteId} name={name} domain={domain}>
@ -80,7 +82,9 @@ export default function WebsiteChart({
<Button variant="primary"> <Button variant="primary">
<Text>{formatMessage(labels.viewDetails)}</Text> <Text>{formatMessage(labels.viewDetails)}</Text>
<Icon> <Icon>
<Icons.ArrowRight /> <Icon rotate={dir === 'rtl' ? 180 : 0}>
<Icons.ArrowRight />
</Icon>
</Icon> </Icon>
</Button> </Button>
</Link> </Link>
@ -124,3 +128,5 @@ export default function WebsiteChart({
</> </>
); );
} }
export default WebsiteChart;

View File

@ -3,7 +3,7 @@ import Favicon from 'components/common/Favicon';
import ActiveUsers from './ActiveUsers'; import ActiveUsers from './ActiveUsers';
import styles from './WebsiteHeader.module.css'; import styles from './WebsiteHeader.module.css';
export default function WebsiteHeader({ websiteId, name, domain, children }) { export function WebsiteHeader({ websiteId, name, domain, children }) {
return ( return (
<Row className={styles.header} justifyContent="center"> <Row className={styles.header} justifyContent="center">
<Column className={styles.title} variant="two"> <Column className={styles.title} variant="two">
@ -17,3 +17,5 @@ export default function WebsiteHeader({ websiteId, name, domain, children }) {
</Row> </Row>
); );
} }
export default WebsiteHeader;

View File

@ -11,7 +11,7 @@ import Script from 'next/script';
import { Button, Column, Row } from 'react-basics'; import { Button, Column, Row } from 'react-basics';
import styles from './TestConsole.module.css'; import styles from './TestConsole.module.css';
export default function TestConsole() { export function TestConsole() {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['websites:me'], () => get('/me/websites')); const { data, isLoading, error } = useQuery(['websites:me'], () => get('/me/websites'));
const router = useRouter(); const router = useRouter();
@ -132,3 +132,5 @@ export default function TestConsole() {
</Page> </Page>
); );
} }
export default TestConsole;

View File

@ -10,8 +10,9 @@ import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useDashboard from 'store/dashboard'; import useDashboard from 'store/dashboard';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import useLocale from 'hooks/useLocale';
export default function Dashboard({ userId }) { export function Dashboard({ userId }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const dashboard = useDashboard(); const dashboard = useDashboard();
const { showCharts, limit, editing } = dashboard; const { showCharts, limit, editing } = dashboard;
@ -19,6 +20,7 @@ export default function Dashboard({ userId }) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['websites'], () => get('/websites', { userId })); const { data, isLoading, error } = useQuery(['websites'], () => get('/websites', { userId }));
const hasData = data && data.length !== 0; const hasData = data && data.length !== 0;
const { dir } = useLocale();
function handleMore() { function handleMore() {
setMax(max + limit); setMax(max + limit);
@ -33,7 +35,7 @@ export default function Dashboard({ userId }) {
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}> <EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
<Link href="/settings/websites"> <Link href="/settings/websites">
<Button> <Button>
<Icon> <Icon rotate={dir === 'rtl' ? 180 : 0}>
<Icons.ArrowRight /> <Icons.ArrowRight />
</Icon> </Icon>
<Text>{formatMessage(messages.goToSettings)}</Text> <Text>{formatMessage(messages.goToSettings)}</Text>
@ -48,7 +50,7 @@ export default function Dashboard({ userId }) {
{max < data.length && ( {max < data.length && (
<Flexbox justifyContent="center"> <Flexbox justifyContent="center">
<Button onClick={handleMore}> <Button onClick={handleMore}>
<Icon> <Icon rotate={dir === 'rtl' ? 180 : 0}>
<Icons.More /> <Icons.More />
</Icon> </Icon>
<Text>{formatMessage(labels.more)}</Text> <Text>{formatMessage(labels.more)}</Text>
@ -60,3 +62,5 @@ export default function Dashboard({ userId }) {
</Page> </Page>
); );
} }
export default Dashboard;

View File

@ -9,7 +9,7 @@ import styles from './DashboardEdit.module.css';
const dragId = 'dashboard-website-ordering'; const dragId = 'dashboard-website-ordering';
export default function DashboardEdit({ websites }) { export function DashboardEdit({ websites }) {
const settings = useDashboard(); const settings = useDashboard();
const { websiteOrder } = settings; const { websiteOrder } = settings;
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -98,3 +98,5 @@ export default function DashboardEdit({ websites }) {
</> </>
); );
} }
export default DashboardEdit;

View File

@ -3,7 +3,7 @@ import Icons from 'components/icons';
import { saveDashboard } from 'store/dashboard'; import { saveDashboard } from 'store/dashboard';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function DashboardSettingsButton() { export function DashboardSettingsButton() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const menuOptions = [ const menuOptions = [
@ -42,3 +42,5 @@ export default function DashboardSettingsButton() {
</PopupTrigger> </PopupTrigger>
); );
} }
export default DashboardSettingsButton;

View File

@ -17,7 +17,7 @@ import useMessages from 'hooks/useMessages';
import Logo from 'assets/logo.svg'; import Logo from 'assets/logo.svg';
import styles from './LoginForm.module.css'; import styles from './LoginForm.module.css';
export default function LoginForm() { export function LoginForm() {
const { formatMessage, labels, getMessage } = useMessages(); const { formatMessage, labels, getMessage } = useMessages();
const router = useRouter(); const router = useRouter();
const { post } = useApi(); const { post } = useApi();
@ -53,10 +53,12 @@ export default function LoginForm() {
</FormRow> </FormRow>
<FormButtons> <FormButtons>
<SubmitButton className={styles.button} variant="primary" disabled={isLoading}> <SubmitButton className={styles.button} variant="primary" disabled={isLoading}>
Log in {formatMessage(labels.login)}
</SubmitButton> </SubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
</div> </div>
); );
} }
export default LoginForm;

View File

@ -2,7 +2,7 @@ import Head from 'next/head';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import styles from './LoginLayout.module.css'; import styles from './LoginLayout.module.css';
export default function LoginLayout({ children }) { export function LoginLayout({ children }) {
const { dir } = useLocale(); const { dir } = useLocale();
return ( return (
@ -14,3 +14,5 @@ export default function LoginLayout({ children }) {
</div> </div>
); );
} }
export default LoginLayout;

View File

@ -4,7 +4,7 @@ import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function RealtimeCountries({ data }) { export function RealtimeCountries({ data }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { locale } = useLocale(); const { locale } = useLocale();
const countryNames = useCountryNames(locale); const countryNames = useCountryNames(locale);
@ -23,3 +23,5 @@ export default function RealtimeCountries({ data }) {
/> />
); );
} }
export default RealtimeCountries;

View File

@ -25,7 +25,7 @@ function mergeData(state = [], data = [], time) {
.filter(({ timestamp }) => timestamp >= time); .filter(({ timestamp }) => timestamp >= time);
} }
export default function RealtimeDashboard({ websiteId }) { export function RealtimeDashboard({ websiteId }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const router = useRouter(); const router = useRouter();
const [currentData, setCurrentData] = useState(); const [currentData, setCurrentData] = useState();
@ -63,7 +63,7 @@ export default function RealtimeDashboard({ websiteId }) {
currentData.countries = percentFilter( currentData.countries = percentFilter(
currentData.sessions currentData.sessions
.reduce((arr, data) => { .reduce((arr, data) => {
if (!arr.find(({ sessionId }) => sessionId === data.sessionId)) { if (!arr.find(({ id }) => id === data.id)) {
return arr.concat(data); return arr.concat(data);
} }
return arr; return arr;
@ -84,7 +84,7 @@ export default function RealtimeDashboard({ websiteId }) {
); );
currentData.visitors = currentData.sessions.reduce((arr, val) => { currentData.visitors = currentData.sessions.reduce((arr, val) => {
if (!arr.find(({ sessionId }) => sessionId === val.sessionId)) { if (!arr.find(({ id }) => id === val.id)) {
return arr.concat(val); return arr.concat(val);
} }
return arr; return arr;
@ -125,3 +125,5 @@ export default function RealtimeDashboard({ websiteId }) {
</Page> </Page>
); );
} }
export default RealtimeDashboard;

View File

@ -2,21 +2,33 @@ import MetricCard from 'components/metrics/MetricCard';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import styles from './RealtimeHeader.module.css'; import styles from './RealtimeHeader.module.css';
export default function RealtimeHeader({ data = {} }) { export function RealtimeHeader({ data = {} }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { pageviews, visitors, events, countries } = data; const { pageviews, visitors, events, countries } = data;
return ( return (
<div className={styles.header}> <div className={styles.header}>
<div className={styles.metrics}> <div className={styles.metrics}>
<MetricCard label={formatMessage(labels.views)} value={pageviews?.length} hideComparison />
<MetricCard <MetricCard
className={styles.card}
label={formatMessage(labels.views)}
value={pageviews?.length}
hideComparison
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.visitors)} label={formatMessage(labels.visitors)}
value={visitors?.length} value={visitors?.length}
hideComparison hideComparison
/> />
<MetricCard label={formatMessage(labels.events)} value={events?.length} hideComparison />
<MetricCard <MetricCard
className={styles.card}
label={formatMessage(labels.events)}
value={events?.length}
hideComparison
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.countries)} label={formatMessage(labels.countries)}
value={countries?.length} value={countries?.length}
hideComparison hideComparison
@ -25,3 +37,5 @@ export default function RealtimeHeader({ data = {} }) {
</div> </div>
); );
} }
export default RealtimeHeader;

View File

@ -7,4 +7,15 @@
.metrics { .metrics {
display: flex; display: flex;
flex-wrap: wrap;
}
.card {
justify-self: flex-start;
}
@media only screen and (max-width: 992px) {
.card {
flex-basis: calc(50% - 20px);
}
} }

View File

@ -6,7 +6,7 @@ import useApi from 'hooks/useApi';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function RealtimeHome() { export function RealtimeHome() {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const router = useRouter(); const router = useRouter();
@ -27,3 +27,5 @@ export default function RealtimeHome() {
</Page> </Page>
); );
} }
export default RealtimeHome;

View File

@ -25,7 +25,7 @@ const icons = {
[TYPE_EVENT]: <Icons.Bolt />, [TYPE_EVENT]: <Icons.Bolt />,
}; };
export default function RealtimeLog({ data, websiteDomain }) { export function RealtimeLog({ data, websiteDomain }) {
const { formatMessage, labels, messages, FormattedMessage } = useMessages(); const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { locale } = useLocale(); const { locale } = useLocale();
const countryNames = useCountryNames(locale); const countryNames = useCountryNames(locale);
@ -142,7 +142,7 @@ export default function RealtimeLog({ data, websiteDomain }) {
return ( return (
<div className={styles.table}> <div className={styles.table}>
<FilterButtons items={buttons} selectedKey={filter} onSelect={setFilter} /> <FilterButtons items={buttons} selectedKey={filter} onSelect={setFilter} />
<div className={styles.header}>{formatMessage(labels.logs)}</div> <div className={styles.header}>{formatMessage(labels.activityLog)}</div>
<div className={styles.body}> <div className={styles.body}>
{logs?.length === 0 && <NoData />} {logs?.length === 0 && <NoData />}
{logs?.length > 0 && ( {logs?.length > 0 && (
@ -154,3 +154,5 @@ export default function RealtimeLog({ data, websiteDomain }) {
</div> </div>
); );
} }
export default RealtimeLog;

View File

@ -9,7 +9,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
font-size: var(--font-size-md); font-size: var(--font-size-md);
line-height: 50px; line-height: 40px;
font-weight: 700; font-weight: 700;
} }
@ -42,6 +42,7 @@
.detail { .detail {
display: flex; display: flex;
align-items: center;
flex: 1; flex: 1;
gap: 10px; gap: 10px;
white-space: nowrap; white-space: nowrap;

View File

@ -6,7 +6,7 @@ import DataTable from 'components/metrics/DataTable';
import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants'; import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function RealtimeUrls({ websiteDomain, data = {} }) { export function RealtimeUrls({ websiteDomain, data = {} }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { pageviews } = data; const { pageviews } = data;
const [filter, setFilter] = useState(FILTER_REFERRERS); const [filter, setFilter] = useState(FILTER_REFERRERS);
@ -97,3 +97,5 @@ export default function RealtimeUrls({ websiteDomain, data = {} }) {
</> </>
); );
} }
export default RealtimeUrls;

View File

@ -4,7 +4,7 @@ import useDateRange from 'hooks/useDateRange';
import { DEFAULT_DATE_RANGE } from 'lib/constants'; import { DEFAULT_DATE_RANGE } from 'lib/constants';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function DateRangeSetting() { export function DateRangeSetting() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const [dateRange, setDateRange] = useDateRange(); const [dateRange, setDateRange] = useDateRange();
const { startDate, endDate, value } = dateRange; const { startDate, endDate, value } = dateRange;
@ -18,3 +18,5 @@ export default function DateRangeSetting() {
</Flexbox> </Flexbox>
); );
} }
export default DateRangeSetting;

View File

@ -4,7 +4,7 @@ import { DEFAULT_LOCALE } from 'lib/constants';
import { languages } from 'lib/lang'; import { languages } from 'lib/lang';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function LanguageSetting() { export function LanguageSetting() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { locale, saveLocale } = useLocale(); const { locale, saveLocale } = useLocale();
const options = Object.keys(languages); const options = Object.keys(languages);
@ -28,3 +28,5 @@ export default function LanguageSetting() {
</Flexbox> </Flexbox>
); );
} }
export default LanguageSetting;

View File

@ -3,7 +3,7 @@ import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm
import Icons from 'components/icons'; import Icons from 'components/icons';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function PasswordChangeButton() { export function PasswordChangeButton() {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { toast, showToast } = useToast(); const { toast, showToast } = useToast();
@ -28,3 +28,5 @@ export default function PasswordChangeButton() {
</> </>
); );
} }
export default PasswordChangeButton;

View File

@ -3,7 +3,7 @@ import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 're
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function PasswordEditForm({ onSave, onClose }) { export function PasswordEditForm({ onSave, onClose }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post('/me/password', data)); const { mutate, error, isLoading } = useMutation(data => post('/me/password', data));
@ -37,7 +37,7 @@ export default function PasswordEditForm({ onSave, onClose }) {
name="newPassword" name="newPassword"
rules={{ rules={{
required: 'Required', required: 'Required',
minLength: { value: 8, message: formatMessage(messages.minPasswordLength) }, minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
}} }}
> >
<PasswordField autoComplete="new-password" /> <PasswordField autoComplete="new-password" />
@ -48,7 +48,7 @@ export default function PasswordEditForm({ onSave, onClose }) {
name="confirmPassword" name="confirmPassword"
rules={{ rules={{
required: formatMessage(labels.required), required: formatMessage(labels.required),
minLength: { value: 8, message: formatMessage(messages.minPasswordLength) }, minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
validate: samePassword, validate: samePassword,
}} }}
> >
@ -64,3 +64,5 @@ export default function PasswordEditForm({ onSave, onClose }) {
</Form> </Form>
); );
} }
export default PasswordEditForm;

View File

@ -8,7 +8,7 @@ import useUser from 'hooks/useUser';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import useConfig from 'hooks/useConfig'; import useConfig from 'hooks/useConfig';
export default function ProfileDetails() { export function ProfileDetails() {
const { user } = useUser(); const { user } = useUser();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { cloudMode } = useConfig(); const { cloudMode } = useConfig();
@ -45,3 +45,5 @@ export default function ProfileDetails() {
</Form> </Form>
); );
} }
export default ProfileDetails;

View File

@ -3,7 +3,7 @@ import PageHeader from 'components/layout/PageHeader';
import ProfileDetails from './ProfileDetails'; import ProfileDetails from './ProfileDetails';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function ProfileSettings() { export function ProfileSettings() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
@ -13,3 +13,5 @@ export default function ProfileSettings() {
</Page> </Page>
); );
} }
export default ProfileSettings;

View File

@ -5,7 +5,7 @@ import Sun from 'assets/sun.svg';
import Moon from 'assets/moon.svg'; import Moon from 'assets/moon.svg';
import styles from './ThemeSetting.module.css'; import styles from './ThemeSetting.module.css';
export default function ThemeSetting() { export function ThemeSetting() {
const [theme, setTheme] = useTheme(); const [theme, setTheme] = useTheme();
return ( return (
@ -29,3 +29,5 @@ export default function ThemeSetting() {
</div> </div>
); );
} }
export default ThemeSetting;

View File

@ -4,7 +4,7 @@ import useTimezone from 'hooks/useTimezone';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
import { getTimezone } from 'lib/date'; import { getTimezone } from 'lib/date';
export default function TimezoneSetting() { export function TimezoneSetting() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const [timezone, saveTimezone] = useTimezone(); const [timezone, saveTimezone] = useTimezone();
const options = listTimeZones(); const options = listTimeZones();
@ -26,3 +26,5 @@ export default function TimezoneSetting() {
</Flexbox> </Flexbox>
); );
} }
export default TimezoneSetting;

View File

@ -11,7 +11,7 @@ import {
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function TeamAddForm({ onSave, onClose }) { export function TeamAddForm({ onSave, onClose }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => post('/teams', data)); const { mutate, error, isLoading } = useMutation(data => post('/teams', data));
@ -44,3 +44,5 @@ export default function TeamAddForm({ onSave, onClose }) {
</Form> </Form>
); );
} }
export default TeamAddForm;

View File

@ -4,7 +4,7 @@ import { Button, Dropdown, Form, FormButtons, FormRow, Item, SubmitButton } from
import WebsiteTags from './WebsiteTags'; import WebsiteTags from './WebsiteTags';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function TeamAddWebsiteForm({ teamId, onSave, onClose }) { export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { get, post, useQuery, useMutation } = useApi(); const { get, post, useQuery, useMutation } = useApi();
const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data)); const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data));
@ -44,7 +44,7 @@ export default function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
<> <>
<Form onSubmit={handleSubmit} error={error} ref={formRef}> <Form onSubmit={handleSubmit} error={error} ref={formRef}>
<FormRow label={formatMessage(labels.websites)}> <FormRow label={formatMessage(labels.websites)}>
<Dropdown items={websites} onChange={handleAddWebsite}> <Dropdown items={websites} onChange={handleAddWebsite} style={{ width: 300 }}>
{({ id, name }) => <Item key={id}>{name}</Item>} {({ id, name }) => <Item key={id}>{name}</Item>}
</Dropdown> </Dropdown>
</FormRow> </FormRow>
@ -59,3 +59,5 @@ export default function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
</> </>
); );
} }
export default TeamAddWebsiteForm;

View File

@ -2,7 +2,7 @@ import { Button, Form, FormButtons, SubmitButton } from 'react-basics';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages'; import useMessages from 'hooks/useMessages';
export default function TeamDeleteForm({ teamId, teamName, onSave, onClose }) { export function TeamDeleteForm({ teamId, teamName, onSave, onClose }) {
const { formatMessage, labels, messages, FormattedMessage } = useMessages(); const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, error, isLoading } = useMutation(data => del(`/teams/${teamId}`, data)); const { mutate, error, isLoading } = useMutation(data => del(`/teams/${teamId}`, data));
@ -30,3 +30,5 @@ export default function TeamDeleteForm({ teamId, teamName, onSave, onClose }) {
</Form> </Form>
); );
} }
export default TeamDeleteForm;

Some files were not shown because too many files have changed in this diff Show More