diff --git a/.eslintrc.json b/.eslintrc.json index a77ed5bd..9d747b87 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,14 +4,6 @@ "es2020": true, "node": true }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 11, - "sourceType": "module" - }, "extends": [ "eslint:recommended", "plugin:prettier/recommended", @@ -19,6 +11,14 @@ "plugin:@typescript-eslint/recommended", "next" ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 11, + "sourceType": "module" + }, "plugins": ["@typescript-eslint", "prettier"], "settings": { "import/resolver": { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 775f9ecf..66e16a03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,6 @@ jobs: strategy: matrix: include: - - node-version: 16.x - db-type: postgresql - - node-version: 16.x - db-type: mysql - node-version: 18.x db-type: postgresql - node-version: 18.x diff --git a/Dockerfile b/Dockerfile index 12951a73..801b2bc2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,9 @@ ENV NEXT_TELEMETRY_DISABLED 1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -RUN yarn add npm-run-all dotenv prisma semver +RUN set -x \ + && apk add --no-cache curl \ + && yarn add npm-run-all dotenv prisma semver # You only need to copy next.config.js if you are NOT using the default configuration COPY --from=builder /app/next.config.js . diff --git a/docker-compose.yml b/docker-compose.yml index b8da9373..08f00b7c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,11 @@ services: db: condition: service_healthy restart: always + healthcheck: + test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"] + interval: 5s + timeout: 5s + retries: 5 db: image: postgres:15-alpine environment: diff --git a/next.config.js b/next.config.js index cf7dce7f..a155ece7 100644 --- a/next.config.js +++ b/next.config.js @@ -3,27 +3,26 @@ require('dotenv').config(); const path = require('path'); const pkg = require('./package.json'); -const contentSecurityPolicy = ` - default-src 'self'; - img-src *; - script-src 'self' 'unsafe-eval' 'unsafe-inline'; - style-src 'self' 'unsafe-inline'; - connect-src 'self' api.umami.is; - frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS}; -`; +const contentSecurityPolicy = [ + `default-src 'self'`, + `img-src *`, + `script-src 'self' 'unsafe-eval' 'unsafe-inline'`, + `style-src 'self' 'unsafe-inline'`, + `connect-src 'self' api.umami.is`, + `frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS || ''}`, +]; const headers = [ { key: 'X-DNS-Prefetch-Control', value: 'on', }, - { - key: 'X-Frame-Options', - value: 'SAMEORIGIN', - }, { key: 'Content-Security-Policy', - value: contentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(), + value: contentSecurityPolicy + .join(';') + .replace(/\s{2,}/g, ' ') + .trim(), }, ]; @@ -81,14 +80,14 @@ const config = { reactStrictMode: false, env: { basePath: basePath || '', - cloudMode: !!process.env.CLOUD_MODE, - cloudUrl: process.env.CLOUD_URL, + cloudMode: process.env.CLOUD_MODE || '', + cloudUrl: process.env.CLOUD_URL || '', configUrl: '/config', currentVersion: pkg.version, - defaultLocale: process.env.DEFAULT_LOCALE, - disableLogin: process.env.DISABLE_LOGIN, - disableUI: process.env.DISABLE_UI, - isProduction: process.env.NODE_ENV === 'production', + defaultLocale: process.env.DEFAULT_LOCALE || '', + disableLogin: process.env.DISABLE_LOGIN || '', + disableUI: process.env.DISABLE_UI || '', + hostUrl: process.env.HOST_URL || '', }, basePath, output: 'standalone', diff --git a/package.json b/package.json index 9b053e30..0f437c35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.8.0", + "version": "2.9.0", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Mike Cao ", "license": "MIT", @@ -63,11 +63,12 @@ "dependencies": { "@clickhouse/client": "^0.2.2", "@fontsource/inter": "^4.5.15", - "@prisma/client": "5.3.1", + "@prisma/client": "5.6.0", + "@prisma/extension-read-replicas": "^0.3.0", "@react-spring/web": "^9.7.3", - "@tanstack/react-query": "^4.33.0", - "@umami/prisma-client": "^0.3.0", - "@umami/redis-client": "^0.16.0", + "@tanstack/react-query": "^5.12.2", + "@umami/prisma-client": "^0.8.0", + "@umami/redis-client": "^0.18.0", "chalk": "^4.1.1", "chart.js": "^4.2.1", "chartjs-adapter-date-fns": "^3.0.0", @@ -92,17 +93,17 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "^13.5.3", - "next-basics": "^0.37.0", + "next": "14.0.4", + "next-basics": "^0.39.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", - "prisma": "5.3.1", + "prisma": "5.6.0", "react": "^18.2.0", - "react-basics": "^0.105.0", + "react-basics": "^0.114.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", - "react-intl": "^6.4.7", + "react-intl": "^6.5.5", "react-simple-maps": "^2.3.0", "react-use-measure": "^2.0.4", "react-window": "^1.8.6", @@ -125,9 +126,10 @@ "@rollup/plugin-replace": "^5.0.2", "@svgr/rollup": "^8.1.0", "@svgr/webpack": "^8.1.0", - "@types/node": "^18.11.9", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.8", + "@types/node": "^20.9.0", + "@types/react": "^18.2.41", + "@types/react-dom": "^18.2.17", + "@types/react-window": "^1.8.8", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", "cross-env": "^7.0.3", diff --git a/src/app/(main)/Shell.tsx b/src/app/(main)/App.tsx similarity index 53% rename from src/app/(main)/Shell.tsx rename to src/app/(main)/App.tsx index 980abb62..4b093165 100644 --- a/src/app/(main)/Shell.tsx +++ b/src/app/(main)/App.tsx @@ -1,14 +1,23 @@ 'use client'; +import { Loading } from 'react-basics'; import Script from 'next/script'; import { usePathname } from 'next/navigation'; -import UpdateNotice from 'components/common/UpdateNotice'; -import { useRequireLogin, useConfig } from 'components/hooks'; +import { useLogin, useConfig } from 'components/hooks'; +import UpdateNotice from './UpdateNotice'; -export function Shell({ children }) { - const { user } = useRequireLogin(); +export function App({ children }) { + const { user, isLoading, error } = useLogin(); const config = useConfig(); const pathname = usePathname(); + if (isLoading) { + return ; + } + + if (error) { + window.location.href = `${process.env.basePath || ''}/login`; + } + if (!user || !config) { return null; } @@ -24,4 +33,4 @@ export function Shell({ children }) { ); } -export default Shell; +export default App; diff --git a/src/app/(main)/NavBar.js b/src/app/(main)/NavBar.tsx similarity index 64% rename from src/app/(main)/NavBar.js rename to src/app/(main)/NavBar.tsx index 211adf5f..e241a059 100644 --- a/src/app/(main)/NavBar.js +++ b/src/app/(main)/NavBar.tsx @@ -14,6 +14,7 @@ import styles from './NavBar.module.css'; export function NavBar() { const pathname = usePathname(); const { formatMessage, labels } = useMessages(); + const cloudMode = Boolean(process.env.cloudMode); const links = [ { label: formatMessage(labels.dashboard), url: '/dashboard' }, @@ -22,6 +23,40 @@ export function NavBar() { { label: formatMessage(labels.settings), url: '/settings' }, ].filter(n => n); + const menuItems = [ + { + label: formatMessage(labels.dashboard), + url: '/dashboard', + }, + !cloudMode && { + label: formatMessage(labels.settings), + url: '/settings', + children: [ + { + label: formatMessage(labels.websites), + url: '/settings/websites', + }, + { + label: formatMessage(labels.teams), + url: '/settings/teams', + }, + { + label: formatMessage(labels.users), + url: '/settings/users', + }, + { + label: formatMessage(labels.profile), + url: '/settings/profile', + }, + ], + }, + cloudMode && { + label: formatMessage(labels.profile), + url: '/settings/profile', + }, + !cloudMode && { label: formatMessage(labels.logout), url: '/logout' }, + ].filter(n => n); + return (
@@ -49,7 +84,7 @@ export function NavBar() {
- +
); diff --git a/src/components/common/UpdateNotice.module.css b/src/app/(main)/UpdateNotice.module.css similarity index 100% rename from src/components/common/UpdateNotice.module.css rename to src/app/(main)/UpdateNotice.module.css diff --git a/src/components/common/UpdateNotice.js b/src/app/(main)/UpdateNotice.tsx similarity index 100% rename from src/components/common/UpdateNotice.js rename to src/app/(main)/UpdateNotice.tsx diff --git a/src/app/(main)/console/TestConsole.js b/src/app/(main)/console/TestConsole.tsx similarity index 82% rename from src/app/(main)/console/TestConsole.js rename to src/app/(main)/console/TestConsole.tsx index b88bfd77..0bb807ff 100644 --- a/src/app/(main)/console/TestConsole.js +++ b/src/app/(main)/console/TestConsole.tsx @@ -1,30 +1,33 @@ 'use client'; +import { Button } from 'react-basics'; +import Head from 'next/head'; +import Link from 'next/link'; +import Script from 'next/script'; import WebsiteSelect from 'components/input/WebsiteSelect'; import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; import EventsChart from 'components/metrics/EventsChart'; -import WebsiteChart from '../../(main)/websites/[id]/WebsiteChart'; +import WebsiteChart from 'app/(main)/websites/[id]/WebsiteChart'; import useApi from 'components/hooks/useApi'; -import Head from 'next/head'; -import Link from 'next/link'; import useNavigation from 'components/hooks/useNavigation'; -import Script from 'next/script'; -import { Button } from 'react-basics'; import styles from './TestConsole.module.css'; -export function TestConsole({ websiteId }) { +export function TestConsole({ websiteId }: { websiteId: string }) { const { get, useQuery } = useApi(); - const { data, isLoading, error } = useQuery(['websites:me'], () => get('/me/websites')); + const { data, isLoading, error } = useQuery({ + queryKey: ['websites:me'], + queryFn: () => get('/me/websites'), + }); const { router } = useNavigation(); - function handleChange(value) { + function handleChange(value: string) { router.push(`/console/${value}`); } function handleClick() { - window.umami.track({ url: '/page-view', referrer: 'https://www.google.com' }); - window.umami.track('track-event-no-data'); - window.umami.track('track-event-with-data', { + window['umami'].track({ url: '/page-view', referrer: 'https://www.google.com' }); + window['umami'].track('track-event-no-data'); + window['umami'].track('track-event-with-data', { test: 'test-data', boolean: true, booleanError: 'true', @@ -44,7 +47,7 @@ export function TestConsole({ websiteId }) { } function handleIdentifyClick() { - window.umami.identify({ + window['umami'].identify({ userId: 123, name: 'brian', number: Math.random() * 100, @@ -71,7 +74,7 @@ export function TestConsole({ websiteId }) { const website = data?.data.find(({ id }) => websiteId === id); return ( - + {website ? `${website.name} | Umami Console` : 'Umami Console'} @@ -113,7 +116,7 @@ export function TestConsole({ websiteId }) {
Click events
-

@@ -122,18 +125,18 @@ export function TestConsole({ websiteId }) { data-umami-event="button-click" data-umami-event-name="bob" data-umami-event-id="123" - variant="action" + variant="primary" > Send event with data

Javascript events
-

-

diff --git a/src/app/(main)/dashboard/Dashboard.js b/src/app/(main)/dashboard/Dashboard.tsx similarity index 76% rename from src/app/(main)/dashboard/Dashboard.js rename to src/app/(main)/dashboard/Dashboard.tsx index 5fb65f23..ec1d793c 100644 --- a/src/app/(main)/dashboard/Dashboard.js +++ b/src/app/(main)/dashboard/Dashboard.tsx @@ -11,23 +11,34 @@ import useApi from 'components/hooks/useApi'; import useDashboard from 'store/dashboard'; import useMessages from 'components/hooks/useMessages'; import useLocale from 'components/hooks/useLocale'; -import useApiFilter from 'components/hooks/useApiFilter'; +import useFilterQuery from 'components/hooks/useFilterQuery'; +import { useUser } from 'components/hooks'; export function Dashboard() { const { formatMessage, labels, messages } = useMessages(); + const { user } = useUser(); const { showCharts, editing } = useDashboard(); const { dir } = useLocale(); - const { get, useQuery } = useApi(); - const { page, handlePageChange } = useApiFilter(); + const { get } = useApi(); const pageSize = 10; - const { data: result, isLoading } = useQuery(['websites', page, pageSize], () => - get('/websites', { includeTeams: 1, page, pageSize }), - ); - const { data, count } = result || {}; - const hasData = data && data?.length !== 0; - if (isLoading) { - return ; + const { query, params, setParams, result } = useFilterQuery({ + queryKey: ['dashboard:websites'], + queryFn: (params: any) => { + return get(`/users/${user.id}/websites`, { ...params, includeTeams: true, pageSize }); + }, + }); + + const handlePageChange = (page: number) => { + setParams({ ...params, page }); + }; + + const { data, count } = result || {}; + const hasData = !!(data as any)?.length; + const { page } = params; + + if (query.isLoading) { + return ; } return ( diff --git a/src/app/(main)/dashboard/DashboardEdit.js b/src/app/(main)/dashboard/DashboardEdit.tsx similarity index 91% rename from src/app/(main)/dashboard/DashboardEdit.js rename to src/app/(main)/dashboard/DashboardEdit.tsx index 3af33867..35638038 100644 --- a/src/app/(main)/dashboard/DashboardEdit.js +++ b/src/app/(main)/dashboard/DashboardEdit.tsx @@ -17,7 +17,10 @@ export function DashboardEdit() { const { formatMessage, labels } = useMessages(); const [order, setOrder] = useState(websiteOrder || []); const { get, useQuery } = useApi(); - const { data: result } = useQuery(['websites'], () => get('/websites', { includeTeams: 1 })); + const { data: result } = useQuery({ + queryKey: ['websites'], + queryFn: () => get('/websites', { includeTeams: 1 }), + }); const { data: websites } = result || {}; const ordered = useMemo(() => { @@ -57,13 +60,13 @@ export function DashboardEdit() { return ( <>
- - -
diff --git a/src/app/(main)/dashboard/DashboardSettingsButton.js b/src/app/(main)/dashboard/DashboardSettingsButton.tsx similarity index 100% rename from src/app/(main)/dashboard/DashboardSettingsButton.js rename to src/app/(main)/dashboard/DashboardSettingsButton.tsx diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx index 91cc9c6e..1853a9f5 100644 --- a/src/app/(main)/dashboard/page.tsx +++ b/src/app/(main)/dashboard/page.tsx @@ -1,7 +1,7 @@ import Dashboard from 'app/(main)/dashboard/Dashboard'; import { Metadata } from 'next'; -export default function DashboardPage() { +export default function () { return ; } diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 1c9cc277..f5aeab67 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -1,11 +1,11 @@ -import Shell from './Shell'; +import App from './App'; import NavBar from './NavBar'; import Page from 'components/layout/Page'; import styles from './layout.module.css'; -export default function AppLayout({ children }) { +export default function ({ children }) { return ( - +
-
+ ); } diff --git a/src/app/(main)/reports/ReportDeleteButton.js b/src/app/(main)/reports/ReportDeleteButton.tsx similarity index 73% rename from src/app/(main)/reports/ReportDeleteButton.js rename to src/app/(main)/reports/ReportDeleteButton.tsx index 35809a98..32ec819e 100644 --- a/src/app/(main)/reports/ReportDeleteButton.js +++ b/src/app/(main)/reports/ReportDeleteButton.tsx @@ -3,13 +3,21 @@ import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm'; import { useApi, useMessages } from 'components/hooks'; import { setValue } from 'store/cache'; -export function ReportDeleteButton({ reportId, reportName, onDelete }) { +export function ReportDeleteButton({ + reportId, + reportName, + onDelete, +}: { + reportId: string; + reportName: string; + onDelete?: () => void; +}) { const { formatMessage, labels } = useMessages(); const { del, useMutation } = useApi(); - const { mutate } = useMutation(reportId => del(`/reports/${reportId}`)); + const { mutate } = useMutation({ mutationFn: reportId => del(`/reports/${reportId}`) }); - const handleConfirm = close => { - mutate(reportId, { + const handleConfirm = (close: () => void) => { + mutate(reportId as any, { onSuccess: () => { setValue('reports', Date.now()); onDelete?.(); diff --git a/src/app/(main)/reports/ReportsDataTable.js b/src/app/(main)/reports/ReportsDataTable.js deleted file mode 100644 index 0daa3d06..00000000 --- a/src/app/(main)/reports/ReportsDataTable.js +++ /dev/null @@ -1,20 +0,0 @@ -'use client'; -import { useApi } from 'components/hooks'; -import ReportsTable from './ReportsTable'; -import useFilterQuery from 'components/hooks/useFilterQuery'; -import DataTable from 'components/common/DataTable'; -import useCache from 'store/cache'; - -export default function ReportsDataTable({ websiteId }) { - const { get } = useApi(); - const modified = useCache(state => state?.reports); - const queryResult = useFilterQuery(['reports', { websiteId, modified }], params => - get(websiteId ? `/websites/${websiteId}/reports` : `/reports`, params), - ); - - return ( - - {({ data }) => } - - ); -} diff --git a/src/app/(main)/reports/ReportsDataTable.tsx b/src/app/(main)/reports/ReportsDataTable.tsx new file mode 100644 index 00000000..3ede4783 --- /dev/null +++ b/src/app/(main)/reports/ReportsDataTable.tsx @@ -0,0 +1,14 @@ +'use client'; +import { useReports } from 'components/hooks'; +import ReportsTable from './ReportsTable'; +import DataTable from 'components/common/DataTable'; + +export default function ReportsDataTable({ websiteId }: { websiteId?: string }) { + const queryResult = useReports(websiteId); + + return ( + + {({ data }) => } + + ); +} diff --git a/src/app/(main)/reports/ReportsHeader.js b/src/app/(main)/reports/ReportsHeader.tsx similarity index 100% rename from src/app/(main)/reports/ReportsHeader.js rename to src/app/(main)/reports/ReportsHeader.tsx diff --git a/src/app/(main)/reports/ReportsTable.js b/src/app/(main)/reports/ReportsTable.tsx similarity index 94% rename from src/app/(main)/reports/ReportsTable.js rename to src/app/(main)/reports/ReportsTable.tsx index 6b2a7932..882110ee 100644 --- a/src/app/(main)/reports/ReportsTable.js +++ b/src/app/(main)/reports/ReportsTable.tsx @@ -5,7 +5,7 @@ import useUser from 'components/hooks/useUser'; import { REPORT_TYPES } from 'lib/constants'; import ReportDeleteButton from './ReportDeleteButton'; -export function ReportsTable({ data = [], showDomain }) { +export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); const breakpoint = useBreakpoint(); diff --git a/src/app/(main)/reports/[id]/BaseParameters.js b/src/app/(main)/reports/[id]/BaseParameters.tsx similarity index 83% rename from src/app/(main)/reports/[id]/BaseParameters.js rename to src/app/(main)/reports/[id]/BaseParameters.tsx index a54ef4f3..9b6be5d8 100644 --- a/src/app/(main)/reports/[id]/BaseParameters.js +++ b/src/app/(main)/reports/[id]/BaseParameters.tsx @@ -6,12 +6,19 @@ import WebsiteSelect from 'components/input/WebsiteSelect'; import { useMessages } from 'components/hooks'; import { ReportContext } from './Report'; +export interface BaseParametersProps { + showWebsiteSelect?: boolean; + allowWebsiteSelect?: boolean; + showDateSelect?: boolean; + allowDateSelect?: boolean; +} + export function BaseParameters({ showWebsiteSelect = true, allowWebsiteSelect = true, showDateSelect = true, allowDateSelect = true, -}) { +}: BaseParametersProps) { const { report, updateReport } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); @@ -19,11 +26,11 @@ export function BaseParameters({ const { websiteId, dateRange } = parameters || {}; const { value, startDate, endDate } = dateRange || {}; - const handleWebsiteSelect = websiteId => { + const handleWebsiteSelect = (websiteId: string) => { updateReport({ websiteId, parameters: { websiteId } }); }; - const handleDateChange = value => { + const handleDateChange = (value: string) => { updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } }); }; diff --git a/src/app/(main)/reports/[id]/FieldAddForm.js b/src/app/(main)/reports/[id]/FieldAddForm.tsx similarity index 76% rename from src/app/(main)/reports/[id]/FieldAddForm.js rename to src/app/(main)/reports/[id]/FieldAddForm.tsx index 6923bceb..9db472d8 100644 --- a/src/app/(main)/reports/[id]/FieldAddForm.js +++ b/src/app/(main)/reports/[id]/FieldAddForm.tsx @@ -7,10 +7,20 @@ import FieldAggregateForm from './FieldAggregateForm'; import FieldFilterForm from './FieldFilterForm'; import styles from './FieldAddForm.module.css'; -export function FieldAddForm({ fields = [], group, onAdd, onClose }) { - const [selected, setSelected] = useState(); +export function FieldAddForm({ + fields = [], + group, + onAdd, + onClose, +}: { + fields?: any[]; + group: string; + onAdd: (group: string, value: string) => void; + onClose: () => void; +}) { + const [selected, setSelected] = useState<{ name: string; type: string; value: string }>(); - const handleSelect = value => { + const handleSelect = (value: any) => { const { type } = value; if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') { @@ -22,7 +32,7 @@ export function FieldAddForm({ fields = [], group, onAdd, onClose }) { setSelected(value); }; - const handleSave = value => { + const handleSave = (value: any) => { onAdd(group, value); onClose(); }; diff --git a/src/app/(main)/reports/[id]/FieldAggregateForm.js b/src/app/(main)/reports/[id]/FieldAggregateForm.tsx similarity index 86% rename from src/app/(main)/reports/[id]/FieldAggregateForm.js rename to src/app/(main)/reports/[id]/FieldAggregateForm.tsx index 34b67980..6b5bf636 100644 --- a/src/app/(main)/reports/[id]/FieldAggregateForm.js +++ b/src/app/(main)/reports/[id]/FieldAggregateForm.tsx @@ -1,7 +1,15 @@ import { Form, FormRow, Menu, Item } from 'react-basics'; import { useMessages } from 'components/hooks'; -export default function FieldAggregateForm({ name, type, onSelect }) { +export default function FieldAggregateForm({ + name, + type, + onSelect, +}: { + name: string; + type: string; + onSelect: (key: any) => void; +}) { const { formatMessage, labels } = useMessages(); const options = { @@ -27,7 +35,7 @@ export default function FieldAggregateForm({ name, type, onSelect }) { const items = options[type]; - const handleSelect = value => { + const handleSelect = (value: any) => { onSelect({ name, type, value }); }; diff --git a/src/app/(main)/reports/[id]/FieldFilterForm.js b/src/app/(main)/reports/[id]/FieldFilterForm.tsx similarity index 61% rename from src/app/(main)/reports/[id]/FieldFilterForm.js rename to src/app/(main)/reports/[id]/FieldFilterForm.tsx index 96ac06b0..4af7febf 100644 --- a/src/app/(main)/reports/[id]/FieldFilterForm.js +++ b/src/app/(main)/reports/[id]/FieldFilterForm.tsx @@ -1,8 +1,17 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics'; -import { useMessages, useFilters, useFormat } from 'components/hooks'; +import { useMessages, useFilters, useFormat, useLocale } from 'components/hooks'; import styles from './FieldFilterForm.module.css'; +export interface FieldFilterFormProps { + name: string; + label?: string; + type: string; + values?: any[]; + onSelect?: (key: any) => void; + allowFilterSelect?: boolean; +} + export default function FieldFilterForm({ name, label, @@ -10,20 +19,36 @@ export default function FieldFilterForm({ values, onSelect, allowFilterSelect = true, -}) { +}: FieldFilterFormProps) { const { formatMessage, labels } = useMessages(); const [filter, setFilter] = useState('eq'); const [value, setValue] = useState(); const { getFilters } = useFilters(); const { formatValue } = useFormat(); + const { locale } = useLocale(); const filters = getFilters(type); + const formattedValues = useMemo(() => { + const formatted = {}; + const format = (val: string) => { + formatted[val] = formatValue(val, name); + return formatted[val]; + }; + if (values.length !== 1) { + const { compare } = new Intl.Collator(locale, { numeric: true }); + values.sort((a, b) => compare(formatted[a] ?? format(a), formatted[b] ?? format(b))); + } else { + format(values[0]); + } + return formatted; + }, [values]); + const renderFilterValue = value => { return filters.find(f => f.value === value)?.label; }; const renderValue = value => { - return formatValue(value, name); + return formattedValues[value]; }; const handleAdd = () => { @@ -40,7 +65,7 @@ export default function FieldFilterForm({ items={filters} value={filter} renderValue={renderFilterValue} - onChange={setFilter} + onChange={(key: any) => setFilter(key)} > {({ value, label }) => { return {label}; @@ -53,13 +78,13 @@ export default function FieldFilterForm({ items={values} value={value} renderValue={renderValue} - onChange={setValue} + onChange={(key: any) => setValue(key)} style={{ minWidth: '250px', }} > - {value => { - return {formatValue(value, name)}; + {(value: string) => { + return {formattedValues[value]}; }} diff --git a/src/app/(main)/reports/[id]/FieldSelectForm.js b/src/app/(main)/reports/[id]/FieldSelectForm.tsx similarity index 65% rename from src/app/(main)/reports/[id]/FieldSelectForm.js rename to src/app/(main)/reports/[id]/FieldSelectForm.tsx index e7661511..dfd402cf 100644 --- a/src/app/(main)/reports/[id]/FieldSelectForm.js +++ b/src/app/(main)/reports/[id]/FieldSelectForm.tsx @@ -1,15 +1,26 @@ import { Menu, Item, Form, FormRow } from 'react-basics'; import { useMessages } from 'components/hooks'; import styles from './FieldSelectForm.module.css'; +import { Key } from 'react'; -export default function FieldSelectForm({ items, onSelect, showType = true }) { +export interface FieldSelectFormProps { + fields?: any[]; + onSelect?: (key: any) => void; + showType?: boolean; +} + +export default function FieldSelectForm({ + fields = [], + onSelect, + showType = true, +}: FieldSelectFormProps) { const { formatMessage, labels } = useMessages(); return (
- onSelect(items[key])}> - {items.map(({ name, label, type }, index) => { + onSelect(fields[key as any])}> + {fields.map(({ name, label, type }: any, index: Key) => { return (
{label || name}
diff --git a/src/app/(main)/reports/[id]/FilterSelectForm.js b/src/app/(main)/reports/[id]/FilterSelectForm.tsx similarity index 56% rename from src/app/(main)/reports/[id]/FilterSelectForm.js rename to src/app/(main)/reports/[id]/FilterSelectForm.tsx index 9ad4cd93..4f9b9264 100644 --- a/src/app/(main)/reports/[id]/FilterSelectForm.js +++ b/src/app/(main)/reports/[id]/FilterSelectForm.tsx @@ -5,29 +5,41 @@ import FieldSelectForm from './FieldSelectForm'; import FieldFilterForm from './FieldFilterForm'; import { useApi } from 'components/hooks'; -function useValues(websiteId, type) { +function useValues(websiteId: string, type: string) { const now = Date.now(); const { get, useQuery } = useApi(); - const { data, error, isLoading } = useQuery( - ['websites:values', websiteId, type], - () => + const { data, error, isLoading } = useQuery({ + queryKey: ['websites:values', websiteId, type], + queryFn: () => get(`/websites/${websiteId}/values`, { type, startAt: +subDays(now, 90), endAt: now, }), - { enabled: !!(websiteId && type) }, - ); + enabled: !!(websiteId && type), + }); return { data, error, isLoading }; } -export default function FilterSelectForm({ websiteId, items, onSelect, allowFilterSelect }) { - const [field, setField] = useState(); +export interface FilterSelectFormProps { + websiteId: string; + items: any[]; + onSelect?: (key: any) => void; + allowFilterSelect?: boolean; +} + +export default function FilterSelectForm({ + websiteId, + items, + onSelect, + allowFilterSelect, +}: FilterSelectFormProps) { + const [field, setField] = useState<{ name: string; label: string; type: string }>(); const { data, isLoading } = useValues(websiteId, field?.name); if (!field) { - return ; + return ; } if (isLoading) { diff --git a/src/app/(main)/reports/[id]/ParameterList.js b/src/app/(main)/reports/[id]/ParameterList.tsx similarity index 82% rename from src/app/(main)/reports/[id]/ParameterList.js rename to src/app/(main)/reports/[id]/ParameterList.tsx index bf77dd9d..eb1a646a 100644 --- a/src/app/(main)/reports/[id]/ParameterList.js +++ b/src/app/(main)/reports/[id]/ParameterList.tsx @@ -1,10 +1,17 @@ +import { ReactNode } from 'react'; import { Icon, TooltipPopup } from 'react-basics'; import Icons from 'components/icons'; import Empty from 'components/common/Empty'; import { useMessages } from 'components/hooks'; import styles from './ParameterList.module.css'; -export function ParameterList({ items = [], children, onRemove }) { +export interface ParameterListProps { + items: any[]; + children?: ReactNode | ((item: any) => ReactNode); + onRemove: (index: number, e: any) => void; +} + +export function ParameterList({ items = [], children, onRemove }: ParameterListProps) { const { formatMessage, labels } = useMessages(); return ( diff --git a/src/app/(main)/reports/[id]/PopupForm.js b/src/app/(main)/reports/[id]/PopupForm.tsx similarity index 59% rename from src/app/(main)/reports/[id]/PopupForm.js rename to src/app/(main)/reports/[id]/PopupForm.tsx index 6b99b00a..f2666199 100644 --- a/src/app/(main)/reports/[id]/PopupForm.js +++ b/src/app/(main)/reports/[id]/PopupForm.tsx @@ -1,7 +1,16 @@ +import { CSSProperties, ReactNode } from 'react'; import classNames from 'classnames'; import styles from './PopupForm.module.css'; -export function PopupForm({ className, style, children }) { +export function PopupForm({ + className, + style, + children, +}: { + className?: string; + style?: CSSProperties; + children: ReactNode; +}) { return (
-
- {children} -
- - ); -} - -export default Report; diff --git a/src/app/(main)/reports/[id]/Report.tsx b/src/app/(main)/reports/[id]/Report.tsx new file mode 100644 index 00000000..c1cc502f --- /dev/null +++ b/src/app/(main)/reports/[id]/Report.tsx @@ -0,0 +1,31 @@ +'use client'; +import { createContext, ReactNode } from 'react'; +import { Loading } from 'react-basics'; +import { useReport } from 'components/hooks'; +import styles from './Report.module.css'; +import classNames from 'classnames'; + +export const ReportContext = createContext(null); + +export interface ReportProps { + reportId: string; + defaultParameters: { [key: string]: any }; + children: ReactNode; + className?: string; +} + +export function Report({ reportId, defaultParameters, children, className }: ReportProps) { + const report = useReport(reportId, defaultParameters); + + if (!report) { + return reportId ? : null; + } + + return ( + +
{children}
+
+ ); +} + +export default Report; diff --git a/src/app/(main)/reports/[id]/ReportBody.js b/src/app/(main)/reports/[id]/ReportBody.tsx similarity index 51% rename from src/app/(main)/reports/[id]/ReportBody.js rename to src/app/(main)/reports/[id]/ReportBody.tsx index a116bf8e..6f4627f6 100644 --- a/src/app/(main)/reports/[id]/ReportBody.js +++ b/src/app/(main)/reports/[id]/ReportBody.tsx @@ -1,6 +1,14 @@ import styles from './ReportBody.module.css'; +import { useContext } from 'react'; +import { ReportContext } from './Report'; export function ReportBody({ children }) { + const { report } = useContext(ReportContext); + + if (!report) { + return null; + } + return
{children}
; } diff --git a/src/app/(main)/reports/[id]/ReportDetails.js b/src/app/(main)/reports/[id]/ReportDetails.tsx similarity index 74% rename from src/app/(main)/reports/[id]/ReportDetails.js rename to src/app/(main)/reports/[id]/ReportDetails.tsx index 8605ffb3..e4d4688a 100644 --- a/src/app/(main)/reports/[id]/ReportDetails.js +++ b/src/app/(main)/reports/[id]/ReportDetails.tsx @@ -12,9 +12,12 @@ const reports = { retention: RetentionReport, }; -export default function ReportDetails({ reportId }) { +export default function ReportDetails({ reportId }: { reportId: string }) { const { get, useQuery } = useApi(); - const { data: report } = useQuery(['reports', reportId], () => get(`/reports/${reportId}`)); + const { data: report } = useQuery({ + queryKey: ['reports', reportId], + queryFn: () => get(`/reports/${reportId}`), + }); if (!report) { return null; diff --git a/src/app/(main)/reports/[id]/ReportHeader.js b/src/app/(main)/reports/[id]/ReportHeader.tsx similarity index 86% rename from src/app/(main)/reports/[id]/ReportHeader.js rename to src/app/(main)/reports/[id]/ReportHeader.tsx index ed3b9736..d69cd0cd 100644 --- a/src/app/(main)/reports/[id]/ReportHeader.js +++ b/src/app/(main)/reports/[id]/ReportHeader.tsx @@ -12,10 +12,12 @@ export function ReportHeader({ icon }) { const { showToast } = useToasts(); const { post, useMutation } = useApi(); const router = useRouter(); - const { mutate: create, isLoading: isCreating } = useMutation(data => post(`/reports`, data)); - const { mutate: update, isLoading: isUpdating } = useMutation(data => - post(`/reports/${data.id}`, data), - ); + const { mutate: create, isPending: isCreating } = useMutation({ + mutationFn: (data: any) => post(`/reports`, data), + }); + const { mutate: update, isPending: isUpdating } = useMutation({ + mutationFn: (data: any) => post(`/reports/${data.id}`, data), + }); const { name, description, parameters } = report || {}; const { websiteId, dateRange } = parameters || {}; @@ -26,7 +28,7 @@ export function ReportHeader({ icon }) { create(report, { onSuccess: async ({ id }) => { showToast({ message: formatMessage(messages.saved), variant: 'success' }); - router.push(`/reports/${id}`, null, { shallow: true }); + router.push(`/reports/${id}`); }, }); } else { @@ -38,11 +40,11 @@ export function ReportHeader({ icon }) { } }; - const handleNameChange = name => { + const handleNameChange = (name: string) => { updateReport({ name: name || defaultName }); }; - const handleDescriptionChange = description => { + const handleDescriptionChange = (description: string) => { updateReport({ description }); }; diff --git a/src/app/(main)/reports/[id]/ReportMenu.js b/src/app/(main)/reports/[id]/ReportMenu.tsx similarity index 51% rename from src/app/(main)/reports/[id]/ReportMenu.js rename to src/app/(main)/reports/[id]/ReportMenu.tsx index 72bc197a..9478a903 100644 --- a/src/app/(main)/reports/[id]/ReportMenu.js +++ b/src/app/(main)/reports/[id]/ReportMenu.tsx @@ -1,6 +1,14 @@ import styles from './ReportMenu.module.css'; +import { useContext } from 'react'; +import { ReportContext } from './Report'; export function ReportMenu({ children }) { + const { report } = useContext(ReportContext); + + if (!report) { + return null; + } + return
{children}
; } diff --git a/src/app/(main)/reports/create/ReportTemplates.js b/src/app/(main)/reports/create/ReportTemplates.tsx similarity index 100% rename from src/app/(main)/reports/create/ReportTemplates.js rename to src/app/(main)/reports/create/ReportTemplates.tsx diff --git a/src/app/(main)/reports/event-data/EventDataParameters.js b/src/app/(main)/reports/event-data/EventDataParameters.tsx similarity index 87% rename from src/app/(main)/reports/event-data/EventDataParameters.js rename to src/app/(main)/reports/event-data/EventDataParameters.tsx index 6b9a0344..7a39131b 100644 --- a/src/app/(main)/reports/event-data/EventDataParameters.js +++ b/src/app/(main)/reports/event-data/EventDataParameters.tsx @@ -1,4 +1,4 @@ -import { useContext, useRef } from 'react'; +import { useContext } from 'react'; import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics'; import Empty from 'components/common/Empty'; import Icons from 'components/icons'; @@ -12,16 +12,16 @@ import styles from './EventDataParameters.module.css'; function useFields(websiteId, startDate, endDate) { const { get, useQuery } = useApi(); - const { data, error, isLoading } = useQuery( - ['fields', websiteId, startDate, endDate], - () => + const { data, error, isLoading } = useQuery({ + queryKey: ['fields', websiteId, startDate, endDate], + queryFn: () => get('/reports/event-data', { websiteId, startAt: +startDate, endAt: +endDate, }), - { enabled: !!(websiteId && startDate && endDate) }, - ); + enabled: !!(websiteId && startDate && endDate), + }); return { data, error, isLoading }; } @@ -29,7 +29,6 @@ function useFields(websiteId, startDate, endDate) { export function EventDataParameters() { const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { formatMessage, labels, messages } = useMessages(); - const ref = useRef(null); const { parameters } = report || {}; const { websiteId, dateRange, fields, filters, groups } = parameters || {}; const { startDate, endDate } = dateRange || {}; @@ -53,28 +52,28 @@ export function EventDataParameters() { runReport(values); }; - const handleAdd = (group, value) => { + const handleAdd = (group: string, value: any) => { const data = parameterData[group]; - if (!data.find(({ name }) => name === value.name)) { + if (!data.find(({ name }) => name === value?.name)) { updateReport({ parameters: { [group]: data.concat(value) } }); } }; - const handleRemove = (group, index) => { + const handleRemove = (group: string, index: number) => { const data = [...parameterData[group]]; data.splice(index, 1); updateReport({ parameters: { [group]: data } }); }; - const AddButton = ({ group }) => { + const AddButton = ({ group, onAdd }) => { return ( - {close => { + {(close: () => void) => { return ( ({ @@ -82,7 +81,7 @@ export function EventDataParameters() { type: DATA_TYPES[eventDataType], }))} group={group} - onAdd={handleAdd} + onAdd={onAdd} onClose={close} /> ); @@ -93,7 +92,7 @@ export function EventDataParameters() { }; return ( - + {!hasData && } {parametersSelected && diff --git a/src/app/(main)/reports/event-data/EventDataReport.js b/src/app/(main)/reports/event-data/EventDataReport.tsx similarity index 89% rename from src/app/(main)/reports/event-data/EventDataReport.js rename to src/app/(main)/reports/event-data/EventDataReport.tsx index e91cb4a2..8b4dc99c 100644 --- a/src/app/(main)/reports/event-data/EventDataReport.js +++ b/src/app/(main)/reports/event-data/EventDataReport.tsx @@ -11,7 +11,7 @@ const defaultParameters = { parameters: { fields: [], filters: [] }, }; -export default function EventDataReport({ reportId }) { +export default function EventDataReport({ reportId }: { reportId: string }) { return ( } /> diff --git a/src/app/(main)/reports/event-data/EventDataTable.js b/src/app/(main)/reports/event-data/EventDataTable.tsx similarity index 100% rename from src/app/(main)/reports/event-data/EventDataTable.js rename to src/app/(main)/reports/event-data/EventDataTable.tsx diff --git a/src/app/(main)/reports/funnel/FunnelChart.js b/src/app/(main)/reports/funnel/FunnelChart.tsx similarity index 52% rename from src/app/(main)/reports/funnel/FunnelChart.js rename to src/app/(main)/reports/funnel/FunnelChart.tsx index 7461afbc..923e8d56 100644 --- a/src/app/(main)/reports/funnel/FunnelChart.js +++ b/src/app/(main)/reports/funnel/FunnelChart.tsx @@ -1,13 +1,18 @@ -import { useCallback, useContext, useMemo } from 'react'; +import { JSX, useCallback, useContext, useMemo } from 'react'; import { Loading, StatusLight } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; import useTheme from 'components/hooks/useTheme'; import BarChart from 'components/metrics/BarChart'; import { formatLongNumber } from 'lib/format'; -import styles from './FunnelChart.module.css'; import { ReportContext } from '../[id]/Report'; +import styles from './FunnelChart.module.css'; -export function FunnelChart({ className, loading }) { +export interface FunnelChartProps { + className?: string; + isLoading?: boolean; +} + +export function FunnelChart({ className, isLoading }: FunnelChartProps) { const { report } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); const { colors } = useTheme(); @@ -15,33 +20,39 @@ export function FunnelChart({ className, loading }) { const { parameters, data } = report || {}; const renderXLabel = useCallback( - (label, index) => { + (label: string, index: number) => { return parameters.urls[index]; }, [parameters], ); - const renderTooltipPopup = useCallback((setTooltipPopup, model) => { - const { opacity, labelColors, dataPoints } = model.tooltip; + const renderTooltipPopup = useCallback( + ( + setTooltipPopup: (arg0: JSX.Element) => void, + model: { tooltip: { opacity: any; labelColors: any; dataPoints: any } }, + ) => { + const { opacity, labelColors, dataPoints } = model.tooltip; - if (!dataPoints?.length || !opacity) { - setTooltipPopup(null); - return; - } + if (!dataPoints?.length || !opacity) { + setTooltipPopup(null); + return; + } - setTooltipPopup( - <> -
- {formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)} -
-
- - {formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)} - -
- , - ); - }, []); + setTooltipPopup( + <> +
+ {formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)} +
+
+ + {formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)} + +
+ , + ); + }, + [], + ); const datasets = useMemo(() => { return [ @@ -54,7 +65,7 @@ export function FunnelChart({ className, loading }) { ]; }, [data, colors, formatMessage, labels]); - if (loading) { + if (isLoading) { return ; } @@ -63,7 +74,7 @@ export function FunnelChart({ className, loading }) { className={className} datasets={datasets} unit="day" - loading={loading} + isLoading={isLoading} renderXLabel={renderXLabel} renderTooltipPopup={renderTooltipPopup} XAxisType="category" diff --git a/src/app/(main)/reports/funnel/FunnelParameters.js b/src/app/(main)/reports/funnel/FunnelParameters.tsx similarity index 84% rename from src/app/(main)/reports/funnel/FunnelParameters.js rename to src/app/(main)/reports/funnel/FunnelParameters.tsx index 135b5db8..0bb45415 100644 --- a/src/app/(main)/reports/funnel/FunnelParameters.js +++ b/src/app/(main)/reports/funnel/FunnelParameters.tsx @@ -1,4 +1,4 @@ -import { useContext, useRef } from 'react'; +import { useContext } from 'react'; import { useMessages } from 'components/hooks'; import { Icon, @@ -21,13 +21,12 @@ import PopupForm from '../[id]/PopupForm'; export function FunnelParameters() { const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); - const ref = useRef(null); const { parameters } = report || {}; const { websiteId, dateRange, urls } = parameters || {}; const queryDisabled = !websiteId || !dateRange || urls?.length < 2; - const handleSubmit = (data, e) => { + const handleSubmit = (data: any, e: any) => { e.stopPropagation(); e.preventDefault(); if (!queryDisabled) { @@ -35,11 +34,11 @@ export function FunnelParameters() { } }; - const handleAddUrl = url => { + const handleAddUrl = (url: string) => { updateReport({ parameters: { urls: parameters.urls.concat(url) } }); }; - const handleRemoveUrl = (index, e) => { + const handleRemoveUrl = (index: number, e: any) => { e.stopPropagation(); const urls = [...parameters.urls]; urls.splice(index, 1); @@ -62,7 +61,7 @@ export function FunnelParameters() { }; return ( - + }> - + handleRemoveUrl(index, e)} + /> diff --git a/src/app/(main)/reports/funnel/FunnelReport.js b/src/app/(main)/reports/funnel/FunnelReport.tsx similarity index 100% rename from src/app/(main)/reports/funnel/FunnelReport.js rename to src/app/(main)/reports/funnel/FunnelReport.tsx diff --git a/src/app/(main)/reports/funnel/FunnelTable.js b/src/app/(main)/reports/funnel/FunnelTable.tsx similarity index 100% rename from src/app/(main)/reports/funnel/FunnelTable.js rename to src/app/(main)/reports/funnel/FunnelTable.tsx diff --git a/src/app/(main)/reports/funnel/UrlAddForm.js b/src/app/(main)/reports/funnel/UrlAddForm.tsx similarity index 86% rename from src/app/(main)/reports/funnel/UrlAddForm.js rename to src/app/(main)/reports/funnel/UrlAddForm.tsx index 3e77ce15..88c27ae9 100644 --- a/src/app/(main)/reports/funnel/UrlAddForm.js +++ b/src/app/(main)/reports/funnel/UrlAddForm.tsx @@ -3,7 +3,12 @@ import { useMessages } from 'components/hooks'; import { Button, Form, FormRow, TextField, Flexbox } from 'react-basics'; import styles from './UrlAddForm.module.css'; -export function UrlAddForm({ defaultValue = '', onAdd }) { +export interface UrlAddFormProps { + defaultValue?: string; + onAdd?: (url: string) => void; +} + +export function UrlAddForm({ defaultValue = '', onAdd }: UrlAddFormProps) { const [url, setUrl] = useState(defaultValue); const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/reports/insights/InsightsParameters.js b/src/app/(main)/reports/insights/InsightsParameters.tsx similarity index 93% rename from src/app/(main)/reports/insights/InsightsParameters.js rename to src/app/(main)/reports/insights/InsightsParameters.tsx index 91dd09f8..cd643eed 100644 --- a/src/app/(main)/reports/insights/InsightsParameters.js +++ b/src/app/(main)/reports/insights/InsightsParameters.tsx @@ -1,4 +1,4 @@ -import { useContext, useRef } from 'react'; +import { useContext } from 'react'; import { useFormat, useMessages, useFilters } from 'components/hooks'; import { Form, @@ -24,7 +24,6 @@ export function InsightsParameters() { const { formatMessage, labels } = useMessages(); const { formatValue } = useFormat(); const { filterLabels } = useFilters(); - const ref = useRef(null); const { parameters } = report || {}; const { websiteId, dateRange, fields, filters } = parameters || {}; const { startDate, endDate } = dateRange || {}; @@ -72,7 +71,7 @@ export function InsightsParameters() { updateReport({ parameters: { [id]: data } }); }; - const AddButton = ({ id }) => { + const AddButton = ({ id, onAdd }) => { return ( @@ -84,8 +83,8 @@ export function InsightsParameters() { {id === 'fields' && ( )} @@ -93,7 +92,7 @@ export function InsightsParameters() { )} @@ -103,7 +102,7 @@ export function InsightsParameters() { }; return ( - + {parametersSelected && parameterGroups.map(({ id, label }) => { diff --git a/src/app/(main)/reports/insights/InsightsReport.js b/src/app/(main)/reports/insights/InsightsReport.tsx similarity index 90% rename from src/app/(main)/reports/insights/InsightsReport.js rename to src/app/(main)/reports/insights/InsightsReport.tsx index f99e187b..b90ff396 100644 --- a/src/app/(main)/reports/insights/InsightsReport.js +++ b/src/app/(main)/reports/insights/InsightsReport.tsx @@ -13,7 +13,7 @@ const defaultParameters = { parameters: { fields: [], filters: [] }, }; -export default function InsightsReport({ reportId }) { +export default function InsightsReport({ reportId }: { reportId: string }) { return ( } /> diff --git a/src/app/(main)/reports/insights/InsightsTable.js b/src/app/(main)/reports/insights/InsightsTable.tsx similarity index 90% rename from src/app/(main)/reports/insights/InsightsTable.js rename to src/app/(main)/reports/insights/InsightsTable.tsx index 05d38042..a4517698 100644 --- a/src/app/(main)/reports/insights/InsightsTable.js +++ b/src/app/(main)/reports/insights/InsightsTable.tsx @@ -5,7 +5,7 @@ import { ReportContext } from '../[id]/Report'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; export function InsightsTable() { - const [fields, setFields] = useState(); + const [fields, setFields] = useState([]); const { report } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); const { formatValue } = useFormat(); @@ -37,10 +37,10 @@ export function InsightsTable() { width="100px" alignment="end" > - {row => row.visitors.toLocaleString()} + {row => row?.visitors?.toLocaleString()} - {row => row.views.toLocaleString()} + {row => row?.views?.toLocaleString()} ); diff --git a/src/app/(main)/reports/page.tsx b/src/app/(main)/reports/page.tsx index aba59db2..22e6e2a7 100644 --- a/src/app/(main)/reports/page.tsx +++ b/src/app/(main)/reports/page.tsx @@ -1,7 +1,7 @@ import ReportsHeader from './ReportsHeader'; import ReportsDataTable from './ReportsDataTable'; -export default function ReportsPage() { +export default function () { return ( <> diff --git a/src/app/(main)/reports/retention/RetentionParameters.js b/src/app/(main)/reports/retention/RetentionParameters.tsx similarity index 87% rename from src/app/(main)/reports/retention/RetentionParameters.js rename to src/app/(main)/reports/retention/RetentionParameters.tsx index 762a313d..fc168695 100644 --- a/src/app/(main)/reports/retention/RetentionParameters.js +++ b/src/app/(main)/reports/retention/RetentionParameters.tsx @@ -1,4 +1,4 @@ -import { useContext, useRef } from 'react'; +import { useContext } from 'react'; import { useMessages } from 'components/hooks'; import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics'; import { ReportContext } from '../[id]/Report'; @@ -9,14 +9,13 @@ import { parseDateRange } from 'lib/date'; export function RetentionParameters() { const { report, runReport, isRunning, updateReport } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); - const ref = useRef(null); const { parameters } = report || {}; const { websiteId, dateRange } = parameters || {}; const { startDate } = dateRange || {}; const queryDisabled = !websiteId || !dateRange; - const handleSubmit = (data, e) => { + const handleSubmit = (data: any, e: any) => { e.stopPropagation(); e.preventDefault(); @@ -30,7 +29,7 @@ export function RetentionParameters() { }; return ( - + diff --git a/src/app/(main)/reports/retention/RetentionReport.js b/src/app/(main)/reports/retention/RetentionReport.tsx similarity index 92% rename from src/app/(main)/reports/retention/RetentionReport.js rename to src/app/(main)/reports/retention/RetentionReport.tsx index ae42e76b..35f0fcb1 100644 --- a/src/app/(main)/reports/retention/RetentionReport.js +++ b/src/app/(main)/reports/retention/RetentionReport.tsx @@ -19,7 +19,7 @@ const defaultParameters = { }, }; -export default function RetentionReport({ reportId }) { +export default function RetentionReport({ reportId }: { reportId: string }) { return ( } /> diff --git a/src/app/(main)/reports/retention/RetentionTable.js b/src/app/(main)/reports/retention/RetentionTable.tsx similarity index 96% rename from src/app/(main)/reports/retention/RetentionTable.js rename to src/app/(main)/reports/retention/RetentionTable.tsx index a71fae6f..d2e7f129 100644 --- a/src/app/(main)/reports/retention/RetentionTable.js +++ b/src/app/(main)/reports/retention/RetentionTable.tsx @@ -18,7 +18,7 @@ export function RetentionTable({ days = DAYS }) { return ; } - const rows = data.reduce((arr, row) => { + const rows = data.reduce((arr: any[], row: { date: any; visitors: any; day: any }) => { const { date, visitors, day } = row; if (day === 0) { return arr.concat({ diff --git a/src/app/(main)/settings/SettingsContext.tsx b/src/app/(main)/settings/SettingsContext.tsx new file mode 100644 index 00000000..898a40cc --- /dev/null +++ b/src/app/(main)/settings/SettingsContext.tsx @@ -0,0 +1,5 @@ +import { createContext } from 'react'; + +export const SettingsContext = createContext(null); + +export default SettingsContext; diff --git a/src/app/(main)/settings/layout.tsx b/src/app/(main)/settings/layout.tsx index f738f883..e36b5b53 100644 --- a/src/app/(main)/settings/layout.tsx +++ b/src/app/(main)/settings/layout.tsx @@ -20,7 +20,7 @@ export default function SettingsLayout({ children }) { const getKey = () => items.find(({ url }) => pathname === url)?.key; - if (cloudMode && pathname != '/settings/profile') { + if (cloudMode && pathname !== '/settings/profile') { return null; } diff --git a/src/app/(main)/settings/profile/DateRangeSetting.js b/src/app/(main)/settings/profile/DateRangeSetting.tsx similarity index 100% rename from src/app/(main)/settings/profile/DateRangeSetting.js rename to src/app/(main)/settings/profile/DateRangeSetting.tsx diff --git a/src/app/(main)/settings/profile/LanguageSetting.js b/src/app/(main)/settings/profile/LanguageSetting.tsx similarity index 100% rename from src/app/(main)/settings/profile/LanguageSetting.js rename to src/app/(main)/settings/profile/LanguageSetting.tsx diff --git a/src/app/(main)/settings/profile/PasswordChangeButton.js b/src/app/(main)/settings/profile/PasswordChangeButton.tsx similarity index 100% rename from src/app/(main)/settings/profile/PasswordChangeButton.js rename to src/app/(main)/settings/profile/PasswordChangeButton.tsx diff --git a/src/app/(main)/settings/profile/PasswordEditForm.js b/src/app/(main)/settings/profile/PasswordEditForm.tsx similarity index 88% rename from src/app/(main)/settings/profile/PasswordEditForm.js rename to src/app/(main)/settings/profile/PasswordEditForm.tsx index 39ecfb77..1384a919 100644 --- a/src/app/(main)/settings/profile/PasswordEditForm.js +++ b/src/app/(main)/settings/profile/PasswordEditForm.tsx @@ -6,10 +6,12 @@ import useMessages from 'components/hooks/useMessages'; export function PasswordEditForm({ onSave, onClose }) { const { formatMessage, labels, messages } = useMessages(); const { post, useMutation } = useApi(); - const { mutate, error, isLoading } = useMutation(data => post('/me/password', data)); + const { mutate, error, isPending } = useMutation({ + mutationFn: (data: any) => post('/me/password', data), + }); const ref = useRef(null); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { onSave(); @@ -18,7 +20,7 @@ export function PasswordEditForm({ onSave, onClose }) { }); }; - const samePassword = value => { + const samePassword = (value: string) => { if (value !== ref?.current?.getValues('newPassword')) { return formatMessage(messages.noMatchPassword); } @@ -56,7 +58,7 @@ export function PasswordEditForm({ onSave, onClose }) { - diff --git a/src/app/(main)/settings/profile/ProfileHeader.js b/src/app/(main)/settings/profile/ProfileHeader.tsx similarity index 100% rename from src/app/(main)/settings/profile/ProfileHeader.js rename to src/app/(main)/settings/profile/ProfileHeader.tsx diff --git a/src/app/(main)/settings/profile/ProfileSettings.js b/src/app/(main)/settings/profile/ProfileSettings.tsx similarity index 100% rename from src/app/(main)/settings/profile/ProfileSettings.js rename to src/app/(main)/settings/profile/ProfileSettings.tsx diff --git a/src/app/(main)/settings/profile/ThemeSetting.js b/src/app/(main)/settings/profile/ThemeSetting.tsx similarity index 100% rename from src/app/(main)/settings/profile/ThemeSetting.js rename to src/app/(main)/settings/profile/ThemeSetting.tsx diff --git a/src/app/(main)/settings/profile/TimezoneSetting.js b/src/app/(main)/settings/profile/TimezoneSetting.tsx similarity index 100% rename from src/app/(main)/settings/profile/TimezoneSetting.js rename to src/app/(main)/settings/profile/TimezoneSetting.tsx diff --git a/src/app/(main)/settings/teams/TeamAddForm.js b/src/app/(main)/settings/teams/TeamAddForm.tsx similarity index 72% rename from src/app/(main)/settings/teams/TeamAddForm.js rename to src/app/(main)/settings/teams/TeamAddForm.tsx index b8bb8c3a..5a242cb9 100644 --- a/src/app/(main)/settings/teams/TeamAddForm.js +++ b/src/app/(main)/settings/teams/TeamAddForm.tsx @@ -1,4 +1,3 @@ -import { useRef } from 'react'; import { Form, FormRow, @@ -12,11 +11,12 @@ import { setValue } from 'store/cache'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; -export function TeamAddForm({ onSave, onClose }) { +export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { const { formatMessage, labels } = useMessages(); const { post, useMutation } = useApi(); - const { mutate, error, isLoading } = useMutation(data => post('/teams', data)); - const ref = useRef(null); + const { mutate, error, isPending } = useMutation({ + mutationFn: (data: any) => post('/teams', data), + }); const handleSubmit = async data => { mutate(data, { @@ -29,17 +29,17 @@ export function TeamAddForm({ onSave, onClose }) { }; return ( - + - + {formatMessage(labels.save)} - diff --git a/src/app/(main)/settings/teams/TeamDeleteButton.js b/src/app/(main)/settings/teams/TeamDeleteButton.tsx similarity index 79% rename from src/app/(main)/settings/teams/TeamDeleteButton.js rename to src/app/(main)/settings/teams/TeamDeleteButton.tsx index 5e4a41ea..36a1a1f8 100644 --- a/src/app/(main)/settings/teams/TeamDeleteButton.js +++ b/src/app/(main)/settings/teams/TeamDeleteButton.tsx @@ -2,7 +2,15 @@ import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; import TeamDeleteForm from './TeamDeleteForm'; -export function TeamDeleteButton({ teamId, teamName, onDelete }) { +export function TeamDeleteButton({ + teamId, + teamName, + onDelete, +}: { + teamId: string; + teamName: string; + onDelete?: () => void; +}) { const { formatMessage, labels } = useMessages(); return ( @@ -14,7 +22,7 @@ export function TeamDeleteButton({ teamId, teamName, onDelete }) { {formatMessage(labels.delete)} - {close => ( + {(close: any) => ( )} diff --git a/src/app/(main)/settings/teams/TeamDeleteForm.js b/src/app/(main)/settings/teams/TeamDeleteForm.tsx similarity index 72% rename from src/app/(main)/settings/teams/TeamDeleteForm.js rename to src/app/(main)/settings/teams/TeamDeleteForm.tsx index 9b80668a..00eedb17 100644 --- a/src/app/(main)/settings/teams/TeamDeleteForm.js +++ b/src/app/(main)/settings/teams/TeamDeleteForm.tsx @@ -3,10 +3,22 @@ import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; import { setValue } from 'store/cache'; -export function TeamDeleteForm({ teamId, teamName, onSave, onClose }) { +export function TeamDeleteForm({ + teamId, + teamName, + onSave, + onClose, +}: { + teamId: string; + teamName: string; + onSave: () => void; + onClose: () => void; +}) { const { formatMessage, labels, messages, FormattedMessage } = useMessages(); const { del, useMutation } = useApi(); - const { mutate, error, isLoading } = useMutation(data => del(`/teams/${teamId}`, data)); + const { mutate, error, isPending } = useMutation({ + mutationFn: (data: any) => del(`/teams/${teamId}`, data), + }); const handleSubmit = async data => { mutate(data, { @@ -24,7 +36,7 @@ export function TeamDeleteForm({ teamId, teamName, onSave, onClose }) { {teamName} }} />

- + {formatMessage(labels.delete)} diff --git a/src/app/(main)/settings/teams/TeamJoinForm.js b/src/app/(main)/settings/teams/TeamJoinForm.tsx similarity index 85% rename from src/app/(main)/settings/teams/TeamJoinForm.js rename to src/app/(main)/settings/teams/TeamJoinForm.tsx index 528e1d75..5cd38225 100644 --- a/src/app/(main)/settings/teams/TeamJoinForm.js +++ b/src/app/(main)/settings/teams/TeamJoinForm.tsx @@ -12,10 +12,10 @@ import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; import { setValue } from 'store/cache'; -export function TeamJoinForm({ onSave, onClose }) { +export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { const { formatMessage, labels, getMessage } = useMessages(); const { post, useMutation } = useApi(); - const { mutate, error } = useMutation(data => post('/teams/join', data)); + const { mutate, error } = useMutation({ mutationFn: (data: any) => post('/teams/join', data) }); const ref = useRef(null); const handleSubmit = async data => { diff --git a/src/app/(main)/settings/teams/TeamLeaveButton.js b/src/app/(main)/settings/teams/TeamLeaveButton.tsx similarity index 87% rename from src/app/(main)/settings/teams/TeamLeaveButton.js rename to src/app/(main)/settings/teams/TeamLeaveButton.tsx index 7b98f082..97676e17 100644 --- a/src/app/(main)/settings/teams/TeamLeaveButton.js +++ b/src/app/(main)/settings/teams/TeamLeaveButton.tsx @@ -4,7 +4,15 @@ import useLocale from 'components/hooks/useLocale'; import useUser from 'components/hooks/useUser'; import TeamDeleteForm from './TeamLeaveForm'; -export function TeamLeaveButton({ teamId, teamName, onLeave }) { +export function TeamLeaveButton({ + teamId, + teamName, + onLeave, +}: { + teamId: string; + teamName: string; + onLeave?: () => void; +}) { const { formatMessage, labels } = useMessages(); const { dir } = useLocale(); const { user } = useUser(); diff --git a/src/app/(main)/settings/teams/TeamLeaveForm.js b/src/app/(main)/settings/teams/TeamLeaveForm.tsx similarity index 60% rename from src/app/(main)/settings/teams/TeamLeaveForm.js rename to src/app/(main)/settings/teams/TeamLeaveForm.tsx index a9b6922a..3b4d4703 100644 --- a/src/app/(main)/settings/teams/TeamLeaveForm.js +++ b/src/app/(main)/settings/teams/TeamLeaveForm.tsx @@ -3,22 +3,33 @@ import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; import { setValue } from 'store/cache'; -export function TeamLeaveForm({ teamId, userId, teamName, onSave, onClose }) { +export function TeamLeaveForm({ + teamId, + userId, + teamName, + onSave, + onClose, +}: { + teamId: string; + userId: string; + teamName: string; + onSave: () => void; + onClose: () => void; +}) { const { formatMessage, labels, messages, FormattedMessage } = useMessages(); const { del, useMutation } = useApi(); - const { mutate, error, isLoading } = useMutation(() => del(`/teams/${teamId}/users/${userId}`)); + const { mutate, error, isPending } = useMutation({ + mutationFn: () => del(`/teams/${teamId}/users/${userId}`), + }); const handleSubmit = async () => { - mutate( - {}, - { - onSuccess: async () => { - setValue('team:members', Date.now()); - onSave(); - onClose(); - }, + mutate(null, { + onSuccess: async () => { + setValue('team:members', Date.now()); + onSave(); + onClose(); }, - ); + }); }; return ( @@ -27,7 +38,7 @@ export function TeamLeaveForm({ teamId, userId, teamName, onSave, onClose }) { {teamName} }} />

- + {formatMessage(labels.leave)} diff --git a/src/app/(main)/settings/teams/TeamsAddButton.js b/src/app/(main)/settings/teams/TeamsAddButton.tsx similarity index 79% rename from src/app/(main)/settings/teams/TeamsAddButton.js rename to src/app/(main)/settings/teams/TeamsAddButton.tsx index b7850812..09f9ecbb 100644 --- a/src/app/(main)/settings/teams/TeamsAddButton.js +++ b/src/app/(main)/settings/teams/TeamsAddButton.tsx @@ -3,7 +3,7 @@ import Icons from 'components/icons'; import useMessages from 'components/hooks/useMessages'; import TeamAddForm from './TeamAddForm'; -export function TeamsAddButton({ onAdd }) { +export function TeamsAddButton({ onAdd }: { onAdd?: () => void }) { const { formatMessage, labels } = useMessages(); return ( @@ -15,7 +15,7 @@ export function TeamsAddButton({ onAdd }) { {formatMessage(labels.createTeam)} - {close => } + {(close: () => void) => } ); diff --git a/src/app/(main)/settings/teams/TeamsDataTable.js b/src/app/(main)/settings/teams/TeamsDataTable.tsx similarity index 69% rename from src/app/(main)/settings/teams/TeamsDataTable.js rename to src/app/(main)/settings/teams/TeamsDataTable.tsx index 164838f9..2424e464 100644 --- a/src/app/(main)/settings/teams/TeamsDataTable.js +++ b/src/app/(main)/settings/teams/TeamsDataTable.tsx @@ -7,11 +7,14 @@ import useCache from 'store/cache'; export function TeamsDataTable() { const { get } = useApi(); - const modified = useCache(state => state?.teams); - const queryResult = useFilterQuery(['teams', { modified }], params => { - return get(`/teams`, { - ...params, - }); + const modified = useCache((state: any) => state?.teams); + const queryResult = useFilterQuery({ + queryKey: ['teams', { modified }], + queryFn: (params: any) => { + return get(`/teams`, { + ...params, + }); + }, }); return ( diff --git a/src/app/(main)/settings/teams/TeamsHeader.js b/src/app/(main)/settings/teams/TeamsHeader.tsx similarity index 100% rename from src/app/(main)/settings/teams/TeamsHeader.js rename to src/app/(main)/settings/teams/TeamsHeader.tsx diff --git a/src/app/(main)/settings/teams/TeamsJoinButton.js b/src/app/(main)/settings/teams/TeamsJoinButton.tsx similarity index 100% rename from src/app/(main)/settings/teams/TeamsJoinButton.js rename to src/app/(main)/settings/teams/TeamsJoinButton.tsx diff --git a/src/app/(main)/settings/teams/TeamsTable.js b/src/app/(main)/settings/teams/TeamsTable.tsx similarity index 96% rename from src/app/(main)/settings/teams/TeamsTable.js rename to src/app/(main)/settings/teams/TeamsTable.tsx index 1f7f1da4..70a7bebb 100644 --- a/src/app/(main)/settings/teams/TeamsTable.js +++ b/src/app/(main)/settings/teams/TeamsTable.tsx @@ -7,7 +7,7 @@ import { Button, GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from import TeamDeleteButton from './TeamDeleteButton'; import TeamLeaveButton from './TeamLeaveButton'; -export function TeamsTable({ data = [] }) { +export function TeamsTable({ data = [] }: { data: any[] }) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); const breakpoint = useBreakpoint(); diff --git a/src/app/(main)/settings/teams/WebsiteTags.js b/src/app/(main)/settings/teams/WebsiteTags.tsx similarity index 83% rename from src/app/(main)/settings/teams/WebsiteTags.js rename to src/app/(main)/settings/teams/WebsiteTags.tsx index c17d5763..4a0f109d 100644 --- a/src/app/(main)/settings/teams/WebsiteTags.js +++ b/src/app/(main)/settings/teams/WebsiteTags.tsx @@ -1,7 +1,15 @@ import { Button, Icon, Icons, Text } from 'react-basics'; import styles from './WebsiteTags.module.css'; -export function WebsiteTags({ items = [], websites = [], onClick }) { +export function WebsiteTags({ + items = [], + websites = [], + onClick, +}: { + items: any[]; + websites: any[]; + onClick: (e: Event) => void; +}) { if (websites.length === 0) { return null; } diff --git a/src/app/(main)/settings/teams/[id]/TeamEditForm.js b/src/app/(main)/settings/teams/[id]/TeamEditForm.tsx similarity index 91% rename from src/app/(main)/settings/teams/[id]/TeamEditForm.js rename to src/app/(main)/settings/teams/[id]/TeamEditForm.tsx index b1dc5854..420afe9b 100644 --- a/src/app/(main)/settings/teams/[id]/TeamEditForm.js +++ b/src/app/(main)/settings/teams/[id]/TeamEditForm.tsx @@ -18,15 +18,17 @@ const generateId = () => getRandomChars(16); export function TeamEditForm({ teamId, data, onSave, readOnly }) { const { formatMessage, labels } = useMessages(); const { post, useMutation } = useApi(); - const { mutate, error } = useMutation(data => post(`/teams/${teamId}`, data)); + const { mutate, error } = useMutation({ + mutationFn: (data: any) => post(`/teams/${teamId}`, data), + }); const ref = useRef(null); const [accessCode, setAccessCode] = useState(data.accessCode); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { ref.current.reset(data); - onSave(data); + onSave?.(data); }, }); }; diff --git a/src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.js b/src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.tsx similarity index 59% rename from src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.js rename to src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.tsx index 603adae3..cef2977e 100644 --- a/src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.js +++ b/src/app/(main)/settings/teams/[id]/TeamMemberRemoveButton.tsx @@ -3,28 +3,37 @@ import useMessages from 'components/hooks/useMessages'; import { Icon, Icons, LoadingButton, Text } from 'react-basics'; import { setValue } from 'store/cache'; -export function TeamMemberRemoveButton({ teamId, userId, disabled, onSave }) { +export function TeamMemberRemoveButton({ + teamId, + userId, + disabled, + onSave, +}: { + teamId: string; + userId: string; + disabled?: boolean; + onSave?: () => void; +}) { const { formatMessage, labels } = useMessages(); const { del, useMutation } = useApi(); - const { mutate, isLoading } = useMutation(() => del(`/teams/${teamId}/users/${userId}`)); + const { mutate, isPending } = useMutation({ + mutationFn: () => del(`/teams/${teamId}/users/${userId}`), + }); const handleRemoveTeamMember = () => { - mutate( - {}, - { - onSuccess: () => { - setValue('team:members', Date.now()); - onSave?.(); - }, + mutate(null, { + onSuccess: () => { + setValue('team:members', Date.now()); + onSave?.(); }, - ); + }); }; return ( handleRemoveTeamMember()} disabled={disabled} - isLoading={isLoading} + isLoading={isPending} > diff --git a/src/app/(main)/settings/teams/[id]/TeamMembers.js b/src/app/(main)/settings/teams/[id]/TeamMembers.tsx similarity index 72% rename from src/app/(main)/settings/teams/[id]/TeamMembers.js rename to src/app/(main)/settings/teams/[id]/TeamMembers.tsx index fb31b6fa..588a5a52 100644 --- a/src/app/(main)/settings/teams/[id]/TeamMembers.js +++ b/src/app/(main)/settings/teams/[id]/TeamMembers.tsx @@ -4,18 +4,18 @@ import useFilterQuery from 'components/hooks/useFilterQuery'; import DataTable from 'components/common/DataTable'; import useCache from 'store/cache'; -export function TeamMembers({ teamId, readOnly }) { +export function TeamMembers({ teamId, readOnly }: { teamId: string; readOnly: boolean }) { const { get } = useApi(); const modified = useCache(state => state?.['team:members']); - const queryResult = useFilterQuery( - ['team:members', { teamId, modified }], - params => { + const queryResult = useFilterQuery({ + queryKey: ['team:members', { teamId, modified }], + queryFn: params => { return get(`/teams/${teamId}/users`, { ...params, }); }, - { enabled: !!teamId }, - ); + enabled: !!teamId, + }); return ( <> diff --git a/src/app/(main)/settings/teams/[id]/TeamMembersTable.js b/src/app/(main)/settings/teams/[id]/TeamMembersTable.tsx similarity index 90% rename from src/app/(main)/settings/teams/[id]/TeamMembersTable.js rename to src/app/(main)/settings/teams/[id]/TeamMembersTable.tsx index 9a402d44..a08c746b 100644 --- a/src/app/(main)/settings/teams/[id]/TeamMembersTable.js +++ b/src/app/(main)/settings/teams/[id]/TeamMembersTable.tsx @@ -4,7 +4,15 @@ import useUser from 'components/hooks/useUser'; import { ROLES } from 'lib/constants'; import TeamMemberRemoveButton from './TeamMemberRemoveButton'; -export function TeamMembersTable({ data = [], teamId, readOnly }) { +export function TeamMembersTable({ + data = [], + teamId, + readOnly, +}: { + data: any[]; + teamId: string; + readOnly: boolean; +}) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); const breakpoint = useBreakpoint(); diff --git a/src/app/(main)/settings/teams/[id]/TeamSettings.js b/src/app/(main)/settings/teams/[id]/TeamSettings.tsx similarity index 87% rename from src/app/(main)/settings/teams/[id]/TeamSettings.js rename to src/app/(main)/settings/teams/[id]/TeamSettings.tsx index 8ec0ad85..cf5ae35c 100644 --- a/src/app/(main)/settings/teams/[id]/TeamSettings.js +++ b/src/app/(main)/settings/teams/[id]/TeamSettings.tsx @@ -10,22 +10,22 @@ import TeamEditForm from './TeamEditForm'; import TeamMembers from './TeamMembers'; import TeamWebsites from './TeamWebsites'; -export function TeamSettings({ teamId }) { +export function TeamSettings({ teamId }: { teamId: string }) { const { formatMessage, labels, messages } = useMessages(); const { user } = useUser(); const [values, setValues] = useState(null); const [tab, setTab] = useState('details'); const { get, useQuery } = useApi(); const { showToast } = useToasts(); - const { data, isLoading } = useQuery( - ['team', teamId], - () => { + const { data, isLoading } = useQuery({ + queryKey: ['team', teamId], + queryFn: () => { if (teamId) { return get(`/teams/${teamId}`); } }, - { cacheTime: 0 }, - ); + gcTime: 0, + }); const canEdit = data?.teamUser?.find( ({ userId, role }) => role === ROLES.teamOwner && userId === user.id, ); @@ -48,7 +48,7 @@ export function TeamSettings({ teamId }) { return ( - + setTab(value)} style={{ marginBottom: 30 }}> {formatMessage(labels.details)} {formatMessage(labels.members)} {formatMessage(labels.websites)} diff --git a/src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.js b/src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.tsx similarity index 75% rename from src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.js rename to src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.tsx index 9c2ae7bd..64a0c58e 100644 --- a/src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.js +++ b/src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.tsx @@ -2,15 +2,30 @@ import useApi from 'components/hooks/useApi'; import { useState } from 'react'; import { Button, Form, FormButtons, GridColumn, Loading, SubmitButton, Toggle } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; -import WebsitesDataTable from '../../websites/WebsitesDataTable'; +import WebsitesDataTable from 'app/(main)/settings/websites/WebsitesDataTable'; import Empty from 'components/common/Empty'; import { setValue } from 'store/cache'; +import { useUser } from 'components/hooks'; -export function TeamWebsiteAddForm({ teamId, onSave, onClose }) { +export function TeamWebsiteAddForm({ + teamId, + onSave, + onClose, +}: { + teamId: string; + onSave: () => void; + onClose: () => void; +}) { + const { user } = useUser(); const { formatMessage, labels } = useMessages(); const { get, post, useQuery, useMutation } = useApi(); - const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data)); - const { data: websites, isLoading } = useQuery(['websites'], () => get('/websites')); + const { mutate, error } = useMutation({ + mutationFn: (data: any) => post(`/teams/${teamId}/websites`, data), + }); + const { data: websites, isLoading } = useQuery({ + queryKey: ['websites'], + queryFn: () => get('/websites'), + }); const [selected, setSelected] = useState([]); const hasData = websites && websites.data.length > 0; @@ -37,7 +52,7 @@ export function TeamWebsiteAddForm({ teamId, onSave, onClose }) { {!isLoading && !hasData && } {hasData && ( - + {row => ( del(`/teams/${teamId}/websites/${websiteId}`)); + const { mutate, isPending } = useMutation({ + mutationFn: () => del(`/teams/${teamId}/websites/${websiteId}`), + }); const handleRemoveTeamMember = async () => { - await mutate(null, { + mutate(null, { onSuccess: () => { onSave(); }, @@ -16,7 +18,7 @@ export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) { }; return ( - handleRemoveTeamMember()} isLoading={isLoading}> + handleRemoveTeamMember()} isLoading={isPending}> diff --git a/src/app/(main)/settings/teams/[id]/TeamWebsites.js b/src/app/(main)/settings/teams/[id]/TeamWebsites.tsx similarity index 78% rename from src/app/(main)/settings/teams/[id]/TeamWebsites.js rename to src/app/(main)/settings/teams/[id]/TeamWebsites.tsx index 9e76ffab..93bb3a10 100644 --- a/src/app/(main)/settings/teams/[id]/TeamWebsites.js +++ b/src/app/(main)/settings/teams/[id]/TeamWebsites.tsx @@ -8,23 +8,23 @@ import useFilterQuery from 'components/hooks/useFilterQuery'; import DataTable from 'components/common/DataTable'; import useCache from 'store/cache'; -export function TeamWebsites({ teamId }) { +export function TeamWebsites({ teamId, readOnly }: { teamId: string; readOnly: boolean }) { const { formatMessage, labels, messages } = useMessages(); const { user } = useUser(); const { get } = useApi(); const modified = useCache(state => state?.['team:websites']); - const queryResult = useFilterQuery( - ['team:websites', { teamId, modified }], - params => { + const queryResult = useFilterQuery({ + queryKey: ['team:websites', { teamId, modified }], + queryFn: params => { return get(`/teams/${teamId}/websites`, { ...params, }); }, - { enabled: !!user }, - ); + enabled: !!user, + }); const handleChange = () => { - queryResult.refetch(); + queryResult.query.refetch(); }; return ( @@ -43,7 +43,9 @@ export function TeamWebsites({ teamId }) { - {({ data }) => } + {({ data }) => ( + + )} ); diff --git a/src/app/(main)/settings/teams/[id]/TeamWebsitesTable.js b/src/app/(main)/settings/teams/[id]/TeamWebsitesTable.tsx similarity index 85% rename from src/app/(main)/settings/teams/[id]/TeamWebsitesTable.js rename to src/app/(main)/settings/teams/[id]/TeamWebsitesTable.tsx index 0f802212..29b95816 100644 --- a/src/app/(main)/settings/teams/[id]/TeamWebsitesTable.js +++ b/src/app/(main)/settings/teams/[id]/TeamWebsitesTable.tsx @@ -4,7 +4,15 @@ import useMessages from 'components/hooks/useMessages'; import useUser from 'components/hooks/useUser'; import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton'; -export function TeamWebsitesTable({ data = [], onRemove }) { +export function TeamWebsitesTable({ + data = [], + readOnly, + onRemove, +}: { + data: any[]; + readOnly: boolean; + onRemove: () => void; +}) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); @@ -17,7 +25,7 @@ export function TeamWebsitesTable({ data = [], onRemove }) { const { id: teamId, teamUser } = row.teamWebsite[0].team; const { id: websiteId, userId } = row; const owner = teamUser[0]; - const canRemove = user.id === userId || user.id === owner.userId; + const canRemove = !readOnly && (user.id === userId || user.id === owner.userId); return ( <> {canRemove && ( diff --git a/src/app/(main)/settings/teams/[id]/page.js b/src/app/(main)/settings/teams/[id]/page.tsx similarity index 100% rename from src/app/(main)/settings/teams/[id]/page.js rename to src/app/(main)/settings/teams/[id]/page.tsx diff --git a/src/app/(main)/settings/users/UserAddButton.js b/src/app/(main)/settings/users/UserAddButton.js deleted file mode 100644 index 8b691362..00000000 --- a/src/app/(main)/settings/users/UserAddButton.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Button, Icon, Text, Modal, Icons, ModalTrigger } from 'react-basics'; -import UserAddForm from './UserAddForm'; -import useMessages from 'components/hooks/useMessages'; - -export function UserAddButton({ onSave }) { - const { formatMessage, labels } = useMessages(); - - const handleSave = () => { - onSave(); - }; - - return ( - - - - {close => } - - - ); -} - -export default UserAddButton; diff --git a/src/app/(main)/settings/users/UserAddButton.tsx b/src/app/(main)/settings/users/UserAddButton.tsx new file mode 100644 index 00000000..1158ecec --- /dev/null +++ b/src/app/(main)/settings/users/UserAddButton.tsx @@ -0,0 +1,31 @@ +import { Button, Icon, Text, Modal, Icons, ModalTrigger, useToasts } from 'react-basics'; +import UserAddForm from './UserAddForm'; +import useMessages from 'components/hooks/useMessages'; +import { setValue } from 'store/cache'; + +export function UserAddButton({ onSave }: { onSave?: () => void }) { + const { formatMessage, labels, messages } = useMessages(); + const { showToast } = useToasts(); + + const handleSave = () => { + showToast({ message: formatMessage(messages.saved), variant: 'success' }); + setValue('users', Date.now()); + onSave?.(); + }; + + return ( + + + + {(close: () => void) => } + + + ); +} + +export default UserAddButton; diff --git a/src/app/(main)/settings/users/UserAddForm.js b/src/app/(main)/settings/users/UserAddForm.tsx similarity index 90% rename from src/app/(main)/settings/users/UserAddForm.js rename to src/app/(main)/settings/users/UserAddForm.tsx index 38c1bedd..8c153775 100644 --- a/src/app/(main)/settings/users/UserAddForm.js +++ b/src/app/(main)/settings/users/UserAddForm.tsx @@ -16,10 +16,12 @@ import useMessages from 'components/hooks/useMessages'; export function UserAddForm({ onSave, onClose }) { const { post, useMutation } = useApi(); - const { mutate, error, isLoading } = useMutation(data => post(`/users`, data)); + const { mutate, error, isPending } = useMutation({ + mutationFn: (data: any) => post(`/users`, data), + }); const { formatMessage, labels } = useMessages(); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { onSave(data); @@ -65,7 +67,7 @@ export function UserAddForm({ onSave, onClose }) { {formatMessage(labels.save)} - diff --git a/src/app/(main)/settings/users/UserDeleteButton.js b/src/app/(main)/settings/users/UserDeleteButton.tsx similarity index 81% rename from src/app/(main)/settings/users/UserDeleteButton.js rename to src/app/(main)/settings/users/UserDeleteButton.tsx index 22d93171..775004a8 100644 --- a/src/app/(main)/settings/users/UserDeleteButton.js +++ b/src/app/(main)/settings/users/UserDeleteButton.tsx @@ -3,7 +3,15 @@ import useMessages from 'components/hooks/useMessages'; import useUser from 'components/hooks/useUser'; import UserDeleteForm from './UserDeleteForm'; -export function UserDeleteButton({ userId, username, onDelete }) { +export function UserDeleteButton({ + userId, + username, + onDelete, +}: { + userId: string; + username: string; + onDelete?: () => void; +}) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); @@ -16,7 +24,7 @@ export function UserDeleteButton({ userId, username, onDelete }) { {formatMessage(labels.delete)} - {close => ( + {(close: () => void) => ( )} diff --git a/src/app/(main)/settings/users/UserDeleteForm.js b/src/app/(main)/settings/users/UserDeleteForm.tsx similarity index 72% rename from src/app/(main)/settings/users/UserDeleteForm.js rename to src/app/(main)/settings/users/UserDeleteForm.tsx index 5a47fdc1..eaa8e481 100644 --- a/src/app/(main)/settings/users/UserDeleteForm.js +++ b/src/app/(main)/settings/users/UserDeleteForm.tsx @@ -1,14 +1,13 @@ -import { useMutation } from '@tanstack/react-query'; import { Button, Form, FormButtons, SubmitButton } from 'react-basics'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; export function UserDeleteForm({ userId, username, onSave, onClose }) { const { formatMessage, FormattedMessage, labels, messages } = useMessages(); - const { del } = useApi(); - const { mutate, error, isLoading } = useMutation(() => del(`/users/${userId}`)); + const { del, useMutation } = useApi(); + const { mutate, error, isPending } = useMutation({ mutationFn: () => del(`/users/${userId}`) }); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { onSave(); @@ -23,10 +22,10 @@ export function UserDeleteForm({ userId, username, onSave, onClose }) { {username} }} />

- + {formatMessage(labels.delete)} - diff --git a/src/app/(main)/settings/users/UserEditForm.js b/src/app/(main)/settings/users/UserEditForm.tsx similarity index 82% rename from src/app/(main)/settings/users/UserEditForm.js rename to src/app/(main)/settings/users/UserEditForm.tsx index 157c9ad6..0b823a94 100644 --- a/src/app/(main)/settings/users/UserEditForm.js +++ b/src/app/(main)/settings/users/UserEditForm.tsx @@ -13,14 +13,30 @@ import useApi from 'components/hooks/useApi'; import { ROLES } from 'lib/constants'; import useMessages from 'components/hooks/useMessages'; -export function UserEditForm({ userId, data, onSave }) { +export function UserEditForm({ + userId, + data, + onSave, +}: { + userId: string; + data: any[]; + onSave: (data: any) => void; +}) { const { formatMessage, labels, messages } = useMessages(); const { post, useMutation } = useApi(); - const { mutate, error } = useMutation(({ username, password, role }) => - post(`/users/${userId}`, { username, password, role }), - ); + const { mutate, error } = useMutation({ + mutationFn: ({ + username, + password, + role, + }: { + username: string; + password: string; + role: string; + }) => post(`/users/${userId}`, { username, password, role }), + }); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { onSave(data); diff --git a/src/app/(main)/settings/users/UserWebsites.js b/src/app/(main)/settings/users/UserWebsites.js deleted file mode 100644 index 18b5f1a7..00000000 --- a/src/app/(main)/settings/users/UserWebsites.js +++ /dev/null @@ -1,36 +0,0 @@ -import Page from 'components/layout/Page'; -import useApi from 'components/hooks/useApi'; -import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function UserWebsites({ userId }) { - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - const { get, useQuery } = useApi(); - const { data, isLoading, error } = useQuery( - ['user:websites', userId, filter, page, pageSize], - () => - get(`/users/${userId}/websites`, { - filter, - page, - pageSize, - }), - ); - const hasData = data && data.length !== 0; - - return ( - - {hasData && ( - - )} - - ); -} - -export default UserWebsites; diff --git a/src/app/(main)/settings/users/UserWebsites.tsx b/src/app/(main)/settings/users/UserWebsites.tsx new file mode 100644 index 00000000..2d06e82a --- /dev/null +++ b/src/app/(main)/settings/users/UserWebsites.tsx @@ -0,0 +1,26 @@ +import Page from 'components/layout/Page'; +import useApi from 'components/hooks/useApi'; +import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable'; +import useFilterQuery from 'components/hooks/useFilterQuery'; +import DataTable from 'components/common/DataTable'; + +export function UserWebsites({ userId }) { + const { get } = useApi(); + const queryResult = useFilterQuery({ + queryKey: ['user:websites', userId], + queryFn: (params: any) => get(`/users/${userId}/websites`, params), + }); + const hasData = queryResult.result && queryResult.result.data.length !== 0; + + return ( + + {hasData && ( + + {({ data }) => } + + )} + + ); +} + +export default UserWebsites; diff --git a/src/app/(main)/settings/users/UsersDataTable.js b/src/app/(main)/settings/users/UsersDataTable.tsx similarity index 71% rename from src/app/(main)/settings/users/UsersDataTable.js rename to src/app/(main)/settings/users/UsersDataTable.tsx index 154e37ad..2495d023 100644 --- a/src/app/(main)/settings/users/UsersDataTable.js +++ b/src/app/(main)/settings/users/UsersDataTable.tsx @@ -8,11 +8,10 @@ import useCache from 'store/cache'; export function UsersDataTable() { const { get } = useApi(); - const modified = useCache(state => state?.users); - const queryResult = useFilterQuery(['users', { modified }], params => { - return get(`/users`, { - ...params, - }); + const modified = useCache((state: any) => state?.users); + const queryResult = useFilterQuery({ + queryKey: ['users', { modified }], + queryFn: (params: { [key: string]: any }) => get(`/admin/users`, params), }); return ( diff --git a/src/app/(main)/settings/users/UsersHeader.js b/src/app/(main)/settings/users/UsersHeader.tsx similarity index 85% rename from src/app/(main)/settings/users/UsersHeader.js rename to src/app/(main)/settings/users/UsersHeader.tsx index caf1f913..0901a6fb 100644 --- a/src/app/(main)/settings/users/UsersHeader.js +++ b/src/app/(main)/settings/users/UsersHeader.tsx @@ -3,7 +3,7 @@ import PageHeader from 'components/layout/PageHeader'; import useMessages from 'components/hooks/useMessages'; import UserAddButton from './UserAddButton'; -export function UsersHeader({ onAdd }) { +export function UsersHeader({ onAdd }: { onAdd?: () => void }) { const { formatMessage, labels } = useMessages(); return ( diff --git a/src/app/(main)/settings/users/UsersTable.js b/src/app/(main)/settings/users/UsersTable.tsx similarity index 96% rename from src/app/(main)/settings/users/UsersTable.js rename to src/app/(main)/settings/users/UsersTable.tsx index a0b5aba1..2b840b64 100644 --- a/src/app/(main)/settings/users/UsersTable.js +++ b/src/app/(main)/settings/users/UsersTable.tsx @@ -6,7 +6,7 @@ import useMessages from 'components/hooks/useMessages'; import useLocale from 'components/hooks/useLocale'; import UserDeleteButton from './UserDeleteButton'; -export function UsersTable({ data = [] }) { +export function UsersTable({ data = [] }: { data: any[] }) { const { formatMessage, labels } = useMessages(); const { dateLocale } = useLocale(); const breakpoint = useBreakpoint(); diff --git a/src/app/(main)/settings/users/[id]/UserSettings.js b/src/app/(main)/settings/users/[id]/UserSettings.tsx similarity index 84% rename from src/app/(main)/settings/users/[id]/UserSettings.js rename to src/app/(main)/settings/users/[id]/UserSettings.tsx index ea340ab7..d9b149c3 100644 --- a/src/app/(main)/settings/users/[id]/UserSettings.js +++ b/src/app/(main)/settings/users/[id]/UserSettings.tsx @@ -1,30 +1,30 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { Key, useEffect, useState } from 'react'; import { Item, Loading, Tabs, useToasts } from 'react-basics'; import UserEditForm from '../UserEditForm'; import PageHeader from 'components/layout/PageHeader'; import useApi from 'components/hooks/useApi'; -import UserWebsites from '../UserWebsites'; import useMessages from 'components/hooks/useMessages'; +import UserWebsites from '../UserWebsites'; export function UserSettings({ userId }) { const { formatMessage, labels, messages } = useMessages(); const [edit, setEdit] = useState(false); const [values, setValues] = useState(null); - const [tab, setTab] = useState('details'); + const [tab, setTab] = useState('details'); const { get, useQuery } = useApi(); const { showToast } = useToasts(); - const { data, isLoading } = useQuery( - ['user', userId], - () => { + const { data, isLoading } = useQuery({ + queryKey: ['user', userId], + queryFn: () => { if (userId) { return get(`/users/${userId}`); } }, - { cacheTime: 0 }, - ); + gcTime: 0, + }); - const handleSave = data => { + const handleSave = (data: any) => { showToast({ message: formatMessage(messages.saved), variant: 'success' }); if (data) { setValues(state => ({ ...state, ...data })); @@ -42,7 +42,7 @@ export function UserSettings({ userId }) { }, [data]); if (isLoading || !values) { - return ; + return ; } return ( diff --git a/src/app/(main)/settings/users/[id]/page.js b/src/app/(main)/settings/users/[id]/page.tsx similarity index 100% rename from src/app/(main)/settings/users/[id]/page.js rename to src/app/(main)/settings/users/[id]/page.tsx diff --git a/src/app/(main)/settings/websites/WebsiteAddButton.js b/src/app/(main)/settings/websites/WebsiteAddButton.tsx similarity index 83% rename from src/app/(main)/settings/websites/WebsiteAddButton.js rename to src/app/(main)/settings/websites/WebsiteAddButton.tsx index b1a69429..7b4c92de 100644 --- a/src/app/(main)/settings/websites/WebsiteAddButton.js +++ b/src/app/(main)/settings/websites/WebsiteAddButton.tsx @@ -3,7 +3,7 @@ import WebsiteAddForm from './WebsiteAddForm'; import useMessages from 'components/hooks/useMessages'; import { setValue } from 'store/cache'; -export function WebsiteAddButton({ onSave }) { +export function WebsiteAddButton({ onSave }: { onSave?: () => void }) { const { formatMessage, labels, messages } = useMessages(); const { showToast } = useToasts(); @@ -22,7 +22,7 @@ export function WebsiteAddButton({ onSave }) { {formatMessage(labels.addWebsite)} - {close => } + {(close: () => void) => } ); diff --git a/src/app/(main)/settings/websites/WebsiteAddForm.js b/src/app/(main)/settings/websites/WebsiteAddForm.tsx similarity index 68% rename from src/app/(main)/settings/websites/WebsiteAddForm.js rename to src/app/(main)/settings/websites/WebsiteAddForm.tsx index 371343ba..6d8fb8c3 100644 --- a/src/app/(main)/settings/websites/WebsiteAddForm.js +++ b/src/app/(main)/settings/websites/WebsiteAddForm.tsx @@ -10,17 +10,22 @@ import { import useApi from 'components/hooks/useApi'; import { DOMAIN_REGEX } from 'lib/constants'; import useMessages from 'components/hooks/useMessages'; +import { useContext } from 'react'; +import SettingsContext from '../SettingsContext'; -export function WebsiteAddForm({ onSave, onClose }) { +export function WebsiteAddForm({ onSave, onClose }: { onSave?: () => void; onClose?: () => void }) { const { formatMessage, labels, messages } = useMessages(); + const { websitesUrl } = useContext(SettingsContext); const { post, useMutation } = useApi(); - const { mutate, error, isLoading } = useMutation(data => post('/websites', data)); + const { mutate, error, isPending } = useMutation({ + mutationFn: (data: any) => post(websitesUrl, data), + }); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { - onSave(); - onClose(); + onSave?.(); + onClose?.(); }, }); }; @@ -47,9 +52,11 @@ export function WebsiteAddForm({ onSave, onClose }) { {formatMessage(labels.save)} - + {onClose && ( + + )}
); diff --git a/src/app/(main)/settings/websites/WebsiteSettings.js b/src/app/(main)/settings/websites/WebsiteSettings.tsx similarity index 68% rename from src/app/(main)/settings/websites/WebsiteSettings.js rename to src/app/(main)/settings/websites/WebsiteSettings.tsx index 71d5fe23..0c5ce614 100644 --- a/src/app/(main)/settings/websites/WebsiteSettings.js +++ b/src/app/(main)/settings/websites/WebsiteSettings.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; -import { Item, Tabs, useToasts, Button, Text, Icon, Icons } from 'react-basics'; +import { useContext, useEffect, useState, Key } from 'react'; +import { Item, Tabs, useToasts, Button, Text, Icon, Icons, Loading } from 'react-basics'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import PageHeader from 'components/layout/PageHeader'; @@ -10,31 +10,35 @@ import TrackingCode from './[id]/TrackingCode'; import ShareUrl from './[id]/ShareUrl'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; +import SettingsContext from '../SettingsContext'; -export function WebsiteSettings({ websiteId, openExternal = false, analyticsUrl }) { +export function WebsiteSettings({ websiteId, openExternal = false }) { const router = useRouter(); const { formatMessage, labels, messages } = useMessages(); const { get, useQuery } = useApi(); const { showToast } = useToasts(); - const { data } = useQuery(['website', websiteId], () => get(`/websites/${websiteId}`), { + const { websitesUrl, websitesPath, settingsPath } = useContext(SettingsContext); + const { data, isLoading } = useQuery({ + queryKey: ['website', websiteId], + queryFn: () => get(`${websitesUrl}/${websiteId}`), enabled: !!websiteId, - cacheTime: 0, + gcTime: 0, }); const [values, setValues] = useState(null); - const [tab, setTab] = useState('details'); + const [tab, setTab] = useState('details'); const showSuccess = () => { showToast({ message: formatMessage(messages.saved), variant: 'success' }); }; - const handleSave = data => { + const handleSave = (data: any) => { showSuccess(); - setValues(state => ({ ...state, ...data })); + setValues((state: any) => ({ ...state, ...data })); }; - const handleReset = async value => { + const handleReset = async (value: string) => { if (value === 'delete') { - await router.push('/settings/websites'); + router.push(settingsPath); } else if (value === 'reset') { showSuccess(); } @@ -46,10 +50,14 @@ export function WebsiteSettings({ websiteId, openExternal = false, analyticsUrl } }, [data]); + if (isLoading || !values) { + return ; + } + return ( <> - + - {close => ( + {(close: () => void) => ( )} @@ -36,7 +42,7 @@ export function WebsiteData({ websiteId, onSave }) { - {close => ( + {(close: () => void) => ( )} diff --git a/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.js b/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.tsx similarity index 72% rename from src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.js rename to src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.tsx index 1548bddb..e0f71041 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.js +++ b/src/app/(main)/settings/websites/[id]/WebsiteDeleteForm.tsx @@ -9,15 +9,28 @@ import { } from 'react-basics'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; +import { useContext } from 'react'; +import SettingsContext from '../../SettingsContext'; const CONFIRM_VALUE = 'DELETE'; -export function WebsiteDeleteForm({ websiteId, onSave, onClose }) { +export function WebsiteDeleteForm({ + websiteId, + onSave, + onClose, +}: { + websiteId: string; + onSave?: () => void; + onClose?: () => void; +}) { const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { websitesUrl } = useContext(SettingsContext); const { del, useMutation } = useApi(); - const { mutate, error } = useMutation(data => del(`/websites/${websiteId}`, data)); + const { mutate, error } = useMutation({ + mutationFn: (data: any) => del(`${websitesUrl}/${websiteId}`, data), + }); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { onSave(); diff --git a/src/app/(main)/settings/websites/[id]/WebsiteEditForm.js b/src/app/(main)/settings/websites/[id]/WebsiteEditForm.tsx similarity index 76% rename from src/app/(main)/settings/websites/[id]/WebsiteEditForm.js rename to src/app/(main)/settings/websites/[id]/WebsiteEditForm.tsx index 18ad0ac9..80b36cae 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteEditForm.js +++ b/src/app/(main)/settings/websites/[id]/WebsiteEditForm.tsx @@ -1,16 +1,28 @@ import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics'; -import { useRef } from 'react'; +import { useContext, useRef } from 'react'; import useApi from 'components/hooks/useApi'; import { DOMAIN_REGEX } from 'lib/constants'; import useMessages from 'components/hooks/useMessages'; +import SettingsContext from '../../SettingsContext'; -export function WebsiteEditForm({ websiteId, data, onSave }) { +export function WebsiteEditForm({ + websiteId, + data, + onSave, +}: { + websiteId: string; + data: any[]; + onSave?: (data: any) => void; +}) { const { formatMessage, labels, messages } = useMessages(); + const { websitesUrl } = useContext(SettingsContext); const { post, useMutation } = useApi(); - const { mutate, error } = useMutation(data => post(`/websites/${websiteId}`, data)); + const { mutate, error } = useMutation({ + mutationFn: (data: any) => post(`${websitesUrl}/${websiteId}`, data), + }); const ref = useRef(null); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { ref.current.reset(data); diff --git a/src/app/(main)/settings/websites/[id]/WebsiteResetForm.js b/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx similarity index 71% rename from src/app/(main)/settings/websites/[id]/WebsiteResetForm.js rename to src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx index 9886429b..0c02c77b 100644 --- a/src/app/(main)/settings/websites/[id]/WebsiteResetForm.js +++ b/src/app/(main)/settings/websites/[id]/WebsiteResetForm.tsx @@ -9,15 +9,28 @@ import { } from 'react-basics'; import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; +import { useContext } from 'react'; +import SettingsContext from '../../SettingsContext'; const CONFIRM_VALUE = 'RESET'; -export function WebsiteResetForm({ websiteId, onSave, onClose }) { +export function WebsiteResetForm({ + websiteId, + onSave, + onClose, +}: { + websiteId: string; + onSave?: () => void; + onClose?: () => void; +}) { const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { websitesUrl } = useContext(SettingsContext); const { post, useMutation } = useApi(); - const { mutate, error } = useMutation(data => post(`/websites/${websiteId}/reset`, data)); + const { mutate, error } = useMutation({ + mutationFn: (data: any) => post(`${websitesUrl}/${websiteId}/reset`, data), + }); - const handleSubmit = async data => { + const handleSubmit = async (data: any) => { mutate(data, { onSuccess: async () => { onSave(); diff --git a/src/app/(main)/settings/websites/[id]/page.js b/src/app/(main)/settings/websites/[id]/page.tsx similarity index 100% rename from src/app/(main)/settings/websites/[id]/page.js rename to src/app/(main)/settings/websites/[id]/page.tsx diff --git a/src/app/(main)/settings/websites/page.tsx b/src/app/(main)/settings/websites/page.tsx index 2c83dce0..d6d11898 100644 --- a/src/app/(main)/settings/websites/page.tsx +++ b/src/app/(main)/settings/websites/page.tsx @@ -1,14 +1,8 @@ -import WebsitesDataTable from './WebsitesDataTable'; -import WebsitesHeader from './WebsitesHeader'; import { Metadata } from 'next'; +import Websites from './Websites'; export default function () { - return ( - <> - - - - ); + return ; } export const metadata: Metadata = { diff --git a/src/app/(main)/websites/WebsitesBrowse.js b/src/app/(main)/websites/WebsitesBrowse.tsx similarity index 62% rename from src/app/(main)/websites/WebsitesBrowse.js rename to src/app/(main)/websites/WebsitesBrowse.tsx index f1bab7bf..c426cc06 100644 --- a/src/app/(main)/websites/WebsitesBrowse.js +++ b/src/app/(main)/websites/WebsitesBrowse.tsx @@ -1,6 +1,6 @@ 'use client'; import WebsitesDataTable from '../settings/websites/WebsitesDataTable'; -import { useMessages } from 'components/hooks'; +import { useMessages, useUser } from 'components/hooks'; import { useState } from 'react'; import { Item, Tabs } from 'react-basics'; @@ -10,19 +10,25 @@ const TABS = { }; export function WebsitesBrowse() { + const { user } = useUser(); const { formatMessage, labels } = useMessages(); const [tab, setTab] = useState(TABS.myWebsites); const allowEdit = !process.env.cloudMode; return ( <> - + setTab(tab)} style={{ marginBottom: 30 }}> {formatMessage(labels.myWebsites)} {formatMessage(labels.teamWebsites)} - {tab === TABS.myWebsites && } + {tab === TABS.myWebsites && } {tab === TABS.teamWebsites && ( - + )} ); diff --git a/src/app/(main)/websites/[id]/WebsiteChart.js b/src/app/(main)/websites/[id]/WebsiteChart.tsx similarity index 84% rename from src/app/(main)/websites/[id]/WebsiteChart.js rename to src/app/(main)/websites/[id]/WebsiteChart.tsx index d05ff422..eba155c1 100644 --- a/src/app/(main)/websites/[id]/WebsiteChart.js +++ b/src/app/(main)/websites/[id]/WebsiteChart.tsx @@ -3,7 +3,7 @@ import PageviewsChart from 'components/metrics/PageviewsChart'; import { useApi, useDateRange, useTimezone, useNavigation } from 'components/hooks'; import { getDateArray } from 'lib/date'; -export function WebsiteChart({ websiteId }) { +export function WebsiteChart({ websiteId }: { websiteId: string }) { const [dateRange] = useDateRange(websiteId); const { startDate, endDate, unit, modified } = dateRange; const [timezone] = useTimezone(); @@ -12,12 +12,12 @@ export function WebsiteChart({ websiteId }) { } = useNavigation(); const { get, useQuery } = useApi(); - const { data, isLoading } = useQuery( - [ + const { data, isLoading } = useQuery({ + queryKey: [ 'websites:pageviews', { websiteId, modified, url, referrer, os, browser, device, country, region, city, title }, ], - () => + queryFn: () => get(`/websites/${websiteId}/pageviews`, { startAt: +startDate, endAt: +endDate, @@ -33,7 +33,7 @@ export function WebsiteChart({ websiteId }) { city, title, }), - ); + }); const chartData = useMemo(() => { if (data) { @@ -45,7 +45,7 @@ export function WebsiteChart({ websiteId }) { return { pageviews: [], sessions: [] }; }, [data, startDate, endDate, unit]); - return ; + return ; } export default WebsiteChart; diff --git a/src/app/(main)/websites/[id]/WebsiteChartList.js b/src/app/(main)/websites/[id]/WebsiteChartList.tsx similarity index 87% rename from src/app/(main)/websites/[id]/WebsiteChartList.js rename to src/app/(main)/websites/[id]/WebsiteChartList.tsx index 23764dbb..b35b6f1f 100644 --- a/src/app/(main)/websites/[id]/WebsiteChartList.js +++ b/src/app/(main)/websites/[id]/WebsiteChartList.tsx @@ -1,4 +1,4 @@ -import { Button, Text, Icon } from 'react-basics'; +import { Button, Text, Icon, Icons } from 'react-basics'; import { useMemo } from 'react'; import { firstBy } from 'thenby'; import Link from 'next/link'; @@ -7,9 +7,16 @@ import useDashboard from 'store/dashboard'; import WebsiteHeader from './WebsiteHeader'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; import { useMessages, useLocale } from 'components/hooks'; -import Icons from 'components/icons'; -export default function WebsiteChartList({ websites, showCharts, limit }) { +export default function WebsiteChartList({ + websites, + showCharts, + limit, +}: { + websites: any[]; + showCharts?: boolean; + limit?: number; +}) { const { formatMessage, labels } = useMessages(); const { websiteOrder } = useDashboard(); const { dir } = useLocale(); diff --git a/src/app/(main)/websites/[id]/WebsiteDetails.js b/src/app/(main)/websites/[id]/WebsiteDetails.tsx similarity index 74% rename from src/app/(main)/websites/[id]/WebsiteDetails.js rename to src/app/(main)/websites/[id]/WebsiteDetails.tsx index c6ad1acc..b234ea0a 100644 --- a/src/app/(main)/websites/[id]/WebsiteDetails.js +++ b/src/app/(main)/websites/[id]/WebsiteDetails.tsx @@ -6,12 +6,12 @@ import FilterTags from 'components/metrics/FilterTags'; import useNavigation from 'components/hooks/useNavigation'; import { useWebsite } from 'components/hooks'; import WebsiteChart from './WebsiteChart'; -import WebsiteMenuView from './WebsiteMenuView'; +import WebsiteExpandedView from './WebsiteExpandedView'; import WebsiteHeader from './WebsiteHeader'; import WebsiteMetricsBar from './WebsiteMetricsBar'; import WebsiteTableView from './WebsiteTableView'; -export default function WebsiteDetails({ websiteId }) { +export default function WebsiteDetails({ websiteId }: { websiteId: string }) { const { data: website, isLoading, error } = useWebsite(websiteId); const pathname = usePathname(); const showLinks = !pathname.includes('/share/'); @@ -27,17 +27,14 @@ export default function WebsiteDetails({ websiteId }) { return ( <> - + {!website && } {website && ( <> - {!view && } - {view && } + {!view && } + {view && } )} diff --git a/src/app/(main)/websites/[id]/WebsiteMenuView.module.css b/src/app/(main)/websites/[id]/WebsiteExpandedView.module.css similarity index 100% rename from src/app/(main)/websites/[id]/WebsiteMenuView.module.css rename to src/app/(main)/websites/[id]/WebsiteExpandedView.module.css diff --git a/src/app/(main)/websites/[id]/WebsiteMenuView.js b/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx similarity index 90% rename from src/app/(main)/websites/[id]/WebsiteMenuView.js rename to src/app/(main)/websites/[id]/WebsiteExpandedView.tsx index c501645a..f01d9363 100644 --- a/src/app/(main)/websites/[id]/WebsiteMenuView.js +++ b/src/app/(main)/websites/[id]/WebsiteExpandedView.tsx @@ -15,7 +15,7 @@ import SideNav from 'components/layout/SideNav'; import useNavigation from 'components/hooks/useNavigation'; import useMessages from 'components/hooks/useMessages'; import LinkButton from 'components/common/LinkButton'; -import styles from './WebsiteMenuView.module.css'; +import styles from './WebsiteExpandedView.module.css'; const views = { url: PagesTable, @@ -33,7 +33,13 @@ const views = { query: QueryParametersTable, }; -export default function WebsiteMenuView({ websiteId, websiteDomain }) { +export default function WebsiteExpandedView({ + websiteId, + domainName, +}: { + websiteId: string; + domainName?: string; +}) { const { formatMessage, labels } = useMessages(); const { router, @@ -107,11 +113,11 @@ export default function WebsiteMenuView({ websiteId, websiteDomain }) { const DetailsComponent = views[view] || (() => null); - const handleChange = view => { + const handleChange = (view: any) => { router.push(makeUrl({ view })); }; - const renderValue = value => items.find(({ key }) => key === value)?.label; + const renderValue = (value: string) => items.find(({ key }) => key === value)?.label; return (
@@ -137,12 +143,12 @@ export default function WebsiteMenuView({ websiteId, websiteDomain }) {
diff --git a/src/app/(main)/websites/[id]/WebsiteFilterButton.js b/src/app/(main)/websites/[id]/WebsiteFilterButton.tsx similarity index 91% rename from src/app/(main)/websites/[id]/WebsiteFilterButton.js rename to src/app/(main)/websites/[id]/WebsiteFilterButton.tsx index e96856f6..6a02cd47 100644 --- a/src/app/(main)/websites/[id]/WebsiteFilterButton.js +++ b/src/app/(main)/websites/[id]/WebsiteFilterButton.tsx @@ -3,7 +3,13 @@ import PopupForm from 'app/(main)/reports/[id]/PopupForm'; import FilterSelectForm from 'app/(main)/reports/[id]/FilterSelectForm'; import { useMessages, useNavigation } from 'components/hooks'; -export function WebsiteFilterButton({ websiteId, className }) { +export function WebsiteFilterButton({ + websiteId, + className, +}: { + websiteId: string; + className?: string; +}) { const { formatMessage, labels } = useMessages(); const { makeUrl, router } = useNavigation(); @@ -31,9 +37,9 @@ export function WebsiteFilterButton({ websiteId, className }) { {formatMessage(labels.filter)} - {close => { + {(close: () => void) => { return ( - + + queryFn: () => get(`/websites/${websiteId}/stats`, { startAt: +startDate, endAt: +endDate, @@ -36,7 +44,7 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { region, city, }), - ); + }); const { pageviews, uniques, bounces, totaltime } = data || {}; const num = Math.min(data && uniques.value, data && bounces.value); @@ -96,7 +104,7 @@ export function WebsiteMetricsBar({ websiteId, showFilter = true, sticky }) { -1 || 0 : 0 } - format={n => `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`} + format={n => `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`} /> )} diff --git a/src/app/(main)/websites/[id]/WebsiteTableView.js b/src/app/(main)/websites/[id]/WebsiteTableView.tsx similarity index 87% rename from src/app/(main)/websites/[id]/WebsiteTableView.js rename to src/app/(main)/websites/[id]/WebsiteTableView.tsx index 7c71b84b..7cc415e5 100644 --- a/src/app/(main)/websites/[id]/WebsiteTableView.js +++ b/src/app/(main)/websites/[id]/WebsiteTableView.tsx @@ -5,15 +5,22 @@ import ReferrersTable from 'components/metrics/ReferrersTable'; import BrowsersTable from 'components/metrics/BrowsersTable'; import OSTable from 'components/metrics/OSTable'; import DevicesTable from 'components/metrics/DevicesTable'; -import WorldMap from 'components/common/WorldMap'; +import WorldMap from 'components/metrics/WorldMap'; import CountriesTable from 'components/metrics/CountriesTable'; import EventsTable from 'components/metrics/EventsTable'; import EventsChart from 'components/metrics/EventsChart'; -export default function WebsiteTableView({ websiteId }) { +export default function WebsiteTableView({ + websiteId, + domainName, +}: { + websiteId: string; + domainName: string; +}) { const [countryData, setCountryData] = useState(); const tableProps = { websiteId, + domainName, limit: 10, }; diff --git a/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.js b/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.tsx similarity index 83% rename from src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.js rename to src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.tsx index 5be19185..419472d5 100644 --- a/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.js +++ b/src/app/(main)/websites/[id]/event-data/EventDataMetricsBar.tsx @@ -5,21 +5,21 @@ import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; import MetricsBar from 'components/metrics/MetricsBar'; import styles from './EventDataMetricsBar.module.css'; -export function EventDataMetricsBar({ websiteId }) { +export function EventDataMetricsBar({ websiteId }: { websiteId: string }) { const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); const [dateRange] = useDateRange(websiteId); const { startDate, endDate, modified } = dateRange; - const { data, error, isLoading, isFetched } = useQuery( - ['event-data:stats', { websiteId, startDate, endDate, modified }], - () => + const { data, error, isLoading, isFetched } = useQuery({ + queryKey: ['event-data:stats', { websiteId, startDate, endDate, modified }], + queryFn: () => get(`/event-data/stats`, { websiteId, startAt: +startDate, endAt: +endDate, }), - ); + }); return (
diff --git a/src/app/(main)/websites/[id]/event-data/EventDataTable.js b/src/app/(main)/websites/[id]/event-data/EventDataTable.tsx similarity index 100% rename from src/app/(main)/websites/[id]/event-data/EventDataTable.js rename to src/app/(main)/websites/[id]/event-data/EventDataTable.tsx diff --git a/src/app/(main)/websites/[id]/event-data/EventDataValueTable.js b/src/app/(main)/websites/[id]/event-data/EventDataValueTable.tsx similarity index 94% rename from src/app/(main)/websites/[id]/event-data/EventDataValueTable.js rename to src/app/(main)/websites/[id]/event-data/EventDataValueTable.tsx index 4e50f5b9..7976ce36 100644 --- a/src/app/(main)/websites/[id]/event-data/EventDataValueTable.js +++ b/src/app/(main)/websites/[id]/event-data/EventDataValueTable.tsx @@ -6,7 +6,7 @@ import PageHeader from 'components/layout/PageHeader'; import Empty from 'components/common/Empty'; import { DATA_TYPES } from 'lib/constants'; -export function EventDataValueTable({ data = [], event }) { +export function EventDataValueTable({ data = [], event }: { data: any[]; event: string }) { const { formatMessage, labels } = useMessages(); const { makeUrl } = useNavigation(); diff --git a/src/app/(main)/websites/[id]/event-data/WebsiteEventData.js b/src/app/(main)/websites/[id]/event-data/WebsiteEventData.tsx similarity index 81% rename from src/app/(main)/websites/[id]/event-data/WebsiteEventData.js rename to src/app/(main)/websites/[id]/event-data/WebsiteEventData.tsx index b5982e32..61a4dc62 100644 --- a/src/app/(main)/websites/[id]/event-data/WebsiteEventData.js +++ b/src/app/(main)/websites/[id]/event-data/WebsiteEventData.tsx @@ -6,21 +6,21 @@ import { EventDataMetricsBar } from './EventDataMetricsBar'; import { useDateRange, useApi, useNavigation } from 'components/hooks'; import styles from './WebsiteEventData.module.css'; -function useData(websiteId, event) { +function useData(websiteId: string, event: string) { const [dateRange] = useDateRange(websiteId); const { startDate, endDate } = dateRange; const { get, useQuery } = useApi(); - const { data, error, isLoading } = useQuery( - ['event-data:events', { websiteId, startDate, endDate, event }], - () => + const { data, error, isLoading } = useQuery({ + queryKey: ['event-data:events', { websiteId, startDate, endDate, event }], + queryFn: () => get('/event-data/events', { websiteId, startAt: +startDate, endAt: +endDate, event, }), - { enabled: !!(websiteId && startDate && endDate) }, - ); + enabled: !!(websiteId && startDate && endDate), + }); return { data, error, isLoading }; } diff --git a/src/app/(main)/websites/[id]/event-data/page.js b/src/app/(main)/websites/[id]/event-data/page.tsx similarity index 100% rename from src/app/(main)/websites/[id]/event-data/page.js rename to src/app/(main)/websites/[id]/event-data/page.tsx diff --git a/src/app/(main)/websites/[id]/realtime/Realtime.js b/src/app/(main)/websites/[id]/realtime/Realtime.tsx similarity index 65% rename from src/app/(main)/websites/[id]/realtime/Realtime.js rename to src/app/(main)/websites/[id]/realtime/Realtime.tsx index b4219b0a..bd9f74bc 100644 --- a/src/app/(main)/websites/[id]/realtime/Realtime.js +++ b/src/app/(main)/websites/[id]/realtime/Realtime.tsx @@ -1,60 +1,59 @@ 'use client'; import { useMemo, useState, useEffect } from 'react'; import { subMinutes, startOfMinute } from 'date-fns'; -import firstBy from 'thenby'; +import thenby from 'thenby'; import { Grid, GridRow } from 'components/layout/Grid'; import Page from 'components/layout/Page'; import RealtimeChart from 'components/metrics/RealtimeChart'; -import WorldMap from 'components/common/WorldMap'; +import WorldMap from 'components/metrics/WorldMap'; +import useApi from 'components/hooks/useApi'; +import { useWebsite } from 'components/hooks'; +import { percentFilter } from 'lib/filters'; +import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; +import { RealtimeData } from 'lib/types'; import RealtimeLog from './RealtimeLog'; import RealtimeHeader from './RealtimeHeader'; import RealtimeUrls from './RealtimeUrls'; import RealtimeCountries from './RealtimeCountries'; import WebsiteHeader from '../WebsiteHeader'; -import useApi from 'components/hooks/useApi'; -import { percentFilter } from 'lib/filters'; -import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; -import { useWebsite } from 'components/hooks'; import styles from './Realtime.module.css'; -function mergeData(state = [], data = [], time) { - const ids = state.map(({ __id }) => __id); +function mergeData(state = [], data = [], time: number) { + const ids = state.map(({ id }) => id); return state - .concat(data.filter(({ __id }) => !ids.includes(__id))) + .concat(data.filter(({ id }) => !ids.includes(id))) .filter(({ timestamp }) => timestamp >= time); } export function Realtime({ websiteId }) { - const [currentData, setCurrentData] = useState(); + const [currentData, setCurrentData] = useState(); const { get, useQuery } = useApi(); const { data: website } = useWebsite(websiteId); - const { data, isLoading, error } = useQuery( - ['realtime', websiteId], - () => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }), - { - enabled: !!(websiteId && website), - refetchInterval: REALTIME_INTERVAL, - cache: false, - }, - ); + const { data, isLoading, error } = useQuery({ + queryKey: ['realtime', websiteId], + queryFn: () => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }), + enabled: !!(websiteId && website), + refetchInterval: REALTIME_INTERVAL, + }); useEffect(() => { if (data) { const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); const time = date.getTime(); + const { pageviews, sessions, events, timestamp } = data; setCurrentData(state => ({ - pageviews: mergeData(state?.pageviews, data.pageviews, time), - sessions: mergeData(state?.sessions, data.sessions, time), - events: mergeData(state?.events, data.events, time), - timestamp: data.timestamp, + pageviews: mergeData(state?.pageviews, pageviews, time), + sessions: mergeData(state?.sessions, sessions, time), + events: mergeData(state?.events, events, time), + timestamp, })); } }, [data]); - const realtimeData = useMemo(() => { + const realtimeData: RealtimeData = useMemo(() => { if (!currentData) { - return { pageviews: [], sessions: [], events: [], countries: [], visitors: [] }; + return { pageviews: [], sessions: [], events: [], countries: [], visitors: [], timestamp: 0 }; } currentData.countries = percentFilter( @@ -65,7 +64,7 @@ export function Realtime({ websiteId }) { } return arr; }, []) - .reduce((arr, { country }) => { + .reduce((arr: { x: any; y: number }[], { country }: any) => { if (country) { const row = arr.find(({ x }) => x === country); @@ -77,7 +76,7 @@ export function Realtime({ websiteId }) { } return arr; }, []) - .sort(firstBy('y', -1)), + .sort(thenby.firstBy('y', -1)), ); currentData.visitors = currentData.sessions.reduce((arr, val) => { @@ -91,18 +90,18 @@ export function Realtime({ websiteId }) { }, [currentData]); if (isLoading || error) { - return ; + return ; } return ( <> - + - - + + diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeCountries.js b/src/app/(main)/websites/[id]/realtime/RealtimeCountries.tsx similarity index 100% rename from src/app/(main)/websites/[id]/realtime/RealtimeCountries.js rename to src/app/(main)/websites/[id]/realtime/RealtimeCountries.tsx diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeHeader.js b/src/app/(main)/websites/[id]/realtime/RealtimeHeader.tsx similarity index 85% rename from src/app/(main)/websites/[id]/realtime/RealtimeHeader.js rename to src/app/(main)/websites/[id]/realtime/RealtimeHeader.tsx index 75f2f2d4..ad03efd1 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeHeader.js +++ b/src/app/(main)/websites/[id]/realtime/RealtimeHeader.tsx @@ -1,10 +1,11 @@ import MetricCard from 'components/metrics/MetricCard'; import useMessages from 'components/hooks/useMessages'; +import { RealtimeData } from 'lib/types'; import styles from './RealtimeHeader.module.css'; -export function RealtimeHeader({ data = {} }) { +export function RealtimeHeader({ data }: { data: RealtimeData }) { const { formatMessage, labels } = useMessages(); - const { pageviews, visitors, events, countries } = data; + const { pageviews, visitors, events, countries } = data || {}; return (
diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeHome.js b/src/app/(main)/websites/[id]/realtime/RealtimeHome.tsx similarity index 88% rename from src/app/(main)/websites/[id]/realtime/RealtimeHome.js rename to src/app/(main)/websites/[id]/realtime/RealtimeHome.tsx index dbaeb541..154ac707 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeHome.js +++ b/src/app/(main)/websites/[id]/realtime/RealtimeHome.tsx @@ -10,7 +10,10 @@ export function RealtimeHome() { const { formatMessage, labels, messages } = useMessages(); const { get, useQuery } = useApi(); const router = useRouter(); - const { data, isLoading, error } = useQuery(['websites:me'], () => get('/me/websites')); + const { data, isLoading, error } = useQuery({ + queryKey: ['websites:me'], + queryFn: () => get('/me/websites'), + }); useEffect(() => { if (data?.length) { diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css b/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css index f400cc1b..e9c0fc1b 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css +++ b/src/app/(main)/websites/[id]/realtime/RealtimeLog.module.css @@ -66,3 +66,25 @@ .row .link:hover { color: var(--primary400); } + +.search { + max-width: 300px; +} + +.actions { + display: flex; + gap: 20px; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + +@media only screen and (max-width: 992px) { + .actions { + flex-direction: column; + } + + .search { + max-width: 100%; + } +} diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeLog.js b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx similarity index 73% rename from src/app/(main)/websites/[id]/realtime/RealtimeLog.js rename to src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx index b388b37b..5293c1f0 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeLog.js +++ b/src/app/(main)/websites/[id]/realtime/RealtimeLog.tsx @@ -1,18 +1,19 @@ import { useMemo, useState } from 'react'; -import { StatusLight, Icon, Text } from 'react-basics'; +import { StatusLight, Icon, Text, SearchField } from 'react-basics'; import { FixedSizeList } from 'react-window'; -import firstBy from 'thenby'; +import { format } from 'date-fns'; +import thenby from 'thenby'; +import { safeDecodeURI } from 'next-basics'; import FilterButtons from 'components/common/FilterButtons'; import Empty from 'components/common/Empty'; import useLocale from 'components/hooks/useLocale'; import useCountryNames from 'components/hooks/useCountryNames'; +import Icons from 'components/icons'; +import useMessages from 'components/hooks/useMessages'; +import useFormat from 'components//hooks/useFormat'; import { BROWSERS } from 'lib/constants'; import { stringToColor } from 'lib/format'; -import { formatDate } from 'lib/date'; -import { safeDecodeURI } from 'next-basics'; -import Icons from 'components/icons'; import styles from './RealtimeLog.module.css'; -import useMessages from 'components/hooks/useMessages'; const TYPE_ALL = 'all'; const TYPE_PAGEVIEW = 'pageview'; @@ -26,7 +27,9 @@ const icons = { }; export function RealtimeLog({ data, websiteDomain }) { + const [search, setSearch] = useState(''); const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { formatValue } = useFormat(); const { locale } = useLocale(); const countryNames = useCountryNames(locale); const [filter, setFilter] = useState(TYPE_ALL); @@ -50,13 +53,21 @@ export function RealtimeLog({ data, websiteDomain }) { }, ]; - const getTime = ({ createdAt }) => formatDate(new Date(createdAt), 'pp', locale); + const getTime = ({ timestamp }) => format(timestamp, 'h:mm:ss'); const getColor = ({ id, sessionId }) => stringToColor(sessionId || id); const getIcon = ({ __type }) => icons[__type]; - const getDetail = log => { + const getDetail = (log: { + __type: any; + eventName: any; + urlPath: any; + browser: any; + os: any; + country: any; + device: any; + }) => { const { __type, eventName, urlPath: url, browser, os, country, device } = log; if (__type === TYPE_EVENT) { @@ -130,23 +141,43 @@ export function RealtimeLog({ data, websiteDomain }) { } const { pageviews, visitors, events } = data; - const logs = [...pageviews, ...visitors, ...events].sort(firstBy('createdAt', -1)); + let logs = [...pageviews, ...visitors, ...events].sort(thenby.firstBy('createdAt', -1)); + + if (search) { + logs = logs.filter(({ eventName, urlPath, browser, os, country, device }) => { + return [ + eventName, + urlPath, + os, + formatValue(browser, 'browser'), + formatValue(country, 'country'), + formatValue(device, 'device'), + ] + .filter(n => n) + .map(n => n.toLowerCase()) + .join('') + .includes(search.toLowerCase()); + }); + } if (filter !== TYPE_ALL) { return logs.filter(({ __type }) => __type === filter); } return logs; - }, [data, filter]); + }, [data, filter, formatValue, search]); return (
- +
+ + +
{formatMessage(labels.activityLog)}
{logs?.length === 0 && } {logs?.length > 0 && ( - + {Row} )} diff --git a/src/app/(main)/websites/[id]/realtime/RealtimeUrls.js b/src/app/(main)/websites/[id]/realtime/RealtimeUrls.tsx similarity index 85% rename from src/app/(main)/websites/[id]/realtime/RealtimeUrls.js rename to src/app/(main)/websites/[id]/realtime/RealtimeUrls.tsx index 674858b2..27a9ec5a 100644 --- a/src/app/(main)/websites/[id]/realtime/RealtimeUrls.js +++ b/src/app/(main)/websites/[id]/realtime/RealtimeUrls.tsx @@ -1,15 +1,22 @@ -import { useMemo, useState } from 'react'; +import { Key, useMemo, useState } from 'react'; import { ButtonGroup, Button, Flexbox } from 'react-basics'; -import firstBy from 'thenby'; +import thenby from 'thenby'; import { percentFilter } from 'lib/filters'; import ListTable from 'components/metrics/ListTable'; import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants'; import useMessages from 'components/hooks/useMessages'; +import { RealtimeData } from 'lib/types'; -export function RealtimeUrls({ websiteDomain, data = {} }) { +export function RealtimeUrls({ + websiteDomain, + data, +}: { + websiteDomain: string; + data: RealtimeData; +}) { const { formatMessage, labels } = useMessages(); - const { pageviews } = data; - const [filter, setFilter] = useState(FILTER_REFERRERS); + const { pageviews } = data || {}; + const [filter, setFilter] = useState(FILTER_REFERRERS); const limit = 15; const buttons = [ @@ -48,7 +55,7 @@ export function RealtimeUrls({ websiteDomain, data = {} }) { } return arr; }, []) - .sort(firstBy('y', -1)) + .sort(thenby.firstBy('y', -1)) .slice(0, limit), ); @@ -64,7 +71,7 @@ export function RealtimeUrls({ websiteDomain, data = {} }) { } return arr; }, []) - .sort(firstBy('y', -1)) + .sort(thenby.firstBy('y', -1)) .slice(0, limit), ); diff --git a/src/app/(main)/websites/[id]/reports/WebsiteReports.js b/src/app/(main)/websites/[id]/reports/WebsiteReports.tsx similarity index 100% rename from src/app/(main)/websites/[id]/reports/WebsiteReports.js rename to src/app/(main)/websites/[id]/reports/WebsiteReports.tsx diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx index c3d62699..f1460b05 100644 --- a/src/app/Providers.tsx +++ b/src/app/Providers.tsx @@ -1,8 +1,10 @@ 'use client'; +import { useEffect, useState } from 'react'; import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactBasicsProvider } from 'react-basics'; import ErrorBoundary from 'components/common/ErrorBoundary'; +import SettingsContext from 'app/(main)/settings/SettingsContext'; import useLocale from 'components/hooks/useLocale'; import 'chartjs-adapter-date-fns'; @@ -24,14 +26,34 @@ function MessagesProvider({ children }) { ); } +function SettingsProvider({ children }) { + const [config, setConfig] = useState({}); + + useEffect(() => { + const hostUrl = process.env.hostUrl || window?.location.origin; + + setConfig({ + shareUrl: hostUrl, + trackingCodeUrl: hostUrl, + websitesUrl: '/websites', + settingsPath: '/settings/websites', + websitesPath: `/websites`, + }); + }, []); + + return {children}; +} + export function Providers({ children }) { return ( - - - {children} - - + + + + {children} + + + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e2478a95..970c33f0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,16 +8,16 @@ import 'styles/locale.css'; import 'styles/index.css'; import 'styles/variables.css'; -export default function RootLayout({ children }) { +export default function ({ children }) { return ( - - - - - - + + + + + + diff --git a/src/app/login/LoginForm.js b/src/app/login/LoginForm.tsx similarity index 90% rename from src/app/login/LoginForm.js rename to src/app/login/LoginForm.tsx index 59d145bf..78cf3dd3 100644 --- a/src/app/login/LoginForm.js +++ b/src/app/login/LoginForm.tsx @@ -1,5 +1,4 @@ 'use client'; -import { useMutation } from '@tanstack/react-query'; import { Form, FormRow, @@ -21,8 +20,10 @@ import styles from './LoginForm.module.css'; export function LoginForm() { const { formatMessage, labels, getMessage } = useMessages(); const router = useRouter(); - const { post } = useApi(); - const { mutate, error, isLoading } = useMutation(data => post('/auth/login', data)); + const { post, useMutation } = useApi(); + const { mutate, error, isPending } = useMutation({ + mutationFn: (data: any) => post('/auth/login', data), + }); const handleSubmit = async data => { mutate(data, { @@ -53,7 +54,7 @@ export function LoginForm() { - + {formatMessage(labels.login)} diff --git a/src/app/logout/Logout.js b/src/app/logout/Logout.tsx similarity index 100% rename from src/app/logout/Logout.js rename to src/app/logout/Logout.tsx diff --git a/src/app/logout/page.tsx b/src/app/logout/page.tsx index bce24736..89a3bce9 100644 --- a/src/app/logout/page.tsx +++ b/src/app/logout/page.tsx @@ -1,5 +1,10 @@ import Logout from './Logout'; +import { Metadata } from 'next'; export default function () { return ; } + +export const metadata: Metadata = { + title: 'Logout | umami', +}; diff --git a/src/app/share/[...id]/Footer.js b/src/app/share/[...id]/Footer.tsx similarity index 100% rename from src/app/share/[...id]/Footer.js rename to src/app/share/[...id]/Footer.tsx diff --git a/src/app/share/[...id]/Header.js b/src/app/share/[...id]/Header.tsx similarity index 86% rename from src/app/share/[...id]/Header.js rename to src/app/share/[...id]/Header.tsx index 41e93f52..2b82908d 100644 --- a/src/app/share/[...id]/Header.js +++ b/src/app/share/[...id]/Header.tsx @@ -19,8 +19,8 @@ export function Header() {
- - + +
diff --git a/src/app/share/[...id]/Share.js b/src/app/share/[...id]/Share.tsx similarity index 100% rename from src/app/share/[...id]/Share.js rename to src/app/share/[...id]/Share.tsx diff --git a/src/app/share/[...id]/page.tsx b/src/app/share/[...id]/page.tsx index ca154165..2a69f406 100644 --- a/src/app/share/[...id]/page.tsx +++ b/src/app/share/[...id]/page.tsx @@ -1,5 +1,10 @@ import Share from './Share'; +import { Metadata } from 'next'; export default function ({ params: { id } }) { return ; } + +export const metadata: Metadata = { + title: 'umami', +}; diff --git a/src/app/sso/page.tsx b/src/app/sso/page.tsx index 75ea945d..e577767a 100644 --- a/src/app/sso/page.tsx +++ b/src/app/sso/page.tsx @@ -18,5 +18,5 @@ export default function SSOPage() { } }, [router, url, token]); - return ; + return ; } diff --git a/src/components/common/ConfirmDeleteForm.js b/src/components/common/ConfirmDeleteForm.tsx similarity index 80% rename from src/components/common/ConfirmDeleteForm.js rename to src/components/common/ConfirmDeleteForm.tsx index 3d2c383d..d4cbf203 100644 --- a/src/components/common/ConfirmDeleteForm.js +++ b/src/components/common/ConfirmDeleteForm.tsx @@ -2,7 +2,13 @@ import { useState } from 'react'; import { Button, LoadingButton, Form, FormButtons } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; -export function ConfirmDeleteForm({ name, onConfirm, onClose }) { +export interface ConfirmDeleteFormProps { + name: string; + onConfirm?: () => void; + onClose?: () => void; +} + +export function ConfirmDeleteForm({ name, onConfirm, onClose }: ConfirmDeleteFormProps) { const [loading, setLoading] = useState(false); const { formatMessage, labels, messages, FormattedMessage } = useMessages(); diff --git a/src/components/common/DataTable.tsx b/src/components/common/DataTable.tsx index a3c63c0a..00aba09c 100644 --- a/src/components/common/DataTable.tsx +++ b/src/components/common/DataTable.tsx @@ -1,29 +1,16 @@ -import { ReactNode, Dispatch, SetStateAction } from 'react'; +import { ReactNode } from 'react'; import classNames from 'classnames'; import { Banner, Loading, SearchField } from 'react-basics'; import { useMessages } from 'components/hooks'; import Empty from 'components/common/Empty'; import Pager from 'components/common/Pager'; import styles from './DataTable.module.css'; +import { FilterQueryResult } from 'components/hooks/useFilterQuery'; const DEFAULT_SEARCH_DELAY = 600; export interface DataTableProps { - queryResult: { - result: { - page: number; - pageSize: number; - count: number; - data: any[]; - }; - params: { - query: string; - page: number; - }; - setParams: Dispatch>; - isLoading: boolean; - error: unknown; - }; + queryResult: FilterQueryResult; searchDelay?: number; allowSearch?: boolean; allowPaging?: boolean; @@ -38,17 +25,22 @@ export function DataTable({ children, }: DataTableProps) { const { formatMessage, labels, messages } = useMessages(); - const { result, error, isLoading, params, setParams } = queryResult || {}; + const { + result, + params, + setParams, + query: { error, isLoading }, + } = queryResult || {}; const { page, pageSize, count, data } = result || {}; const { query } = params || {}; const hasData = Boolean(!isLoading && data?.length); const noResults = Boolean(!isLoading && query && !hasData); - const handleSearch = query => { + const handleSearch = (query: string) => { setParams({ ...params, query, page: params.page ? page : 1 }); }; - const handlePageChange = page => { + const handlePageChange = (page: number) => { setParams({ ...params, query, page }); }; @@ -62,7 +54,7 @@ export function DataTable({ diff --git a/src/components/common/ErrorBoundry.module.css b/src/components/common/ErrorBoundary.module.css similarity index 100% rename from src/components/common/ErrorBoundry.module.css rename to src/components/common/ErrorBoundary.module.css diff --git a/src/components/common/ErrorBoundary.js b/src/components/common/ErrorBoundary.tsx similarity index 73% rename from src/components/common/ErrorBoundary.js rename to src/components/common/ErrorBoundary.tsx index 32cedb39..49b7e671 100644 --- a/src/components/common/ErrorBoundary.js +++ b/src/components/common/ErrorBoundary.tsx @@ -1,14 +1,15 @@ -/* eslint-disable no-console */ +import { ErrorInfo, ReactNode } from 'react'; import { ErrorBoundary as Boundary } from 'react-error-boundary'; import { Button } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; -import styles from './ErrorBoundry.module.css'; +import styles from './ErrorBoundary.module.css'; -const logError = (error, info) => { +const logError = (error: Error, info: ErrorInfo) => { + // eslint-disable-next-line no-console console.error(error, info.componentStack); }; -export function ErrorBoundary({ children }) { +export function ErrorBoundary({ children }: { children: ReactNode }) { const { formatMessage, messages } = useMessages(); const fallbackRender = ({ error, resetErrorBoundary }) => { diff --git a/src/components/common/ErrorMessage.js b/src/components/common/ErrorMessage.tsx similarity index 89% rename from src/components/common/ErrorMessage.js rename to src/components/common/ErrorMessage.tsx index f8129c6b..0deb6f92 100644 --- a/src/components/common/ErrorMessage.js +++ b/src/components/common/ErrorMessage.tsx @@ -7,7 +7,7 @@ export function ErrorMessage() { return (
- + {formatMessage(messages.error)} diff --git a/src/components/common/Favicon.js b/src/components/common/Favicon.tsx similarity index 93% rename from src/components/common/Favicon.js rename to src/components/common/Favicon.tsx index 55059cc0..2bf43c77 100644 --- a/src/components/common/Favicon.js +++ b/src/components/common/Favicon.tsx @@ -1,6 +1,6 @@ import styles from './Favicon.module.css'; -function getHostName(url) { +function getHostName(url: string) { const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n?=]+)/im); return match && match.length > 1 ? match[1] : null; } diff --git a/src/components/common/FilterButtons.js b/src/components/common/FilterButtons.js deleted file mode 100644 index f5a54fb6..00000000 --- a/src/components/common/FilterButtons.js +++ /dev/null @@ -1,13 +0,0 @@ -import { ButtonGroup, Button, Flexbox } from 'react-basics'; - -export function FilterButtons({ items, selectedKey, onSelect }) { - return ( - - - {({ key, label }) => } - - - ); -} - -export default FilterButtons; diff --git a/src/components/common/FilterButtons.tsx b/src/components/common/FilterButtons.tsx new file mode 100644 index 00000000..a64a6482 --- /dev/null +++ b/src/components/common/FilterButtons.tsx @@ -0,0 +1,20 @@ +import { Key } from 'react'; +import { ButtonGroup, Button, Flexbox } from 'react-basics'; + +export interface FilterButtonsProps { + items: any[]; + selectedKey?: Key; + onSelect: (key: any) => void; +} + +export function FilterButtons({ items, selectedKey, onSelect }: FilterButtonsProps) { + return ( + + + {({ key, label }) => } + + + ); +} + +export default FilterButtons; diff --git a/src/components/common/FilterLink.js b/src/components/common/FilterLink.tsx similarity index 79% rename from src/components/common/FilterLink.js rename to src/components/common/FilterLink.tsx index 89648255..bc0a4d48 100644 --- a/src/components/common/FilterLink.js +++ b/src/components/common/FilterLink.tsx @@ -1,3 +1,4 @@ +import { ReactNode } from 'react'; import { Icon, Icons } from 'react-basics'; import classNames from 'classnames'; import Link from 'next/link'; @@ -6,7 +7,23 @@ import useNavigation from 'components/hooks/useNavigation'; import useMessages from 'components/hooks/useMessages'; import styles from './FilterLink.module.css'; -export function FilterLink({ id, value, label, externalUrl, children, className }) { +export interface FilterLinkProps { + id: string; + value: string; + label?: string; + externalUrl?: string; + className?: string; + children?: ReactNode; +} + +export function FilterLink({ + id, + value, + label, + externalUrl, + children, + className, +}: FilterLinkProps) { const { formatMessage, labels } = useMessages(); const { makeUrl, query } = useNavigation(); const active = query[id] !== undefined; diff --git a/src/components/common/HamburgerButton.js b/src/components/common/HamburgerButton.js deleted file mode 100644 index f97006ef..00000000 --- a/src/components/common/HamburgerButton.js +++ /dev/null @@ -1,59 +0,0 @@ -import { Button, Icon } from 'react-basics'; -import { useState } from 'react'; -import MobileMenu from './MobileMenu'; -import Icons from 'components/icons'; -import useMessages from 'components/hooks/useMessages'; - -export function HamburgerButton() { - const { formatMessage, labels } = useMessages(); - const [active, setActive] = useState(false); - const cloudMode = Boolean(process.env.cloudMode); - - const menuItems = [ - { - label: formatMessage(labels.dashboard), - url: '/dashboard', - }, - !cloudMode && { - label: formatMessage(labels.settings), - url: '/settings', - children: [ - { - label: formatMessage(labels.websites), - url: '/settings/websites', - }, - { - label: formatMessage(labels.teams), - url: '/settings/teams', - }, - { - label: formatMessage(labels.users), - url: '/settings/users', - }, - { - label: formatMessage(labels.profile), - url: '/settings/profile', - }, - ], - }, - cloudMode && { - label: formatMessage(labels.profile), - url: '/settings/profile', - }, - !cloudMode && { label: formatMessage(labels.logout), url: '/logout' }, - ].filter(n => n); - - const handleClick = () => setActive(state => !state); - const handleClose = () => setActive(false); - - return ( - <> - - {active && } - - ); -} - -export default HamburgerButton; diff --git a/src/components/common/HamburgerButton.tsx b/src/components/common/HamburgerButton.tsx new file mode 100644 index 00000000..5a81f3a3 --- /dev/null +++ b/src/components/common/HamburgerButton.tsx @@ -0,0 +1,21 @@ +import { Button, Icon, Icons } from 'react-basics'; +import { useState } from 'react'; +import MobileMenu from './MobileMenu'; + +export function HamburgerButton({ menuItems }: { menuItems: any[] }) { + const [active, setActive] = useState(false); + + const handleClick = () => setActive(state => !state); + const handleClose = () => setActive(false); + + return ( + <> + + {active && } + + ); +} + +export default HamburgerButton; diff --git a/src/components/common/HoverTooltip.js b/src/components/common/HoverTooltip.tsx similarity index 82% rename from src/components/common/HoverTooltip.js rename to src/components/common/HoverTooltip.tsx index 614841df..e5e31219 100644 --- a/src/components/common/HoverTooltip.js +++ b/src/components/common/HoverTooltip.tsx @@ -1,8 +1,8 @@ -import { useEffect, useState } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import { Tooltip } from 'react-basics'; import styles from './HoverTooltip.module.css'; -export function HoverTooltip({ children }) { +export function HoverTooltip({ children }: { children: ReactNode }) { const [position, setPosition] = useState({ x: -1000, y: -1000 }); useEffect(() => { diff --git a/src/components/common/LinkButton.js b/src/components/common/LinkButton.tsx similarity index 69% rename from src/components/common/LinkButton.js rename to src/components/common/LinkButton.tsx index a9a8562d..83d95151 100644 --- a/src/components/common/LinkButton.js +++ b/src/components/common/LinkButton.tsx @@ -2,8 +2,17 @@ import classNames from 'classnames'; import Link from 'next/link'; import { useLocale } from 'components/hooks'; import styles from './LinkButton.module.css'; +import { ReactNode } from 'react'; -export function LinkButton({ href, className, variant, scroll = true, children }) { +export interface LinkButtonProps { + href: string; + className?: string; + variant?: string; + scroll?: boolean; + children?: ReactNode; +} + +export function LinkButton({ href, className, variant, scroll = true, children }: LinkButtonProps) { const { dir } = useLocale(); return ( diff --git a/src/components/common/MobileMenu.js b/src/components/common/MobileMenu.tsx similarity index 74% rename from src/components/common/MobileMenu.js rename to src/components/common/MobileMenu.tsx index 83a05dff..e14f0b83 100644 --- a/src/components/common/MobileMenu.js +++ b/src/components/common/MobileMenu.tsx @@ -4,12 +4,19 @@ import { usePathname } from 'next/navigation'; import Link from 'next/link'; import styles from './MobileMenu.module.css'; -export function MobileMenu({ items = [], onClose }) { +export function MobileMenu({ + items = [], + onClose, +}: { + items: any[]; + className?: string; + onClose: () => void; +}): any { const pathname = usePathname(); - const Items = ({ items, className }) => ( + const Items = ({ items, className }: { items: any[]; className?: string }): any => (
- {items.map(({ label, url, children }) => { + {items.map(({ label, url, children }: { label: string; url: string; children: any[] }) => { const selected = pathname.startsWith(url); return ( diff --git a/src/components/common/Pager.js b/src/components/common/Pager.tsx similarity index 86% rename from src/components/common/Pager.js rename to src/components/common/Pager.tsx index a21d35d9..2fe7c6db 100644 --- a/src/components/common/Pager.js +++ b/src/components/common/Pager.tsx @@ -3,7 +3,15 @@ import { Button, Icon, Icons } from 'react-basics'; import useMessages from 'components/hooks/useMessages'; import styles from './Pager.module.css'; -export function Pager({ page, pageSize, count, onPageChange, className }) { +export interface PagerProps { + page: number; + pageSize: number; + count: number; + onPageChange: (nextPage: number) => void; + className?: string; +} + +export function Pager({ page, pageSize, count, onPageChange, className }: PagerProps) { const { formatMessage, labels } = useMessages(); const maxPage = pageSize && count ? Math.ceil(count / pageSize) : 0; const lastPage = page === maxPage; @@ -13,7 +21,7 @@ export function Pager({ page, pageSize, count, onPageChange, className }) { return null; } - const handlePageChange = value => { + const handlePageChange = (value: number) => { const nextPage = page + value; if (nextPage > 0 && nextPage <= maxPage) { onPageChange(nextPage); diff --git a/src/components/declarations.d.ts b/src/components/declarations.d.ts index 31e44ff3..ca55157b 100644 --- a/src/components/declarations.d.ts +++ b/src/components/declarations.d.ts @@ -1,2 +1,4 @@ declare module '*.css'; declare module '*.svg'; +declare module '*.json'; +declare module 'uuid'; diff --git a/src/components/hooks/index.js b/src/components/hooks/index.js index 697d54c3..b851eeb7 100644 --- a/src/components/hooks/index.js +++ b/src/components/hooks/index.js @@ -13,7 +13,7 @@ export * from './useMessages'; export * from './useNavigation'; export * from './useReport'; export * from './useReports'; -export * from './useRequireLogin'; +export * from './useLogin'; export * from './useShareToken'; export * from './useSticky'; export * from './useTheme'; diff --git a/src/components/hooks/useApiFilter.ts b/src/components/hooks/useApiFilter.ts deleted file mode 100644 index d411fd43..00000000 --- a/src/components/hooks/useApiFilter.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useState } from 'react'; - -export function useApiFilter() { - const [filter, setFilter] = useState(); - const [filterType, setFilterType] = useState('All'); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - - const handleFilterChange = value => setFilter(value); - const handlePageChange = value => setPage(value); - const handlePageSizeChange = value => setPageSize(value); - - return { - filter, - setFilter, - filterType, - setFilterType, - page, - setPage, - pageSize, - setPageSize, - handleFilterChange, - handlePageChange, - handlePageSizeChange, - }; -} - -export default useApiFilter; diff --git a/src/components/hooks/useConfig.js b/src/components/hooks/useConfig.ts similarity index 100% rename from src/components/hooks/useConfig.js rename to src/components/hooks/useConfig.ts diff --git a/src/components/hooks/useCountryNames.js b/src/components/hooks/useCountryNames.ts similarity index 87% rename from src/components/hooks/useCountryNames.js rename to src/components/hooks/useCountryNames.ts index 40611865..22f20666 100644 --- a/src/components/hooks/useCountryNames.js +++ b/src/components/hooks/useCountryNames.ts @@ -6,10 +6,10 @@ const countryNames = { 'en-US': enUS, }; -export function useCountryNames(locale) { +export function useCountryNames(locale: string) { const [list, setList] = useState(countryNames[locale] || enUS); - async function loadData(locale) { + async function loadData(locale: string) { const { data } = await httpGet(`${process.env.basePath}/intl/country/${locale}.json`); if (data) { diff --git a/src/components/hooks/useDateRange.js b/src/components/hooks/useDateRange.ts similarity index 72% rename from src/components/hooks/useDateRange.js rename to src/components/hooks/useDateRange.ts index 1e1b0616..efaa717f 100644 --- a/src/components/hooks/useDateRange.js +++ b/src/components/hooks/useDateRange.ts @@ -1,12 +1,13 @@ import { getMinimumUnit, parseDateRange } from 'lib/date'; import { setItem } from 'next-basics'; import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants'; -import useLocale from './useLocale'; import websiteStore, { setWebsiteDateRange } from 'store/websites'; import appStore, { setDateRange } from 'store/app'; +import { DateRange } from 'lib/types'; +import useLocale from './useLocale'; import useApi from './useApi'; -export function useDateRange(websiteId) { +export function useDateRange(websiteId?: string) { const { get } = useApi(); const { locale } = useLocale(); const websiteConfig = websiteStore(state => state[websiteId]?.dateRange); @@ -14,13 +15,13 @@ export function useDateRange(websiteId) { const globalConfig = appStore(state => state.dateRange); const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale); - const saveDateRange = async value => { + const saveDateRange = async (value: DateRange | string) => { if (websiteId) { - let dateRange = value; + let dateRange: DateRange | string = value; if (typeof value === 'string') { if (value === 'all') { - const result = await get(`/websites/${websiteId}/daterange`); + const result: any = await get(`/websites/${websiteId}/daterange`); const { mindate, maxdate } = result; const startDate = new Date(mindate); @@ -37,14 +38,17 @@ export function useDateRange(websiteId) { } } - setWebsiteDateRange(websiteId, dateRange); + setWebsiteDateRange(websiteId, dateRange as DateRange); } else { setItem(DATE_RANGE_CONFIG, value); setDateRange(value); } }; - return [dateRange, saveDateRange]; + return [dateRange, saveDateRange] as [ + { startDate: Date; endDate: Date; modified?: number }, + (value: string | DateRange) => void, + ]; } export default useDateRange; diff --git a/src/components/hooks/useDocumentClick.js b/src/components/hooks/useDocumentClick.ts similarity index 77% rename from src/components/hooks/useDocumentClick.js rename to src/components/hooks/useDocumentClick.ts index be3d09be..eefd9366 100644 --- a/src/components/hooks/useDocumentClick.js +++ b/src/components/hooks/useDocumentClick.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -export function useDocumentClick(handler) { +export function useDocumentClick(handler: (event: MouseEvent) => any) { useEffect(() => { document.addEventListener('click', handler); diff --git a/src/components/hooks/useEscapeKey.js b/src/components/hooks/useEscapeKey.js deleted file mode 100644 index 1a17f18f..00000000 --- a/src/components/hooks/useEscapeKey.js +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect, useCallback } from 'react'; - -export function useEscapeKey(handler) { - const escFunction = useCallback(event => { - if (event.keyCode === 27) { - handler(event); - } - }, []); - - useEffect(() => { - document.addEventListener('keydown', escFunction, false); - - return () => { - document.removeEventListener('keydown', escFunction, false); - }; - }, [escFunction]); - - return null; -} - -export default useEscapeKey; diff --git a/src/components/hooks/useEscapeKey.ts b/src/components/hooks/useEscapeKey.ts new file mode 100644 index 00000000..5c3350e7 --- /dev/null +++ b/src/components/hooks/useEscapeKey.ts @@ -0,0 +1,21 @@ +import { useEffect, useCallback, KeyboardEvent } from 'react'; + +export function useEscapeKey(handler: (event: KeyboardEvent) => void) { + const escFunction = useCallback((event: KeyboardEvent) => { + if (event.key === 'Escape') { + handler(event); + } + }, []); + + useEffect(() => { + document.addEventListener('keydown', escFunction as any, false); + + return () => { + document.removeEventListener('keydown', escFunction as any, false); + }; + }, [escFunction]); + + return null; +} + +export default useEscapeKey; diff --git a/src/components/hooks/useFilterQuery.ts b/src/components/hooks/useFilterQuery.ts index 37c28b7e..030da27d 100644 --- a/src/components/hooks/useFilterQuery.ts +++ b/src/components/hooks/useFilterQuery.ts @@ -1,24 +1,35 @@ -import { useState } from 'react'; -import { useApi } from 'components/hooks/useApi'; import { UseQueryOptions } from '@tanstack/react-query'; +import { useState, Dispatch, SetStateAction } from 'react'; +import { useApi } from 'components/hooks/useApi'; +import { FilterResult, SearchFilter } from 'lib/types'; -export function useFilterQuery(key: any[], fn, options?: UseQueryOptions) { - const [params, setParams] = useState({ +export interface FilterQueryResult { + result: FilterResult; + query: any; + params: SearchFilter; + setParams: Dispatch>; +} + +export function useFilterQuery({ + queryKey, + queryFn, + ...options +}: UseQueryOptions): FilterQueryResult { + const [params, setParams] = useState({ query: '', page: 1, }); - const { useQuery } = useApi(); - const { data, ...other } = useQuery([...key, params], fn.bind(null, params), options); + const { useQuery } = useApi(); + const { data, ...query } = useQuery({ + queryKey: [{ ...queryKey, ...params }], + queryFn: () => queryFn(params as any), + ...options, + }); return { - result: data as { - page: number; - pageSize: number; - count: number; - data: any[]; - }, - ...other, + result: data as FilterResult, + query, params, setParams, }; diff --git a/src/components/hooks/useFilters.js b/src/components/hooks/useFilters.ts similarity index 100% rename from src/components/hooks/useFilters.js rename to src/components/hooks/useFilters.ts diff --git a/src/components/hooks/useForceUpdate.js b/src/components/hooks/useForceUpdate.ts similarity index 100% rename from src/components/hooks/useForceUpdate.js rename to src/components/hooks/useForceUpdate.ts diff --git a/src/components/hooks/useFormat.js b/src/components/hooks/useFormat.ts similarity index 60% rename from src/components/hooks/useFormat.js rename to src/components/hooks/useFormat.ts index 0e609c48..06585e49 100644 --- a/src/components/hooks/useFormat.js +++ b/src/components/hooks/useFormat.ts @@ -9,23 +9,28 @@ export function useFormat() { const { locale } = useLocale(); const countryNames = useCountryNames(locale); - const formatBrowser = value => { + const formatBrowser = (value: string): string => { return BROWSERS[value] || value; }; - const formatCountry = value => { + const formatCountry = (value: string): string => { return countryNames[value] || value; }; - const formatRegion = value => { - return regions[value] ? regions[value] : value; + const formatRegion = (value: string): string => { + const [country] = value.split('-'); + return regions[value] ? `${regions[value]}, ${countryNames[country]}` : value; }; - const formatDevice = value => { + const formatCity = (value: string, country?: string): string => { + return `${value}, ${countryNames[country]}`; + }; + + const formatDevice = (value: string): string => { return formatMessage(labels[value] || labels.unknown); }; - const formatValue = (value, type) => { + const formatValue = (value: string, type: string, data?: { [key: string]: any }): string => { switch (type) { case 'browser': return formatBrowser(value); @@ -33,6 +38,8 @@ export function useFormat() { return formatCountry(value); case 'region': return formatRegion(value); + case 'city': + return formatCity(value, data?.country); case 'device': return formatDevice(value); default: diff --git a/src/components/hooks/useLanguageNames.js b/src/components/hooks/useLanguageNames.ts similarity index 100% rename from src/components/hooks/useLanguageNames.js rename to src/components/hooks/useLanguageNames.ts diff --git a/src/components/hooks/useLocale.js b/src/components/hooks/useLocale.ts similarity index 100% rename from src/components/hooks/useLocale.js rename to src/components/hooks/useLocale.ts diff --git a/src/components/hooks/useLogin.ts b/src/components/hooks/useLogin.ts new file mode 100644 index 00000000..a4ac9d3b --- /dev/null +++ b/src/components/hooks/useLogin.ts @@ -0,0 +1,22 @@ +import useApi from 'components/hooks/useApi'; +import useUser from 'components/hooks/useUser'; + +export function useLogin() { + const { get, useQuery } = useApi(); + const { user, setUser } = useUser(); + + const query = useQuery({ + queryKey: ['login'], + queryFn: async () => { + const data = await get('/auth/verify'); + + setUser(data); + + return data; + }, + }); + + return { user, ...query }; +} + +export default useLogin; diff --git a/src/components/hooks/useMessages.js b/src/components/hooks/useMessages.js deleted file mode 100644 index e3a6c20b..00000000 --- a/src/components/hooks/useMessages.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useIntl, FormattedMessage } from 'react-intl'; -import { messages, labels } from 'components/messages'; - -export function useMessages() { - const intl = useIntl(); - - const getMessage = id => { - const message = Object.values(messages).find(value => value.id === id); - - return message ? formatMessage(message) : id; - }; - - const formatMessage = (descriptor, values, opts) => { - return descriptor ? intl.formatMessage(descriptor, values, opts) : null; - }; - - return { formatMessage, FormattedMessage, messages, labels, getMessage }; -} - -export default useMessages; diff --git a/src/components/hooks/useMessages.ts b/src/components/hooks/useMessages.ts new file mode 100644 index 00000000..594a3c61 --- /dev/null +++ b/src/components/hooks/useMessages.ts @@ -0,0 +1,30 @@ +import { useIntl, FormattedMessage, MessageDescriptor, PrimitiveType } from 'react-intl'; +import { messages, labels } from 'components/messages'; +import { FormatXMLElementFn, Options } from 'intl-messageformat'; + +export function useMessages(): any { + const intl = useIntl(); + + const getMessage = (id: string) => { + const message = Object.values(messages).find(value => value.id === id); + + return message ? formatMessage(message) : id; + }; + + const formatMessage = ( + descriptor: + | MessageDescriptor + | { + id: string; + defaultMessage: string; + }, + values?: Record>, + opts?: Options, + ) => { + return descriptor ? intl.formatMessage(descriptor, values, opts) : null; + }; + + return { formatMessage, FormattedMessage, messages, labels, getMessage }; +} + +export default useMessages; diff --git a/src/components/hooks/useNavigation.js b/src/components/hooks/useNavigation.ts similarity index 73% rename from src/components/hooks/useNavigation.js rename to src/components/hooks/useNavigation.ts index 658e81ed..fb9bffc5 100644 --- a/src/components/hooks/useNavigation.js +++ b/src/components/hooks/useNavigation.ts @@ -2,7 +2,12 @@ import { useMemo } from 'react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { buildUrl } from 'next-basics'; -export function useNavigation() { +export function useNavigation(): { + pathname: string; + query: { [key: string]: string }; + router: any; + makeUrl: (params: any, reset?: boolean) => string; +} { const router = useRouter(); const pathname = usePathname(); const params = useSearchParams(); @@ -17,7 +22,7 @@ export function useNavigation() { return obj; }, [params]); - function makeUrl(params, reset) { + function makeUrl(params: any, reset?: boolean) { return reset ? pathname : buildUrl(pathname, { ...query, ...params }); } diff --git a/src/components/hooks/useReport.js b/src/components/hooks/useReport.ts similarity index 81% rename from src/components/hooks/useReport.js rename to src/components/hooks/useReport.ts index 7c698b4e..7769ed6c 100644 --- a/src/components/hooks/useReport.js +++ b/src/components/hooks/useReport.ts @@ -4,7 +4,7 @@ import { useTimezone } from './useTimezone'; import useApi from './useApi'; import useMessages from './useMessages'; -export function useReport(reportId, defaultParameters) { +export function useReport(reportId: string, defaultParameters: { [key: string]: any }) { const [report, setReport] = useState(null); const [isRunning, setIsRunning] = useState(false); const { get, post } = useApi(); @@ -17,8 +17,8 @@ export function useReport(reportId, defaultParameters) { parameters: {}, }; - const loadReport = async id => { - const data = await get(`/reports/${id}`); + const loadReport = async (id: string) => { + const data: any = await get(`/reports/${id}`); const { dateRange } = data?.parameters || {}; const { startDate, endDate } = dateRange || {}; @@ -32,7 +32,7 @@ export function useReport(reportId, defaultParameters) { }; const runReport = useCallback( - async parameters => { + async (parameters: { [key: string]: any }) => { setIsRunning(true); const { type } = report; @@ -40,7 +40,7 @@ export function useReport(reportId, defaultParameters) { const data = await post(`/reports/${type}`, { ...parameters, timezone }); setReport( - produce(state => { + produce((state: any) => { state.parameters = parameters; state.data = data; @@ -50,13 +50,13 @@ export function useReport(reportId, defaultParameters) { setIsRunning(false); }, - [report], + [report, timezone], ); const updateReport = useCallback( - async data => { + async (data: { [x: string]: any; parameters: any }) => { setReport( - produce(state => { + produce((state: any) => { const { parameters, ...rest } = data; if (parameters) { diff --git a/src/components/hooks/useReports.js b/src/components/hooks/useReports.js deleted file mode 100644 index d9292aeb..00000000 --- a/src/components/hooks/useReports.js +++ /dev/null @@ -1,38 +0,0 @@ -import { useState } from 'react'; -import useApi from './useApi'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function useReports() { - const [modified, setModified] = useState(Date.now()); - const { get, useQuery, del, useMutation } = useApi(); - const { mutate } = useMutation(reportId => del(`/reports/${reportId}`)); - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - const { data, error, isLoading } = useQuery( - ['reports', { modified, filter, page, pageSize }], - () => get(`/reports`, { filter, page, pageSize }), - ); - - const deleteReport = id => { - mutate(id, { - onSuccess: () => { - setModified(Date.now()); - }, - }); - }; - - return { - reports: data, - error, - isLoading, - deleteReport, - filter, - page, - pageSize, - handleFilterChange, - handlePageChange, - handlePageSizeChange, - }; -} - -export default useReports; diff --git a/src/components/hooks/useReports.ts b/src/components/hooks/useReports.ts new file mode 100644 index 00000000..d2473002 --- /dev/null +++ b/src/components/hooks/useReports.ts @@ -0,0 +1,30 @@ +import { useState } from 'react'; +import useApi from './useApi'; +import useFilterQuery from 'components/hooks/useFilterQuery'; + +export function useReports(websiteId?: string) { + const [modified, setModified] = useState(Date.now()); + const { get, del, useMutation } = useApi(); + const { mutate } = useMutation({ mutationFn: (reportId: string) => del(`/reports/${reportId}`) }); + const queryResult = useFilterQuery({ + queryKey: ['reports', { websiteId, modified }], + queryFn: (params: any) => { + return get(websiteId ? `/websites/${websiteId}/reports` : `/reports`, params); + }, + }); + + const deleteReport = (id: any) => { + mutate(id, { + onSuccess: () => { + setModified(Date.now()); + }, + }); + }; + + return { + ...queryResult, + deleteReport, + }; +} + +export default useReports; diff --git a/src/components/hooks/useRequireLogin.ts b/src/components/hooks/useRequireLogin.ts deleted file mode 100644 index 76460a55..00000000 --- a/src/components/hooks/useRequireLogin.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect } from 'react'; -import useApi from 'components/hooks/useApi'; -import useUser from 'components/hooks/useUser'; - -export function useRequireLogin(handler?: (data?: object) => void) { - const { get } = useApi(); - const { user, setUser } = useUser(); - - useEffect(() => { - async function loadUser() { - try { - const data = await get('/auth/verify'); - - setUser(typeof handler === 'function' ? handler(data) : (data as any)?.user); - } catch { - location.href = `${process.env.basePath || ''}/login`; - } - } - - if (!user) { - loadUser(); - } - }, [user]); - - return { user }; -} - -export default useRequireLogin; diff --git a/src/components/hooks/useShareToken.js b/src/components/hooks/useShareToken.js deleted file mode 100644 index 5062c73e..00000000 --- a/src/components/hooks/useShareToken.js +++ /dev/null @@ -1,20 +0,0 @@ -import useStore, { setShareToken } from 'store/app'; -import useApi from './useApi'; - -const selector = state => state.shareToken; - -export function useShareToken(shareId) { - const shareToken = useStore(selector); - const { get, useQuery } = useApi(); - const { isLoading, error } = useQuery(['share', shareId], async () => { - const data = await get(`/share/${shareId}`); - - setShareToken(data); - - return data; - }); - - return { shareToken, isLoading, error }; -} - -export default useShareToken; diff --git a/src/components/hooks/useShareToken.ts b/src/components/hooks/useShareToken.ts new file mode 100644 index 00000000..189657be --- /dev/null +++ b/src/components/hooks/useShareToken.ts @@ -0,0 +1,27 @@ +import useStore, { setShareToken } from 'store/app'; +import useApi from './useApi'; + +const selector = (state: { shareToken: string }) => state.shareToken; + +export function useShareToken(shareId: string): { + shareToken: any; + isLoading?: boolean; + error?: Error; +} { + const shareToken = useStore(selector); + const { get, useQuery } = useApi(); + const { isLoading, error } = useQuery({ + queryKey: ['share', shareId], + queryFn: async () => { + const data = await get(`/share/${shareId}`); + + setShareToken(data); + + return data; + }, + }); + + return { shareToken, isLoading, error }; +} + +export default useShareToken; diff --git a/src/components/hooks/useSticky.js b/src/components/hooks/useSticky.ts similarity index 76% rename from src/components/hooks/useSticky.js rename to src/components/hooks/useSticky.ts index be33f6ed..459c489a 100644 --- a/src/components/hooks/useSticky.js +++ b/src/components/hooks/useSticky.ts @@ -5,8 +5,9 @@ export function useSticky({ enabled = true, threshold = 1 }) { const ref = useRef(null); useEffect(() => { - let observer; - const handler = ([entry]) => setIsSticky(entry.intersectionRatio < threshold); + let observer: IntersectionObserver | undefined; + const handler: IntersectionObserverCallback = ([entry]) => + setIsSticky(entry.intersectionRatio < threshold); if (enabled && ref.current) { observer = new IntersectionObserver(handler, { threshold: [threshold] }); diff --git a/src/components/hooks/useTheme.js b/src/components/hooks/useTheme.ts similarity index 97% rename from src/components/hooks/useTheme.js rename to src/components/hooks/useTheme.ts index 7e40f601..099bf962 100644 --- a/src/components/hooks/useTheme.js +++ b/src/components/hooks/useTheme.ts @@ -4,7 +4,7 @@ import { getItem, setItem } from 'next-basics'; import { THEME_COLORS, THEME_CONFIG } from 'lib/constants'; import { colord } from 'colord'; -const selector = state => state.theme; +const selector = (state: { theme: string }) => state.theme; export function useTheme() { const defaultTheme = diff --git a/src/components/hooks/useTimezone.js b/src/components/hooks/useTimezone.ts similarity index 100% rename from src/components/hooks/useTimezone.js rename to src/components/hooks/useTimezone.ts diff --git a/src/components/hooks/useWebsite.js b/src/components/hooks/useWebsite.js deleted file mode 100644 index 5315f0dc..00000000 --- a/src/components/hooks/useWebsite.js +++ /dev/null @@ -1,10 +0,0 @@ -import useApi from './useApi'; - -export function useWebsite(websiteId) { - const { get, useQuery } = useApi(); - return useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), { - enabled: !!websiteId, - }); -} - -export default useWebsite; diff --git a/src/components/hooks/useWebsite.ts b/src/components/hooks/useWebsite.ts new file mode 100644 index 00000000..d18e96ba --- /dev/null +++ b/src/components/hooks/useWebsite.ts @@ -0,0 +1,12 @@ +import useApi from './useApi'; + +export function useWebsite(websiteId: string) { + const { get, useQuery } = useApi(); + return useQuery({ + queryKey: ['websites', websiteId], + queryFn: () => get(`/websites/${websiteId}`), + enabled: !!websiteId, + }); +} + +export default useWebsite; diff --git a/src/components/icons.ts b/src/components/icons.ts index 8eb1f8b0..01d7caf5 100644 --- a/src/components/icons.ts +++ b/src/components/icons.ts @@ -22,7 +22,7 @@ import User from 'assets/user.svg'; import Users from 'assets/users.svg'; import Visitor from 'assets/visitor.svg'; -const icons: any = { +const icons = { ...Icons, AddUser, Bars, diff --git a/src/components/input/DateFilter.js b/src/components/input/DateFilter.tsx similarity index 89% rename from src/components/input/DateFilter.js rename to src/components/input/DateFilter.tsx index 9fde27ca..f7739f17 100644 --- a/src/components/input/DateFilter.js +++ b/src/components/input/DateFilter.tsx @@ -3,9 +3,20 @@ import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics'; import { endOfYear, isSameDay } from 'date-fns'; import DatePickerForm from 'components/metrics/DatePickerForm'; import useLocale from 'components/hooks/useLocale'; -import { formatDate } from 'lib/date'; -import Icons from 'components/icons'; import useMessages from 'components/hooks/useMessages'; +import Icons from 'components/icons'; +import { formatDate } from 'lib/date'; + +export interface DateFilterProps { + value: string; + startDate: Date; + endDate: Date; + className?: string; + onChange?: (value: string) => void; + selectedUnit?: string; + showAllTime?: boolean; + alignment?: 'start' | 'center' | 'end'; +} export function DateFilter({ value, @@ -16,7 +27,7 @@ export function DateFilter({ selectedUnit, showAllTime = false, alignment = 'end', -}) { +}: DateFilterProps) { const { formatMessage, labels } = useMessages(); const [showPicker, setShowPicker] = useState(false); @@ -65,7 +76,7 @@ export function DateFilter({ }, ].filter(n => n); - const renderValue = value => { + const renderValue = (value: string) => { return value.startsWith('range') ? ( { + const handleChange = (value: string) => { if (value === 'custom') { setShowPicker(true); return; @@ -86,7 +97,7 @@ export function DateFilter({ onChange(value); }; - const handlePickerChange = value => { + const handlePickerChange = (value: string) => { setShowPicker(false); onChange(value); }; @@ -102,7 +113,7 @@ export function DateFilter({ value={value} alignment={alignment} placeholder={formatMessage(labels.selectDate)} - onChange={handleChange} + onChange={key => handleChange(key as any)} > {({ label, value, divider }) => ( diff --git a/src/components/input/LanguageButton.js b/src/components/input/LanguageButton.tsx similarity index 88% rename from src/components/input/LanguageButton.js rename to src/components/input/LanguageButton.tsx index 3c0d0cd6..1151da0b 100644 --- a/src/components/input/LanguageButton.js +++ b/src/components/input/LanguageButton.tsx @@ -9,7 +9,7 @@ export function LanguageButton() { const { locale, saveLocale, dir } = useLocale(); const items = Object.keys(languages).map(key => ({ ...languages[key], value: key })); - function handleSelect(value, close, e) { + function handleSelect(value: string, close: () => void, e: MouseEvent) { e.stopPropagation(); saveLocale(value); close(); @@ -23,7 +23,7 @@ export function LanguageButton() { - {close => { + {(close: () => void) => { return (
{items.map(({ value, label }) => { @@ -31,7 +31,7 @@ export function LanguageButton() {
handleSelect(value, close, e)} > {label} {value === locale && ( diff --git a/src/components/input/LogoutButton.js b/src/components/input/LogoutButton.tsx similarity index 80% rename from src/components/input/LogoutButton.js rename to src/components/input/LogoutButton.tsx index 6ca358a1..c787f229 100644 --- a/src/components/input/LogoutButton.js +++ b/src/components/input/LogoutButton.tsx @@ -2,7 +2,11 @@ import { Button, Icon, Icons, TooltipPopup } from 'react-basics'; import Link from 'next/link'; import useMessages from 'components/hooks/useMessages'; -export function LogoutButton({ tooltipPosition = 'top' }) { +export function LogoutButton({ + tooltipPosition = 'top', +}: { + tooltipPosition?: 'top' | 'bottom' | 'left' | 'right'; +}) { const { formatMessage, labels } = useMessages(); return ( diff --git a/src/components/input/MonthSelect.js b/src/components/input/MonthSelect.tsx similarity index 87% rename from src/components/input/MonthSelect.js rename to src/components/input/MonthSelect.tsx index 312c6854..acb17dfe 100644 --- a/src/components/input/MonthSelect.js +++ b/src/components/input/MonthSelect.tsx @@ -20,7 +20,7 @@ export function MonthSelect({ date = new Date(), onChange }) { const year = date.getFullYear(); const ref = useRef(); - const handleChange = (close, date) => { + const handleChange = (close: () => void, date: Date) => { onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`); close(); }; @@ -53,12 +53,8 @@ export function MonthSelect({ date = new Date(), onChange }) { - {close => ( - + {(close: any) => ( + )} diff --git a/src/components/input/ProfileButton.js b/src/components/input/ProfileButton.tsx similarity index 100% rename from src/components/input/ProfileButton.js rename to src/components/input/ProfileButton.tsx diff --git a/src/components/input/RefreshButton.js b/src/components/input/RefreshButton.tsx similarity index 87% rename from src/components/input/RefreshButton.js rename to src/components/input/RefreshButton.tsx index 8b40cafa..01e80378 100644 --- a/src/components/input/RefreshButton.js +++ b/src/components/input/RefreshButton.tsx @@ -4,7 +4,13 @@ import useDateRange from 'components/hooks/useDateRange'; import Icons from 'components/icons'; import useMessages from 'components/hooks/useMessages'; -export function RefreshButton({ websiteId, isLoading }) { +export function RefreshButton({ + websiteId, + isLoading, +}: { + websiteId: string; + isLoading?: boolean; +}) { const { formatMessage, labels } = useMessages(); const [dateRange] = useDateRange(websiteId); diff --git a/src/components/input/SettingsButton.js b/src/components/input/SettingsButton.tsx similarity index 86% rename from src/components/input/SettingsButton.js rename to src/components/input/SettingsButton.tsx index 46c72597..2a076d42 100644 --- a/src/components/input/SettingsButton.js +++ b/src/components/input/SettingsButton.tsx @@ -15,12 +15,7 @@ export function SettingsButton() { - e.stopPropagation()} - > +
diff --git a/src/components/input/ThemeButton.js b/src/components/input/ThemeButton.tsx similarity index 100% rename from src/components/input/ThemeButton.js rename to src/components/input/ThemeButton.tsx diff --git a/src/components/input/WebsiteDateFilter.js b/src/components/input/WebsiteDateFilter.tsx similarity index 95% rename from src/components/input/WebsiteDateFilter.js rename to src/components/input/WebsiteDateFilter.tsx index 1725ca3b..cf1beaa1 100644 --- a/src/components/input/WebsiteDateFilter.js +++ b/src/components/input/WebsiteDateFilter.tsx @@ -5,7 +5,7 @@ import { Button, Icon, Icons } from 'react-basics'; import DateFilter from './DateFilter'; import styles from './WebsiteDateFilter.module.css'; -export function WebsiteDateFilter({ websiteId }) { +export function WebsiteDateFilter({ websiteId }: { websiteId: string }) { const [dateRange, setDateRange] = useDateRange(websiteId); const { value, startDate, endDate, selectedUnit } = dateRange; const isFutureDate = diff --git a/src/components/input/WebsiteSelect.js b/src/components/input/WebsiteSelect.tsx similarity index 75% rename from src/components/input/WebsiteSelect.js rename to src/components/input/WebsiteSelect.tsx index 078389d3..e125e258 100644 --- a/src/components/input/WebsiteSelect.js +++ b/src/components/input/WebsiteSelect.tsx @@ -3,10 +3,19 @@ import useApi from 'components/hooks/useApi'; import useMessages from 'components/hooks/useMessages'; import styles from './WebsiteSelect.module.css'; -export function WebsiteSelect({ websiteId, onSelect }) { +export function WebsiteSelect({ + websiteId, + onSelect, +}: { + websiteId: string; + onSelect?: (key: any) => void; +}) { const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); - const { data } = useQuery(['websites:me'], () => get('/me/websites', { pageSize: 100 })); + const { data } = useQuery({ + queryKey: ['websites:me'], + queryFn: () => get('/me/websites', { pageSize: 100 }), + }); const renderValue = value => { return data?.data?.find(({ id }) => id === value)?.name; diff --git a/src/components/layout/Grid.js b/src/components/layout/Grid.js deleted file mode 100644 index 86b08887..00000000 --- a/src/components/layout/Grid.js +++ /dev/null @@ -1,18 +0,0 @@ -import classNames from 'classnames'; -import { mapChildren } from 'react-basics'; -import styles from './Grid.module.css'; - -export function Grid({ className, ...otherProps }) { - return
; -} - -export function GridRow(props) { - const { columns = 'two', className, children, ...otherProps } = props; - return ( -
- {mapChildren(children, child => { - return
{child}
; - })} -
- ); -} diff --git a/src/components/layout/Grid.tsx b/src/components/layout/Grid.tsx new file mode 100644 index 00000000..2a34fdc4 --- /dev/null +++ b/src/components/layout/Grid.tsx @@ -0,0 +1,34 @@ +import { CSSProperties } from 'react'; +import classNames from 'classnames'; +import { mapChildren } from 'react-basics'; +import styles from './Grid.module.css'; + +export interface GridProps { + className?: string; + style?: CSSProperties; + children?: any; +} + +export function Grid({ className, style, children }: GridProps) { + return ( +
+ {children} +
+ ); +} + +export function GridRow(props: { + [x: string]: any; + columns?: 'one' | 'two' | 'three' | 'one-two' | 'two-one'; + className?: string; + children?: any; +}) { + const { columns = 'two', className, children, ...otherProps } = props; + return ( +
+ {mapChildren(children, child => { + return
{child}
; + })} +
+ ); +} diff --git a/src/components/layout/NavGroup.js b/src/components/layout/NavGroup.tsx similarity index 90% rename from src/components/layout/NavGroup.js rename to src/components/layout/NavGroup.tsx index 361dffb5..e95b61fa 100644 --- a/src/components/layout/NavGroup.js +++ b/src/components/layout/NavGroup.tsx @@ -6,13 +6,21 @@ import Link from 'next/link'; import Icons from 'components/icons'; import styles from './NavGroup.module.css'; +export interface NavGroupProps { + title: string; + items: any[]; + defaultExpanded?: boolean; + allowExpand?: boolean; + minimized?: boolean; +} + export function NavGroup({ title, items, defaultExpanded = true, allowExpand = true, minimized = false, -}) { +}: NavGroupProps) { const pathname = usePathname(); const [expanded, setExpanded] = useState(defaultExpanded); diff --git a/src/components/layout/Page.tsx b/src/components/layout/Page.tsx index 2f702012..e32a09a3 100644 --- a/src/components/layout/Page.tsx +++ b/src/components/layout/Page.tsx @@ -23,7 +23,7 @@ export function Page({ } if (isLoading) { - return ; + return ; } return
{children}
; diff --git a/src/components/layout/SideNav.js b/src/components/layout/SideNav.tsx similarity index 82% rename from src/components/layout/SideNav.js rename to src/components/layout/SideNav.tsx index c93881e4..0b5c9856 100644 --- a/src/components/layout/SideNav.js +++ b/src/components/layout/SideNav.tsx @@ -4,6 +4,15 @@ import { usePathname } from 'next/navigation'; import Link from 'next/link'; import styles from './SideNav.module.css'; +export interface SideNavProps { + selectedKey: string; + items: any[]; + shallow?: boolean; + scroll?: boolean; + className?: string; + onSelect?: () => void; +} + export function SideNav({ selectedKey, items, @@ -11,7 +20,7 @@ export function SideNav({ scroll = false, className, onSelect = () => {}, -}) { +}: SideNavProps) { const pathname = usePathname(); return ( get(`/websites/${websiteId}/active`), - { - refetchInterval, - enabled: !!websiteId, - }, - ); + const { data } = useQuery({ + queryKey: ['websites:active', websiteId], + queryFn: () => get(`/websites/${websiteId}/active`), + enabled: !!websiteId, + refetchInterval, + }); const count = useMemo(() => { if (websiteId) { diff --git a/src/components/metrics/BarChart.js b/src/components/metrics/BarChart.tsx similarity index 83% rename from src/components/metrics/BarChart.js rename to src/components/metrics/BarChart.tsx index 8341fbc7..6d28dc20 100644 --- a/src/components/metrics/BarChart.js +++ b/src/components/metrics/BarChart.tsx @@ -10,12 +10,28 @@ import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; import { renderNumberLabels } from 'lib/charts'; import styles from './BarChart.module.css'; +export interface BarChartProps { + datasets?: any[]; + unit?: string; + animationDuration?: number; + stacked?: boolean; + isLoading?: boolean; + renderXLabel?: (label: string, index: number, values: any[]) => string; + renderYLabel?: (label: string, index: number, values: any[]) => string; + XAxisType?: string; + YAxisType?: string; + renderTooltipPopup?: (setTooltipPopup: (data: any) => void, model: any) => void; + onCreate?: (chart: any) => void; + onUpdate?: (chart: any) => void; + className?: string; +} + export function BarChart({ - datasets, + datasets = [], unit, animationDuration = DEFAULT_ANIMATION_DURATION, stacked = false, - loading = false, + isLoading = false, renderXLabel, renderYLabel, XAxisType = 'time', @@ -24,7 +40,7 @@ export function BarChart({ onCreate, onUpdate, className, -}) { +}: BarChartProps) { const canvas = useRef(); const chart = useRef(null); const [tooltip, setTooltipPopup] = useState(null); @@ -85,7 +101,7 @@ export function BarChart({ color: colors.chart.line, }, ticks: { - color: colors.text, + color: colors.chart.text, callback: renderYLabel || renderNumberLabels, }, }, @@ -106,14 +122,12 @@ export function BarChart({ const createChart = () => { Chart.defaults.font.family = 'Inter'; - const options = getOptions(); - chart.current = new Chart(canvas.current, { type: 'bar', data: { datasets, }, - options, + options: getOptions() as any, }); onCreate?.(chart.current); @@ -145,9 +159,9 @@ export function BarChart({ }, [datasets, unit, theme, animationDuration, locale]); return ( -
+ <>
- {loading && } + {isLoading && }
@@ -156,7 +170,7 @@ export function BarChart({
{tooltip}
)} -
+ ); } diff --git a/src/components/metrics/BrowsersTable.js b/src/components/metrics/BrowsersTable.tsx similarity index 85% rename from src/components/metrics/BrowsersTable.js rename to src/components/metrics/BrowsersTable.tsx index afc53663..e1c05435 100644 --- a/src/components/metrics/BrowsersTable.js +++ b/src/components/metrics/BrowsersTable.tsx @@ -1,9 +1,9 @@ import FilterLink from 'components/common/FilterLink'; -import MetricsTable from 'components/metrics/MetricsTable'; +import MetricsTable, { MetricsTableProps } from 'components/metrics/MetricsTable'; import useMessages from 'components/hooks/useMessages'; import useFormat from 'components/hooks/useFormat'; -export function BrowsersTable({ websiteId, ...props }) { +export function BrowsersTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); const { formatBrowser } = useFormat(); @@ -26,7 +26,6 @@ export function BrowsersTable({ websiteId, ...props }) { title={formatMessage(labels.browsers)} type="browser" metric={formatMessage(labels.visitors)} - websiteId={websiteId} renderLabel={renderLink} /> ); diff --git a/src/components/metrics/CitiesTable.js b/src/components/metrics/CitiesTable.tsx similarity index 78% rename from src/components/metrics/CitiesTable.js rename to src/components/metrics/CitiesTable.tsx index c5f1b1d7..067e07e9 100644 --- a/src/components/metrics/CitiesTable.js +++ b/src/components/metrics/CitiesTable.tsx @@ -1,18 +1,18 @@ -import MetricsTable from './MetricsTable'; +import MetricsTable, { MetricsTableProps } from './MetricsTable'; import { emptyFilter } from 'lib/filters'; import FilterLink from 'components/common/FilterLink'; import useLocale from 'components/hooks/useLocale'; import useMessages from 'components/hooks/useMessages'; import useCountryNames from 'components/hooks/useCountryNames'; -export function CitiesTable({ websiteId, ...props }) { +export function CitiesTable(props: MetricsTableProps) { const { locale } = useLocale(); const { formatMessage, labels } = useMessages(); const countryNames = useCountryNames(locale); - const renderLabel = (city, country) => { - const name = countryNames[country]; - return name ? `${city}, ${name}` : city; + const renderLabel = (city: string, country: string) => { + const countryName = countryNames[country]; + return countryName ? `${city}, ${countryName}` : city; }; const renderLink = ({ x: city, country }) => { @@ -34,7 +34,6 @@ export function CitiesTable({ websiteId, ...props }) { title={formatMessage(labels.cities)} type="city" metric={formatMessage(labels.visitors)} - websiteId={websiteId} dataFilter={emptyFilter} renderLabel={renderLink} /> diff --git a/src/components/metrics/CountriesTable.js b/src/components/metrics/CountriesTable.tsx similarity index 73% rename from src/components/metrics/CountriesTable.js rename to src/components/metrics/CountriesTable.tsx index 6f3b75b0..99f9ca2f 100644 --- a/src/components/metrics/CountriesTable.js +++ b/src/components/metrics/CountriesTable.tsx @@ -1,15 +1,24 @@ import FilterLink from 'components/common/FilterLink'; import useCountryNames from 'components/hooks/useCountryNames'; import { useLocale, useMessages, useFormat } from 'components/hooks'; -import MetricsTable from './MetricsTable'; +import MetricsTable, { MetricsTableProps } from './MetricsTable'; -export function CountriesTable({ websiteId, ...props }) { +export function CountriesTable({ + onDataLoad, + ...props +}: { + onDataLoad: (data: any) => void; +} & MetricsTableProps) { const { locale } = useLocale(); const countryNames = useCountryNames(locale); const { formatMessage, labels } = useMessages(); const { formatCountry } = useFormat(); - function renderLink({ x: code }) { + const handleDataLoad = (data: any) => { + onDataLoad?.(data); + }; + + const renderLink = ({ x: code }) => { return ( ); - } + }; return ( ); } diff --git a/src/components/metrics/DatePickerForm.js b/src/components/metrics/DatePickerForm.tsx similarity index 96% rename from src/components/metrics/DatePickerForm.js rename to src/components/metrics/DatePickerForm.tsx index 5e1906c3..b4f0781e 100644 --- a/src/components/metrics/DatePickerForm.js +++ b/src/components/metrics/DatePickerForm.tsx @@ -39,7 +39,7 @@ export function DatePickerForm({ return (
- + setSelected(key as any)}> diff --git a/src/components/metrics/DevicesTable.js b/src/components/metrics/DevicesTable.tsx similarity index 86% rename from src/components/metrics/DevicesTable.js rename to src/components/metrics/DevicesTable.tsx index 606b020a..4ebdac1b 100644 --- a/src/components/metrics/DevicesTable.js +++ b/src/components/metrics/DevicesTable.tsx @@ -1,9 +1,9 @@ -import MetricsTable from './MetricsTable'; +import MetricsTable, { MetricsTableProps } from './MetricsTable'; import FilterLink from 'components/common/FilterLink'; import useMessages from 'components/hooks/useMessages'; import { useFormat } from 'components/hooks'; -export function DevicesTable({ websiteId, ...props }) { +export function DevicesTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); const { formatDevice } = useFormat(); @@ -26,7 +26,6 @@ export function DevicesTable({ websiteId, ...props }) { title={formatMessage(labels.devices)} type="device" metric={formatMessage(labels.visitors)} - websiteId={websiteId} renderLabel={renderLink} /> ); diff --git a/src/components/metrics/EventsChart.js b/src/components/metrics/EventsChart.tsx similarity index 77% rename from src/components/metrics/EventsChart.js rename to src/components/metrics/EventsChart.tsx index f2cf48d1..be6d4a0c 100644 --- a/src/components/metrics/EventsChart.js +++ b/src/components/metrics/EventsChart.tsx @@ -7,7 +7,13 @@ import { useApi, useLocale, useDateRange, useTimezone, useNavigation } from 'com import { EVENT_COLORS } from 'lib/constants'; import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts'; -export function EventsChart({ websiteId, className, token }) { +export interface EventsChartProps { + websiteId: string; + className?: string; + token?: string; +} + +export function EventsChart({ websiteId, className, token }: EventsChartProps) { const { get, useQuery } = useApi(); const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId); const { locale } = useLocale(); @@ -16,17 +22,20 @@ export function EventsChart({ websiteId, className, token }) { query: { url, event }, } = useNavigation(); - const { data, isLoading } = useQuery(['events', websiteId, modified, event], () => - get(`/websites/${websiteId}/events`, { - startAt: +startDate, - endAt: +endDate, - unit, - timezone, - url, - event, - token, - }), - ); + const { data, isLoading } = useQuery({ + queryKey: ['events', websiteId, modified, event], + queryFn: () => + get(`/websites/${websiteId}/events`, { + startAt: +startDate, + endAt: +endDate, + unit, + timezone, + url, + event, + token, + }), + enabled: !!websiteId, + }); const datasets = useMemo(() => { if (!data) return []; @@ -68,7 +77,6 @@ export function EventsChart({ websiteId, className, token }) { className={className} datasets={datasets} unit={unit} - height={300} loading={isLoading} stacked renderXLabel={renderDateLabels(unit, locale)} diff --git a/src/components/metrics/EventsTable.js b/src/components/metrics/EventsTable.tsx similarity index 69% rename from src/components/metrics/EventsTable.js rename to src/components/metrics/EventsTable.tsx index a8ae82aa..26d9529b 100644 --- a/src/components/metrics/EventsTable.js +++ b/src/components/metrics/EventsTable.tsx @@ -1,10 +1,10 @@ -import MetricsTable from './MetricsTable'; +import MetricsTable, { MetricsTableProps } from './MetricsTable'; import useMessages from 'components/hooks/useMessages'; -export function EventsTable({ websiteId, ...props }) { +export function EventsTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); - function handleDataLoad(data) { + function handleDataLoad(data: any) { props.onDataLoad?.(data); } @@ -14,7 +14,6 @@ export function EventsTable({ websiteId, ...props }) { title={formatMessage(labels.events)} type="event" metric={formatMessage(labels.actions)} - websiteId={websiteId} onDataLoad={handleDataLoad} /> ); diff --git a/src/components/metrics/FilterTags.module.css b/src/components/metrics/FilterTags.module.css index c228dc4e..32bc2f6f 100644 --- a/src/components/metrics/FilterTags.module.css +++ b/src/components/metrics/FilterTags.module.css @@ -24,3 +24,7 @@ .tag:hover { background: var(--blue200); } + +.tag b { + text-transform: lowercase; +} diff --git a/src/components/metrics/FilterTags.js b/src/components/metrics/FilterTags.tsx similarity index 81% rename from src/components/metrics/FilterTags.js rename to src/components/metrics/FilterTags.tsx index 554c223a..140385f5 100644 --- a/src/components/metrics/FilterTags.js +++ b/src/components/metrics/FilterTags.tsx @@ -2,10 +2,12 @@ import { safeDecodeURI } from 'next-basics'; import { Button, Icon, Icons, Text } from 'react-basics'; import useNavigation from 'components/hooks/useNavigation'; import useMessages from 'components/hooks/useMessages'; +import useFormat from 'components/hooks/useFormat'; import styles from './FilterTags.module.css'; export function FilterTags({ params }) { const { formatMessage, labels } = useMessages(); + const { formatValue } = useFormat(); const { router, makeUrl, @@ -16,7 +18,7 @@ export function FilterTags({ params }) { return null; } - function handleCloseFilter(param) { + function handleCloseFilter(param?: string) { if (!param) { router.push(makeUrl({ view }, true)); } else { @@ -34,7 +36,7 @@ export function FilterTags({ params }) { return (
handleCloseFilter(key)}> - {`${key}`} = {`${safeDecodeURI(params[key])}`} + {formatMessage(labels[key])} = {formatValue(safeDecodeURI(params[key]), key)} @@ -42,7 +44,7 @@ export function FilterTags({ params }) {
); })} -