diff --git a/next.config.js b/next.config.js index eaaf8fe72..a155ece75 100644 --- a/next.config.js +++ b/next.config.js @@ -9,38 +9,21 @@ const contentSecurityPolicy = [ `script-src 'self' 'unsafe-eval' 'unsafe-inline'`, `style-src 'self' 'unsafe-inline'`, `connect-src 'self' api.umami.is`, - `frame-src *`, + `frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS || ''}`, ]; -const cspHeader = (values = []) => ({ - key: 'Content-Security-Policy', - value: values - .join(';') - .replace(/\s{2,}/g, ' ') - .trim(), -}); - const headers = [ { key: 'X-DNS-Prefetch-Control', value: 'on', }, { - key: 'X-Frame-Options', - value: 'SAMEORIGIN', + key: 'Content-Security-Policy', + value: contentSecurityPolicy + .join(';') + .replace(/\s{2,}/g, ' ') + .trim(), }, - cspHeader(contentSecurityPolicy), -]; - -const shareHeaders = [ - { - key: 'X-DNS-Prefetch-Control', - value: 'on', - }, - cspHeader([ - ...contentSecurityPolicy, - `frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS || ''}`, - ]), ]; if (process.env.FORCE_SSL) { @@ -104,6 +87,7 @@ const config = { defaultLocale: process.env.DEFAULT_LOCALE || '', disableLogin: process.env.DISABLE_LOGIN || '', disableUI: process.env.DISABLE_UI || '', + hostUrl: process.env.HOST_URL || '', }, basePath, output: 'standalone', @@ -142,10 +126,6 @@ const config = { source: '/:path*', headers, }, - { - source: '/share/:path*', - headers: shareHeaders, - }, ]; }, async rewrites() { diff --git a/package.json b/package.json index ba85fb3e7..0f437c355 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", @@ -66,8 +66,8 @@ "@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.7.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", @@ -93,13 +93,13 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "13.5.6", - "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.6.0", "react": "^18.2.0", - "react-basics": "^0.109.0", + "react-basics": "^0.114.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", @@ -127,8 +127,9 @@ "@svgr/rollup": "^8.1.0", "@svgr/webpack": "^8.1.0", "@types/node": "^20.9.0", - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", + "@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)/NavBar.js b/src/app/(main)/NavBar.tsx similarity index 100% rename from src/app/(main)/NavBar.js rename to src/app/(main)/NavBar.tsx diff --git a/src/app/(main)/UpdateNotice.js b/src/app/(main)/UpdateNotice.tsx similarity index 100% rename from src/app/(main)/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 b88bfd77d..0bb807ff5 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 5fb65f238..ec1d793c2 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 3af338670..356380388 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)/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 35809a985..32ec819e4 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.tsx b/src/app/(main)/reports/ReportsDataTable.tsx index 0ca853dc7..3ede47839 100644 --- a/src/app/(main)/reports/ReportsDataTable.tsx +++ b/src/app/(main)/reports/ReportsDataTable.tsx @@ -1,16 +1,10 @@ 'use client'; -import { useApi } from 'components/hooks'; +import { useReports } 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 }: { websiteId?: string }) { - const { get } = useApi(); - const modified = useCache(state => (state as any)?.reports); - const queryResult = useFilterQuery(['reports', { websiteId, modified }], params => - get(websiteId ? `/websites/${websiteId}/reports` : `/reports`, params), - ); + const queryResult = useReports(websiteId); return ( 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 6b2a79320..882110eec 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 a54ef4f36..9b6be5d8c 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 6923bceb6..9db472d8d 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 34b679808..6b5bf6360 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 86% rename from src/app/(main)/reports/[id]/FieldFilterForm.js rename to src/app/(main)/reports/[id]/FieldFilterForm.tsx index ea80f82a1..4af7febfb 100644 --- a/src/app/(main)/reports/[id]/FieldFilterForm.js +++ b/src/app/(main)/reports/[id]/FieldFilterForm.tsx @@ -3,6 +3,15 @@ import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics'; 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,7 +19,7 @@ export default function FieldFilterForm({ values, onSelect, allowFilterSelect = true, -}) { +}: FieldFilterFormProps) { const { formatMessage, labels } = useMessages(); const [filter, setFilter] = useState('eq'); const [value, setValue] = useState(); @@ -21,7 +30,7 @@ export default function FieldFilterForm({ const formattedValues = useMemo(() => { const formatted = {}; - const format = val => { + const format = (val: string) => { formatted[val] = formatValue(val, name); return formatted[val]; }; @@ -56,7 +65,7 @@ export default function FieldFilterForm({ items={filters} value={filter} renderValue={renderFilterValue} - onChange={setFilter} + onChange={(key: any) => setFilter(key)} > {({ value, label }) => { return {label}; @@ -69,12 +78,12 @@ export default function FieldFilterForm({ items={values} value={value} renderValue={renderValue} - onChange={setValue} + onChange={(key: any) => setValue(key)} style={{ minWidth: '250px', }} > - {value => { + {(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 e7661511d..dfd402cf0 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 9ad4cd932..4f9b92642 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 bf77dd9d2..eb1a646a4 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 6b99b00a9..f26661991 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} -
+
{children}
); } diff --git a/src/app/(main)/reports/[id]/ReportBody.js b/src/app/(main)/reports/[id]/ReportBody.tsx similarity index 100% rename from src/app/(main)/reports/[id]/ReportBody.js rename to src/app/(main)/reports/[id]/ReportBody.tsx 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 8605ffb33..e4d4688a0 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 ed3b97364..d69cd0cd2 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 100% rename from src/app/(main)/reports/[id]/ReportMenu.js rename to src/app/(main)/reports/[id]/ReportMenu.tsx 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 6b9a03445..7a39131ba 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 e91cb4a2b..8b4dc99cd 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 7461afbcd..923e8d56b 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 135b5db84..0bb454156 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 3e77ce156..88c27ae91 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 91dd09f84..cd643eedf 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 f99e187bb..b90ff396d 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 05d380427..a45176983 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/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 762a313df..fc168695f 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 ae42e76bf..35f0fcb1e 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 a71fae6f4..d2e7f129e 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 000000000..898a40cc0 --- /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 f738f8834..e36b5b53d 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 39ecfb770..1384a9196 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 b8bb8c3a7..5a242cb90 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 5e4a41ea0..36a1a1f83 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 9b80668a8..00eedb178 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 528e1d75b..5cd382256 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 7b98f082f..97676e176 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 a9b6922aa..3b4d4703c 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 b7850812b..09f9ecbbf 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 164838f96..2424e464e 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 1f7f1da4a..70a7bebbd 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 c17d5763d..4a0f109d8 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 b1dc58545..420afe9bb 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 603adae3a..cef2977ef 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 fb31b6fab..588a5a52b 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 9a402d444..a08c746bb 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 8ec0ad856..cf5ae35cb 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 81% rename from src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.js rename to src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.tsx index c83ec3d08..64a0c58e4 100644 --- a/src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.js +++ b/src/app/(main)/settings/teams/[id]/TeamWebsiteAddForm.tsx @@ -7,12 +7,25 @@ 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; @@ -39,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 9e76ffab4..93bb3a106 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 0f8022121..29b958160 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.tsx similarity index 84% rename from src/app/(main)/settings/users/UserAddButton.js rename to src/app/(main)/settings/users/UserAddButton.tsx index 0f4bf7349..1158ecec6 100644 --- a/src/app/(main)/settings/users/UserAddButton.js +++ b/src/app/(main)/settings/users/UserAddButton.tsx @@ -3,7 +3,7 @@ import UserAddForm from './UserAddForm'; import useMessages from 'components/hooks/useMessages'; import { setValue } from 'store/cache'; -export function UserAddButton({ onSave }) { +export function UserAddButton({ onSave }: { onSave?: () => void }) { const { formatMessage, labels, messages } = useMessages(); const { showToast } = useToasts(); @@ -22,7 +22,7 @@ export function UserAddButton({ onSave }) { {formatMessage(labels.createUser)} - {close => } + {(close: () => void) => } ); 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 38c1bedd7..8c1537757 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 22d931713..775004a84 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 5a47fdc12..eaa8e4818 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 157c9ad69..0b823a94e 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 18b5f1a71..000000000 --- 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 000000000..2d06e82a9 --- /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 72% rename from src/app/(main)/settings/users/UsersDataTable.js rename to src/app/(main)/settings/users/UsersDataTable.tsx index 154e37ad0..b77164515 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(`/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 caf1f913f..0901a6fb2 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 a0b5aba1b..2b840b64c 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 ea340ab79..d9b149c33 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 b1a694296..7b4c92deb 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.tsx b/src/app/(main)/settings/websites/WebsiteAddForm.tsx index 996241037..6d8fb8c32 100644 --- a/src/app/(main)/settings/websites/WebsiteAddForm.tsx +++ b/src/app/(main)/settings/websites/WebsiteAddForm.tsx @@ -10,11 +10,16 @@ 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 }: { 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: any) => { mutate(data, { @@ -26,7 +31,7 @@ export function WebsiteAddForm({ onSave, onClose }: { onSave?: () => void; onClo }; return ( - + @@ -48,7 +53,7 @@ export function WebsiteAddForm({ onSave, onClose }: { onSave?: () => void; onClo {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 82b380482..4607b423c 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, settingsUrl } = 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') { - router.push('/settings/websites'); + router.push(settingsUrl); } 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 1548bddbd..e0f71041d 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 18ad0ac98..80b36cae1 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 9886429ba..0c02c77bc 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)/websites/WebsitesBrowse.js b/src/app/(main)/websites/WebsitesBrowse.tsx similarity index 91% rename from src/app/(main)/websites/WebsitesBrowse.js rename to src/app/(main)/websites/WebsitesBrowse.tsx index 3e8df2b26..c426cc069 100644 --- a/src/app/(main)/websites/WebsitesBrowse.js +++ b/src/app/(main)/websites/WebsitesBrowse.tsx @@ -17,7 +17,7 @@ export function WebsitesBrowse() { return ( <> - + setTab(tab)} style={{ marginBottom: 30 }}> {formatMessage(labels.myWebsites)} {formatMessage(labels.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 d05ff4220..eba155c11 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 91% rename from src/app/(main)/websites/[id]/WebsiteChartList.js rename to src/app/(main)/websites/[id]/WebsiteChartList.tsx index bc2439ded..b35b6f1f0 100644 --- a/src/app/(main)/websites/[id]/WebsiteChartList.js +++ b/src/app/(main)/websites/[id]/WebsiteChartList.tsx @@ -8,7 +8,15 @@ import WebsiteHeader from './WebsiteHeader'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; import { useMessages, useLocale } from 'components/hooks'; -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 80% rename from src/app/(main)/websites/[id]/WebsiteDetails.js rename to src/app/(main)/websites/[id]/WebsiteDetails.tsx index c6ad1acc9..7d8d2d999 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 && } )} 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 91% rename from src/app/(main)/websites/[id]/WebsiteMenuView.js rename to src/app/(main)/websites/[id]/WebsiteExpandedView.tsx index c501645a4..9fb1b3f6f 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, + websiteDomain, +}: { + websiteId: string; + websiteDomain?: 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 (
@@ -138,11 +144,11 @@ 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 e96856f62..6a02cd474 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 94% rename from src/app/(main)/websites/[id]/WebsiteTableView.js rename to src/app/(main)/websites/[id]/WebsiteTableView.tsx index 28a8fad60..e530f2bae 100644 --- a/src/app/(main)/websites/[id]/WebsiteTableView.js +++ b/src/app/(main)/websites/[id]/WebsiteTableView.tsx @@ -10,7 +10,7 @@ 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 }: { websiteId: string }) { const [countryData, setCountryData] = useState(); const tableProps = { websiteId, 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 5be191854..419472d59 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 4e50f5b9b..7976ce36f 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 b5982e321..61a4dc622 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 66% rename from src/app/(main)/websites/[id]/realtime/Realtime.js rename to src/app/(main)/websites/[id]/realtime/Realtime.tsx index 737bcd1ba..bd9f74bc1 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/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 75f2f2d43..ad03efd14 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 dbaeb541a..154ac707f 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 f400cc1bc..e9c0fc1b5 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 b388b37b5..5293c1f06 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 674858b2d..27a9ec5a6 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 c3d626993..c2ddd2ff7 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,33 @@ function MessagesProvider({ children }) { ); } +function SettingsProvider({ children }) { + const [config, setConfig] = useState({}); + + useEffect(() => { + const hostUrl = process.env.hostUrl || window?.location.origin; + + setConfig({ + settingsUrl: '/settings/websites', + shareUrl: hostUrl, + trackingCodeUrl: hostUrl, + websitesUrl: `/websites`, + }); + }, []); + + return {children}; +} + export function Providers({ children }) { return ( - - - {children} - - + + + + {children} + + + ); } 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 59d145bf3..78cf3dd37 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/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 41e93f52e..2b82908df 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/sso/page.tsx b/src/app/sso/page.tsx index 75ea945d4..e577767a8 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/DataTable.tsx b/src/components/common/DataTable.tsx index a3c63c0ab..00aba09c8 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.tsx b/src/components/common/ErrorBoundary.tsx index 4eb2700fd..49b7e6710 100644 --- a/src/components/common/ErrorBoundary.tsx +++ b/src/components/common/ErrorBoundary.tsx @@ -1,19 +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: Error, info: ErrorInfo) => { + // eslint-disable-next-line no-console console.error(error, info.componentStack); }; -export interface ErrorBoundaryProps { - children: ReactNode; -} - -export function ErrorBoundary({ children }: ErrorBoundaryProps) { +export function ErrorBoundary({ children }: { children: ReactNode }) { const { formatMessage, messages } = useMessages(); const fallbackRender = ({ error, resetErrorBoundary }) => { diff --git a/src/components/common/FilterButtons.tsx b/src/components/common/FilterButtons.tsx index e1860c783..a64a6482a 100644 --- a/src/components/common/FilterButtons.tsx +++ b/src/components/common/FilterButtons.tsx @@ -4,7 +4,7 @@ import { ButtonGroup, Button, Flexbox } from 'react-basics'; export interface FilterButtonsProps { items: any[]; selectedKey?: Key; - onSelect: () => void; + onSelect: (key: any) => void; } export function FilterButtons({ items, selectedKey, onSelect }: FilterButtonsProps) { diff --git a/src/components/common/FilterLink.tsx b/src/components/common/FilterLink.tsx index a90302270..bc0a4d48d 100644 --- a/src/components/common/FilterLink.tsx +++ b/src/components/common/FilterLink.tsx @@ -13,7 +13,7 @@ export interface FilterLinkProps { label?: string; externalUrl?: string; className?: string; - children: ReactNode; + children?: ReactNode; } export function FilterLink({ diff --git a/src/components/declarations.d.ts b/src/components/declarations.d.ts index 81533301a..ca55157ba 100644 --- a/src/components/declarations.d.ts +++ b/src/components/declarations.d.ts @@ -1,3 +1,4 @@ declare module '*.css'; declare module '*.svg'; declare module '*.json'; +declare module 'uuid'; diff --git a/src/components/hooks/useApiFilter.ts b/src/components/hooks/useApiFilter.ts deleted file mode 100644 index d411fd434..000000000 --- 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/useDateRange.ts b/src/components/hooks/useDateRange.ts index 6e70a3689..efaa717fc 100644 --- a/src/components/hooks/useDateRange.ts +++ 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: string) { +export function useDateRange(websiteId?: string) { const { get } = useApi(); const { locale } = useLocale(); const websiteConfig = websiteStore(state => state[websiteId]?.dateRange); @@ -14,9 +15,9 @@ export function useDateRange(websiteId: string) { 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') { @@ -37,14 +38,17 @@ export function useDateRange(websiteId: string) { } } - 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/useFilterQuery.ts b/src/components/hooks/useFilterQuery.ts index 37c28b7e6..030da27da 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/useFormat.ts b/src/components/hooks/useFormat.ts index c1160162d..06585e497 100644 --- a/src/components/hooks/useFormat.ts +++ b/src/components/hooks/useFormat.ts @@ -9,23 +9,28 @@ export function useFormat() { const { locale } = useLocale(); const countryNames = useCountryNames(locale); - const formatBrowser = (value: string) => { + const formatBrowser = (value: string): string => { return BROWSERS[value] || value; }; - const formatCountry = (value: string) => { + const formatCountry = (value: string): string => { return countryNames[value] || value; }; - const formatRegion = (value: string) => { - 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: string) => { + 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: string, type: string) => { + 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/useNavigation.ts b/src/components/hooks/useNavigation.ts index 9f01cd802..fb9bffc58 100644 --- a/src/components/hooks/useNavigation.ts +++ 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(); diff --git a/src/components/hooks/useReports.ts b/src/components/hooks/useReports.ts index d9292aeb4..d2473002d 100644 --- a/src/components/hooks/useReports.ts +++ b/src/components/hooks/useReports.ts @@ -1,19 +1,19 @@ import { useState } from 'react'; import useApi from './useApi'; -import useApiFilter from 'components/hooks/useApiFilter'; +import useFilterQuery from 'components/hooks/useFilterQuery'; -export function useReports() { +export function useReports(websiteId?: string) { 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 { 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 => { + const deleteReport = (id: any) => { mutate(id, { onSuccess: () => { setModified(Date.now()); @@ -22,16 +22,8 @@ export function useReports() { }; return { - reports: data, - error, - isLoading, + ...queryResult, deleteReport, - filter, - page, - pageSize, - handleFilterChange, - handlePageChange, - handlePageSizeChange, }; } diff --git a/src/components/hooks/useShareToken.ts b/src/components/hooks/useShareToken.ts index 088f643eb..189657be4 100644 --- a/src/components/hooks/useShareToken.ts +++ b/src/components/hooks/useShareToken.ts @@ -3,15 +3,22 @@ import useApi from './useApi'; const selector = (state: { shareToken: string }) => state.shareToken; -export function useShareToken(shareId: string) { +export function useShareToken(shareId: string): { + shareToken: any; + isLoading?: boolean; + error?: Error; +} { const shareToken = useStore(selector); const { get, useQuery } = useApi(); - const { isLoading, error } = useQuery(['share', shareId], async () => { - const data = await get(`/share/${shareId}`); + const { isLoading, error } = useQuery({ + queryKey: ['share', shareId], + queryFn: async () => { + const data = await get(`/share/${shareId}`); - setShareToken(data); + setShareToken(data); - return data; + return data; + }, }); return { shareToken, isLoading, error }; diff --git a/src/components/hooks/useWebsite.ts b/src/components/hooks/useWebsite.ts index 7b68335ac..d18e96ba6 100644 --- a/src/components/hooks/useWebsite.ts +++ b/src/components/hooks/useWebsite.ts @@ -2,7 +2,9 @@ import useApi from './useApi'; export function useWebsite(websiteId: string) { const { get, useQuery } = useApi(); - return useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), { + return useQuery({ + queryKey: ['websites', websiteId], + queryFn: () => get(`/websites/${websiteId}`), enabled: !!websiteId, }); } 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 9fde27ca1..f7739f170 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 3c0d0cd6f..1151da0be 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 6ca358a12..c787f2296 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 312c6854a..acb17dfe6 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 8b40cafa1..01e80378d 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 46c725973..2a076d42b 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 1725ca3be..cf1beaa1d 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 078389d31..e125e2589 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 86b08887b..000000000 --- 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 000000000..2a34fdc4f --- /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 361dffb5a..e95b61faf 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 2f7020128..e32a09a3a 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 c93881e47..0b5c98562 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 8341fbc74..6d28dc201 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 afc536637..e1c054358 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 c5f1b1d73..067e07e93 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 6f3b75b04..99f9ca2fd 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 5e1906c3c..b4f0781e4 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 606b020ab..4ebdac1b5 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 f2cf48d11..be6d4a0c3 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 a8ae82aa2..26d9529b4 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.js b/src/components/metrics/FilterTags.tsx similarity index 92% rename from src/components/metrics/FilterTags.js rename to src/components/metrics/FilterTags.tsx index db8fdcbdd..140385f5c 100644 --- a/src/components/metrics/FilterTags.js +++ b/src/components/metrics/FilterTags.tsx @@ -18,7 +18,7 @@ export function FilterTags({ params }) { return null; } - function handleCloseFilter(param) { + function handleCloseFilter(param?: string) { if (!param) { router.push(makeUrl({ view }, true)); } else { @@ -44,7 +44,7 @@ export function FilterTags({ params }) {
); })} -