From 4047a7ec23222ad6d62a71a6dabe1e5c1d1fd56f Mon Sep 17 00:00:00 2001 From: WaylonWalker Date: Wed, 8 Dec 2021 18:25:42 +0000 Subject: [PATCH] Initial commit Created from https://vercel.com/new --- .dockerignore | 6 + .eslintrc.json | 24 + .github/stale.yml | 19 + .gitignore | 39 + .husky/pre-commit | 4 + .prettierignore | 1 + .prettierrc.json | 7 + .stylelintrc.json | 17 + Dockerfile | 43 + LICENSE | 21 + Procfile | 1 + README.md | 107 + app.json | 26 + assets/arrow-right.svg | 1 + assets/arrow-up-right-from-square.svg | 1 + assets/bars.svg | 1 + assets/bolt.svg | 1 + assets/calendar-alt.svg | 1 + assets/chart-bar.svg | 1 + assets/check.svg | 1 + assets/chevron-down.svg | 1 + assets/code.svg | 1 + assets/ellipsis-h.svg | 1 + assets/exclamation-triangle.svg | 1 + assets/external-link.svg | 1 + assets/eye.svg | 1 + assets/globe.svg | 1 + assets/link.svg | 1 + assets/list-ul.svg | 1 + assets/logo.svg | 2 + assets/moon.svg | 1 + assets/pen.svg | 1 + assets/plus.svg | 1 + assets/redo.svg | 1 + assets/sun.svg | 1 + assets/times.svg | 1 + assets/trash.svg | 1 + assets/user.svg | 1 + assets/visitor.svg | 1 + assets/xmark.svg | 1 + components/common/Button.js | 62 + components/common/Button.module.css | 102 + components/common/ButtonGroup.js | 42 + components/common/ButtonGroup.module.css | 31 + components/common/Calendar.js | 268 + components/common/Calendar.module.css | 111 + components/common/Checkbox.js | 39 + components/common/Checkbox.module.css | 30 + components/common/CopyButton.js | 37 + components/common/DateFilter.js | 130 + components/common/Dot.js | 26 + components/common/Dot.module.css | 22 + components/common/DropDown.js | 64 + components/common/Dropdown.module.css | 28 + components/common/EmptyPlaceholder.js | 22 + components/common/EmptyPlaceholder.module.css | 15 + components/common/ErrorMessage.js | 14 + components/common/ErrorMessage.module.css | 15 + components/common/Favicon.js | 28 + components/common/Favicon.module.css | 3 + components/common/FilterButtons.js | 25 + components/common/Icon.js | 29 + components/common/Icon.module.css | 35 + components/common/Link.js | 34 + components/common/Link.module.css | 50 + components/common/Loading.js | 20 + components/common/Loading.module.css | 43 + components/common/Menu.js | 70 + components/common/Menu.module.css | 51 + components/common/MenuButton.js | 86 + components/common/MenuButton.module.css | 20 + components/common/Modal.js | 26 + components/common/Modal.module.css | 46 + components/common/NavMenu.js | 47 + components/common/NavMenu.module.css | 22 + components/common/NoData.js | 19 + components/common/NoData.module.css | 11 + components/common/RefreshButton.js | 46 + components/common/Table.js | 89 + components/common/Table.module.css | 30 + components/common/Tag.js | 15 + components/common/Tag.module.css | 6 + components/common/Toast.js | 35 + components/common/Toast.module.css | 25 + components/common/UpdateNotice.js | 47 + components/common/UpdateNotice.module.css | 13 + components/common/WorldMap.js | 103 + components/common/WorldMap.module.css | 4 + components/forms/AccountEditForm.js | 88 + components/forms/ChangePasswordForm.js | 105 + components/forms/DatePickerForm.js | 83 + components/forms/DatePickerForm.module.css | 40 + components/forms/DeleteForm.js | 98 + components/forms/LoginForm.js | 102 + components/forms/LoginForm.module.css | 23 + components/forms/ResetForm.js | 98 + components/forms/ShareUrlForm.js | 42 + components/forms/TrackingCodeForm.js | 39 + components/forms/WebsiteEditForm.js | 109 + components/helpers/CheckVisible.js | 41 + components/helpers/StickyHeader.js | 49 + components/layout/ButtonLayout.js | 17 + components/layout/ButtonLayout.module.css | 20 + components/layout/Footer.js | 36 + components/layout/Footer.module.css | 18 + components/layout/FormLayout.js | 32 + components/layout/FormLayout.module.css | 94 + components/layout/GridLayout.js | 31 + components/layout/GridLayout.module.css | 40 + components/layout/Header.js | 71 + components/layout/Header.module.css | 116 + components/layout/Layout.js | 24 + components/layout/Layout.module.css | 6 + components/layout/MenuLayout.js | 39 + components/layout/MenuLayout.module.css | 31 + components/layout/Page.js | 26 + components/layout/Page.module.css | 7 + components/layout/PageHeader.js | 7 + components/layout/PageHeader.module.css | 9 + components/messages.js | 17 + components/metrics/ActiveUsers.js | 38 + components/metrics/ActiveUsers.module.css | 14 + components/metrics/BarChart.js | 226 + components/metrics/BarChart.module.css | 3 + components/metrics/BrowsersTable.js | 17 + components/metrics/ChartTooltip.js | 26 + components/metrics/ChartTooltip.module.css | 43 + components/metrics/CountriesTable.js | 31 + components/metrics/DataTable.js | 96 + components/metrics/DataTable.module.css | 97 + components/metrics/DevicesTable.js | 17 + components/metrics/EventsChart.js | 89 + components/metrics/EventsChart.module.css | 3 + components/metrics/EventsTable.js | 55 + components/metrics/EventsTable.module.css | 6 + components/metrics/FilterTags.js | 27 + components/metrics/FilterTags.module.css | 14 + components/metrics/Legend.js | 40 + components/metrics/Legend.module.css | 21 + components/metrics/MetricCard.js | 42 + components/metrics/MetricCard.module.css | 39 + components/metrics/MetricsBar.js | 110 + components/metrics/MetricsBar.module.css | 16 + components/metrics/MetricsTable.js | 84 + components/metrics/MetricsTable.module.css | 19 + components/metrics/OSTable.js | 15 + components/metrics/PagesTable.js | 60 + components/metrics/PagesTable.module.css | 8 + components/metrics/PageviewsChart.js | 94 + components/metrics/RealtimeChart.js | 60 + components/metrics/RealtimeHeader.js | 49 + components/metrics/RealtimeHeader.module.css | 4 + components/metrics/RealtimeLog.js | 192 + components/metrics/RealtimeLog.module.css | 59 + components/metrics/RealtimeViews.js | 113 + components/metrics/ReferrersTable.js | 77 + components/metrics/ReferrersTable.module.css | 31 + components/metrics/WebsiteChart.js | 108 + components/metrics/WebsiteChart.module.css | 49 + components/metrics/WebsiteHeader.js | 48 + components/metrics/WebsiteHeader.module.css | 15 + components/pages/RealtimeDashboard.js | 158 + components/pages/RealtimeDashboard.module.css | 7 + components/pages/Settings.js | 46 + components/pages/TestConsole.js | 101 + components/pages/TestConsole.module.css | 5 + components/pages/WebsiteDetails.js | 182 + components/pages/WebsiteDetails.module.css | 35 + components/pages/WebsiteList.js | 62 + components/pages/WebsiteList.module.css | 18 + components/settings/AccountSettings.js | 132 + .../settings/AccountSettings.module.css | 5 + components/settings/DateRangeSetting.js | 28 + .../settings/DateRangeSetting.module.css | 3 + components/settings/LanguageButton.js | 26 + components/settings/LanguageButton.module.css | 25 + components/settings/ProfileSettings.js | 72 + .../settings/ProfileSettings.module.css | 3 + components/settings/ThemeButton.js | 43 + components/settings/ThemeButton.module.css | 13 + components/settings/TimezoneSetting.js | 31 + .../settings/TimezoneSetting.module.css | 8 + components/settings/UserButton.js | 47 + components/settings/UserButton.module.css | 7 + components/settings/WebsiteSettings.js | 186 + .../settings/WebsiteSettings.module.css | 7 + docker-compose.yml | 26 + hooks/useCountryNames.js | 34 + hooks/useDateRange.js | 43 + hooks/useDelete.js | 11 + hooks/useDocumentClick.js | 13 + hooks/useEscapeKey.js | 19 + hooks/useFetch.js | 62 + hooks/useForceSSL.js | 14 + hooks/useForceUpdate.js | 9 + hooks/useLocale.js | 52 + hooks/usePageQuery.js | 34 + hooks/usePost.js | 11 + hooks/useRequireLogin.js | 38 + hooks/useShareToken.js | 27 + hooks/useTheme.js | 27 + hooks/useTimezone.js | 18 + hooks/useVersion.js | 23 + jsconfig.json | 5 + lang-ignore.json | 22 + lang/ar-SA.json | 105 + lang/ca-ES.json | 105 + lang/cs-CZ.json | 105 + lang/da-DK.json | 105 + lang/de-DE.json | 105 + lang/el-GR.json | 105 + lang/en-GB.json | 105 + lang/en-US.json | 105 + lang/es-MX.json | 105 + lang/fa-IR.json | 105 + lang/fi-FI.json | 105 + lang/fo-FO.json | 105 + lang/fr-FR.json | 105 + lang/he-IL.json | 105 + lang/hi-IN.json | 105 + lang/hu-HU.json | 105 + lang/id-ID.json | 105 + lang/it-IT.json | 105 + lang/ja-JP.json | 105 + lang/ko-KR.json | 105 + lang/mn-MN.json | 105 + lang/ms-MY.json | 105 + lang/nb-NO.json | 105 + lang/nl-NL.json | 105 + lang/pl-PL.json | 105 + lang/pt-BR.json | 105 + lang/pt-PT.json | 105 + lang/ro-RO.json | 105 + lang/ru-RU.json | 105 + lang/sk-SK.json | 105 + lang/sl-SI.json | 105 + lang/sv-SE.json | 105 + lang/ta-IN.json | 105 + lang/tr-TR.json | 105 + lang/uk-UA.json | 105 + lang/vi-VN.json | 105 + lang/zh-CN.json | 105 + lang/zh-TW.json | 105 + lib/array.js | 11 + lib/auth.js | 50 + lib/constants.js | 397 + lib/crypto.js | 74 + lib/date.js | 168 + lib/db.js | 33 + lib/filters.js | 130 + lib/format.js | 80 + lib/lang.js | 88 + lib/middleware.js | 47 + lib/queries.js | 542 ++ lib/request.js | 88 + lib/response.js | 33 + lib/session.js | 58 + lib/url.js | 43 + lib/web.js | 66 + next.config.js | 35 + package.json | 135 + pages/404.js | 15 + pages/_app.js | 51 + pages/api/account/[id].js | 29 + pages/api/account/index.js | 61 + pages/api/account/password.js | 32 + pages/api/accounts.js | 21 + pages/api/auth/login.js | 32 + pages/api/auth/logout.js | 15 + pages/api/auth/verify.js | 12 + pages/api/collect.js | 60 + pages/api/realtime/init.js | 26 + pages/api/realtime/update.js | 27 + pages/api/share/[id].js | 22 + pages/api/website/[id]/active.js | 21 + pages/api/website/[id]/events.js | 33 + pages/api/website/[id]/index.js | 31 + pages/api/website/[id]/metrics.js | 74 + pages/api/website/[id]/pageviews.js | 36 + pages/api/website/[id]/reset.js | 20 + pages/api/website/[id]/stats.js | 36 + pages/api/website/index.js | 43 + pages/api/websites.js | 23 + pages/dashboard/[[...id]].js | 22 + pages/index.js | 12 + pages/login.js | 11 + pages/logout.js | 18 + pages/realtime.js | 18 + pages/settings/accounts.js | 3 + pages/settings/index.js | 18 + pages/settings/profile.js | 3 + pages/share/[...id].js | 24 + pages/test.js | 18 + pages/website/[...id].js | 23 + postcss.config.js | 18 + .../20210320112658_init/migration.sql | 99 + prisma/mysql/migrations/migration_lock.toml | 3 + prisma/mysql/schema.mysql.prisma | 1 + prisma/mysql/seed.js | 1 + .../20210320112717_init/migration.sql | 129 + .../postgresql/migrations/migration_lock.toml | 3 + prisma/postgresql/schema.postgresql.prisma | 1 + prisma/postgresql/seed.js | 1 + prisma/schema.mysql.prisma | 87 + prisma/schema.postgresql.prisma | 87 + prisma/seed.js | 30 + public/android-chrome-192x192.png | Bin 0 -> 7895 bytes public/android-chrome-512x512.png | Bin 0 -> 22690 bytes public/apple-touch-icon.png | Bin 0 -> 2075 bytes public/browserconfig.xml | 9 + public/country/ar-SA.json | 1 + public/country/ca-ES.json | 1 + public/country/cs-CZ.json | 1 + public/country/da-DK.json | 1 + public/country/de-DE.json | 1 + public/country/el-GR.json | 1 + public/country/en-GB.json | 1 + public/country/en-US.json | 1 + public/country/es-MX.json | 1 + public/country/fa-IR.json | 1 + public/country/fi-FI.json | 1 + public/country/fo-FO.json | 1 + public/country/fr-FR.json | 1 + public/country/he-IL.json | 1 + public/country/hi-IN.json | 1 + public/country/hu-HU.json | 1 + public/country/id-ID.json | 1 + public/country/it-IT.json | 1 + public/country/ja-JP.json | 1 + public/country/ko-KR.json | 1 + public/country/mn-MN.json | 1 + public/country/ms-MY.json | 1 + public/country/nb-NO.json | 1 + public/country/nl-NL.json | 1 + public/country/pl-PL.json | 1 + public/country/pt-BR.json | 1 + public/country/pt-PT.json | 1 + public/country/ro-RO.json | 1 + public/country/ru-RU.json | 1 + public/country/sk-SK.json | 1 + public/country/sl-SI.json | 1 + public/country/sv-SE.json | 1 + public/country/ta-IN.json | 1 + public/country/tr-TR.json | 1 + public/country/uk-UA.json | 1 + public/country/vi-VN.json | 1 + public/country/zh-CN.json | 1 + public/country/zh-TW.json | 1 + public/datamaps.world.json | 180 + public/favicon-16x16.png | Bin 0 -> 597 bytes public/favicon-32x32.png | Bin 0 -> 888 bytes public/favicon.ico | Bin 0 -> 15086 bytes public/mstile-150x150.png | Bin 0 -> 3003 bytes public/safari-pinned-tab.svg | 75 + public/site.webmanifest | 19 + redux/actions/app.js | 87 + redux/actions/queries.js | 17 + redux/actions/user.js | 16 + redux/actions/websites.js | 29 + redux/reducers.js | 7 + redux/store.js | 40 + rollup.tracker.config.js | 13 + scripts/build-geo.js | 49 + scripts/change-password.js | 98 + scripts/check-lang.js | 38 + scripts/copy-db-schema.js | 30 + scripts/download-country-names.js | 39 + scripts/format-lang.js | 35 + scripts/loadtest.js | 142 + scripts/merge-lang.js | 30 + scripts/start-env.js | 3 + sql/schema.mysql.sql | 80 + sql/schema.postgresql.sql | 74 + styles/bootstrap-grid.css | 3981 ++++++++ styles/index.css | 165 + styles/variables.css | 68 + tracker/index.js | 185 + yarn.lock | 8439 +++++++++++++++++ 378 files changed, 29334 insertions(+) create mode 100644 .dockerignore create mode 100644 .eslintrc.json create mode 100644 .github/stale.yml create mode 100644 .gitignore create mode 100755 .husky/pre-commit create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 .stylelintrc.json create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Procfile create mode 100644 README.md create mode 100644 app.json create mode 100644 assets/arrow-right.svg create mode 100644 assets/arrow-up-right-from-square.svg create mode 100644 assets/bars.svg create mode 100644 assets/bolt.svg create mode 100644 assets/calendar-alt.svg create mode 100644 assets/chart-bar.svg create mode 100644 assets/check.svg create mode 100644 assets/chevron-down.svg create mode 100644 assets/code.svg create mode 100644 assets/ellipsis-h.svg create mode 100644 assets/exclamation-triangle.svg create mode 100644 assets/external-link.svg create mode 100644 assets/eye.svg create mode 100644 assets/globe.svg create mode 100644 assets/link.svg create mode 100644 assets/list-ul.svg create mode 100644 assets/logo.svg create mode 100644 assets/moon.svg create mode 100644 assets/pen.svg create mode 100644 assets/plus.svg create mode 100644 assets/redo.svg create mode 100644 assets/sun.svg create mode 100644 assets/times.svg create mode 100644 assets/trash.svg create mode 100644 assets/user.svg create mode 100644 assets/visitor.svg create mode 100644 assets/xmark.svg create mode 100644 components/common/Button.js create mode 100644 components/common/Button.module.css create mode 100644 components/common/ButtonGroup.js create mode 100644 components/common/ButtonGroup.module.css create mode 100644 components/common/Calendar.js create mode 100644 components/common/Calendar.module.css create mode 100644 components/common/Checkbox.js create mode 100644 components/common/Checkbox.module.css create mode 100644 components/common/CopyButton.js create mode 100644 components/common/DateFilter.js create mode 100644 components/common/Dot.js create mode 100644 components/common/Dot.module.css create mode 100644 components/common/DropDown.js create mode 100644 components/common/Dropdown.module.css create mode 100644 components/common/EmptyPlaceholder.js create mode 100644 components/common/EmptyPlaceholder.module.css create mode 100644 components/common/ErrorMessage.js create mode 100644 components/common/ErrorMessage.module.css create mode 100644 components/common/Favicon.js create mode 100644 components/common/Favicon.module.css create mode 100644 components/common/FilterButtons.js create mode 100644 components/common/Icon.js create mode 100644 components/common/Icon.module.css create mode 100644 components/common/Link.js create mode 100644 components/common/Link.module.css create mode 100644 components/common/Loading.js create mode 100644 components/common/Loading.module.css create mode 100644 components/common/Menu.js create mode 100644 components/common/Menu.module.css create mode 100644 components/common/MenuButton.js create mode 100644 components/common/MenuButton.module.css create mode 100644 components/common/Modal.js create mode 100644 components/common/Modal.module.css create mode 100644 components/common/NavMenu.js create mode 100644 components/common/NavMenu.module.css create mode 100644 components/common/NoData.js create mode 100644 components/common/NoData.module.css create mode 100644 components/common/RefreshButton.js create mode 100644 components/common/Table.js create mode 100644 components/common/Table.module.css create mode 100644 components/common/Tag.js create mode 100644 components/common/Tag.module.css create mode 100644 components/common/Toast.js create mode 100644 components/common/Toast.module.css create mode 100644 components/common/UpdateNotice.js create mode 100644 components/common/UpdateNotice.module.css create mode 100644 components/common/WorldMap.js create mode 100644 components/common/WorldMap.module.css create mode 100644 components/forms/AccountEditForm.js create mode 100644 components/forms/ChangePasswordForm.js create mode 100644 components/forms/DatePickerForm.js create mode 100644 components/forms/DatePickerForm.module.css create mode 100644 components/forms/DeleteForm.js create mode 100644 components/forms/LoginForm.js create mode 100644 components/forms/LoginForm.module.css create mode 100644 components/forms/ResetForm.js create mode 100644 components/forms/ShareUrlForm.js create mode 100644 components/forms/TrackingCodeForm.js create mode 100644 components/forms/WebsiteEditForm.js create mode 100644 components/helpers/CheckVisible.js create mode 100644 components/helpers/StickyHeader.js create mode 100644 components/layout/ButtonLayout.js create mode 100644 components/layout/ButtonLayout.module.css create mode 100644 components/layout/Footer.js create mode 100644 components/layout/Footer.module.css create mode 100644 components/layout/FormLayout.js create mode 100644 components/layout/FormLayout.module.css create mode 100644 components/layout/GridLayout.js create mode 100644 components/layout/GridLayout.module.css create mode 100644 components/layout/Header.js create mode 100644 components/layout/Header.module.css create mode 100644 components/layout/Layout.js create mode 100644 components/layout/Layout.module.css create mode 100644 components/layout/MenuLayout.js create mode 100644 components/layout/MenuLayout.module.css create mode 100644 components/layout/Page.js create mode 100644 components/layout/Page.module.css create mode 100644 components/layout/PageHeader.js create mode 100644 components/layout/PageHeader.module.css create mode 100644 components/messages.js create mode 100644 components/metrics/ActiveUsers.js create mode 100644 components/metrics/ActiveUsers.module.css create mode 100644 components/metrics/BarChart.js create mode 100644 components/metrics/BarChart.module.css create mode 100644 components/metrics/BrowsersTable.js create mode 100644 components/metrics/ChartTooltip.js create mode 100644 components/metrics/ChartTooltip.module.css create mode 100644 components/metrics/CountriesTable.js create mode 100644 components/metrics/DataTable.js create mode 100644 components/metrics/DataTable.module.css create mode 100644 components/metrics/DevicesTable.js create mode 100644 components/metrics/EventsChart.js create mode 100644 components/metrics/EventsChart.module.css create mode 100644 components/metrics/EventsTable.js create mode 100644 components/metrics/EventsTable.module.css create mode 100644 components/metrics/FilterTags.js create mode 100644 components/metrics/FilterTags.module.css create mode 100644 components/metrics/Legend.js create mode 100644 components/metrics/Legend.module.css create mode 100644 components/metrics/MetricCard.js create mode 100644 components/metrics/MetricCard.module.css create mode 100644 components/metrics/MetricsBar.js create mode 100644 components/metrics/MetricsBar.module.css create mode 100644 components/metrics/MetricsTable.js create mode 100644 components/metrics/MetricsTable.module.css create mode 100644 components/metrics/OSTable.js create mode 100644 components/metrics/PagesTable.js create mode 100644 components/metrics/PagesTable.module.css create mode 100644 components/metrics/PageviewsChart.js create mode 100644 components/metrics/RealtimeChart.js create mode 100644 components/metrics/RealtimeHeader.js create mode 100644 components/metrics/RealtimeHeader.module.css create mode 100644 components/metrics/RealtimeLog.js create mode 100644 components/metrics/RealtimeLog.module.css create mode 100644 components/metrics/RealtimeViews.js create mode 100644 components/metrics/ReferrersTable.js create mode 100644 components/metrics/ReferrersTable.module.css create mode 100644 components/metrics/WebsiteChart.js create mode 100644 components/metrics/WebsiteChart.module.css create mode 100644 components/metrics/WebsiteHeader.js create mode 100644 components/metrics/WebsiteHeader.module.css create mode 100644 components/pages/RealtimeDashboard.js create mode 100644 components/pages/RealtimeDashboard.module.css create mode 100644 components/pages/Settings.js create mode 100644 components/pages/TestConsole.js create mode 100644 components/pages/TestConsole.module.css create mode 100644 components/pages/WebsiteDetails.js create mode 100644 components/pages/WebsiteDetails.module.css create mode 100644 components/pages/WebsiteList.js create mode 100644 components/pages/WebsiteList.module.css create mode 100644 components/settings/AccountSettings.js create mode 100644 components/settings/AccountSettings.module.css create mode 100644 components/settings/DateRangeSetting.js create mode 100644 components/settings/DateRangeSetting.module.css create mode 100644 components/settings/LanguageButton.js create mode 100644 components/settings/LanguageButton.module.css create mode 100644 components/settings/ProfileSettings.js create mode 100644 components/settings/ProfileSettings.module.css create mode 100644 components/settings/ThemeButton.js create mode 100644 components/settings/ThemeButton.module.css create mode 100644 components/settings/TimezoneSetting.js create mode 100644 components/settings/TimezoneSetting.module.css create mode 100644 components/settings/UserButton.js create mode 100644 components/settings/UserButton.module.css create mode 100644 components/settings/WebsiteSettings.js create mode 100644 components/settings/WebsiteSettings.module.css create mode 100644 docker-compose.yml create mode 100644 hooks/useCountryNames.js create mode 100644 hooks/useDateRange.js create mode 100644 hooks/useDelete.js create mode 100644 hooks/useDocumentClick.js create mode 100644 hooks/useEscapeKey.js create mode 100644 hooks/useFetch.js create mode 100644 hooks/useForceSSL.js create mode 100644 hooks/useForceUpdate.js create mode 100644 hooks/useLocale.js create mode 100644 hooks/usePageQuery.js create mode 100644 hooks/usePost.js create mode 100644 hooks/useRequireLogin.js create mode 100644 hooks/useShareToken.js create mode 100644 hooks/useTheme.js create mode 100644 hooks/useTimezone.js create mode 100644 hooks/useVersion.js create mode 100644 jsconfig.json create mode 100644 lang-ignore.json create mode 100644 lang/ar-SA.json create mode 100644 lang/ca-ES.json create mode 100644 lang/cs-CZ.json create mode 100644 lang/da-DK.json create mode 100644 lang/de-DE.json create mode 100644 lang/el-GR.json create mode 100644 lang/en-GB.json create mode 100644 lang/en-US.json create mode 100644 lang/es-MX.json create mode 100644 lang/fa-IR.json create mode 100644 lang/fi-FI.json create mode 100644 lang/fo-FO.json create mode 100644 lang/fr-FR.json create mode 100644 lang/he-IL.json create mode 100644 lang/hi-IN.json create mode 100644 lang/hu-HU.json create mode 100644 lang/id-ID.json create mode 100644 lang/it-IT.json create mode 100644 lang/ja-JP.json create mode 100644 lang/ko-KR.json create mode 100644 lang/mn-MN.json create mode 100644 lang/ms-MY.json create mode 100644 lang/nb-NO.json create mode 100644 lang/nl-NL.json create mode 100644 lang/pl-PL.json create mode 100644 lang/pt-BR.json create mode 100644 lang/pt-PT.json create mode 100644 lang/ro-RO.json create mode 100644 lang/ru-RU.json create mode 100644 lang/sk-SK.json create mode 100644 lang/sl-SI.json create mode 100644 lang/sv-SE.json create mode 100644 lang/ta-IN.json create mode 100644 lang/tr-TR.json create mode 100644 lang/uk-UA.json create mode 100644 lang/vi-VN.json create mode 100644 lang/zh-CN.json create mode 100644 lang/zh-TW.json create mode 100644 lib/array.js create mode 100644 lib/auth.js create mode 100644 lib/constants.js create mode 100644 lib/crypto.js create mode 100644 lib/date.js create mode 100644 lib/db.js create mode 100644 lib/filters.js create mode 100644 lib/format.js create mode 100644 lib/lang.js create mode 100644 lib/middleware.js create mode 100644 lib/queries.js create mode 100644 lib/request.js create mode 100644 lib/response.js create mode 100644 lib/session.js create mode 100644 lib/url.js create mode 100644 lib/web.js create mode 100644 next.config.js create mode 100644 package.json create mode 100644 pages/404.js create mode 100644 pages/_app.js create mode 100644 pages/api/account/[id].js create mode 100644 pages/api/account/index.js create mode 100644 pages/api/account/password.js create mode 100644 pages/api/accounts.js create mode 100644 pages/api/auth/login.js create mode 100644 pages/api/auth/logout.js create mode 100644 pages/api/auth/verify.js create mode 100644 pages/api/collect.js create mode 100644 pages/api/realtime/init.js create mode 100644 pages/api/realtime/update.js create mode 100644 pages/api/share/[id].js create mode 100644 pages/api/website/[id]/active.js create mode 100644 pages/api/website/[id]/events.js create mode 100644 pages/api/website/[id]/index.js create mode 100644 pages/api/website/[id]/metrics.js create mode 100644 pages/api/website/[id]/pageviews.js create mode 100644 pages/api/website/[id]/reset.js create mode 100644 pages/api/website/[id]/stats.js create mode 100644 pages/api/website/index.js create mode 100644 pages/api/websites.js create mode 100644 pages/dashboard/[[...id]].js create mode 100644 pages/index.js create mode 100644 pages/login.js create mode 100644 pages/logout.js create mode 100644 pages/realtime.js create mode 100644 pages/settings/accounts.js create mode 100644 pages/settings/index.js create mode 100644 pages/settings/profile.js create mode 100644 pages/share/[...id].js create mode 100644 pages/test.js create mode 100644 pages/website/[...id].js create mode 100644 postcss.config.js create mode 100644 prisma/mysql/migrations/20210320112658_init/migration.sql create mode 100644 prisma/mysql/migrations/migration_lock.toml create mode 120000 prisma/mysql/schema.mysql.prisma create mode 120000 prisma/mysql/seed.js create mode 100644 prisma/postgresql/migrations/20210320112717_init/migration.sql create mode 100644 prisma/postgresql/migrations/migration_lock.toml create mode 120000 prisma/postgresql/schema.postgresql.prisma create mode 120000 prisma/postgresql/seed.js create mode 100644 prisma/schema.mysql.prisma create mode 100644 prisma/schema.postgresql.prisma create mode 100644 prisma/seed.js create mode 100644 public/android-chrome-192x192.png create mode 100644 public/android-chrome-512x512.png create mode 100644 public/apple-touch-icon.png create mode 100644 public/browserconfig.xml create mode 100644 public/country/ar-SA.json create mode 100644 public/country/ca-ES.json create mode 100644 public/country/cs-CZ.json create mode 100644 public/country/da-DK.json create mode 100644 public/country/de-DE.json create mode 100644 public/country/el-GR.json create mode 100644 public/country/en-GB.json create mode 100644 public/country/en-US.json create mode 100644 public/country/es-MX.json create mode 100644 public/country/fa-IR.json create mode 100644 public/country/fi-FI.json create mode 100644 public/country/fo-FO.json create mode 100644 public/country/fr-FR.json create mode 100644 public/country/he-IL.json create mode 100644 public/country/hi-IN.json create mode 100644 public/country/hu-HU.json create mode 100644 public/country/id-ID.json create mode 100644 public/country/it-IT.json create mode 100644 public/country/ja-JP.json create mode 100644 public/country/ko-KR.json create mode 100644 public/country/mn-MN.json create mode 100644 public/country/ms-MY.json create mode 100644 public/country/nb-NO.json create mode 100644 public/country/nl-NL.json create mode 100644 public/country/pl-PL.json create mode 100644 public/country/pt-BR.json create mode 100644 public/country/pt-PT.json create mode 100644 public/country/ro-RO.json create mode 100644 public/country/ru-RU.json create mode 100644 public/country/sk-SK.json create mode 100644 public/country/sl-SI.json create mode 100644 public/country/sv-SE.json create mode 100644 public/country/ta-IN.json create mode 100644 public/country/tr-TR.json create mode 100644 public/country/uk-UA.json create mode 100644 public/country/vi-VN.json create mode 100644 public/country/zh-CN.json create mode 100644 public/country/zh-TW.json create mode 100644 public/datamaps.world.json create mode 100644 public/favicon-16x16.png create mode 100644 public/favicon-32x32.png create mode 100644 public/favicon.ico create mode 100644 public/mstile-150x150.png create mode 100644 public/safari-pinned-tab.svg create mode 100644 public/site.webmanifest create mode 100644 redux/actions/app.js create mode 100644 redux/actions/queries.js create mode 100644 redux/actions/user.js create mode 100644 redux/actions/websites.js create mode 100644 redux/reducers.js create mode 100644 redux/store.js create mode 100644 rollup.tracker.config.js create mode 100644 scripts/build-geo.js create mode 100644 scripts/change-password.js create mode 100644 scripts/check-lang.js create mode 100644 scripts/copy-db-schema.js create mode 100644 scripts/download-country-names.js create mode 100644 scripts/format-lang.js create mode 100644 scripts/loadtest.js create mode 100644 scripts/merge-lang.js create mode 100644 scripts/start-env.js create mode 100644 sql/schema.mysql.sql create mode 100644 sql/schema.postgresql.sql create mode 100644 styles/bootstrap-grid.css create mode 100644 styles/index.css create mode 100644 styles/variables.css create mode 100644 tracker/index.js create mode 100644 yarn.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..40d5f5b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +docker-compose.yml +Dockerfile +.gitignore +.DS_Store +node_modules diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..168665f --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,24 @@ +{ + "env": { + "browser": true, + "es2020": true, + "node": true + }, + "extends": ["eslint:recommended", "plugin:react/recommended", "prettier", "next"], + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 11, + "sourceType": "module" + }, + "plugins": ["react"], + "rules": { + "react/display-name": "off", + "react/react-in-jsx-scope": "off", + "react/prop-types": "off" + }, + "globals": { + "React": "writable" + } +} diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..2dc5b67 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,19 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - enhancement + - bug +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaff7c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ +/prisma/schema.prisma + +# production +/build +/public/umami.js +/public/geo +/public/lang +/lang-compiled + +# misc +.DS_Store +.idea +*.iml +*.log +/.vscode/ + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env.development.local +.env.test.local +.env.production.local diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..36af219 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..15ce475 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +/public/ \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..1193784 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "arrowParens": "avoid", + "endOfLine": "lf", + "printWidth": 100, + "singleQuote": true, + "trailingComma": "all" +} diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000..117fac2 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,17 @@ +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-css-modules", + "stylelint-config-prettier" + ], + "rules": { + "no-descending-specificity": null, + "selector-pseudo-class-no-unknown": [ + true, + { + "ignorePseudoClasses": ["global", "horizontal", "vertical"] + } + ] + }, + "ignoreFiles": ["**/*.js"] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e66ec5d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# Build image +FROM node:12.22-alpine AS build +ARG BASE_PATH +ARG DATABASE_TYPE +ENV BASE_PATH=$BASE_PATH +ENV DATABASE_URL "postgresql://umami:umami@db:5432/umami" \ + DATABASE_TYPE=$DATABASE_TYPE +WORKDIR /build + +RUN yarn config set --home enableTelemetry 0 +COPY package.json yarn.lock /build/ + +# Install only the production dependencies +RUN yarn install --production --frozen-lockfile + +# Cache these modules for production +RUN cp -R node_modules/ prod_node_modules/ + +# Install development dependencies +RUN yarn install --frozen-lockfile + +COPY . /build +RUN yarn next telemetry disable +RUN yarn build + +# Production image +FROM node:12.22-alpine AS production +WORKDIR /app + +# Copy cached dependencies +COPY --from=build /build/prod_node_modules ./node_modules + +# Copy generated Prisma client +COPY --from=build /build/node_modules/.prisma/ ./node_modules/.prisma/ + +COPY --from=build /build/yarn.lock /build/package.json ./ +COPY --from=build /build/.next ./.next +COPY --from=build /build/public ./public + +USER node + +EXPOSE 3000 +CMD ["yarn", "start"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5127765 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Mike Cao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..edc6c9a --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: npm run start-env diff --git a/README.md b/README.md new file mode 100644 index 0000000..98fe215 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# umami + +Umami is a simple, fast, website analytics alternative to Google Analytics. + +## Getting started + +A detailed getting started guide can be found at [https://umami.is/docs/](https://umami.is/docs/) + +## Installing from source + +### Requirements + +- A server with Node.js 12 or newer +- A database (MySQL or Postgresql) + +### Get the source code and install packages + +``` +git clone https://github.com/mikecao/umami.git +cd umami +npm install +``` + +### Create database tables + +Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/). +Create a database for your Umami installation and install the tables with the included scripts. + +For MySQL: + +``` +mysql -u username -p databasename < sql/schema.mysql.sql +``` + +For Postgresql: + +``` +psql -h hostname -U username -d databasename -f sql/schema.postgresql.sql +``` + +This will also create a login account with username **admin** and password **umami**. + +### Configure umami + +Create an `.env` file with the following + +``` +DATABASE_URL=(connection url) +HASH_SALT=(any random string) +``` + +The connection url is in the following format: +``` +postgresql://username:mypassword@localhost:5432/mydb + +mysql://username:mypassword@localhost:3306/mydb +``` + +The `HASH_SALT` is used to generate unique values for your installation. + +### Build the application + +```bash +npm run build +``` + +### Start the application + +```bash +npm start +``` + +By default this will launch the application on `http://localhost:3000`. You will need to either +[proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server +or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly. + +## Installing with Docker + +To build the umami container and start up a Postgres database, run: + +```bash +docker-compose up +``` + +Alternatively, to pull just the Umami Docker image with PostgreSQL support: +```bash +docker pull ghcr.io/mikecao/umami:postgresql-latest +``` + +Or with MySQL support: +```bash +docker pull ghcr.io/mikecao/umami:mysql-latest +``` + +## Getting updates + +To get the latest features, simply do a pull, install any new dependencies, and rebuild: + +```bash +git pull +npm install +npm run build +``` + +## License + +MIT diff --git a/app.json b/app.json new file mode 100644 index 0000000..a27dc6f --- /dev/null +++ b/app.json @@ -0,0 +1,26 @@ +{ + "name": "Umami", + "description": "Umami is a simple, fast, website analytics alternative to Google Analytics.", + "keywords": [ + "analytics", + "charts", + "statistics", + "web-analytics" + ], + "website": "https://umami.is", + "repository": "https://github.com/mikecao/umami", + "addons": [ + "heroku-postgresql" + ], + "env": { + "HASH_SALT": { + "description": "Used to generate unique values for your installation", + "required": true, + "generator": "secret" + } + }, + "scripts": { + "postdeploy": "psql $DATABASE_URL -f sql/schema.postgresql.sql" + }, + "success_url": "/" +} diff --git a/assets/arrow-right.svg b/assets/arrow-right.svg new file mode 100644 index 0000000..6fc9390 --- /dev/null +++ b/assets/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/arrow-up-right-from-square.svg b/assets/arrow-up-right-from-square.svg new file mode 100644 index 0000000..90ad457 --- /dev/null +++ b/assets/arrow-up-right-from-square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/bars.svg b/assets/bars.svg new file mode 100644 index 0000000..91c83c4 --- /dev/null +++ b/assets/bars.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/bolt.svg b/assets/bolt.svg new file mode 100644 index 0000000..4654a1e --- /dev/null +++ b/assets/bolt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/calendar-alt.svg b/assets/calendar-alt.svg new file mode 100644 index 0000000..230c4e6 --- /dev/null +++ b/assets/calendar-alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/chart-bar.svg b/assets/chart-bar.svg new file mode 100644 index 0000000..d1d72fd --- /dev/null +++ b/assets/chart-bar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/check.svg b/assets/check.svg new file mode 100644 index 0000000..1a7abdc --- /dev/null +++ b/assets/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/chevron-down.svg b/assets/chevron-down.svg new file mode 100644 index 0000000..cb9d8fe --- /dev/null +++ b/assets/chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/code.svg b/assets/code.svg new file mode 100644 index 0000000..cd29765 --- /dev/null +++ b/assets/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ellipsis-h.svg b/assets/ellipsis-h.svg new file mode 100644 index 0000000..5bb0835 --- /dev/null +++ b/assets/ellipsis-h.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/exclamation-triangle.svg b/assets/exclamation-triangle.svg new file mode 100644 index 0000000..46bef5b --- /dev/null +++ b/assets/exclamation-triangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/external-link.svg b/assets/external-link.svg new file mode 100644 index 0000000..ed09306 --- /dev/null +++ b/assets/external-link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/eye.svg b/assets/eye.svg new file mode 100644 index 0000000..09c9345 --- /dev/null +++ b/assets/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/globe.svg b/assets/globe.svg new file mode 100644 index 0000000..509eaba --- /dev/null +++ b/assets/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/link.svg b/assets/link.svg new file mode 100644 index 0000000..c53d1ad --- /dev/null +++ b/assets/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/list-ul.svg b/assets/list-ul.svg new file mode 100644 index 0000000..5e63212 --- /dev/null +++ b/assets/list-ul.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 0000000..c80f166 --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/assets/moon.svg b/assets/moon.svg new file mode 100644 index 0000000..6c8955a --- /dev/null +++ b/assets/moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/pen.svg b/assets/pen.svg new file mode 100644 index 0000000..426c520 --- /dev/null +++ b/assets/pen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/plus.svg b/assets/plus.svg new file mode 100644 index 0000000..e4774d8 --- /dev/null +++ b/assets/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/redo.svg b/assets/redo.svg new file mode 100644 index 0000000..4544eb1 --- /dev/null +++ b/assets/redo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/sun.svg b/assets/sun.svg new file mode 100644 index 0000000..ebc20eb --- /dev/null +++ b/assets/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/times.svg b/assets/times.svg new file mode 100644 index 0000000..c528bcd --- /dev/null +++ b/assets/times.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/trash.svg b/assets/trash.svg new file mode 100644 index 0000000..2f525c8 --- /dev/null +++ b/assets/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/user.svg b/assets/user.svg new file mode 100644 index 0000000..c009466 --- /dev/null +++ b/assets/user.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/assets/visitor.svg b/assets/visitor.svg new file mode 100644 index 0000000..591873a --- /dev/null +++ b/assets/visitor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/xmark.svg b/assets/xmark.svg new file mode 100644 index 0000000..6d72bf6 --- /dev/null +++ b/assets/xmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/common/Button.js b/components/common/Button.js new file mode 100644 index 0000000..0cdb5fb --- /dev/null +++ b/components/common/Button.js @@ -0,0 +1,62 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactTooltip from 'react-tooltip'; +import classNames from 'classnames'; +import Icon from './Icon'; +import styles from './Button.module.css'; + +function Button({ + type = 'button', + icon, + size, + variant, + children, + className, + tooltip, + tooltipId, + disabled, + iconRight, + onClick = () => {}, + ...props +}) { + return ( + + ); +} + +Button.propTypes = { + type: PropTypes.oneOf(['button', 'submit', 'reset']), + icon: PropTypes.node, + size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']), + variant: PropTypes.oneOf(['action', 'danger', 'light']), + children: PropTypes.node, + className: PropTypes.string, + tooltip: PropTypes.node, + tooltipId: PropTypes.string, + disabled: PropTypes.bool, + iconRight: PropTypes.bool, + onClick: PropTypes.func, +}; + +export default Button; diff --git a/components/common/Button.module.css b/components/common/Button.module.css new file mode 100644 index 0000000..b911095 --- /dev/null +++ b/components/common/Button.module.css @@ -0,0 +1,102 @@ +.button { + display: flex; + justify-content: center; + align-items: center; + font-size: var(--font-size-normal); + color: var(--gray900); + background: var(--gray100); + padding: 8px 16px; + border-radius: 4px; + border: 0; + outline: none; + cursor: pointer; + position: relative; +} + +.button:hover { + background: var(--gray200); +} + +.button:active { + color: var(--gray900); +} + +.label { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 300px; +} + +.large { + font-size: var(--font-size-large); +} + +.small { + font-size: var(--font-size-small); +} + +.xsmall { + font-size: var(--font-size-xsmall); +} + +.action, +.action:active { + color: var(--gray50); + background: var(--gray900); +} + +.action:hover { + background: var(--gray800); +} + +.danger, +.danger:active { + color: var(--gray50); + background: var(--red500); +} + +.danger:hover { + background: var(--red400); +} + +.light, +.light:active { + color: var(--gray900); + background: transparent; +} + +.light:hover { + background: inherit; +} + +.button .icon + * { + margin-left: 10px; +} + +.button.iconRight .icon { + order: 1; + margin-left: 10px; +} + +.button.iconRight .icon + * { + margin: 0; +} + +.button:disabled { + cursor: default; + color: var(--gray500); + background: var(--gray75); +} + +.button:disabled:active { + color: var(--gray500); +} + +.button:disabled:hover { + background: var(--gray75); +} + +.button.light:disabled { + background: var(--gray50); +} diff --git a/components/common/ButtonGroup.js b/components/common/ButtonGroup.js new file mode 100644 index 0000000..353ce69 --- /dev/null +++ b/components/common/ButtonGroup.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import Button from './Button'; +import styles from './ButtonGroup.module.css'; + +function ButtonGroup({ items = [], selectedItem, className, size, icon, onClick = () => {} }) { + return ( +
+ {items.map(item => { + const { label, value } = item; + return ( + + ); + })} +
+ ); +} + +ButtonGroup.propTypes = { + items: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.node, + value: PropTypes.any.isRequired, + }), + ), + selectedItem: PropTypes.any, + className: PropTypes.string, + size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']), + icon: PropTypes.node, + onClick: PropTypes.func, +}; + +export default ButtonGroup; diff --git a/components/common/ButtonGroup.module.css b/components/common/ButtonGroup.module.css new file mode 100644 index 0000000..bc60f8d --- /dev/null +++ b/components/common/ButtonGroup.module.css @@ -0,0 +1,31 @@ +.group { + display: inline-flex; + border-radius: 4px; + overflow: hidden; + border: 1px solid var(--gray500); +} + +.group .button { + border-radius: 0; + color: var(--gray800); + background: var(--gray50); + border-left: 1px solid var(--gray500); + padding: 4px 8px; +} + +.group .button:first-child { + border: 0; +} + +.group .button:hover { + background: var(--gray100); +} + +.group .button + .button { + margin: 0; +} + +.group .button.selected { + color: var(--gray900); + font-weight: 600; +} diff --git a/components/common/Calendar.js b/components/common/Calendar.js new file mode 100644 index 0000000..7aea34c --- /dev/null +++ b/components/common/Calendar.js @@ -0,0 +1,268 @@ +import React, { useState } from 'react'; +import classNames from 'classnames'; +import { + startOfWeek, + startOfMonth, + startOfYear, + endOfMonth, + addDays, + subDays, + addYears, + subYears, + addMonths, + setMonth, + setYear, + isSameDay, + isBefore, + isAfter, +} from 'date-fns'; +import Button from './Button'; +import useLocale from 'hooks/useLocale'; +import { dateFormat } from 'lib/date'; +import { chunk } from 'lib/array'; +import { getDateLocale } from 'lib/lang'; +import Chevron from 'assets/chevron-down.svg'; +import Cross from 'assets/times.svg'; +import styles from './Calendar.module.css'; +import Icon from './Icon'; + +export default function Calendar({ date, minDate, maxDate, onChange }) { + const { locale } = useLocale(); + const [selectMonth, setSelectMonth] = useState(false); + const [selectYear, setSelectYear] = useState(false); + + const month = dateFormat(date, 'MMMM', locale); + const year = date.getFullYear(); + + function toggleMonthSelect() { + setSelectYear(false); + setSelectMonth(state => !state); + } + + function toggleYearSelect() { + setSelectMonth(false); + setSelectYear(state => !state); + } + + function handleChange(value) { + setSelectMonth(false); + setSelectYear(false); + if (value) { + onChange(value); + } + } + + return ( +
+
+
{date.getDate()}
+
+ {month} + : } size="small" /> +
+
+ {year} + : } size="small" /> +
+
+
+ {!selectMonth && !selectYear && ( + + )} + {selectMonth && ( + + )} + {selectYear && ( + + )} +
+
+ ); +} + +const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => { + const startWeek = startOfWeek(date, { locale: getDateLocale(locale) }); + const startMonth = startOfMonth(date, { locale: getDateLocale(locale) }); + const startDay = subDays(startMonth, startMonth.getDay()); + const month = date.getMonth(); + const year = date.getFullYear(); + + const daysOfWeek = []; + for (let i = 0; i < 7; i++) { + daysOfWeek.push(addDays(startWeek, i)); + } + + const days = []; + for (let i = 0; i < 35; i++) { + days.push(addDays(startDay, i)); + } + + return ( + + + + {daysOfWeek.map((day, i) => ( + + ))} + + + + {chunk(days, 7).map((week, i) => ( + + {week.map((day, j) => { + const disabled = isBefore(day, minDate) || isAfter(day, maxDate); + return ( + + ); + })} + + ))} + +
+ {dateFormat(day, 'EEE', locale)} +
onSelect(day) : null} + > + {day.getDate()} +
+ ); +}; + +const MonthSelector = ({ date, minDate, maxDate, locale, onSelect }) => { + const start = startOfYear(date); + const months = []; + for (let i = 0; i < 12; i++) { + months.push(addMonths(start, i)); + } + + function handleSelect(value) { + onSelect(setMonth(date, value)); + } + + return ( + + + {chunk(months, 3).map((row, i) => ( + + {row.map((month, j) => { + const disabled = + isBefore(endOfMonth(month), minDate) || isAfter(startOfMonth(month), maxDate); + return ( + + ); + })} + + ))} + +
handleSelect(month.getMonth()) : null} + > + {dateFormat(month, 'MMMM', locale)} +
+ ); +}; + +const YearSelector = ({ date, minDate, maxDate, onSelect }) => { + const [currentDate, setCurrentDate] = useState(date); + const year = date.getFullYear(); + const currentYear = currentDate.getFullYear(); + const minYear = minDate.getFullYear(); + const maxYear = maxDate.getFullYear(); + const years = []; + for (let i = 0; i < 15; i++) { + years.push(currentYear - 7 + i); + } + + function handleSelect(value) { + onSelect(setYear(date, value)); + } + + function handlePrevClick() { + setCurrentDate(state => subYears(state, 15)); + } + + function handleNextClick() { + setCurrentDate(state => addYears(state, 15)); + } + + return ( +
+
+
+
+ + + {chunk(years, 5).map((row, i) => ( + + {row.map((n, j) => ( + + ))} + + ))} + +
maxYear, + })} + onClick={() => (n < minYear || n > maxYear ? null : handleSelect(n))} + > + {n} +
+
+
+
+
+ ); +}; diff --git a/components/common/Calendar.module.css b/components/common/Calendar.module.css new file mode 100644 index 0000000..9751cf2 --- /dev/null +++ b/components/common/Calendar.module.css @@ -0,0 +1,111 @@ +.calendar { + display: flex; + flex-direction: column; + font-size: var(--font-size-small); + flex: 1; + min-height: 306px; +} + +.calendar table { + width: 100%; + border-spacing: 5px; +} + +.calendar td { + color: var(--gray800); + cursor: pointer; + text-align: center; + vertical-align: center; + height: 40px; + width: 40px; + border-radius: 5px; + border: 1px solid transparent; +} + +.calendar td:hover { + border: 1px solid var(--gray300); + background: var(--gray75); +} + +.calendar td.faded { + color: var(--gray500); +} + +.calendar td.selected { + font-weight: 600; + border: 1px solid var(--gray600); +} + +.calendar td.selected:hover { + background: transparent; +} + +.calendar td.disabled { + color: var(--gray400); + background: var(--gray75); +} + +.calendar td.disabled:hover { + cursor: default; + background: var(--gray75); + border-color: transparent; +} + +.calendar td.faded.disabled { + background: var(--gray100); +} + +.header { + display: flex; + justify-content: space-evenly; + align-items: center; + font-weight: 700; + line-height: 40px; + font-size: var(--font-size-normal); +} + +.body { + display: flex; +} + +.selector { + cursor: pointer; +} + +.pager { + display: flex; + flex: 1; +} + +.pager button { + align-self: center; +} + +.middle { + flex: 1; +} + +.left, +.right { + display: flex; + justify-content: center; + align-items: center; +} + +.left svg { + transform: rotate(90deg); +} + +.right svg { + transform: rotate(-90deg); +} + +.icon { + margin-left: 10px; +} + +@media only screen and (max-width: 992px) { + .calendar table { + max-width: calc(100vw - 30px); + } +} diff --git a/components/common/Checkbox.js b/components/common/Checkbox.js new file mode 100644 index 0000000..0cd0dca --- /dev/null +++ b/components/common/Checkbox.js @@ -0,0 +1,39 @@ +import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; +import Icon from 'components/common/Icon'; +import Check from 'assets/check.svg'; +import styles from './Checkbox.module.css'; + +function Checkbox({ name, value, label, onChange }) { + const ref = useRef(); + + const onClick = () => ref.current.click(); + + return ( +
+
+ {value && } size="small" />} +
+ + +
+ ); +} + +Checkbox.propTypes = { + name: PropTypes.string, + value: PropTypes.any, + label: PropTypes.node, + onChange: PropTypes.func, +}; + +export default Checkbox; diff --git a/components/common/Checkbox.module.css b/components/common/Checkbox.module.css new file mode 100644 index 0000000..c9a01ea --- /dev/null +++ b/components/common/Checkbox.module.css @@ -0,0 +1,30 @@ +.container { + display: flex; + align-items: center; + position: relative; + overflow: hidden; +} + +.checkbox { + display: flex; + justify-content: center; + align-items: center; + width: 20px; + height: 20px; + border: 1px solid var(--gray500); + border-radius: 4px; +} + +.label { + margin-left: 10px; + user-select: none; /* disable text selection when clicking to toggle the checkbox */ +} + +.input { + position: absolute; + visibility: hidden; + height: 0; + width: 0; + bottom: 100%; + right: 100%; +} diff --git a/components/common/CopyButton.js b/components/common/CopyButton.js new file mode 100644 index 0000000..b300ef3 --- /dev/null +++ b/components/common/CopyButton.js @@ -0,0 +1,37 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import Button from './Button'; +import { FormattedMessage } from 'react-intl'; + +const defaultText = ( + +); + +function CopyButton({ element, ...props }) { + const [text, setText] = useState(defaultText); + + function handleClick() { + if (element?.current) { + element.current.select(); + document.execCommand('copy'); + setText(); + window.getSelection().removeAllRanges(); + } + } + + return ( + + ); +} + +CopyButton.propTypes = { + element: PropTypes.shape({ + current: PropTypes.shape({ + select: PropTypes.func.isRequired, + }), + }), +}; + +export default CopyButton; diff --git a/components/common/DateFilter.js b/components/common/DateFilter.js new file mode 100644 index 0000000..ba3417d --- /dev/null +++ b/components/common/DateFilter.js @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { endOfYear, isSameDay } from 'date-fns'; +import Modal from './Modal'; +import DropDown from './DropDown'; +import DatePickerForm from 'components/forms/DatePickerForm'; +import useLocale from 'hooks/useLocale'; +import { getDateRange, dateFormat } from 'lib/date'; +import Calendar from 'assets/calendar-alt.svg'; +import Icon from './Icon'; + +const filterOptions = [ + { label: , value: '1day' }, + { + label: ( + + ), + value: '24hour', + }, + { + label: , + value: '1week', + divider: true, + }, + { + label: ( + + ), + value: '7day', + }, + { + label: , + value: '1month', + divider: true, + }, + { + label: ( + + ), + value: '30day', + }, + { + label: ( + + ), + value: '90day', + }, + { label: , value: '1year' }, + { + label: , + value: 'custom', + divider: true, + }, +]; + +function DateFilter({ value, startDate, endDate, onChange, className }) { + const { locale } = useLocale(); + const [showPicker, setShowPicker] = useState(false); + const displayValue = + value === 'custom' ? ( + handleChange('custom')} /> + ) : ( + value + ); + + function handleChange(value) { + if (value === 'custom') { + setShowPicker(true); + return; + } + onChange(getDateRange(value, locale)); + } + + function handlePickerChange(value) { + setShowPicker(false); + onChange(value); + } + + return ( + <> + + {showPicker && ( + + setShowPicker(false)} + /> + + )} + + ); +} + +const CustomRange = ({ startDate, endDate, onClick }) => { + const { locale } = useLocale(); + + function handleClick(e) { + e.stopPropagation(); + + onClick(); + } + + return ( + <> + } className="mr-2" onClick={handleClick} /> + {dateFormat(startDate, 'd LLL y', locale)} + {!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`} + + ); +}; + +DateFilter.propTypes = { + value: PropTypes.string, + startDate: PropTypes.instanceOf(Date), + endDate: PropTypes.instanceOf(Date), + onChange: PropTypes.func, + className: PropTypes.string, +}; + +export default DateFilter; diff --git a/components/common/Dot.js b/components/common/Dot.js new file mode 100644 index 0000000..81454c4 --- /dev/null +++ b/components/common/Dot.js @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import styles from './Dot.module.css'; + +function Dot({ color, size, className }) { + return ( +
+
+
+ ); +} + +Dot.propTypes = { + color: PropTypes.string, + size: PropTypes.oneOf(['small', 'large']), + className: PropTypes.string, +}; + +export default Dot; diff --git a/components/common/Dot.module.css b/components/common/Dot.module.css new file mode 100644 index 0000000..258d6e8 --- /dev/null +++ b/components/common/Dot.module.css @@ -0,0 +1,22 @@ +.wrapper { + background: var(--gray50); + margin-right: 10px; + border-radius: 100%; +} + +.dot { + background: var(--green400); + width: 10px; + height: 10px; + border-radius: 100%; +} + +.dot.small { + width: 8px; + height: 8px; +} + +.dot.large { + width: 16px; + height: 16px; +} diff --git a/components/common/DropDown.js b/components/common/DropDown.js new file mode 100644 index 0000000..00d20e3 --- /dev/null +++ b/components/common/DropDown.js @@ -0,0 +1,64 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import Menu from './Menu'; +import useDocumentClick from 'hooks/useDocumentClick'; +import Chevron from 'assets/chevron-down.svg'; +import styles from './Dropdown.module.css'; +import Icon from './Icon'; + +function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) { + const [showMenu, setShowMenu] = useState(false); + const ref = useRef(); + const selectedOption = options.find(e => e.value === value); + + function handleShowMenu() { + setShowMenu(state => !state); + } + + function handleSelect(selected, e) { + e.stopPropagation(); + setShowMenu(false); + + onChange(selected); + } + + useDocumentClick(e => { + if (!ref.current?.contains(e.target)) { + setShowMenu(false); + } + }); + + return ( +
+
+
{options.find(e => e.value === value)?.label || value}
+ } className={styles.icon} size="small" /> +
+ {showMenu && ( + + )} +
+ ); +} + +DropDown.propTypes = { + value: PropTypes.any, + className: PropTypes.string, + menuClassName: PropTypes.string, + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.any.isRequired, + label: PropTypes.node, + }), + ), + onChange: PropTypes.func, +}; + +export default DropDown; diff --git a/components/common/Dropdown.module.css b/components/common/Dropdown.module.css new file mode 100644 index 0000000..9738b00 --- /dev/null +++ b/components/common/Dropdown.module.css @@ -0,0 +1,28 @@ +.dropdown { + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid var(--gray500); + border-radius: 4px; + cursor: pointer; +} + +.value { + flex: 1; + display: flex; + justify-content: space-between; + font-size: var(--font-size-small); + flex-wrap: nowrap; + white-space: nowrap; + padding: 4px 16px; + min-width: 160px; +} + +.text { + flex: 1; +} + +.icon { + padding-left: 20px; +} diff --git a/components/common/EmptyPlaceholder.js b/components/common/EmptyPlaceholder.js new file mode 100644 index 0000000..b5394e8 --- /dev/null +++ b/components/common/EmptyPlaceholder.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Icon from 'components/common/Icon'; +import Logo from 'assets/logo.svg'; +import styles from './EmptyPlaceholder.module.css'; + +function EmptyPlaceholder({ msg, children }) { + return ( +
+ } size="xlarge" /> +

{msg}

+ {children} +
+ ); +} + +EmptyPlaceholder.propTypes = { + msg: PropTypes.node, + children: PropTypes.node, +}; + +export default EmptyPlaceholder; diff --git a/components/common/EmptyPlaceholder.module.css b/components/common/EmptyPlaceholder.module.css new file mode 100644 index 0000000..a923183 --- /dev/null +++ b/components/common/EmptyPlaceholder.module.css @@ -0,0 +1,15 @@ +.placeholder { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 600px; +} + +.icon { + margin-bottom: 30px; +} + +.msg { + margin-bottom: 15px; +} diff --git a/components/common/ErrorMessage.js b/components/common/ErrorMessage.js new file mode 100644 index 0000000..5747f22 --- /dev/null +++ b/components/common/ErrorMessage.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import Icon from './Icon'; +import Exclamation from 'assets/exclamation-triangle.svg'; +import styles from './ErrorMessage.module.css'; + +export default function ErrorMessage() { + return ( +
+ } className={styles.icon} size="large" /> + +
+ ); +} diff --git a/components/common/ErrorMessage.module.css b/components/common/ErrorMessage.module.css new file mode 100644 index 0000000..88769cf --- /dev/null +++ b/components/common/ErrorMessage.module.css @@ -0,0 +1,15 @@ +.error { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: auto; + display: flex; + z-index: 1; + background-color: var(--gray50); + padding: 10px; +} + +.icon { + margin-right: 10px; +} diff --git a/components/common/Favicon.js b/components/common/Favicon.js new file mode 100644 index 0000000..d72cd3c --- /dev/null +++ b/components/common/Favicon.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './Favicon.module.css'; + +function getHostName(url) { + const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n?=]+)/im); + return match && match.length > 1 ? match[1] : null; +} + +function Favicon({ domain, ...props }) { + const hostName = domain ? getHostName(domain) : null; + + return hostName ? ( + + ) : null; +} + +Favicon.propTypes = { + domain: PropTypes.string, +}; + +export default Favicon; diff --git a/components/common/Favicon.module.css b/components/common/Favicon.module.css new file mode 100644 index 0000000..82c85c4 --- /dev/null +++ b/components/common/Favicon.module.css @@ -0,0 +1,3 @@ +.favicon { + margin-right: 8px; +} diff --git a/components/common/FilterButtons.js b/components/common/FilterButtons.js new file mode 100644 index 0000000..ea81121 --- /dev/null +++ b/components/common/FilterButtons.js @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ButtonLayout from 'components/layout/ButtonLayout'; +import ButtonGroup from './ButtonGroup'; + +function FilterButtons({ buttons, selected, onClick }) { + return ( + + + + ); +} + +FilterButtons.propTypes = { + buttons: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.node, + value: PropTypes.any.isRequired, + }), + ), + selected: PropTypes.any, + onClick: PropTypes.func, +}; + +export default FilterButtons; diff --git a/components/common/Icon.js b/components/common/Icon.js new file mode 100644 index 0000000..e9d96eb --- /dev/null +++ b/components/common/Icon.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import styles from './Icon.module.css'; + +function Icon({ icon, className, size = 'medium', ...props }) { + return ( +
+ {icon} +
+ ); +} + +Icon.propTypes = { + className: PropTypes.string, + icon: PropTypes.node.isRequired, + size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']), +}; + +export default Icon; diff --git a/components/common/Icon.module.css b/components/common/Icon.module.css new file mode 100644 index 0000000..5b43166 --- /dev/null +++ b/components/common/Icon.module.css @@ -0,0 +1,35 @@ +.icon { + display: inline-flex; + justify-content: center; + align-items: center; + vertical-align: middle; +} + +.icon svg { + fill: currentColor; +} + +.xlarge > svg { + width: 48px; + height: 48px; +} + +.large > svg { + width: 24px; + height: 24px; +} + +.medium > svg { + width: 16px; + height: 16px; +} + +.small > svg { + width: 12px; + height: 12px; +} + +.xsmall > svg { + width: 10px; + height: 10px; +} diff --git a/components/common/Link.js b/components/common/Link.js new file mode 100644 index 0000000..f0fad73 --- /dev/null +++ b/components/common/Link.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import NextLink from 'next/link'; +import Icon from './Icon'; +import styles from './Link.module.css'; + +function Link({ className, icon, children, size, iconRight, ...props }) { + return ( + + + {icon && } + {children} + + + ); +} + +Link.propTypes = { + className: PropTypes.string, + icon: PropTypes.node, + children: PropTypes.node, + size: PropTypes.oneOf(['large', 'small', 'xsmall']), + iconRight: PropTypes.bool, +}; + +export default Link; diff --git a/components/common/Link.module.css b/components/common/Link.module.css new file mode 100644 index 0000000..ea6d281 --- /dev/null +++ b/components/common/Link.module.css @@ -0,0 +1,50 @@ +a.link, +a.link:active, +a.link:visited { + position: relative; + color: var(--gray900); + text-decoration: none; + display: inline-flex; + align-items: center; +} + +a.link:before { + content: ''; + position: absolute; + bottom: -2px; + width: 0; + height: 2px; + background: var(--primary400); + opacity: 0.5; + transition: width 100ms; +} + +a.link:hover:before { + width: 100%; + transition: width 100ms; +} + +a.link.large { + font-size: var(--font-size-large); +} + +a.link.small { + font-size: var(--font-size-small); +} + +a.link.xsmall { + font-size: var(--font-size-xsmall); +} + +a.link .icon + * { + margin-left: 10px; +} + +a.link.iconRight .icon { + order: 1; + margin-left: 10px; +} + +a.link.iconRight .icon + * { + margin: 0; +} diff --git a/components/common/Loading.js b/components/common/Loading.js new file mode 100644 index 0000000..16d8bb8 --- /dev/null +++ b/components/common/Loading.js @@ -0,0 +1,20 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import styles from './Loading.module.css'; + +function Loading({ className }) { + return ( +
+
+
+
+
+ ); +} + +Loading.propTypes = { + className: PropTypes.string, +}; + +export default Loading; diff --git a/components/common/Loading.module.css b/components/common/Loading.module.css new file mode 100644 index 0000000..4e56dd8 --- /dev/null +++ b/components/common/Loading.module.css @@ -0,0 +1,43 @@ +@keyframes blink { + 0% { + opacity: 0.2; + } + 20% { + opacity: 1; + } + 100% { + opacity: 0.2; + } +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; +} + +.loading div { + width: 10px; + height: 10px; + border-radius: 100%; + background: var(--gray400); + animation: blink 1.4s infinite; + animation-fill-mode: both; +} + +.loading div + div { + margin-left: 10px; +} + +.loading div:nth-child(2) { + animation-delay: 0.2s; +} + +.loading div:nth-child(3) { + animation-delay: 0.4s; +} diff --git a/components/common/Menu.js b/components/common/Menu.js new file mode 100644 index 0000000..91eeee9 --- /dev/null +++ b/components/common/Menu.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import styles from './Menu.module.css'; + +function Menu({ + options = [], + selectedOption, + className, + float, + align = 'left', + optionClassName, + selectedClassName, + onSelect = () => {}, +}) { + return ( +
+ {options + .filter(({ hidden }) => !hidden) + .map(option => { + const { label, value, className: customClassName, render, divider } = option; + + return render ? ( + render(option) + ) : ( +
onSelect(value, e)} + > + {label} +
+ ); + })} +
+ ); +} + +Menu.propTypes = { + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.node, + value: PropTypes.any, + className: PropTypes.string, + render: PropTypes.func, + divider: PropTypes.bool, + }), + ), + selectedOption: PropTypes.any, + className: PropTypes.string, + float: PropTypes.oneOf(['top', 'bottom']), + align: PropTypes.oneOf(['left', 'right']), + optionClassName: PropTypes.string, + selectedClassName: PropTypes.string, + onSelect: PropTypes.func, +}; + +export default Menu; diff --git a/components/common/Menu.module.css b/components/common/Menu.module.css new file mode 100644 index 0000000..d2ad2cc --- /dev/null +++ b/components/common/Menu.module.css @@ -0,0 +1,51 @@ +.menu { + background: var(--gray50); + border: 1px solid var(--gray500); + border-radius: 4px; + overflow: hidden; + z-index: 100; +} + +.option { + font-size: var(--font-size-small); + font-weight: normal; + background: var(--gray50); + padding: 4px 16px; + cursor: pointer; + white-space: nowrap; +} + +.option:hover { + background: var(--gray100); +} + +.float { + position: absolute; + min-width: 100px; +} + +.top { + bottom: 100%; + margin-bottom: 5px; +} + +.bottom { + top: 100%; + margin-top: 5px; +} + +.left { + left: 0; +} + +.right { + right: 0; +} + +.divider { + border-top: 1px solid var(--gray300); +} + +.selected { + font-weight: 600; +} diff --git a/components/common/MenuButton.js b/components/common/MenuButton.js new file mode 100644 index 0000000..eb8d62f --- /dev/null +++ b/components/common/MenuButton.js @@ -0,0 +1,86 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import Menu from 'components/common/Menu'; +import Button from 'components/common/Button'; +import useDocumentClick from 'hooks/useDocumentClick'; +import styles from './MenuButton.module.css'; + +function MenuButton({ + icon, + value, + options, + buttonClassName, + menuClassName, + menuPosition = 'bottom', + menuAlign = 'right', + onSelect, + renderValue, + hideLabel, +}) { + const [showMenu, setShowMenu] = useState(false); + const ref = useRef(); + const selectedOption = options.find(e => e.value === value); + + function handleSelect(value) { + onSelect(value); + setShowMenu(false); + } + + function toggleMenu() { + setShowMenu(state => !state); + } + + useDocumentClick(e => { + if (!ref.current?.contains(e.target)) { + setShowMenu(false); + } + }); + + return ( +
+ + {showMenu && ( + + )} +
+ ); +} + +MenuButton.propTypes = { + icon: PropTypes.node, + value: PropTypes.any, + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.node, + value: PropTypes.any, + className: PropTypes.string, + render: PropTypes.func, + divider: PropTypes.bool, + }), + ), + buttonClassName: PropTypes.string, + menuClassName: PropTypes.string, + menuPosition: PropTypes.oneOf(['top', 'bottom']), + menuAlign: PropTypes.oneOf(['left', 'right']), + onSelect: PropTypes.func, + renderValue: PropTypes.func, +}; + +export default MenuButton; diff --git a/components/common/MenuButton.module.css b/components/common/MenuButton.module.css new file mode 100644 index 0000000..7e9dd7e --- /dev/null +++ b/components/common/MenuButton.module.css @@ -0,0 +1,20 @@ +.container { + display: flex; + position: relative; + cursor: pointer; +} + +.button { + border: 1px solid transparent; + border-radius: 4px; +} + +.text { + font-size: var(--font-size-small); +} + +.open, +.open:hover { + background: var(--gray50); + border: 1px solid var(--gray500); +} diff --git a/components/common/Modal.js b/components/common/Modal.js new file mode 100644 index 0000000..694fba6 --- /dev/null +++ b/components/common/Modal.js @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import { useSpring, animated } from 'react-spring'; +import styles from './Modal.module.css'; + +function Modal({ title, children }) { + const props = useSpring({ opacity: 1, from: { opacity: 0 } }); + + return ReactDOM.createPortal( + +
+ {title &&
{title}
} +
{children}
+
+
, + document.getElementById('__modals'), + ); +} + +Modal.propTypes = { + title: PropTypes.node, + children: PropTypes.node, +}; + +export default Modal; diff --git a/components/common/Modal.module.css b/components/common/Modal.module.css new file mode 100644 index 0000000..bf2491c --- /dev/null +++ b/components/common/Modal.module.css @@ -0,0 +1,46 @@ +.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + z-index: 2; +} + +.modal:before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + background: #000; + opacity: 0.5; +} + +.content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--gray50); + min-width: 400px; + min-height: 100px; + max-width: 100vw; + z-index: 1; + border: 1px solid var(--gray300); + padding: 30px; + border-radius: 4px; +} + +.header { + font-weight: 600; + margin-bottom: 20px; +} + +.body { + display: flex; + flex-direction: column; +} diff --git a/components/common/NavMenu.js b/components/common/NavMenu.js new file mode 100644 index 0000000..82d97ff --- /dev/null +++ b/components/common/NavMenu.js @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useRouter } from 'next/router'; +import classNames from 'classnames'; +import styles from './NavMenu.module.css'; + +function NavMenu({ options = [], className, onSelect = () => {} }) { + const router = useRouter(); + + return ( +
+ {options + .filter(({ hidden }) => !hidden) + .map(option => { + const { label, value, className: customClassName, render } = option; + + return render ? ( + render(option) + ) : ( +
onSelect(value, e)} + > + {label} +
+ ); + })} +
+ ); +} + +NavMenu.propTypes = { + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.node, + value: PropTypes.any, + className: PropTypes.string, + render: PropTypes.func, + }), + ), + className: PropTypes.string, + onSelect: PropTypes.func, +}; +export default NavMenu; diff --git a/components/common/NavMenu.module.css b/components/common/NavMenu.module.css new file mode 100644 index 0000000..7be7397 --- /dev/null +++ b/components/common/NavMenu.module.css @@ -0,0 +1,22 @@ +.menu { + color: var(--gray800); + border: 1px solid var(--gray500); + border-radius: 4px; + overflow: hidden; + z-index: 2; +} + +.option { + padding: 8px 16px; + cursor: pointer; + border-radius: 4px; +} + +.option:hover { + background: var(--gray75); +} + +.selected { + color: var(--gray900); + font-weight: 600; +} diff --git a/components/common/NoData.js b/components/common/NoData.js new file mode 100644 index 0000000..9d52343 --- /dev/null +++ b/components/common/NoData.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { FormattedMessage } from 'react-intl'; +import styles from './NoData.module.css'; + +function NoData({ className }) { + return ( +
+ +
+ ); +} + +NoData.propTypes = { + className: PropTypes.string, +}; + +export default NoData; diff --git a/components/common/NoData.module.css b/components/common/NoData.module.css new file mode 100644 index 0000000..518fa48 --- /dev/null +++ b/components/common/NoData.module.css @@ -0,0 +1,11 @@ +.container { + color: var(--gray500); + font-size: var(--font-size-normal); + position: relative; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + width: 100%; + height: 100%; +} diff --git a/components/common/RefreshButton.js b/components/common/RefreshButton.js new file mode 100644 index 0000000..91a43ab --- /dev/null +++ b/components/common/RefreshButton.js @@ -0,0 +1,46 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import { setDateRange } from 'redux/actions/websites'; +import Button from './Button'; +import Refresh from 'assets/redo.svg'; +import Dots from 'assets/ellipsis-h.svg'; +import useDateRange from 'hooks/useDateRange'; +import { getDateRange } from '../../lib/date'; +import useLocale from 'hooks/useLocale'; + +function RefreshButton({ websiteId }) { + const dispatch = useDispatch(); + const { locale } = useLocale(); + const [dateRange] = useDateRange(websiteId); + const [loading, setLoading] = useState(false); + const completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]); + + function handleClick() { + if (dateRange) { + setLoading(true); + dispatch(setDateRange(websiteId, getDateRange(dateRange.value, locale))); + } + } + + useEffect(() => { + setLoading(false); + }, [completed]); + + return ( + + + +
+ ); +} diff --git a/components/common/UpdateNotice.module.css b/components/common/UpdateNotice.module.css new file mode 100644 index 0000000..52a97c3 --- /dev/null +++ b/components/common/UpdateNotice.module.css @@ -0,0 +1,13 @@ +.notice { + display: flex; + justify-content: center; + align-items: center; + padding-top: 10px; + font-size: var(--font-size-small); + font-weight: 600; +} + +.message { + text-align: center; + margin-right: 20px; +} diff --git a/components/common/WorldMap.js b/components/common/WorldMap.js new file mode 100644 index 0000000..6dc8a35 --- /dev/null +++ b/components/common/WorldMap.js @@ -0,0 +1,103 @@ +import React, { useState, useMemo } from 'react'; +import { useRouter } from 'next/router'; +import PropTypes from 'prop-types'; +import ReactTooltip from 'react-tooltip'; +import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps'; +import classNames from 'classnames'; +import tinycolor from 'tinycolor2'; +import useTheme from 'hooks/useTheme'; +import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants'; +import styles from './WorldMap.module.css'; +import useCountryNames from 'hooks/useCountryNames'; +import useLocale from 'hooks/useLocale'; + +function WorldMap({ data, className }) { + const { basePath } = useRouter(); + const [tooltip, setTooltip] = useState(); + const [theme] = useTheme(); + const colors = useMemo( + () => ({ + baseColor: THEME_COLORS[theme].primary, + fillColor: THEME_COLORS[theme].gray100, + strokeColor: THEME_COLORS[theme].primary, + hoverColor: THEME_COLORS[theme].primary, + }), + [theme], + ); + const { locale } = useLocale(); + const countryNames = useCountryNames(locale); + + function getFillColor(code) { + if (code === 'AQ') return; + const country = data?.find(({ x }) => x === code); + + if (!country) { + return colors.fillColor; + } + + return tinycolor(colors.baseColor)[theme === 'light' ? 'lighten' : 'darken']( + 40 * (1.0 - country.z / 100), + ); + } + + function getOpacity(code) { + return code === 'AQ' ? 0 : 1; + } + + function handleHover(code) { + if (code === 'AQ') return; + const country = data?.find(({ x }) => x === code); + setTooltip(`${countryNames[code]}: ${country?.y || 0} visitors`); + } + + return ( +
+ + + + {({ geographies }) => { + return geographies.map(geo => { + const code = ISO_COUNTRIES[geo.id]; + + return ( + handleHover(code)} + onMouseOut={() => setTooltip(null)} + /> + ); + }); + }} + + + + {tooltip} +
+ ); +} + +WorldMap.propTypes = { + data: PropTypes.arrayOf( + PropTypes.shape({ + x: PropTypes.string, + y: PropTypes.number, + z: PropTypes.number, + }), + ), + className: PropTypes.string, +}; + +export default WorldMap; diff --git a/components/common/WorldMap.module.css b/components/common/WorldMap.module.css new file mode 100644 index 0000000..c252803 --- /dev/null +++ b/components/common/WorldMap.module.css @@ -0,0 +1,4 @@ +.container { + overflow: hidden; + position: relative; +} diff --git a/components/forms/AccountEditForm.js b/components/forms/AccountEditForm.js new file mode 100644 index 0000000..4b185f1 --- /dev/null +++ b/components/forms/AccountEditForm.js @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Formik, Form, Field } from 'formik'; +import Button from 'components/common/Button'; +import FormLayout, { + FormButtons, + FormError, + FormMessage, + FormRow, +} from 'components/layout/FormLayout'; +import usePost from 'hooks/usePost'; + +const initialValues = { + username: '', + password: '', +}; + +const validate = ({ user_id, username, password }) => { + const errors = {}; + + if (!username) { + errors.username = ; + } + if (!user_id && !password) { + errors.password = ; + } + + return errors; +}; + +export default function AccountEditForm({ values, onSave, onClose }) { + const post = usePost(); + const [message, setMessage] = useState(); + + const handleSubmit = async values => { + const { ok, data } = await post('/api/account', values); + + if (ok) { + onSave(); + } else { + setMessage( + data || , + ); + } + }; + + return ( + + + {() => ( +
+ + +
+ + +
+
+ + +
+ + +
+
+ + + + + {message} +
+ )} +
+
+ ); +} diff --git a/components/forms/ChangePasswordForm.js b/components/forms/ChangePasswordForm.js new file mode 100644 index 0000000..d62b553 --- /dev/null +++ b/components/forms/ChangePasswordForm.js @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Formik, Form, Field } from 'formik'; +import Button from 'components/common/Button'; +import FormLayout, { + FormButtons, + FormError, + FormMessage, + FormRow, +} from 'components/layout/FormLayout'; +import usePost from 'hooks/usePost'; + +const initialValues = { + current_password: '', + new_password: '', + confirm_password: '', +}; + +const validate = ({ current_password, new_password, confirm_password }) => { + const errors = {}; + + if (!current_password) { + errors.current_password = ; + } + if (!new_password) { + errors.new_password = ; + } + if (!confirm_password) { + errors.confirm_password = ; + } else if (new_password !== confirm_password) { + errors.confirm_password = ( + + ); + } + + return errors; +}; + +export default function ChangePasswordForm({ values, onSave, onClose }) { + const post = usePost(); + const [message, setMessage] = useState(); + + const handleSubmit = async values => { + const { ok, data } = await post('/api/account/password', values); + + if (ok) { + onSave(); + } else { + setMessage( + data || , + ); + } + }; + + return ( + + + {() => ( +
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + + + + {message} +
+ )} +
+
+ ); +} diff --git a/components/forms/DatePickerForm.js b/components/forms/DatePickerForm.js new file mode 100644 index 0000000..9669f74 --- /dev/null +++ b/components/forms/DatePickerForm.js @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { isAfter, isBefore, isSameDay } from 'date-fns'; +import Calendar from 'components/common/Calendar'; +import Button from 'components/common/Button'; +import { FormButtons } from 'components/layout/FormLayout'; +import { getDateRangeValues } from 'lib/date'; +import styles from './DatePickerForm.module.css'; +import ButtonGroup from 'components/common/ButtonGroup'; + +const FILTER_DAY = 0; +const FILTER_RANGE = 1; + +export default function DatePickerForm({ + startDate: defaultStartDate, + endDate: defaultEndDate, + minDate, + maxDate, + onChange, + onClose, +}) { + const [selected, setSelected] = useState( + isSameDay(defaultStartDate, defaultEndDate) ? FILTER_DAY : FILTER_RANGE, + ); + const [date, setDate] = useState(defaultStartDate); + const [startDate, setStartDate] = useState(defaultStartDate); + const [endDate, setEndDate] = useState(defaultEndDate); + + const disabled = + selected === FILTER_DAY + ? isAfter(minDate, date) && isBefore(maxDate, date) + : isAfter(startDate, endDate); + + const buttons = [ + { + label: , + value: FILTER_DAY, + }, + { + label: , + value: FILTER_RANGE, + }, + ]; + + function handleSave() { + if (selected === FILTER_DAY) { + onChange({ ...getDateRangeValues(date, date), value: 'custom' }); + } else { + onChange({ ...getDateRangeValues(startDate, endDate), value: 'custom' }); + } + } + + return ( +
+
+ +
+
+ {selected === FILTER_DAY ? ( + + ) : ( + <> + + + + )} +
+ + + + +
+ ); +} diff --git a/components/forms/DatePickerForm.module.css b/components/forms/DatePickerForm.module.css new file mode 100644 index 0000000..96e2d2e --- /dev/null +++ b/components/forms/DatePickerForm.module.css @@ -0,0 +1,40 @@ +.container { + display: flex; + flex-direction: column; + max-width: 100vw; +} + +.calendars { + display: flex; + justify-content: center; +} + +.calendars > div { + width: 380px; +} + +.calendars > div + div { + margin-left: 20px; + padding-left: 20px; + border-left: 1px solid var(--gray300); +} + +.filter { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 20px; +} + +@media only screen and (max-width: 768px) { + .calendars { + flex-direction: column; + } + + .calendars > div + div { + padding: 0; + margin-left: 0; + margin-top: 20px; + border: 0; + } +} diff --git a/components/forms/DeleteForm.js b/components/forms/DeleteForm.js new file mode 100644 index 0000000..7860030 --- /dev/null +++ b/components/forms/DeleteForm.js @@ -0,0 +1,98 @@ +import React, { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Formik, Form, Field } from 'formik'; +import Button from 'components/common/Button'; +import FormLayout, { + FormButtons, + FormError, + FormMessage, + FormRow, +} from 'components/layout/FormLayout'; +import useDelete from 'hooks/useDelete'; + +const CONFIRMATION_WORD = 'DELETE'; + +const validate = ({ confirmation }) => { + const errors = {}; + + if (confirmation !== CONFIRMATION_WORD) { + errors.confirmation = !confirmation ? ( + + ) : ( + + ); + } + + return errors; +}; + +export default function DeleteForm({ values, onSave, onClose }) { + const del = useDelete(); + const [message, setMessage] = useState(); + + const handleSubmit = async ({ type, id }) => { + const { ok, data } = await del(`/api/${type}/${id}`); + + if (ok) { + onSave(); + } else { + setMessage( + data || , + ); + } + }; + + return ( + + + {props => ( +
+
+ {values.name} }} + /> +
+
+ +
+

+ {CONFIRMATION_WORD} }} + /> +

+ +
+ + +
+
+ + + + + {message} +
+ )} +
+
+ ); +} diff --git a/components/forms/LoginForm.js b/components/forms/LoginForm.js new file mode 100644 index 0000000..8ac3a55 --- /dev/null +++ b/components/forms/LoginForm.js @@ -0,0 +1,102 @@ +import React, { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Formik, Form, Field } from 'formik'; +import { useRouter } from 'next/router'; +import Button from 'components/common/Button'; +import FormLayout, { + FormButtons, + FormError, + FormMessage, + FormRow, +} from 'components/layout/FormLayout'; +import Icon from 'components/common/Icon'; +import Logo from 'assets/logo.svg'; +import styles from './LoginForm.module.css'; +import usePost from 'hooks/usePost'; + +const validate = ({ username, password }) => { + const errors = {}; + + if (!username) { + errors.username = ; + } + if (!password) { + errors.password = ; + } + + return errors; +}; + +export default function LoginForm() { + const post = usePost(); + const router = useRouter(); + const [message, setMessage] = useState(); + + const handleSubmit = async ({ username, password }) => { + const { ok, status, data } = await post('/api/auth/login', { + username, + password, + }); + + if (ok) { + return router.push('/'); + } else { + setMessage( + status === 401 ? ( + + ) : ( + data + ), + ); + } + }; + + return ( + + + {() => ( +
+
+ } size="xlarge" className={styles.icon} /> +

umami

+
+ + +
+ + +
+
+ + +
+ + +
+
+ + + + {message} +
+ )} +
+
+ ); +} diff --git a/components/forms/LoginForm.module.css b/components/forms/LoginForm.module.css new file mode 100644 index 0000000..dfd5456 --- /dev/null +++ b/components/forms/LoginForm.module.css @@ -0,0 +1,23 @@ +.login { + display: flex; + flex-direction: column; + margin-top: 80px; +} + +.login form { + margin: 0 auto; +} + +.icon { + display: flex; + justify-content: center; + margin: 0 auto; +} + +.header { + margin-bottom: 30px; +} + +.header h1 { + margin: 12px 0; +} diff --git a/components/forms/ResetForm.js b/components/forms/ResetForm.js new file mode 100644 index 0000000..791039a --- /dev/null +++ b/components/forms/ResetForm.js @@ -0,0 +1,98 @@ +import React, { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Formik, Form, Field } from 'formik'; +import Button from 'components/common/Button'; +import FormLayout, { + FormButtons, + FormError, + FormMessage, + FormRow, +} from 'components/layout/FormLayout'; +import usePost from 'hooks/usePost'; + +const CONFIRMATION_WORD = 'RESET'; + +const validate = ({ confirmation }) => { + const errors = {}; + + if (confirmation !== CONFIRMATION_WORD) { + errors.confirmation = !confirmation ? ( + + ) : ( + + ); + } + + return errors; +}; + +export default function ResetForm({ values, onSave, onClose }) { + const post = usePost(); + const [message, setMessage] = useState(); + + const handleSubmit = async ({ type, id }) => { + const { ok, data } = await post(`/api/${type}/${id}/reset`); + + if (ok) { + onSave(); + } else { + setMessage( + data || , + ); + } + }; + + return ( + + + {props => ( +
+
+ {values.name} }} + /> +
+
+ +
+

+ {CONFIRMATION_WORD} }} + /> +

+ +
+ + +
+
+ + + + + {message} +
+ )} +
+
+ ); +} diff --git a/components/forms/ShareUrlForm.js b/components/forms/ShareUrlForm.js new file mode 100644 index 0000000..a77d3fb --- /dev/null +++ b/components/forms/ShareUrlForm.js @@ -0,0 +1,42 @@ +import React, { useRef } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { useRouter } from 'next/router'; +import Button from 'components/common/Button'; +import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout'; +import CopyButton from 'components/common/CopyButton'; + +export default function TrackingCodeForm({ values, onClose }) { + const ref = useRef(); + const { basePath } = useRouter(); + const { name, share_id } = values; + + return ( + +

+ {values.name} }} + /> +

+ +