-
-
Page links
+
+
+
+
+
+ Page links
- page one
+ page one
- page two
+ page two
-
-
-
Click events
+
+
+ Click events
Send event
@@ -184,21 +172,17 @@ export function TestConsole({ websiteId }: { websiteId?: string }) {
data-umami-event-id="123"
variant="primary"
>
- Button with div
+ Button with div
-
- DIV with attribute
-
-
-
-
- Nested DIV
-
+
DIV with attribute
+
-
-
-
Javascript events
+
+
+ Javascript events
Run script
@@ -208,14 +192,16 @@ export function TestConsole({ websiteId }: { websiteId?: string }) {
Revenue script
-
-
-
-
-
- )}
-
+
+
+
+
Pageviews
+
+
Events
+
+
+
+
+
);
}
-
-export default TestConsole;
diff --git a/src/app/(main)/console/[[...websiteId]]/page.tsx b/src/app/(main)/console/[websiteId]/page.tsx
similarity index 71%
rename from src/app/(main)/console/[[...websiteId]]/page.tsx
rename to src/app/(main)/console/[websiteId]/page.tsx
index 6c504f1e..28b81615 100644
--- a/src/app/(main)/console/[[...websiteId]]/page.tsx
+++ b/src/app/(main)/console/[websiteId]/page.tsx
@@ -1,5 +1,5 @@
-import { Metadata } from 'next';
-import TestConsole from '../TestConsole';
+import type { Metadata } from 'next';
+import { TestConsolePage } from './TestConsolePage';
async function getEnabled() {
return !!process.env.ENABLE_TEST_CONSOLE;
@@ -14,7 +14,7 @@ export default async function ({ params }: { params: Promise<{ websiteId: string
return null;
}
- return
;
+ return
;
}
export const metadata: Metadata = {
diff --git a/src/app/(main)/dashboard/DashboardEdit.module.css b/src/app/(main)/dashboard/DashboardEdit.module.css
deleted file mode 100644
index 19266d17..00000000
--- a/src/app/(main)/dashboard/DashboardEdit.module.css
+++ /dev/null
@@ -1,57 +0,0 @@
-.buttons {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: 10px;
-}
-
-.item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- width: 100%;
- padding: 20px;
- border-radius: 5px;
- border: 1px solid var(--base400);
- background: var(--base50);
- margin-bottom: 10px;
-}
-
-.text {
- position: relative;
-}
-
-.name {
- font-weight: 600;
- font-size: 16px;
-}
-
-.domain {
- font-size: 14px;
- color: var(--base700);
-}
-
-.dragActive {
- cursor: grab;
-}
-
-.dragActive:active {
- cursor: grabbing;
-}
-
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 20px;
- gap: 20px;
-}
-
-.search {
- max-width: 360px;
-}
-
-.active {
- border-color: var(--base600);
- box-shadow: 4px 4px 4px var(--base100);
-}
diff --git a/src/app/(main)/dashboard/DashboardEdit.tsx b/src/app/(main)/dashboard/DashboardEdit.tsx
deleted file mode 100644
index d15ae197..00000000
--- a/src/app/(main)/dashboard/DashboardEdit.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-import { useState, useMemo, useEffect } from 'react';
-import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
-import classNames from 'classnames';
-import { Button, Loading, Toggle, SearchField } from 'react-basics';
-import { firstBy } from 'thenby';
-import useDashboard, { saveDashboard } from '@/store/dashboard';
-import { useMessages, useWebsites } from '@/components/hooks';
-import styles from './DashboardEdit.module.css';
-
-const DRAG_ID = 'dashboard-website-ordering';
-
-export function DashboardEdit({ teamId }: { teamId: string }) {
- const settings = useDashboard();
- const { websiteOrder, websiteActive, isEdited } = settings;
- const { formatMessage, labels } = useMessages();
- const [order, setOrder] = useState(websiteOrder || []);
- const [active, setActive] = useState(websiteActive || []);
- const [edited, setEdited] = useState(isEdited);
- const [websites, setWebsites] = useState([]);
- const [search, setSearch] = useState('');
-
- const {
- result,
- query: { isLoading },
- setParams,
- } = useWebsites({ teamId });
-
- useEffect(() => {
- if (result?.data) {
- setWebsites(prevWebsites => {
- const newWebsites = [...prevWebsites, ...result.data];
- if (newWebsites.length < result.count) {
- setParams(prevParams => ({ ...prevParams, page: prevParams.page + 1 }));
- }
- return newWebsites;
- });
- }
- }, [result]);
-
- const ordered = useMemo(() => {
- if (websites) {
- return websites
- .map((website: { id: any; name: string; domain: string }) => ({
- ...website,
- order: order.indexOf(website.id),
- }))
- .sort(firstBy('order'));
- }
- return [];
- }, [websites, order]);
-
- function handleWebsiteDrag({ destination, source }) {
- if (!destination || destination.index === source.index) return;
-
- const orderedWebsites = [...ordered];
- const [removed] = orderedWebsites.splice(source.index, 1);
- orderedWebsites.splice(destination.index, 0, removed);
-
- setOrder(orderedWebsites.map(website => website?.id || 0));
- setEdited(true);
- }
-
- function handleActiveWebsites(id: string) {
- setActive(prevActive =>
- prevActive.includes(id) ? prevActive.filter(a => a !== id) : [...prevActive, id],
- );
- setEdited(true);
- }
-
- function handleSave() {
- saveDashboard({
- editing: false,
- isEdited: edited,
- websiteOrder: order,
- websiteActive: active,
- });
- }
-
- function handleCancel() {
- saveDashboard({ editing: false, websiteOrder, websiteActive, isEdited });
- }
-
- function handleReset() {
- setOrder([]);
- setActive([]);
- setEdited(false);
- }
-
- if (isLoading) {
- return
;
- }
-
- return (
- <>
-
-
-
-
- {formatMessage(labels.save)}
-
-
- {formatMessage(labels.cancel)}
-
-
- {formatMessage(labels.reset)}
-
-
-
-
-
-
- {(provided, snapshot) => (
-
- {ordered.map(({ id, name, domain }, index) => {
- if (
- search &&
- !`${name.toLowerCase()}${domain.toLowerCase()}`.includes(search.toLowerCase())
- ) {
- return null;
- }
-
- return (
-
- {(provided, snapshot) => (
-
-
-
handleActiveWebsites(id)}
- />
-
- )}
-
- );
- })}
- {provided.placeholder}
-
- )}
-
-
-
- >
- );
-}
-
-export default DashboardEdit;
diff --git a/src/app/(main)/dashboard/DashboardPage.tsx b/src/app/(main)/dashboard/DashboardPage.tsx
index 83b27e09..c2c7e75f 100644
--- a/src/app/(main)/dashboard/DashboardPage.tsx
+++ b/src/app/(main)/dashboard/DashboardPage.tsx
@@ -1,71 +1,17 @@
'use client';
-import { Icon, Icons, Loading, Text } from 'react-basics';
-import PageHeader from '@/components/layout/PageHeader';
-import Pager from '@/components/common/Pager';
-import WebsiteChartList from '../websites/[websiteId]/WebsiteChartList';
-import DashboardSettingsButton from '@/app/(main)/dashboard/DashboardSettingsButton';
-import DashboardEdit from '@/app/(main)/dashboard/DashboardEdit';
-import EmptyPlaceholder from '@/components/common/EmptyPlaceholder';
-import { useMessages, useLocale, useTeamUrl, useWebsites } from '@/components/hooks';
-import useDashboard from '@/store/dashboard';
-import LinkButton from '@/components/common/LinkButton';
+import { Column } from '@umami/react-zen';
+import { PageBody } from '@/components/common/PageBody';
+import { PageHeader } from '@/components/common/PageHeader';
+import { useMessages } from '@/components/hooks';
export function DashboardPage() {
- const { formatMessage, labels, messages } = useMessages();
- const { teamId, renderTeamUrl } = useTeamUrl();
- const { showCharts, editing, isEdited } = useDashboard();
- const { dir } = useLocale();
- const pageSize = isEdited ? 200 : 10;
-
- const { result, query, params, setParams } = useWebsites({ teamId }, { pageSize });
- const { page } = params;
- const hasData = !!result?.data?.length;
-
- const handlePageChange = (page: number) => {
- setParams({ ...params, page });
- };
-
- if (query.isLoading) {
- return
;
- }
+ const { formatMessage, labels } = useMessages();
return (
-
-
- {!editing && hasData && }
-
- {!hasData && (
-
-
-
-
-
- {formatMessage(messages.goToSettings)}
-
-
- )}
- {hasData && (
- <>
- {editing && }
- {!editing && (
- <>
-
-
- >
- )}
- >
- )}
-
+
+
+
+
+
);
}
-
-export default DashboardPage;
diff --git a/src/app/(main)/dashboard/DashboardSettingsButton.module.css b/src/app/(main)/dashboard/DashboardSettingsButton.module.css
deleted file mode 100644
index 6e0d19c2..00000000
--- a/src/app/(main)/dashboard/DashboardSettingsButton.module.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.buttonGroup {
- display: flex;
- place-items: center;
- gap: 10px;
-}
diff --git a/src/app/(main)/dashboard/DashboardSettingsButton.tsx b/src/app/(main)/dashboard/DashboardSettingsButton.tsx
deleted file mode 100644
index 1c473a22..00000000
--- a/src/app/(main)/dashboard/DashboardSettingsButton.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { TooltipPopup, Icon, Text, Flexbox, Button } from 'react-basics';
-import Icons from '@/components/icons';
-import { saveDashboard } from '@/store/dashboard';
-import { useMessages } from '@/components/hooks';
-
-export function DashboardSettingsButton() {
- const { formatMessage, labels } = useMessages();
-
- const handleToggleCharts = () => {
- saveDashboard(state => ({ showCharts: !state.showCharts }));
- };
-
- const handleEdit = () => {
- saveDashboard({ editing: true });
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {formatMessage(labels.edit)}
-
-
- );
-}
-
-export default DashboardSettingsButton;
diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx
index 7a605110..4b79b598 100644
--- a/src/app/(main)/dashboard/page.tsx
+++ b/src/app/(main)/dashboard/page.tsx
@@ -1,7 +1,7 @@
-import DashboardPage from './DashboardPage';
-import { Metadata } from 'next';
+import type { Metadata } from 'next';
+import { DashboardPage } from './DashboardPage';
-export default function () {
+export default async function () {
return
;
}
diff --git a/src/app/(main)/layout.module.css b/src/app/(main)/layout.module.css
deleted file mode 100644
index 290c38a2..00000000
--- a/src/app/(main)/layout.module.css
+++ /dev/null
@@ -1,22 +0,0 @@
-.layout {
- display: grid;
- grid-template-rows: max-content 1fr;
- grid-template-columns: 1fr;
- overflow: hidden;
-}
-
-.nav {
- height: 60px;
- width: 100vw;
- grid-column: 1;
- grid-row: 1 / 2;
-}
-
-.body {
- grid-column: 1;
- grid-row: 2 / 3;
- min-height: 0;
- height: calc(100vh - 60px);
- height: calc(100dvh - 60px);
- overflow-y: auto;
-}
diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx
index dd1baec8..98fca4ab 100644
--- a/src/app/(main)/layout.tsx
+++ b/src/app/(main)/layout.tsx
@@ -1,21 +1,12 @@
-import { Metadata } from 'next';
-import App from './App';
-import NavBar from './NavBar';
-import Page from '@/components/layout/Page';
-import styles from './layout.module.css';
+import type { Metadata } from 'next';
+import { Suspense } from 'react';
+import { App } from './App';
-export default async function ({ children }) {
+export default function ({ children }) {
return (
-
-
-
-
-
-
-
-
+
+ {children}
+
);
}
diff --git a/src/app/(main)/links/LinkAddButton.tsx b/src/app/(main)/links/LinkAddButton.tsx
new file mode 100644
index 00000000..4276895d
--- /dev/null
+++ b/src/app/(main)/links/LinkAddButton.tsx
@@ -0,0 +1,19 @@
+import { useMessages } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { LinkEditForm } from './LinkEditForm';
+
+export function LinkAddButton({ teamId }: { teamId?: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
}
+ label={formatMessage(labels.addLink)}
+ variant="primary"
+ width="600px"
+ >
+ {({ close }) =>
}
+
+ );
+}
diff --git a/src/app/(main)/links/LinkDeleteButton.tsx b/src/app/(main)/links/LinkDeleteButton.tsx
new file mode 100644
index 00000000..78f85f89
--- /dev/null
+++ b/src/app/(main)/links/LinkDeleteButton.tsx
@@ -0,0 +1,57 @@
+import { ConfirmationForm } from '@/components/common/ConfirmationForm';
+import { useDeleteQuery, useMessages } from '@/components/hooks';
+import { Trash } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { messages } from '@/components/messages';
+
+export function LinkDeleteButton({
+ linkId,
+ name,
+ onSave,
+}: {
+ linkId: string;
+ websiteId: string;
+ name: string;
+ onSave?: () => void;
+}) {
+ const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
+ const { mutateAsync, isPending, error, touch } = useDeleteQuery(`/links/${linkId}`);
+
+ const handleConfirm = async (close: () => void) => {
+ await mutateAsync(null, {
+ onSuccess: () => {
+ touch('links');
+ onSave?.();
+ close();
+ },
+ });
+ };
+
+ return (
+
}
+ title={formatMessage(labels.confirm)}
+ variant="quiet"
+ width="400px"
+ >
+ {({ close }) => (
+
{name},
+ }}
+ />
+ }
+ isLoading={isPending}
+ error={getErrorMessage(error)}
+ onConfirm={handleConfirm.bind(null, close)}
+ onClose={close}
+ buttonLabel={formatMessage(labels.delete)}
+ buttonVariant="danger"
+ />
+ )}
+
+ );
+}
diff --git a/src/app/(main)/links/LinkEditButton.tsx b/src/app/(main)/links/LinkEditButton.tsx
new file mode 100644
index 00000000..4d858796
--- /dev/null
+++ b/src/app/(main)/links/LinkEditButton.tsx
@@ -0,0 +1,16 @@
+import { useMessages } from '@/components/hooks';
+import { Edit } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { LinkEditForm } from './LinkEditForm';
+
+export function LinkEditButton({ linkId }: { linkId: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ } title={formatMessage(labels.link)} variant="quiet" width="800px">
+ {({ close }) => {
+ return ;
+ }}
+
+ );
+}
diff --git a/src/app/(main)/links/LinkEditForm.tsx b/src/app/(main)/links/LinkEditForm.tsx
new file mode 100644
index 00000000..6c10c7f0
--- /dev/null
+++ b/src/app/(main)/links/LinkEditForm.tsx
@@ -0,0 +1,148 @@
+import {
+ Button,
+ Column,
+ Form,
+ FormField,
+ FormSubmitButton,
+ Icon,
+ Label,
+ Loading,
+ Row,
+ TextField,
+} from '@umami/react-zen';
+import { useEffect, useState } from 'react';
+import { useConfig, useLinkQuery, useMessages } from '@/components/hooks';
+import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
+import { RefreshCw } from '@/components/icons';
+import { LINKS_URL } from '@/lib/constants';
+import { getRandomChars } from '@/lib/generate';
+import { isValidUrl } from '@/lib/url';
+
+const generateId = () => getRandomChars(9);
+
+export function LinkEditForm({
+ linkId,
+ teamId,
+ onSave,
+ onClose,
+}: {
+ linkId?: string;
+ teamId?: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { formatMessage, labels, messages, getErrorMessage } = useMessages();
+ const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(
+ linkId ? `/links/${linkId}` : '/links',
+ {
+ id: linkId,
+ teamId,
+ },
+ );
+ const { linksUrl } = useConfig();
+ const hostUrl = linksUrl || LINKS_URL;
+ const { data, isLoading } = useLinkQuery(linkId);
+ const [slug, setSlug] = useState(generateId());
+
+ const handleSubmit = async (data: any) => {
+ await mutateAsync(data, {
+ onSuccess: async () => {
+ toast(formatMessage(messages.saved));
+ touch('links');
+ onSave?.();
+ onClose?.();
+ },
+ });
+ };
+
+ const handleSlug = () => {
+ const slug = generateId();
+
+ setSlug(slug);
+
+ return slug;
+ };
+
+ const checkUrl = (url: string) => {
+ if (!isValidUrl(url)) {
+ return formatMessage(labels.invalidUrl);
+ }
+ return true;
+ };
+
+ useEffect(() => {
+ if (data) {
+ setSlug(data.slug);
+ }
+ }, [data]);
+
+ if (linkId && isLoading) {
+ return ;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/links/LinkProvider.tsx b/src/app/(main)/links/LinkProvider.tsx
new file mode 100644
index 00000000..c29e13cf
--- /dev/null
+++ b/src/app/(main)/links/LinkProvider.tsx
@@ -0,0 +1,21 @@
+'use client';
+import { Loading } from '@umami/react-zen';
+import { createContext, type ReactNode } from 'react';
+import { useLinkQuery } from '@/components/hooks/queries/useLinkQuery';
+import type { Link } from '@/generated/prisma/client';
+
+export const LinkContext = createContext (null);
+
+export function LinkProvider({ linkId, children }: { linkId?: string; children: ReactNode }) {
+ const { data: link, isLoading, isFetching } = useLinkQuery(linkId);
+
+ if (isFetching && isLoading) {
+ return ;
+ }
+
+ if (!link) {
+ return null;
+ }
+
+ return {children} ;
+}
diff --git a/src/app/(main)/links/LinksDataTable.tsx b/src/app/(main)/links/LinksDataTable.tsx
new file mode 100644
index 00000000..0b3d660b
--- /dev/null
+++ b/src/app/(main)/links/LinksDataTable.tsx
@@ -0,0 +1,14 @@
+import { DataGrid } from '@/components/common/DataGrid';
+import { useLinksQuery, useNavigation } from '@/components/hooks';
+import { LinksTable } from './LinksTable';
+
+export function LinksDataTable() {
+ const { teamId } = useNavigation();
+ const query = useLinksQuery({ teamId });
+
+ return (
+
+ {({ data }) => }
+
+ );
+}
diff --git a/src/app/(main)/links/LinksPage.tsx b/src/app/(main)/links/LinksPage.tsx
new file mode 100644
index 00000000..a6e4c7c4
--- /dev/null
+++ b/src/app/(main)/links/LinksPage.tsx
@@ -0,0 +1,26 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { LinksDataTable } from '@/app/(main)/links/LinksDataTable';
+import { PageBody } from '@/components/common/PageBody';
+import { PageHeader } from '@/components/common/PageHeader';
+import { Panel } from '@/components/common/Panel';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { LinkAddButton } from './LinkAddButton';
+
+export function LinksPage() {
+ const { formatMessage, labels } = useMessages();
+ const { teamId } = useNavigation();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/links/LinksTable.tsx b/src/app/(main)/links/LinksTable.tsx
new file mode 100644
index 00000000..a3b4a86a
--- /dev/null
+++ b/src/app/(main)/links/LinksTable.tsx
@@ -0,0 +1,51 @@
+import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
+import Link from 'next/link';
+import { DateDistance } from '@/components/common/DateDistance';
+import { ExternalLink } from '@/components/common/ExternalLink';
+import { useMessages, useNavigation, useSlug } from '@/components/hooks';
+import { LinkDeleteButton } from './LinkDeleteButton';
+import { LinkEditButton } from './LinkEditButton';
+
+export function LinksTable(props: DataTableProps) {
+ const { formatMessage, labels } = useMessages();
+ const { websiteId, renderUrl } = useNavigation();
+ const { getSlugUrl } = useSlug('link');
+
+ return (
+
+
+ {({ id, name }: any) => {
+ return {name};
+ }}
+
+
+ {({ slug }: any) => {
+ const url = getSlugUrl(slug);
+ return (
+
+ {url}
+
+ );
+ }}
+
+
+ {({ url }: any) => {
+ return {url} ;
+ }}
+
+
+ {(row: any) => }
+
+
+ {({ id, name }: any) => {
+ return (
+
+
+
+
+ );
+ }}
+
+
+ );
+}
diff --git a/src/app/(main)/links/[linkId]/LinkControls.tsx b/src/app/(main)/links/[linkId]/LinkControls.tsx
new file mode 100644
index 00000000..1d1147a8
--- /dev/null
+++ b/src/app/(main)/links/[linkId]/LinkControls.tsx
@@ -0,0 +1,32 @@
+import { Column, Row } from '@umami/react-zen';
+import { ExportButton } from '@/components/input/ExportButton';
+import { FilterBar } from '@/components/input/FilterBar';
+import { MonthFilter } from '@/components/input/MonthFilter';
+import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
+import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
+
+export function LinkControls({
+ linkId: websiteId,
+ allowFilter = true,
+ allowDateFilter = true,
+ allowMonthFilter,
+ allowDownload = false,
+}: {
+ linkId: string;
+ allowFilter?: boolean;
+ allowDateFilter?: boolean;
+ allowMonthFilter?: boolean;
+ allowDownload?: boolean;
+}) {
+ return (
+
+
+ {allowFilter ? :
}
+ {allowDateFilter && }
+ {allowDownload && }
+ {allowMonthFilter && }
+
+ {allowFilter && }
+
+ );
+}
diff --git a/src/app/(main)/links/[linkId]/LinkHeader.tsx b/src/app/(main)/links/[linkId]/LinkHeader.tsx
new file mode 100644
index 00000000..a84a6260
--- /dev/null
+++ b/src/app/(main)/links/[linkId]/LinkHeader.tsx
@@ -0,0 +1,19 @@
+import { IconLabel } from '@umami/react-zen';
+import { LinkButton } from '@/components/common/LinkButton';
+import { PageHeader } from '@/components/common/PageHeader';
+import { useLink, useMessages, useSlug } from '@/components/hooks';
+import { ExternalLink, Link } from '@/components/icons';
+
+export function LinkHeader() {
+ const { formatMessage, labels } = useMessages();
+ const { getSlugUrl } = useSlug('link');
+ const link = useLink();
+
+ return (
+ }>
+
+ } label={formatMessage(labels.view)} />
+
+
+ );
+}
diff --git a/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx b/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx
new file mode 100644
index 00000000..1fe8c45f
--- /dev/null
+++ b/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx
@@ -0,0 +1,70 @@
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
+import { formatLongNumber } from '@/lib/format';
+
+export function LinkMetricsBar({
+ linkId,
+}: {
+ linkId: string;
+ showChange?: boolean;
+ compareMode?: boolean;
+}) {
+ const { isAllTime } = useDateRange();
+ const { formatMessage, labels } = useMessages();
+ const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId);
+
+ const { pageviews, visitors, visits, comparison } = data || {};
+
+ const metrics = data
+ ? [
+ {
+ value: visitors,
+ label: formatMessage(labels.visitors),
+ change: visitors - comparison.visitors,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: visits,
+ label: formatMessage(labels.visits),
+ change: visits - comparison.visits,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: pageviews,
+ label: formatMessage(labels.views),
+ change: pageviews - comparison.pageviews,
+ formatValue: formatLongNumber,
+ },
+ ]
+ : null;
+
+ return (
+
+
+ {metrics?.map(({ label, value, prev, change, formatValue, reverseColors }: any) => {
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/app/(main)/links/[linkId]/LinkPage.tsx b/src/app/(main)/links/[linkId]/LinkPage.tsx
new file mode 100644
index 00000000..ddacf08f
--- /dev/null
+++ b/src/app/(main)/links/[linkId]/LinkPage.tsx
@@ -0,0 +1,34 @@
+'use client';
+import { Column, Grid } from '@umami/react-zen';
+import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls';
+import { LinkHeader } from '@/app/(main)/links/[linkId]/LinkHeader';
+import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar';
+import { LinkPanels } from '@/app/(main)/links/[linkId]/LinkPanels';
+import { LinkProvider } from '@/app/(main)/links/LinkProvider';
+import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
+import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
+import { PageBody } from '@/components/common/PageBody';
+import { Panel } from '@/components/common/Panel';
+
+const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
+
+export function LinkPage({ linkId }: { linkId: string }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/links/[linkId]/LinkPanels.tsx b/src/app/(main)/links/[linkId]/LinkPanels.tsx
new file mode 100644
index 00000000..f33525e7
--- /dev/null
+++ b/src/app/(main)/links/[linkId]/LinkPanels.tsx
@@ -0,0 +1,83 @@
+import { Grid, Heading, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
+import { GridRow } from '@/components/common/GridRow';
+import { Panel } from '@/components/common/Panel';
+import { useMessages } from '@/components/hooks';
+import { MetricsTable } from '@/components/metrics/MetricsTable';
+import { WorldMap } from '@/components/metrics/WorldMap';
+
+export function LinkPanels({ linkId }: { linkId: string }) {
+ const { formatMessage, labels } = useMessages();
+ const tableProps = {
+ websiteId: linkId,
+ limit: 10,
+ allowDownload: false,
+ showMore: true,
+ metric: formatMessage(labels.visitors),
+ };
+ const rowProps = { minHeight: 570 };
+
+ return (
+
+
+
+ {formatMessage(labels.sources)}
+
+
+ {formatMessage(labels.referrers)}
+ {formatMessage(labels.channels)}
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.environment)}
+
+
+ {formatMessage(labels.browsers)}
+ {formatMessage(labels.os)}
+ {formatMessage(labels.devices)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.location)}
+
+
+ {formatMessage(labels.countries)}
+ {formatMessage(labels.regions)}
+ {formatMessage(labels.cities)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/links/[linkId]/page.tsx b/src/app/(main)/links/[linkId]/page.tsx
new file mode 100644
index 00000000..4317ada2
--- /dev/null
+++ b/src/app/(main)/links/[linkId]/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { LinkPage } from './LinkPage';
+
+export default async function ({ params }: { params: Promise<{ linkId: string }> }) {
+ const { linkId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Link',
+};
diff --git a/src/app/(main)/links/page.tsx b/src/app/(main)/links/page.tsx
new file mode 100644
index 00000000..24c9c18e
--- /dev/null
+++ b/src/app/(main)/links/page.tsx
@@ -0,0 +1,10 @@
+import type { Metadata } from 'next';
+import { LinksPage } from './LinksPage';
+
+export default function () {
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Links',
+};
diff --git a/src/app/(main)/pixels/PixelAddButton.tsx b/src/app/(main)/pixels/PixelAddButton.tsx
new file mode 100644
index 00000000..1573b9e0
--- /dev/null
+++ b/src/app/(main)/pixels/PixelAddButton.tsx
@@ -0,0 +1,19 @@
+import { useMessages } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { PixelEditForm } from './PixelEditForm';
+
+export function PixelAddButton({ teamId }: { teamId?: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ }
+ label={formatMessage(labels.addPixel)}
+ variant="primary"
+ width="600px"
+ >
+ {({ close }) => }
+
+ );
+}
diff --git a/src/app/(main)/pixels/PixelDeleteButton.tsx b/src/app/(main)/pixels/PixelDeleteButton.tsx
new file mode 100644
index 00000000..436dba5c
--- /dev/null
+++ b/src/app/(main)/pixels/PixelDeleteButton.tsx
@@ -0,0 +1,57 @@
+import { ConfirmationForm } from '@/components/common/ConfirmationForm';
+import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
+import { Trash } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { messages } from '@/components/messages';
+
+export function PixelDeleteButton({
+ pixelId,
+ name,
+ onSave,
+}: {
+ pixelId: string;
+ name: string;
+ onSave?: () => void;
+}) {
+ const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
+ const { mutateAsync, isPending, error } = useDeleteQuery(`/pixels/${pixelId}`);
+ const { touch } = useModified();
+
+ const handleConfirm = async (close: () => void) => {
+ await mutateAsync(null, {
+ onSuccess: () => {
+ touch('pixels');
+ onSave?.();
+ close();
+ },
+ });
+ };
+
+ return (
+ }
+ variant="quiet"
+ title={formatMessage(labels.confirm)}
+ width="400px"
+ >
+ {({ close }) => (
+ {name},
+ }}
+ />
+ }
+ isLoading={isPending}
+ error={getErrorMessage(error)}
+ onConfirm={handleConfirm.bind(null, close)}
+ onClose={close}
+ buttonLabel={formatMessage(labels.delete)}
+ buttonVariant="danger"
+ />
+ )}
+
+ );
+}
diff --git a/src/app/(main)/pixels/PixelEditButton.tsx b/src/app/(main)/pixels/PixelEditButton.tsx
new file mode 100644
index 00000000..3c5924da
--- /dev/null
+++ b/src/app/(main)/pixels/PixelEditButton.tsx
@@ -0,0 +1,21 @@
+import { useMessages } from '@/components/hooks';
+import { Edit } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { PixelEditForm } from './PixelEditForm';
+
+export function PixelEditButton({ pixelId }: { pixelId: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ }
+ title={formatMessage(labels.addPixel)}
+ variant="quiet"
+ width="600px"
+ >
+ {({ close }) => {
+ return ;
+ }}
+
+ );
+}
diff --git a/src/app/(main)/pixels/PixelEditForm.tsx b/src/app/(main)/pixels/PixelEditForm.tsx
new file mode 100644
index 00000000..aedd3a3b
--- /dev/null
+++ b/src/app/(main)/pixels/PixelEditForm.tsx
@@ -0,0 +1,129 @@
+import {
+ Button,
+ Column,
+ Form,
+ FormField,
+ FormSubmitButton,
+ Icon,
+ Label,
+ Loading,
+ Row,
+ TextField,
+} from '@umami/react-zen';
+import { useEffect, useState } from 'react';
+import { useConfig, useMessages, usePixelQuery } from '@/components/hooks';
+import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
+import { RefreshCw } from '@/components/icons';
+import { PIXELS_URL } from '@/lib/constants';
+import { getRandomChars } from '@/lib/generate';
+
+const generateId = () => getRandomChars(9);
+
+export function PixelEditForm({
+ pixelId,
+ teamId,
+ onSave,
+ onClose,
+}: {
+ pixelId?: string;
+ teamId?: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { formatMessage, labels, messages, getErrorMessage } = useMessages();
+ const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(
+ pixelId ? `/pixels/${pixelId}` : '/pixels',
+ {
+ id: pixelId,
+ teamId,
+ },
+ );
+ const { pixelsUrl } = useConfig();
+ const hostUrl = pixelsUrl || PIXELS_URL;
+ const { data, isLoading } = usePixelQuery(pixelId);
+ const [slug, setSlug] = useState(generateId());
+
+ const handleSubmit = async (data: any) => {
+ await mutateAsync(data, {
+ onSuccess: async () => {
+ toast(formatMessage(messages.saved));
+ touch('pixels');
+ onSave?.();
+ onClose?.();
+ },
+ });
+ };
+
+ const handleSlug = () => {
+ const slug = generateId();
+
+ setSlug(slug);
+
+ return slug;
+ };
+
+ useEffect(() => {
+ if (data) {
+ setSlug(data.slug);
+ }
+ }, [data]);
+
+ if (pixelId && isLoading) {
+ return ;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/pixels/PixelProvider.tsx b/src/app/(main)/pixels/PixelProvider.tsx
new file mode 100644
index 00000000..9e929d8c
--- /dev/null
+++ b/src/app/(main)/pixels/PixelProvider.tsx
@@ -0,0 +1,21 @@
+'use client';
+import { Loading } from '@umami/react-zen';
+import { createContext, type ReactNode } from 'react';
+import { usePixelQuery } from '@/components/hooks/queries/usePixelQuery';
+import type { Pixel } from '@/generated/prisma/client';
+
+export const PixelContext = createContext(null);
+
+export function PixelProvider({ pixelId, children }: { pixelId?: string; children: ReactNode }) {
+ const { data: pixel, isLoading, isFetching } = usePixelQuery(pixelId);
+
+ if (isFetching && isLoading) {
+ return ;
+ }
+
+ if (!pixel) {
+ return null;
+ }
+
+ return {children} ;
+}
diff --git a/src/app/(main)/pixels/PixelsDataTable.tsx b/src/app/(main)/pixels/PixelsDataTable.tsx
new file mode 100644
index 00000000..51b8c5a0
--- /dev/null
+++ b/src/app/(main)/pixels/PixelsDataTable.tsx
@@ -0,0 +1,14 @@
+import { DataGrid } from '@/components/common/DataGrid';
+import { useNavigation, usePixelsQuery } from '@/components/hooks';
+import { PixelsTable } from './PixelsTable';
+
+export function PixelsDataTable() {
+ const { teamId } = useNavigation();
+ const query = usePixelsQuery({ teamId });
+
+ return (
+
+ {({ data }) => }
+
+ );
+}
diff --git a/src/app/(main)/pixels/PixelsPage.tsx b/src/app/(main)/pixels/PixelsPage.tsx
new file mode 100644
index 00000000..4f6acefe
--- /dev/null
+++ b/src/app/(main)/pixels/PixelsPage.tsx
@@ -0,0 +1,26 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { PageBody } from '@/components/common/PageBody';
+import { PageHeader } from '@/components/common/PageHeader';
+import { Panel } from '@/components/common/Panel';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { PixelAddButton } from './PixelAddButton';
+import { PixelsDataTable } from './PixelsDataTable';
+
+export function PixelsPage() {
+ const { formatMessage, labels } = useMessages();
+ const { teamId } = useNavigation();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/pixels/PixelsTable.tsx b/src/app/(main)/pixels/PixelsTable.tsx
new file mode 100644
index 00000000..48a84589
--- /dev/null
+++ b/src/app/(main)/pixels/PixelsTable.tsx
@@ -0,0 +1,48 @@
+import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
+import Link from 'next/link';
+import { DateDistance } from '@/components/common/DateDistance';
+import { ExternalLink } from '@/components/common/ExternalLink';
+import { useMessages, useNavigation, useSlug } from '@/components/hooks';
+import { PixelDeleteButton } from './PixelDeleteButton';
+import { PixelEditButton } from './PixelEditButton';
+
+export function PixelsTable(props: DataTableProps) {
+ const { formatMessage, labels } = useMessages();
+ const { renderUrl } = useNavigation();
+ const { getSlugUrl } = useSlug('pixel');
+
+ return (
+
+
+ {({ id, name }: any) => {
+ return {name};
+ }}
+
+
+ {({ slug }: any) => {
+ const url = getSlugUrl(slug);
+ return (
+
+ {url}
+
+ );
+ }}
+
+
+ {(row: any) => }
+
+
+ {(row: any) => {
+ const { id, name } = row;
+
+ return (
+
+
+
+
+ );
+ }}
+
+
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelControls.tsx b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx
new file mode 100644
index 00000000..55dcd576
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelControls.tsx
@@ -0,0 +1,32 @@
+import { Column, Row } from '@umami/react-zen';
+import { ExportButton } from '@/components/input/ExportButton';
+import { FilterBar } from '@/components/input/FilterBar';
+import { MonthFilter } from '@/components/input/MonthFilter';
+import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
+import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
+
+export function PixelControls({
+ pixelId: websiteId,
+ allowFilter = true,
+ allowDateFilter = true,
+ allowMonthFilter,
+ allowDownload = false,
+}: {
+ pixelId: string;
+ allowFilter?: boolean;
+ allowDateFilter?: boolean;
+ allowMonthFilter?: boolean;
+ allowDownload?: boolean;
+}) {
+ return (
+
+
+ {allowFilter ? :
}
+ {allowDateFilter && }
+ {allowDownload && }
+ {allowMonthFilter && }
+
+ {allowFilter && }
+
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx
new file mode 100644
index 00000000..c771687f
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx
@@ -0,0 +1,19 @@
+import { IconLabel } from '@umami/react-zen';
+import { LinkButton } from '@/components/common/LinkButton';
+import { PageHeader } from '@/components/common/PageHeader';
+import { useMessages, usePixel, useSlug } from '@/components/hooks';
+import { ExternalLink, Grid2x2 } from '@/components/icons';
+
+export function PixelHeader() {
+ const { formatMessage, labels } = useMessages();
+ const { getSlugUrl } = useSlug('pixel');
+ const pixel = usePixel();
+
+ return (
+ }>
+
+ } label={formatMessage(labels.view)} />
+
+
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx b/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx
new file mode 100644
index 00000000..c9dcd357
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx
@@ -0,0 +1,70 @@
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
+import { formatLongNumber } from '@/lib/format';
+
+export function PixelMetricsBar({
+ pixelId,
+}: {
+ pixelId: string;
+ showChange?: boolean;
+ compareMode?: boolean;
+}) {
+ const { isAllTime } = useDateRange();
+ const { formatMessage, labels } = useMessages();
+ const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId);
+
+ const { pageviews, visitors, visits, comparison } = data || {};
+
+ const metrics = data
+ ? [
+ {
+ value: visitors,
+ label: formatMessage(labels.visitors),
+ change: visitors - comparison.visitors,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: visits,
+ label: formatMessage(labels.visits),
+ change: visits - comparison.visits,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: pageviews,
+ label: formatMessage(labels.views),
+ change: pageviews - comparison.pageviews,
+ formatValue: formatLongNumber,
+ },
+ ]
+ : null;
+
+ return (
+
+
+ {metrics?.map(({ label, value, prev, change, formatValue, reverseColors }: any) => {
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx
new file mode 100644
index 00000000..7a4ae9d7
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx
@@ -0,0 +1,34 @@
+'use client';
+import { Column, Grid } from '@umami/react-zen';
+import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls';
+import { PixelHeader } from '@/app/(main)/pixels/[pixelId]/PixelHeader';
+import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar';
+import { PixelPanels } from '@/app/(main)/pixels/[pixelId]/PixelPanels';
+import { PixelProvider } from '@/app/(main)/pixels/PixelProvider';
+import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
+import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
+import { PageBody } from '@/components/common/PageBody';
+import { Panel } from '@/components/common/Panel';
+
+const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
+
+export function PixelPage({ pixelId }: { pixelId: string }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx b/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx
new file mode 100644
index 00000000..9cc24c92
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelPanels.tsx
@@ -0,0 +1,83 @@
+import { Grid, Heading, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
+import { GridRow } from '@/components/common/GridRow';
+import { Panel } from '@/components/common/Panel';
+import { useMessages } from '@/components/hooks';
+import { MetricsTable } from '@/components/metrics/MetricsTable';
+import { WorldMap } from '@/components/metrics/WorldMap';
+
+export function PixelPanels({ pixelId }: { pixelId: string }) {
+ const { formatMessage, labels } = useMessages();
+ const tableProps = {
+ websiteId: pixelId,
+ limit: 10,
+ allowDownload: false,
+ showMore: true,
+ metric: formatMessage(labels.visitors),
+ };
+ const rowProps = { minHeight: 570 };
+
+ return (
+
+
+
+ {formatMessage(labels.sources)}
+
+
+ {formatMessage(labels.referrers)}
+ {formatMessage(labels.channels)}
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.environment)}
+
+
+ {formatMessage(labels.browsers)}
+ {formatMessage(labels.os)}
+ {formatMessage(labels.devices)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.location)}
+
+
+ {formatMessage(labels.countries)}
+ {formatMessage(labels.regions)}
+ {formatMessage(labels.cities)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/page.tsx b/src/app/(main)/pixels/[pixelId]/page.tsx
new file mode 100644
index 00000000..d1db92f3
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { PixelPage } from './PixelPage';
+
+export default async function ({ params }: { params: { pixelId: string } }) {
+ const { pixelId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Pixel',
+};
diff --git a/src/app/(main)/pixels/page.tsx b/src/app/(main)/pixels/page.tsx
new file mode 100644
index 00000000..cc240cd2
--- /dev/null
+++ b/src/app/(main)/pixels/page.tsx
@@ -0,0 +1,10 @@
+import type { Metadata } from 'next';
+import { PixelsPage } from './PixelsPage';
+
+export default function () {
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Pixels',
+};
diff --git a/src/app/(main)/profile/DateRangeSetting.module.css b/src/app/(main)/profile/DateRangeSetting.module.css
deleted file mode 100644
index 9de13efe..00000000
--- a/src/app/(main)/profile/DateRangeSetting.module.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.field {
- width: 200px;
-}
diff --git a/src/app/(main)/profile/DateRangeSetting.tsx b/src/app/(main)/profile/DateRangeSetting.tsx
deleted file mode 100644
index 37d2ca43..00000000
--- a/src/app/(main)/profile/DateRangeSetting.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import DateFilter from '@/components/input/DateFilter';
-import { Button, Flexbox } from 'react-basics';
-import { useDateRange, useMessages } from '@/components/hooks';
-import { DEFAULT_DATE_RANGE } from '@/lib/constants';
-import { DateRange } from '@/lib/types';
-import styles from './DateRangeSetting.module.css';
-
-export function DateRangeSetting() {
- const { formatMessage, labels } = useMessages();
- const { dateRange, saveDateRange } = useDateRange();
- const { value } = dateRange;
-
- const handleChange = (value: string | DateRange) => saveDateRange(value);
- const handleReset = () => saveDateRange(DEFAULT_DATE_RANGE);
-
- return (
-
-
- {formatMessage(labels.reset)}
-
- );
-}
-
-export default DateRangeSetting;
diff --git a/src/app/(main)/profile/LanguageSetting.module.css b/src/app/(main)/profile/LanguageSetting.module.css
deleted file mode 100644
index 141445ec..00000000
--- a/src/app/(main)/profile/LanguageSetting.module.css
+++ /dev/null
@@ -1,4 +0,0 @@
-div.menu {
- max-height: 300px;
- width: 300px;
-}
diff --git a/src/app/(main)/profile/PasswordChangeButton.tsx b/src/app/(main)/profile/PasswordChangeButton.tsx
deleted file mode 100644
index 63249a2b..00000000
--- a/src/app/(main)/profile/PasswordChangeButton.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics';
-import PasswordEditForm from '@/app/(main)/profile/PasswordEditForm';
-import Icons from '@/components/icons';
-import { useMessages } from '@/components/hooks';
-
-export function PasswordChangeButton() {
- const { formatMessage, labels, messages } = useMessages();
- const { showToast } = useToasts();
-
- const handleSave = () => {
- showToast({ message: formatMessage(messages.saved), variant: 'success' });
- };
-
- return (
- <>
-
-
-
-
-
- {formatMessage(labels.changePassword)}
-
-
- {close => }
-
-
- >
- );
-}
-
-export default PasswordChangeButton;
diff --git a/src/app/(main)/profile/PasswordEditForm.tsx b/src/app/(main)/profile/PasswordEditForm.tsx
deleted file mode 100644
index c352d516..00000000
--- a/src/app/(main)/profile/PasswordEditForm.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { useRef } from 'react';
-import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
-import { useApi, useMessages } from '@/components/hooks';
-
-export function PasswordEditForm({ onSave, onClose }) {
- const { formatMessage, labels, messages } = useMessages();
- const { post, useMutation } = useApi();
- const { mutate, error, isPending } = useMutation({
- mutationFn: (data: any) => post('/me/password', data),
- });
- const ref = useRef(null);
-
- const handleSubmit = async (data: any) => {
- mutate(data, {
- onSuccess: async () => {
- onSave();
- onClose();
- },
- });
- };
-
- const samePassword = (value: string) => {
- if (value !== ref?.current?.getValues('newPassword')) {
- return formatMessage(messages.noMatchPassword);
- }
- return true;
- };
-
- return (
-
- );
-}
-
-export default PasswordEditForm;
diff --git a/src/app/(main)/profile/ProfileHeader.tsx b/src/app/(main)/profile/ProfileHeader.tsx
deleted file mode 100644
index 05871fba..00000000
--- a/src/app/(main)/profile/ProfileHeader.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import PageHeader from '@/components/layout/PageHeader';
-import { useMessages } from '@/components/hooks';
-
-export function ProfileHeader() {
- const { formatMessage, labels } = useMessages();
-
- return ;
-}
-
-export default ProfileHeader;
diff --git a/src/app/(main)/profile/ProfilePage.module.css b/src/app/(main)/profile/ProfilePage.module.css
deleted file mode 100644
index 77ab4919..00000000
--- a/src/app/(main)/profile/ProfilePage.module.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.container {
- margin: 0 auto;
-}
-
-@media screen and (max-width: 768px) {
- .container {
- margin: 0;
- }
-}
diff --git a/src/app/(main)/profile/ProfilePage.tsx b/src/app/(main)/profile/ProfilePage.tsx
deleted file mode 100644
index 428ce284..00000000
--- a/src/app/(main)/profile/ProfilePage.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-'use client';
-import ProfileHeader from './ProfileHeader';
-import ProfileSettings from './ProfileSettings';
-import styles from './ProfilePage.module.css';
-
-export default function () {
- return (
-
- );
-}
diff --git a/src/app/(main)/profile/ProfileSettings.tsx b/src/app/(main)/profile/ProfileSettings.tsx
deleted file mode 100644
index f9dfe06d..00000000
--- a/src/app/(main)/profile/ProfileSettings.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { Form, FormRow } from 'react-basics';
-import TimezoneSetting from '@/app/(main)/profile/TimezoneSetting';
-import DateRangeSetting from '@/app/(main)/profile/DateRangeSetting';
-import LanguageSetting from '@/app/(main)/profile/LanguageSetting';
-import ThemeSetting from '@/app/(main)/profile/ThemeSetting';
-import PasswordChangeButton from './PasswordChangeButton';
-import { useLogin, useMessages } from '@/components/hooks';
-import { ROLES } from '@/lib/constants';
-
-export function ProfileSettings() {
- const { user } = useLogin();
- const { formatMessage, labels } = useMessages();
- const cloudMode = !!process.env.cloudMode;
-
- if (!user) {
- return null;
- }
-
- const { username, role } = user;
-
- const renderRole = (value: string) => {
- if (value === ROLES.user) {
- return formatMessage(labels.user);
- }
- if (value === ROLES.admin) {
- return formatMessage(labels.admin);
- }
- if (value === ROLES.viewOnly) {
- return formatMessage(labels.viewOnly);
- }
-
- return formatMessage(labels.unknown);
- };
-
- return (
-
- );
-}
-
-export default ProfileSettings;
diff --git a/src/app/(main)/profile/ThemeSetting.module.css b/src/app/(main)/profile/ThemeSetting.module.css
deleted file mode 100644
index a9810355..00000000
--- a/src/app/(main)/profile/ThemeSetting.module.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.buttons {
- display: flex;
- gap: 10px;
-}
-
-.active {
- border: 2px solid var(--primary400);
-}
diff --git a/src/app/(main)/profile/ThemeSetting.tsx b/src/app/(main)/profile/ThemeSetting.tsx
deleted file mode 100644
index 49ea7161..00000000
--- a/src/app/(main)/profile/ThemeSetting.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import classNames from 'classnames';
-import { Button, Icon } from 'react-basics';
-import { useTheme } from '@/components/hooks';
-import Sun from '@/assets/sun.svg';
-import Moon from '@/assets/moon.svg';
-import styles from './ThemeSetting.module.css';
-
-export function ThemeSetting() {
- const { theme, saveTheme } = useTheme();
-
- return (
-
- saveTheme('light')}
- >
-
-
-
-
- saveTheme('dark')}
- >
-
-
-
-
-
- );
-}
-
-export default ThemeSetting;
diff --git a/src/app/(main)/profile/TimezoneSetting.module.css b/src/app/(main)/profile/TimezoneSetting.module.css
deleted file mode 100644
index 31601641..00000000
--- a/src/app/(main)/profile/TimezoneSetting.module.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.dropdown {
- width: 200px;
-}
-
-div.menu {
- max-height: 300px;
- width: 300px;
-}
diff --git a/src/app/(main)/reports/ReportDeleteButton.tsx b/src/app/(main)/reports/ReportDeleteButton.tsx
deleted file mode 100644
index ca096675..00000000
--- a/src/app/(main)/reports/ReportDeleteButton.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
-import { useApi, useMessages, useModified } from '@/components/hooks';
-import ConfirmationForm from '@/components/common/ConfirmationForm';
-
-export function ReportDeleteButton({
- reportId,
- reportName,
- onDelete,
-}: {
- reportId: string;
- reportName: string;
- onDelete?: () => void;
-}) {
- const { formatMessage, labels, messages } = useMessages();
- const { del, useMutation } = useApi();
- const { mutate, isPending, error } = useMutation({
- mutationFn: reportId => del(`/reports/${reportId}`),
- });
- const { touch } = useModified();
-
- const handleConfirm = (close: () => void) => {
- mutate(reportId as any, {
- onSuccess: () => {
- touch('reports');
- onDelete?.();
- close();
- },
- });
- };
-
- return (
-
-
-
-
-
- {formatMessage(labels.delete)}
-
-
- {(close: () => void) => (
- {reportName},
- })}
- isLoading={isPending}
- error={error}
- onConfirm={handleConfirm.bind(null, close)}
- onClose={close}
- buttonLabel={formatMessage(labels.delete)}
- />
- )}
-
-
- );
-}
-
-export default ReportDeleteButton;
diff --git a/src/app/(main)/reports/ReportsDataTable.tsx b/src/app/(main)/reports/ReportsDataTable.tsx
deleted file mode 100644
index 0cc5a96c..00000000
--- a/src/app/(main)/reports/ReportsDataTable.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useReports } from '@/components/hooks';
-import ReportsTable from './ReportsTable';
-import DataTable from '@/components/common/DataTable';
-import { ReactNode } from 'react';
-
-export default function ReportsDataTable({
- websiteId,
- teamId,
- children,
-}: {
- websiteId?: string;
- teamId?: string;
- children?: ReactNode;
-}) {
- const queryResult = useReports({ websiteId, teamId });
-
- return (
- children}>
- {({ data }) => }
-
- );
-}
diff --git a/src/app/(main)/reports/ReportsHeader.tsx b/src/app/(main)/reports/ReportsHeader.tsx
deleted file mode 100644
index ff9cb294..00000000
--- a/src/app/(main)/reports/ReportsHeader.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import PageHeader from '@/components/layout/PageHeader';
-import { Icon, Icons, Text } from 'react-basics';
-import { useLogin, useMessages, useTeamUrl } from '@/components/hooks';
-import LinkButton from '@/components/common/LinkButton';
-import { ROLES } from '@/lib/constants';
-
-export function ReportsHeader() {
- const { formatMessage, labels } = useMessages();
- const { renderTeamUrl } = useTeamUrl();
- const { user } = useLogin();
- const canEdit = user.role !== ROLES.viewOnly;
-
- return (
-
- {canEdit && (
-
-
-
-
- {formatMessage(labels.createReport)}
-
- )}
-
- );
-}
-
-export default ReportsHeader;
diff --git a/src/app/(main)/reports/ReportsPage.tsx b/src/app/(main)/reports/ReportsPage.tsx
deleted file mode 100644
index 64d43c70..00000000
--- a/src/app/(main)/reports/ReportsPage.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-'use client';
-import { Metadata } from 'next';
-import ReportsHeader from './ReportsHeader';
-import ReportsDataTable from './ReportsDataTable';
-import { useTeamUrl } from '@/components/hooks';
-
-export default function ReportsPage() {
- const { teamId } = useTeamUrl();
-
- return (
- <>
-
-
- >
- );
-}
-
-export const metadata: Metadata = {
- title: 'Reports',
-};
diff --git a/src/app/(main)/reports/ReportsTable.tsx b/src/app/(main)/reports/ReportsTable.tsx
deleted file mode 100644
index a891b6d0..00000000
--- a/src/app/(main)/reports/ReportsTable.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { GridColumn, GridTable, Icon, Icons, Text } from 'react-basics';
-import LinkButton from '@/components/common/LinkButton';
-import { useMessages, useLogin, useTeamUrl } from '@/components/hooks';
-import { REPORT_TYPES } from '@/lib/constants';
-import ReportDeleteButton from './ReportDeleteButton';
-
-export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) {
- const { formatMessage, labels } = useMessages();
- const { user } = useLogin();
- const { renderTeamUrl } = useTeamUrl();
-
- return (
-
-
-
-
- {row => {
- return formatMessage(
- labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)],
- );
- }}
-
- {showDomain && (
-
- {row => row?.website?.domain}
-
- )}
-
- {row => {
- const { id, name, userId, website } = row;
- return (
- <>
- {(user.id === userId || user.id === website?.userId) && (
-
- )}
-
-
-
-
- {formatMessage(labels.view)}
-
- >
- );
- }}
-
-
- );
-}
-
-export default ReportsTable;
diff --git a/src/app/(main)/reports/[reportId]/BaseParameters.module.css b/src/app/(main)/reports/[reportId]/BaseParameters.module.css
deleted file mode 100644
index 4750c7d7..00000000
--- a/src/app/(main)/reports/[reportId]/BaseParameters.module.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.dropdown div {
- max-height: 300px;
-}
diff --git a/src/app/(main)/reports/[reportId]/BaseParameters.tsx b/src/app/(main)/reports/[reportId]/BaseParameters.tsx
deleted file mode 100644
index 1f4881be..00000000
--- a/src/app/(main)/reports/[reportId]/BaseParameters.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { useContext } from 'react';
-import { FormRow } from 'react-basics';
-import { parseDateRange } from '@/lib/date';
-import DateFilter from '@/components/input/DateFilter';
-import WebsiteSelect from '@/components/input/WebsiteSelect';
-import { useMessages, useTeamUrl, useWebsite } from '@/components/hooks';
-import { ReportContext } from './Report';
-import styles from './BaseParameters.module.css';
-
-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();
- const { teamId } = useTeamUrl();
- const { parameters } = report || {};
- const { websiteId, dateRange } = parameters || {};
- const { value, startDate, endDate } = dateRange || {};
- const { data: website } = useWebsite(websiteId);
- const { name } = website || {};
-
- const handleWebsiteSelect = (websiteId: string) => {
- updateReport({ websiteId, parameters: { websiteId } });
- };
-
- const handleDateChange = (value: string) => {
- updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } });
- };
-
- return (
- <>
- {showWebsiteSelect && (
-
- {allowWebsiteSelect ? (
-
- ) : (
- name
- )}
-
- )}
- {showDateSelect && (
-
- {allowDateSelect && (
-
- )}
-
- )}
- >
- );
-}
-
-export default BaseParameters;
diff --git a/src/app/(main)/reports/[reportId]/FieldAddForm.tsx b/src/app/(main)/reports/[reportId]/FieldAddForm.tsx
deleted file mode 100644
index 6560a947..00000000
--- a/src/app/(main)/reports/[reportId]/FieldAddForm.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { useState } from 'react';
-import { createPortal } from 'react-dom';
-import { REPORT_PARAMETERS } from '@/lib/constants';
-import PopupForm from './PopupForm';
-import FieldSelectForm from './FieldSelectForm';
-
-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: any) => {
- const { type } = value;
-
- if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') {
- value.value = group === REPORT_PARAMETERS.groups ? '' : 'total';
- handleSave(value);
- return;
- }
-
- setSelected(value);
- };
-
- const handleSave = (value: any) => {
- onAdd(group, value);
- onClose();
- };
-
- return createPortal(
-
- {!selected && }
- ,
- document.body,
- );
-}
-
-export default FieldAddForm;
diff --git a/src/app/(main)/reports/[reportId]/FieldAggregateForm.tsx b/src/app/(main)/reports/[reportId]/FieldAggregateForm.tsx
deleted file mode 100644
index 5db0e580..00000000
--- a/src/app/(main)/reports/[reportId]/FieldAggregateForm.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Form, FormRow, Menu, Item } from 'react-basics';
-import { useMessages } from '@/components/hooks';
-
-export default function FieldAggregateForm({
- name,
- type,
- onSelect,
-}: {
- name: string;
- type: string;
- onSelect: (key: any) => void;
-}) {
- const { formatMessage, labels } = useMessages();
-
- const options = {
- number: [
- { label: formatMessage(labels.sum), value: 'sum' },
- { label: formatMessage(labels.average), value: 'average' },
- { label: formatMessage(labels.min), value: 'min' },
- { label: formatMessage(labels.max), value: 'max' },
- ],
- date: [
- { label: formatMessage(labels.min), value: 'min' },
- { label: formatMessage(labels.max), value: 'max' },
- ],
- string: [
- { label: formatMessage(labels.total), value: 'total' },
- { label: formatMessage(labels.unique), value: 'unique' },
- ],
- uuid: [
- { label: formatMessage(labels.total), value: 'total' },
- { label: formatMessage(labels.unique), value: 'unique' },
- ],
- };
-
- const items = options[type];
-
- const handleSelect = (value: any) => {
- onSelect({ name, type, value });
- };
-
- return (
-
- );
-}
diff --git a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.module.css b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.module.css
deleted file mode 100644
index 43a34438..00000000
--- a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.module.css
+++ /dev/null
@@ -1,36 +0,0 @@
-.menu {
- position: absolute;
- max-width: 300px;
- max-height: 210px;
-}
-
-.filter {
- display: flex;
- flex-direction: column;
- gap: 20px;
-}
-
-.dropdown {
- min-width: 200px;
-}
-
-.text {
- min-width: 200px;
-}
-
-.selected {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 8px 16px;
- white-space: nowrap;
- min-width: 200px;
- font-weight: 900;
- background: var(--base100);
- border-radius: var(--border-radius);
- cursor: pointer;
-}
-
-.search {
- position: relative;
-}
diff --git a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx
deleted file mode 100644
index c1f95e80..00000000
--- a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx
+++ /dev/null
@@ -1,221 +0,0 @@
-import { useMemo, useState } from 'react';
-import { useFilters, useFormat, useMessages, useWebsiteValues } from '@/components/hooks';
-import { OPERATORS } from '@/lib/constants';
-import { isEqualsOperator } from '@/lib/params';
-import {
- Button,
- Dropdown,
- Flexbox,
- Form,
- FormRow,
- Icon,
- Icons,
- Item,
- Loading,
- Menu,
- SearchField,
- Text,
- TextField,
-} from 'react-basics';
-import styles from './FieldFilterEditForm.module.css';
-
-export interface FieldFilterFormProps {
- websiteId?: string;
- name: string;
- label?: string;
- type: string;
- startDate: Date;
- endDate: Date;
- operator?: string;
- defaultValue?: string;
- onChange?: (filter: { name: string; type: string; operator: string; value: string }) => void;
- allowFilterSelect?: boolean;
- isNew?: boolean;
-}
-
-export default function FieldFilterEditForm({
- websiteId,
- name,
- label,
- type,
- startDate,
- endDate,
- operator: defaultOperator = 'eq',
- defaultValue = '',
- onChange,
- allowFilterSelect = true,
- isNew,
-}: FieldFilterFormProps) {
- const { formatMessage, labels } = useMessages();
- const [operator, setOperator] = useState(defaultOperator);
- const [value, setValue] = useState(defaultValue);
- const [showMenu, setShowMenu] = useState(false);
- const isEquals = isEqualsOperator(operator);
- const [search, setSearch] = useState('');
- const [selected, setSelected] = useState(isEquals ? value : '');
- const { filters } = useFilters();
- const { formatValue } = useFormat();
- const isDisabled = !operator || (isEquals && !selected) || (!isEquals && !value);
- const {
- data: values = [],
- isLoading,
- refetch,
- } = useWebsiteValues({
- websiteId,
- type: name,
- startDate,
- endDate,
- search,
- });
-
- const filterDropdownItems = (name: string) => {
- const limitedFilters = ['country', 'region', 'city'];
-
- if (limitedFilters.includes(name)) {
- return filters.filter(f => f.type === type && !f.label.match(/contain/gi));
- } else {
- return filters.filter(f => f.type === type);
- }
- };
-
- const formattedValues = useMemo(() => {
- return values.reduce((obj: { [x: string]: string }, { value }: { value: string }) => {
- obj[value] = formatValue(value, name);
-
- return obj;
- }, {});
- }, [formatValue, name, values]);
-
- const filteredValues = useMemo(() => {
- return value
- ? values.filter((n: string | number) =>
- formattedValues[n]?.toLowerCase()?.includes(value.toLowerCase()),
- )
- : values;
- }, [value, formattedValues]);
-
- const renderFilterValue = (value: any) => {
- return filters.find((filter: { value: any }) => filter.value === value)?.label;
- };
-
- const handleAdd = () => {
- onChange({ name, type, operator, value: isEquals ? selected : value });
- };
-
- const handleMenuSelect = (value: string) => {
- setSelected(value);
- setShowMenu(false);
- };
-
- const handleSearch = (value: string) => {
- setSearch(value);
- };
-
- const handleReset = () => {
- setSelected('');
- setValue('');
- setSearch('');
- refetch();
- };
-
- const handleOperatorChange = (value: any) => {
- setOperator(value);
-
- if ([OPERATORS.equals, OPERATORS.notEquals].includes(value)) {
- setValue('');
- } else {
- setSelected('');
- }
- };
-
- const handleBlur = () => {
- window.setTimeout(() => setShowMenu(false), 500);
- };
-
- return (
-
- );
-}
-
-const ResultsMenu = ({ values, type, isLoading, onSelect }) => {
- const { formatValue } = useFormat();
- if (isLoading) {
- return (
-
- -
-
-
-
- );
- }
-
- if (!values?.length) {
- return null;
- }
-
- return (
-
- {values?.map(({ value }) => {
- return - {formatValue(value, type)}
;
- })}
-
- );
-};
diff --git a/src/app/(main)/reports/[reportId]/FieldParameters.tsx b/src/app/(main)/reports/[reportId]/FieldParameters.tsx
deleted file mode 100644
index de80cc69..00000000
--- a/src/app/(main)/reports/[reportId]/FieldParameters.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { useFields, useMessages } from '@/components/hooks';
-import Icons from '@/components/icons';
-import { useContext } from 'react';
-import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics';
-import FieldSelectForm from '../[reportId]/FieldSelectForm';
-import ParameterList from '../[reportId]/ParameterList';
-import PopupForm from '../[reportId]/PopupForm';
-import { ReportContext } from './Report';
-
-export function FieldParameters() {
- const { report, updateReport } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
- const { parameters } = report || {};
- const { fields } = parameters || {};
- const { fields: fieldOptions } = useFields();
-
- const handleAdd = (value: { name: any }) => {
- if (!fields.find(({ name }) => name === value.name)) {
- updateReport({ parameters: { fields: fields.concat(value) } });
- }
- };
-
- const handleRemove = (name: string) => {
- updateReport({ parameters: { fields: fields.filter(f => f.name !== name) } });
- };
-
- const AddButton = () => {
- return (
-
-
-
-
-
-
-
-
- !fields.find(f => f.name === name))}
- onSelect={handleAdd}
- showType={false}
- />
-
-
-
- );
- };
-
- return (
- }>
-
- {fields.map(({ name }) => {
- return (
- handleRemove(name)}>
- {fieldOptions.find(f => f.name === name)?.label}
-
- );
- })}
-
-
- );
-}
-
-export default FieldParameters;
diff --git a/src/app/(main)/reports/[reportId]/FieldSelectForm.module.css b/src/app/(main)/reports/[reportId]/FieldSelectForm.module.css
deleted file mode 100644
index 3a5ed9b8..00000000
--- a/src/app/(main)/reports/[reportId]/FieldSelectForm.module.css
+++ /dev/null
@@ -1,20 +0,0 @@
-.menu {
- width: 360px;
- max-height: 300px;
- overflow: auto;
-}
-
-.item {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- border-radius: var(--border-radius);
-}
-
-.item:hover {
- background: var(--base75);
-}
-
-.type {
- color: var(--font-color300);
-}
diff --git a/src/app/(main)/reports/[reportId]/FieldSelectForm.tsx b/src/app/(main)/reports/[reportId]/FieldSelectForm.tsx
deleted file mode 100644
index f73d59f7..00000000
--- a/src/app/(main)/reports/[reportId]/FieldSelectForm.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Menu, Item, Form, FormRow } from 'react-basics';
-import { useMessages } from '@/components/hooks';
-import styles from './FieldSelectForm.module.css';
-import { Key } from 'react';
-
-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 (
-
- );
-}
diff --git a/src/app/(main)/reports/[reportId]/FilterParameters.module.css b/src/app/(main)/reports/[reportId]/FilterParameters.module.css
deleted file mode 100644
index 939d0652..00000000
--- a/src/app/(main)/reports/[reportId]/FilterParameters.module.css
+++ /dev/null
@@ -1,40 +0,0 @@
-.item {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 10px;
- overflow: hidden;
-}
-
-.label {
- color: var(--base800);
- border: 1px solid var(--base300);
- font-weight: 900;
- padding: 2px 8px;
- border-radius: 5px;
- white-space: nowrap;
-}
-
-.op {
- color: var(--blue900);
- background-color: var(--blue100);
- font-size: 12px;
- font-weight: 900;
- padding: 2px 8px;
- border-radius: 5px;
- text-transform: uppercase;
- white-space: nowrap;
-}
-
-.value {
- color: var(--base900);
- background-color: var(--base100);
- font-weight: 900;
- padding: 2px 8px;
- border-radius: 5px;
- white-space: nowrap;
-}
-
-.edit {
- margin-top: 20px;
-}
diff --git a/src/app/(main)/reports/[reportId]/FilterParameters.tsx b/src/app/(main)/reports/[reportId]/FilterParameters.tsx
deleted file mode 100644
index 538c4ce5..00000000
--- a/src/app/(main)/reports/[reportId]/FilterParameters.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import { useContext } from 'react';
-import { useMessages, useFormat, useFilters, useFields } from '@/components/hooks';
-import Icons from '@/components/icons';
-import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics';
-import FilterSelectForm from '../[reportId]/FilterSelectForm';
-import ParameterList from '../[reportId]/ParameterList';
-import PopupForm from '../[reportId]/PopupForm';
-import { ReportContext } from './Report';
-import FieldFilterEditForm from '../[reportId]/FieldFilterEditForm';
-import { isSearchOperator } from '@/lib/params';
-import styles from './FilterParameters.module.css';
-
-export function FilterParameters() {
- const { report, updateReport } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
- const { formatValue } = useFormat();
- const { parameters } = report || {};
- const { websiteId, filters, dateRange } = parameters || {};
- const { fields } = useFields();
-
- const handleAdd = (value: { name: any }) => {
- if (!filters.find(({ name }) => name === value.name)) {
- updateReport({ parameters: { filters: filters.concat(value) } });
- }
- };
-
- const handleRemove = (name: string) => {
- updateReport({ parameters: { filters: filters.filter(f => f.name !== name) } });
- };
-
- const handleChange = (close: () => void, filter: { name: any }) => {
- updateReport({
- parameters: {
- filters: filters.map(f => {
- if (filter.name === f.name) {
- return filter;
- }
- return f;
- }),
- },
- });
- close();
- };
-
- const AddButton = () => {
- return (
-
-
-
-
-
-
-
-
- !filters.find(f => f.name === name))}
- startDate={dateRange?.startDate}
- endDate={dateRange?.endDate}
- onChange={handleAdd}
- />
-
-
-
- );
- };
-
- return (
- }>
-
- {filters.map(
- ({ name, operator, value }: { name: string; operator: string; value: string }) => {
- const label = fields.find(f => f.name === name)?.label;
- const isSearch = isSearchOperator(operator);
-
- return (
- handleRemove(name)}>
-
-
- );
- },
- )}
-
-
- );
-}
-
-const FilterParameter = ({
- websiteId,
- name,
- label,
- operator,
- value,
- type = 'string',
- startDate,
- endDate,
- onChange,
-}) => {
- const { operatorLabels } = useFilters();
-
- return (
-
-
-
{label}
-
{operatorLabels[operator]}
-
{value}
-
-
- {(close: any) => (
-
-
-
- )}
-
-
- );
-};
-
-export default FilterParameters;
diff --git a/src/app/(main)/reports/[reportId]/FilterSelectForm.tsx b/src/app/(main)/reports/[reportId]/FilterSelectForm.tsx
deleted file mode 100644
index 77a36c3c..00000000
--- a/src/app/(main)/reports/[reportId]/FilterSelectForm.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { useState } from 'react';
-import FieldSelectForm from './FieldSelectForm';
-import FieldFilterEditForm from './FieldFilterEditForm';
-
-export interface FilterSelectFormProps {
- websiteId?: string;
- fields: any[];
- startDate?: Date;
- endDate?: Date;
- onChange?: (filter: { name: string; type: string; operator: string; value: string }) => void;
- allowFilterSelect?: boolean;
-}
-
-export default function FilterSelectForm({
- websiteId,
- fields,
- startDate,
- endDate,
- onChange,
- allowFilterSelect,
-}: FilterSelectFormProps) {
- const [field, setField] = useState<{ name: string; label: string; type: string }>();
-
- if (!field) {
- return ;
- }
-
- const { name, label, type } = field;
-
- return (
-
- );
-}
diff --git a/src/app/(main)/reports/[reportId]/ParameterList.module.css b/src/app/(main)/reports/[reportId]/ParameterList.module.css
deleted file mode 100644
index 75860b25..00000000
--- a/src/app/(main)/reports/[reportId]/ParameterList.module.css
+++ /dev/null
@@ -1,29 +0,0 @@
-.list {
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-
-.item {
- display: flex;
- gap: 12px;
- width: 100%;
- flex-wrap: nowrap;
- padding: 12px;
- border: 1px solid var(--base400);
- border-radius: var(--border-radius);
- box-shadow: 1px 1px 1px var(--base400);
-}
-
-.value {
- display: flex;
- flex-direction: row;
- align-items: center;
- flex-wrap: wrap;
- flex: 1;
-}
-
-.icon,
-.close {
- height: 1.5rem;
-}
diff --git a/src/app/(main)/reports/[reportId]/ParameterList.tsx b/src/app/(main)/reports/[reportId]/ParameterList.tsx
deleted file mode 100644
index 3c0401a0..00000000
--- a/src/app/(main)/reports/[reportId]/ParameterList.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { ReactNode } from 'react';
-import { Icon } 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';
-import classNames from 'classnames';
-
-export interface ParameterListProps {
- children?: ReactNode;
-}
-
-export function ParameterList({ children }: ParameterListProps) {
- const { formatMessage, labels } = useMessages();
-
- return (
-
- {!children && }
- {children}
-
- );
-}
-
-const Item = ({
- children,
- className,
- icon,
- onClick,
- onRemove,
-}: {
- children?: ReactNode;
- className?: string;
- icon?: ReactNode;
- onClick?: () => void;
- onRemove?: () => void;
-}) => {
- return (
-
- {icon &&
{icon} }
-
{children}
-
-
-
-
- );
-};
-
-ParameterList.Item = Item;
-
-export default ParameterList;
diff --git a/src/app/(main)/reports/[reportId]/PopupForm.module.css b/src/app/(main)/reports/[reportId]/PopupForm.module.css
deleted file mode 100644
index 5d069dd4..00000000
--- a/src/app/(main)/reports/[reportId]/PopupForm.module.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.form {
- background: var(--base50);
- min-width: 300px;
- padding: 20px;
- border: 1px solid var(--base400);
- border-radius: var(--border-radius);
- box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
- z-index: 1000;
-}
diff --git a/src/app/(main)/reports/[reportId]/PopupForm.tsx b/src/app/(main)/reports/[reportId]/PopupForm.tsx
deleted file mode 100644
index f2666199..00000000
--- a/src/app/(main)/reports/[reportId]/PopupForm.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { CSSProperties, ReactNode } from 'react';
-import classNames from 'classnames';
-import styles from './PopupForm.module.css';
-
-export function PopupForm({
- className,
- style,
- children,
-}: {
- className?: string;
- style?: CSSProperties;
- children: ReactNode;
-}) {
- return (
- e.stopPropagation()}
- >
- {children}
-
- );
-}
-
-export default PopupForm;
diff --git a/src/app/(main)/reports/[reportId]/Report.module.css b/src/app/(main)/reports/[reportId]/Report.module.css
deleted file mode 100644
index 6aa6a9b3..00000000
--- a/src/app/(main)/reports/[reportId]/Report.module.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.container {
- display: grid;
- grid-template-rows: max-content 1fr;
- grid-template-columns: max-content 1fr;
- margin-bottom: 60px;
- height: 90vh;
-}
diff --git a/src/app/(main)/reports/[reportId]/Report.tsx b/src/app/(main)/reports/[reportId]/Report.tsx
deleted file mode 100644
index 1aed007c..00000000
--- a/src/app/(main)/reports/[reportId]/Report.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { createContext, ReactNode } from 'react';
-import { Loading } from 'react-basics';
-import classNames from 'classnames';
-import { useReport } from '@/components/hooks';
-import styles from './Report.module.css';
-
-export const ReportContext = createContext(null);
-
-export function Report({
- reportId,
- defaultParameters,
- children,
- className,
-}: {
- reportId: string;
- defaultParameters: { type: string; parameters: { [key: string]: any } };
- children: ReactNode;
- className?: string;
-}) {
- const report = useReport(reportId, defaultParameters);
-
- if (!report) {
- return reportId ? : null;
- }
-
- return (
-
- {children}
-
- );
-}
-
-export default Report;
diff --git a/src/app/(main)/reports/[reportId]/ReportBody.module.css b/src/app/(main)/reports/[reportId]/ReportBody.module.css
deleted file mode 100644
index 9af1070a..00000000
--- a/src/app/(main)/reports/[reportId]/ReportBody.module.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.body {
- padding-inline-start: 20px;
- grid-row: 2 / 3;
- grid-column: 2 / 3;
-}
diff --git a/src/app/(main)/reports/[reportId]/ReportBody.tsx b/src/app/(main)/reports/[reportId]/ReportBody.tsx
deleted file mode 100644
index 9a740c5e..00000000
--- a/src/app/(main)/reports/[reportId]/ReportBody.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { useContext } from 'react';
-import { ReportContext } from './Report';
-import styles from './ReportBody.module.css';
-
-export function ReportBody({ children }) {
- const { report } = useContext(ReportContext);
-
- if (!report) {
- return null;
- }
-
- return {children}
;
-}
-
-export default ReportBody;
diff --git a/src/app/(main)/reports/[reportId]/ReportHeader.module.css b/src/app/(main)/reports/[reportId]/ReportHeader.module.css
deleted file mode 100644
index 5ff26104..00000000
--- a/src/app/(main)/reports/[reportId]/ReportHeader.module.css
+++ /dev/null
@@ -1,36 +0,0 @@
-.header {
- display: grid;
- grid-template-columns: 1fr min-content;
- align-items: center;
- grid-row: 1 / 2;
- grid-column: 1 / 3;
- margin: 20px 0 40px 0;
-}
-
-.title {
- display: flex;
- flex-direction: row;
- align-items: center;
- font-size: 24px;
- font-weight: 700;
- gap: 20px;
- height: 60px;
-}
-
-.type {
- font-size: 11px;
- font-weight: 700;
- text-transform: uppercase;
- color: var(--base600);
-}
-
-.description {
- color: var(--font-color300);
- max-width: 500px;
- height: 30px;
-}
-
-.actions {
- display: flex;
- align-items: center;
-}
diff --git a/src/app/(main)/reports/[reportId]/ReportHeader.tsx b/src/app/(main)/reports/[reportId]/ReportHeader.tsx
deleted file mode 100644
index 816a2df3..00000000
--- a/src/app/(main)/reports/[reportId]/ReportHeader.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import { useContext } from 'react';
-import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics';
-import { useMessages, useApi, useNavigation, useTeamUrl } from '@/components/hooks';
-import { ReportContext } from './Report';
-import styles from './ReportHeader.module.css';
-import { REPORT_TYPES } from '@/lib/constants';
-import Breadcrumb from '@/components/common/Breadcrumb';
-
-export function ReportHeader({ icon }) {
- const { report, updateReport } = useContext(ReportContext);
- const { formatMessage, labels, messages } = useMessages();
- const { showToast } = useToasts();
- const { router } = useNavigation();
- const { renderTeamUrl } = useTeamUrl();
-
- const { post, useMutation } = useApi();
- 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 || {};
- const defaultName = formatMessage(labels.untitled);
-
- const handleSave = async () => {
- if (!report.id) {
- create(report, {
- onSuccess: async ({ id }) => {
- showToast({ message: formatMessage(messages.saved), variant: 'success' });
- router.push(renderTeamUrl(`/reports/${id}`));
- },
- });
- } else {
- update(report, {
- onSuccess: async () => {
- showToast({ message: formatMessage(messages.saved), variant: 'success' });
- },
- });
- }
- };
-
- const handleNameChange = (name: string) => {
- updateReport({ name: name || defaultName });
- };
-
- const handleDescriptionChange = (description: string) => {
- updateReport({ description });
- };
-
- if (!report) {
- return null;
- }
-
- return (
-
-
-
- REPORT_TYPES[key] === report?.type)],
- ),
- },
- ]}
- />
-
-
- {icon}
-
-
-
-
-
-
-
-
- {formatMessage(labels.save)}
-
-
-
- );
-}
-
-export default ReportHeader;
diff --git a/src/app/(main)/reports/[reportId]/ReportMenu.module.css b/src/app/(main)/reports/[reportId]/ReportMenu.module.css
deleted file mode 100644
index 21368411..00000000
--- a/src/app/(main)/reports/[reportId]/ReportMenu.module.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.menu {
- position: relative;
- width: 300px;
- padding-top: 20px;
- padding-inline-end: 20px;
- border-inline-end: 1px solid var(--base300);
- grid-row: 2 / 3;
- grid-column: 1 / 2;
-}
-
-.button {
- position: absolute;
- top: 0;
- right: 0;
- display: flex;
- place-content: center;
- border: 1px solid var(--base400);
- border-right: 0;
- width: 30px;
- padding: 5px;
- cursor: pointer;
- border-radius: 4px 0 0 4px;
- z-index: 1;
-}
-
-.button:hover {
- background: var(--base75);
-}
-
-.menu.collapsed {
- width: 0;
- padding: 0;
-}
-
-.menu.collapsed .button {
- right: 0;
- border-radius: 4px 0 0 4px;
-}
diff --git a/src/app/(main)/reports/[reportId]/ReportMenu.tsx b/src/app/(main)/reports/[reportId]/ReportMenu.tsx
deleted file mode 100644
index 5cca5640..00000000
--- a/src/app/(main)/reports/[reportId]/ReportMenu.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useContext, useState } from 'react';
-import { ReportContext } from './Report';
-import styles from './ReportMenu.module.css';
-import { Icon, Icons } from 'react-basics';
-import classNames from 'classnames';
-
-export function ReportMenu({ children }) {
- const [collapsed, setCollapsed] = useState(false);
- const { report } = useContext(ReportContext);
-
- if (!report) {
- return null;
- }
-
- return (
-
-
setCollapsed(!collapsed)}>
-
-
-
-
- {!collapsed && children}
-
- );
-}
-
-export default ReportMenu;
diff --git a/src/app/(main)/reports/[reportId]/ReportPage.tsx b/src/app/(main)/reports/[reportId]/ReportPage.tsx
deleted file mode 100644
index 5e215cd2..00000000
--- a/src/app/(main)/reports/[reportId]/ReportPage.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-'use client';
-import { useReport } from '@/components/hooks';
-import EventDataReport from '../event-data/EventDataReport';
-import FunnelReport from '../funnel/FunnelReport';
-import GoalReport from '../goals/GoalsReport';
-import InsightsReport from '../insights/InsightsReport';
-import JourneyReport from '../journey/JourneyReport';
-import RetentionReport from '../retention/RetentionReport';
-import RevenueReport from '../revenue/RevenueReport';
-import UTMReport from '../utm/UTMReport';
-import AttributionReport from '../attribution/AttributionReport';
-
-const reports = {
- funnel: FunnelReport,
- 'event-data': EventDataReport,
- insights: InsightsReport,
- retention: RetentionReport,
- utm: UTMReport,
- goals: GoalReport,
- journey: JourneyReport,
- revenue: RevenueReport,
- attribution: AttributionReport,
-};
-
-export default function ReportPage({ reportId }: { reportId: string }) {
- const { report } = useReport(reportId);
-
- if (!report) {
- return null;
- }
-
- const ReportComponent = reports[report.type];
-
- return ;
-}
diff --git a/src/app/(main)/reports/[reportId]/page.tsx b/src/app/(main)/reports/[reportId]/page.tsx
deleted file mode 100644
index 85a97d1c..00000000
--- a/src/app/(main)/reports/[reportId]/page.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Metadata } from 'next';
-import ReportPage from './ReportPage';
-
-export default async function ({ params }: { params: { reportId: string } }) {
- const { reportId } = await params;
-
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Reports',
-};
diff --git a/src/app/(main)/reports/attribution/AttributionParameters.module.css b/src/app/(main)/reports/attribution/AttributionParameters.module.css
deleted file mode 100644
index 0f27d515..00000000
--- a/src/app/(main)/reports/attribution/AttributionParameters.module.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.item {
- display: flex;
- align-items: center;
- gap: 10px;
- width: 100%;
-}
-
-.value {
- display: flex;
- align-self: center;
- gap: 20px;
-}
diff --git a/src/app/(main)/reports/attribution/AttributionParameters.tsx b/src/app/(main)/reports/attribution/AttributionParameters.tsx
deleted file mode 100644
index 2763273d..00000000
--- a/src/app/(main)/reports/attribution/AttributionParameters.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-import { useMessages } from '@/components/hooks';
-import Icons from '@/components/icons';
-import { useContext, useState } from 'react';
-import {
- Button,
- Dropdown,
- Form,
- FormButtons,
- FormInput,
- FormRow,
- Icon,
- Item,
- Popup,
- PopupTrigger,
- SubmitButton,
- Toggle,
-} from 'react-basics';
-import BaseParameters from '../[reportId]/BaseParameters';
-import ParameterList from '../[reportId]/ParameterList';
-import PopupForm from '../[reportId]/PopupForm';
-import { ReportContext } from '../[reportId]/Report';
-import FunnelStepAddForm from '../funnel/FunnelStepAddForm';
-import styles from './AttributionParameters.module.css';
-import AttributionStepAddForm from './AttributionStepAddForm';
-import useRevenueValues from '@/components/hooks/queries/useRevenueValues';
-
-export function AttributionParameters() {
- const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
- const { id, parameters } = report || {};
- const { websiteId, dateRange, steps } = parameters || {};
- const queryEnabled = websiteId && dateRange && steps.length > 0;
- const [model, setModel] = useState('');
- const [revenueMode, setRevenueMode] = useState(false);
-
- const { data: currencyValues = [] } = useRevenueValues(
- websiteId,
- dateRange?.startDate,
- dateRange?.endDate,
- );
-
- const handleSubmit = (data: any, e: any) => {
- if (revenueMode === false) {
- delete data.currency;
- }
-
- e.stopPropagation();
- e.preventDefault();
- runReport(data);
- };
-
- const handleCheck = () => {
- setRevenueMode(!revenueMode);
- };
-
- const handleAddStep = (step: { type: string; value: string }) => {
- if (step.type === 'url') {
- setRevenueMode(false);
- }
- updateReport({ parameters: { steps: parameters.steps.concat(step) } });
- };
-
- const handleUpdateStep = (
- close: () => void,
- index: number,
- step: { type: string; value: string },
- ) => {
- if (step.type === 'url') {
- setRevenueMode(false);
- }
- const steps = [...parameters.steps];
- steps[index] = step;
- updateReport({ parameters: { steps } });
- close();
- };
-
- const handleRemoveStep = (index: number) => {
- const steps = [...parameters.steps];
- delete steps[index];
- updateReport({ parameters: { steps: steps.filter(n => n) } });
- };
-
- const AddStepButton = () => {
- return (
- 0}>
- 0}>
-
-
-
-
-
-
-
-
-
-
- );
- };
-
- const items = [
- { label: 'First-Click', value: 'firstClick' },
- { label: 'Last-Click', value: 'lastClick' },
- ];
-
- const renderModelValue = (value: any) => {
- return items.find(item => item.value === value)?.label;
- };
-
- const onModelChange = (value: any) => {
- setModel(value);
- updateReport({ parameters: { model } });
- };
-
- return (
-
- );
-}
-
-export default AttributionParameters;
diff --git a/src/app/(main)/reports/attribution/AttributionReport.tsx b/src/app/(main)/reports/attribution/AttributionReport.tsx
deleted file mode 100644
index c33a4195..00000000
--- a/src/app/(main)/reports/attribution/AttributionReport.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import Money from '@/assets/money.svg';
-import { REPORT_TYPES } from '@/lib/constants';
-import Report from '../[reportId]/Report';
-import ReportBody from '../[reportId]/ReportBody';
-import ReportHeader from '../[reportId]/ReportHeader';
-import ReportMenu from '../[reportId]/ReportMenu';
-import AttributionParameters from './AttributionParameters';
-import AttributionView from './AttributionView';
-
-const defaultParameters = {
- type: REPORT_TYPES.attribution,
- parameters: { model: 'firstClick', steps: [] },
-};
-
-export default function AttributionReport({ reportId }: { reportId?: string }) {
- return (
-
- } />
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/reports/attribution/AttributionReportPage.tsx b/src/app/(main)/reports/attribution/AttributionReportPage.tsx
deleted file mode 100644
index ed730704..00000000
--- a/src/app/(main)/reports/attribution/AttributionReportPage.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-'use client';
-import AttributionReport from './AttributionReport';
-
-export default function AttributionReportPage() {
- return ;
-}
diff --git a/src/app/(main)/reports/attribution/AttributionStepAddForm.module.css b/src/app/(main)/reports/attribution/AttributionStepAddForm.module.css
deleted file mode 100644
index a254ff08..00000000
--- a/src/app/(main)/reports/attribution/AttributionStepAddForm.module.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.dropdown {
- width: 140px;
-}
-
-.input {
- width: 200px;
-}
diff --git a/src/app/(main)/reports/attribution/AttributionStepAddForm.tsx b/src/app/(main)/reports/attribution/AttributionStepAddForm.tsx
deleted file mode 100644
index d36b0591..00000000
--- a/src/app/(main)/reports/attribution/AttributionStepAddForm.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { useState } from 'react';
-import { useMessages } from '@/components/hooks';
-import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics';
-import styles from './AttributionStepAddForm.module.css';
-
-export interface AttributionStepAddFormProps {
- type?: string;
- value?: string;
- onChange?: (step: { type: string; value: string }) => void;
-}
-
-export function AttributionStepAddForm({
- type: defaultType = 'url',
- value: defaultValue = '',
- onChange,
-}: AttributionStepAddFormProps) {
- const [type, setType] = useState(defaultType);
- const [value, setValue] = useState(defaultValue);
- const { formatMessage, labels } = useMessages();
- const items = [
- { label: formatMessage(labels.url), value: 'url' },
- { label: formatMessage(labels.event), value: 'event' },
- ];
- const isDisabled = !type || !value;
-
- const handleSave = () => {
- onChange({ type, value });
- setValue('');
- };
-
- const handleChange = e => {
- setValue(e.target.value);
- };
-
- const handleKeyDown = e => {
- if (e.key === 'Enter') {
- e.stopPropagation();
- handleSave();
- }
- };
-
- const renderTypeValue = (value: any) => {
- return items.find(item => item.value === value)?.label;
- };
-
- return (
-
-
-
- setType(value)}
- >
- {({ value, label }) => {
- return - {label}
;
- }}
-
-
-
-
-
-
- {formatMessage(defaultValue ? labels.update : labels.add)}
-
-
-
- );
-}
-
-export default AttributionStepAddForm;
diff --git a/src/app/(main)/reports/attribution/AttributionView.module.css b/src/app/(main)/reports/attribution/AttributionView.module.css
deleted file mode 100644
index f242b1d9..00000000
--- a/src/app/(main)/reports/attribution/AttributionView.module.css
+++ /dev/null
@@ -1,20 +0,0 @@
-.container {
- display: grid;
- gap: 20px;
- margin-bottom: 40px;
-}
-
-.title {
- font-size: 24px;
- line-height: 36px;
- font-weight: 700;
-}
-
-.row {
- display: grid;
- grid-template-columns: 50% 50%;
- gap: 20px;
- border-top: 1px solid var(--base300);
- padding-top: 30px;
- margin-bottom: 30px;
-}
diff --git a/src/app/(main)/reports/attribution/AttributionView.tsx b/src/app/(main)/reports/attribution/AttributionView.tsx
deleted file mode 100644
index e5b22451..00000000
--- a/src/app/(main)/reports/attribution/AttributionView.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import PieChart from '@/components/charts/PieChart';
-import { useMessages } from '@/components/hooks';
-import { Grid, GridRow } from '@/components/layout/Grid';
-import ListTable from '@/components/metrics/ListTable';
-import MetricCard from '@/components/metrics/MetricCard';
-import MetricsBar from '@/components/metrics/MetricsBar';
-import { CHART_COLORS } from '@/lib/constants';
-import { formatLongNumber } from '@/lib/format';
-import { useContext } from 'react';
-import { ReportContext } from '../[reportId]/Report';
-import styles from './AttributionView.module.css';
-
-export interface AttributionViewProps {
- isLoading?: boolean;
-}
-
-export function AttributionView({ isLoading }: AttributionViewProps) {
- const { formatMessage, labels } = useMessages();
- const { report } = useContext(ReportContext);
- const {
- data,
- parameters: { currency },
- } = report || {};
- const ATTRIBUTION_PARAMS = [
- { value: 'referrer', label: formatMessage(labels.referrers) },
- { value: 'paidAds', label: formatMessage(labels.paidAds) },
- ];
-
- if (!data) {
- return null;
- }
-
- const { pageviews, visitors, visits } = data.total;
-
- const metrics = data
- ? [
- {
- value: pageviews,
- label: formatMessage(labels.views),
- formatValue: formatLongNumber,
- },
- {
- value: visits,
- label: formatMessage(labels.visits),
- formatValue: formatLongNumber,
- },
- {
- value: visitors,
- label: formatMessage(labels.visitors),
- formatValue: formatLongNumber,
- },
- ]
- : [];
-
- function UTMTable(UTMTableProps: { data: any; title: string; utm: string }) {
- const { data, title, utm } = UTMTableProps;
- const total = data[utm].reduce((sum, { value }) => {
- return +sum + +value;
- }, 0);
-
- return (
- ({
- x: name,
- y: Number(value),
- z: (value / total) * 100,
- }))}
- />
- );
- }
-
- return (
-
-
- {metrics?.map(({ label, value, formatValue }) => {
- return ;
- })}
-
- {ATTRIBUTION_PARAMS.map(({ value, label }) => {
- const items = data[value];
- const total = items.reduce((sum, { value }) => {
- return +sum + +value;
- }, 0);
-
- const chartData = {
- labels: items.map(({ name }) => name),
- datasets: [
- {
- data: items.map(({ value }) => value),
- backgroundColor: CHART_COLORS,
- borderWidth: 0,
- },
- ],
- };
-
- return (
-
-
-
{label}
-
({
- x: name,
- y: Number(value),
- z: (value / total) * 100,
- }))}
- />
-
-
-
- );
- })}
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default AttributionView;
diff --git a/src/app/(main)/reports/attribution/page.tsx b/src/app/(main)/reports/attribution/page.tsx
deleted file mode 100644
index 9efd6220..00000000
--- a/src/app/(main)/reports/attribution/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import AttributionReportPage from './AttributionReportPage';
-import { Metadata } from 'next';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Attribution Report',
-};
diff --git a/src/app/(main)/reports/create/ReportCreatePage.tsx b/src/app/(main)/reports/create/ReportCreatePage.tsx
deleted file mode 100644
index ff3a761a..00000000
--- a/src/app/(main)/reports/create/ReportCreatePage.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-'use client';
-import ReportTemplates from './ReportTemplates';
-
-export default function ReportCreatePage() {
- return ;
-}
diff --git a/src/app/(main)/reports/create/ReportTemplates.module.css b/src/app/(main)/reports/create/ReportTemplates.module.css
deleted file mode 100644
index 0cdcb835..00000000
--- a/src/app/(main)/reports/create/ReportTemplates.module.css
+++ /dev/null
@@ -1,32 +0,0 @@
-.reports {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
- gap: 20px;
-}
-
-.report {
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding: 20px;
- border: 1px solid var(--base500);
- border-radius: var(--border-radius);
-}
-
-.title {
- display: flex;
- gap: 10px;
- align-items: center;
- font-size: var(--font-size-lg);
- font-weight: 700;
-}
-
-.description {
- flex: 1;
-}
-
-.buttons {
- display: flex;
- align-items: center;
- justify-content: center;
-}
diff --git a/src/app/(main)/reports/create/ReportTemplates.tsx b/src/app/(main)/reports/create/ReportTemplates.tsx
deleted file mode 100644
index 292b7322..00000000
--- a/src/app/(main)/reports/create/ReportTemplates.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import Funnel from '@/assets/funnel.svg';
-import Money from '@/assets/money.svg';
-import Lightbulb from '@/assets/lightbulb.svg';
-import Magnet from '@/assets/magnet.svg';
-import Path from '@/assets/path.svg';
-import Tag from '@/assets/tag.svg';
-import Target from '@/assets/target.svg';
-import Network from '@/assets/network.svg';
-import { useMessages, useTeamUrl } from '@/components/hooks';
-import PageHeader from '@/components/layout/PageHeader';
-import Link from 'next/link';
-import { Button, Icon, Icons, Text } from 'react-basics';
-import styles from './ReportTemplates.module.css';
-
-export function ReportTemplates({ showHeader = true }: { showHeader?: boolean }) {
- const { formatMessage, labels } = useMessages();
- const { renderTeamUrl } = useTeamUrl();
-
- const reports = [
- {
- title: formatMessage(labels.insights),
- description: formatMessage(labels.insightsDescription),
- url: renderTeamUrl('/reports/insights'),
- icon: ,
- },
- {
- title: formatMessage(labels.funnel),
- description: formatMessage(labels.funnelDescription),
- url: renderTeamUrl('/reports/funnel'),
- icon: ,
- },
- {
- title: formatMessage(labels.retention),
- description: formatMessage(labels.retentionDescription),
- url: renderTeamUrl('/reports/retention'),
- icon: ,
- },
- {
- title: formatMessage(labels.utm),
- description: formatMessage(labels.utmDescription),
- url: renderTeamUrl('/reports/utm'),
- icon: ,
- },
- {
- title: formatMessage(labels.goals),
- description: formatMessage(labels.goalsDescription),
- url: renderTeamUrl('/reports/goals'),
- icon: ,
- },
- {
- title: formatMessage(labels.journey),
- description: formatMessage(labels.journeyDescription),
- url: renderTeamUrl('/reports/journey'),
- icon: ,
- },
- {
- title: formatMessage(labels.revenue),
- description: formatMessage(labels.revenueDescription),
- url: renderTeamUrl('/reports/revenue'),
- icon: ,
- },
- {
- title: formatMessage(labels.attribution),
- description: formatMessage(labels.attributionDescription),
- url: renderTeamUrl('/reports/attribution'),
- icon: ,
- },
- ];
-
- return (
- <>
- {showHeader && }
-
- {reports.map(({ title, description, url, icon }) => {
- return (
-
- );
- })}
-
- >
- );
-}
-
-function ReportItem({ title, description, url, icon }) {
- const { formatMessage, labels } = useMessages();
-
- return (
-
-
- {icon}
- {title}
-
-
{description}
-
-
-
-
-
-
- {formatMessage(labels.create)}
-
-
-
-
- );
-}
-
-export default ReportTemplates;
diff --git a/src/app/(main)/reports/create/page.tsx b/src/app/(main)/reports/create/page.tsx
deleted file mode 100644
index c2b1c18c..00000000
--- a/src/app/(main)/reports/create/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import ReportCreatePage from './ReportCreatePage';
-import { Metadata } from 'next';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Create Report',
-};
diff --git a/src/app/(main)/reports/event-data/EventDataParameters.module.css b/src/app/(main)/reports/event-data/EventDataParameters.module.css
deleted file mode 100644
index 06b62414..00000000
--- a/src/app/(main)/reports/event-data/EventDataParameters.module.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.parameter {
- display: flex;
- gap: 10px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- min-width: 0;
-}
-
-.op {
- font-weight: bold;
-}
diff --git a/src/app/(main)/reports/event-data/EventDataParameters.tsx b/src/app/(main)/reports/event-data/EventDataParameters.tsx
deleted file mode 100644
index 9e931cf5..00000000
--- a/src/app/(main)/reports/event-data/EventDataParameters.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-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';
-import { useApi, useMessages } from '@/components/hooks';
-import { DATA_TYPES, REPORT_PARAMETERS } from '@/lib/constants';
-import { ReportContext } from '../[reportId]/Report';
-import FieldAddForm from '../[reportId]/FieldAddForm';
-import ParameterList from '../[reportId]/ParameterList';
-import BaseParameters from '../[reportId]/BaseParameters';
-import styles from './EventDataParameters.module.css';
-
-function useFields(websiteId, startDate, endDate) {
- const { get, useQuery } = useApi();
- const { data, error, isLoading } = useQuery({
- queryKey: ['fields', websiteId, startDate, endDate],
- queryFn: () =>
- get('/reports/event-data', {
- websiteId,
- startAt: +startDate,
- endAt: +endDate,
- }),
- enabled: !!(websiteId && startDate && endDate),
- });
-
- return { data, error, isLoading };
-}
-
-export function EventDataParameters() {
- const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
- const { formatMessage, labels, messages } = useMessages();
- const { id, parameters } = report || {};
- const { websiteId, dateRange, fields, filters, groups } = parameters || {};
- const { startDate, endDate } = dateRange || {};
- const queryEnabled = websiteId && dateRange && fields?.length;
- const { data, error } = useFields(websiteId, startDate, endDate);
- const parametersSelected = websiteId && startDate && endDate;
- const hasData = data?.length !== 0;
-
- const parameterGroups = [
- { label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
- { label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
- ];
-
- const parameterData = {
- fields,
- filters,
- groups,
- };
-
- const handleSubmit = (values: any) => {
- runReport(values);
- };
-
- const handleAdd = (group: string, value: any) => {
- const data = parameterData[group];
-
- if (!data.find(({ name }) => name === value?.name)) {
- updateReport({ parameters: { [group]: data.concat(value) } });
- }
- };
-
- const handleRemove = (group: string) => {
- const data = [...parameterData[group]];
- updateReport({ parameters: { [group]: data.filter(({ name }) => name !== group) } });
- };
-
- const AddButton = ({ group, onAdd }) => {
- return (
-
-
-
-
-
- {(close: () => void) => {
- return (
- ({
- name: dataKey,
- type: DATA_TYPES[eventDataType],
- }))}
- group={group}
- onAdd={onAdd}
- onClose={close}
- />
- );
- }}
-
-
- );
- };
-
- return (
-
- );
-}
-
-export default EventDataParameters;
diff --git a/src/app/(main)/reports/event-data/EventDataReport.tsx b/src/app/(main)/reports/event-data/EventDataReport.tsx
deleted file mode 100644
index 8205a488..00000000
--- a/src/app/(main)/reports/event-data/EventDataReport.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import Report from '../[reportId]/Report';
-import ReportHeader from '../[reportId]/ReportHeader';
-import ReportMenu from '../[reportId]/ReportMenu';
-import ReportBody from '../[reportId]/ReportBody';
-import EventDataParameters from './EventDataParameters';
-import EventDataTable from './EventDataTable';
-import Nodes from '@/assets/nodes.svg';
-
-const defaultParameters = {
- type: 'event-data',
- parameters: { fields: [], filters: [] },
-};
-
-export default function EventDataReport({ reportId }: { reportId?: string }) {
- return (
-
- } />
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/reports/event-data/EventDataReportPage.tsx b/src/app/(main)/reports/event-data/EventDataReportPage.tsx
deleted file mode 100644
index 8276acfb..00000000
--- a/src/app/(main)/reports/event-data/EventDataReportPage.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-'use client';
-import EventDataReport from './EventDataReport';
-
-export default function EventDataReportPage() {
- return ;
-}
diff --git a/src/app/(main)/reports/event-data/EventDataTable.tsx b/src/app/(main)/reports/event-data/EventDataTable.tsx
deleted file mode 100644
index f42e792d..00000000
--- a/src/app/(main)/reports/event-data/EventDataTable.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { useContext } from 'react';
-import { GridTable, GridColumn } from 'react-basics';
-import { useMessages } from '@/components/hooks';
-import { ReportContext } from '../[reportId]/Report';
-
-export function EventDataTable() {
- const { report } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
-
- return (
-
-
-
-
-
- );
-}
-
-export default EventDataTable;
diff --git a/src/app/(main)/reports/event-data/page.tsx b/src/app/(main)/reports/event-data/page.tsx
deleted file mode 100644
index 2d6477e1..00000000
--- a/src/app/(main)/reports/event-data/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Metadata } from 'next';
-import EventDataReportPage from './EventDataReportPage';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Event Data Report',
-};
diff --git a/src/app/(main)/reports/funnel/FunnelChart.module.css b/src/app/(main)/reports/funnel/FunnelChart.module.css
deleted file mode 100644
index 7972d573..00000000
--- a/src/app/(main)/reports/funnel/FunnelChart.module.css
+++ /dev/null
@@ -1,105 +0,0 @@
-.chart {
- display: grid;
-}
-
-.num {
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 100%;
- width: 50px;
- height: 50px;
- font-size: 16px;
- font-weight: 700;
- color: var(--base800);
- background: var(--base100);
- z-index: 1;
-}
-
-.step {
- display: grid;
- grid-template-columns: max-content 1fr;
- column-gap: 30px;
- position: relative;
- padding-bottom: 60px;
-}
-
-.step::before {
- content: '';
- position: absolute;
- top: 0;
- left: 25px;
- bottom: 0;
- width: 2px;
- background-color: var(--base100);
-}
-
-.step:last-child::before {
- display: none;
-}
-
-.card {
- display: grid;
- gap: 20px;
- margin-top: 14px;
-}
-
-.header {
- display: flex;
- flex-direction: column;
- gap: 20px;
-}
-
-.bar {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- background: var(--base900);
- height: 30px;
- border-radius: 5px;
- overflow: hidden;
- position: relative;
-}
-
-.label {
- color: var(--base600);
- font-weight: 700;
- text-transform: uppercase;
-}
-
-.track {
- background-color: var(--base100);
- border-radius: 5px;
-}
-
-.info {
- text-transform: lowercase;
-}
-
-.item {
- font-size: 20px;
- color: var(--base900);
- font-weight: 700;
-}
-
-.metric {
- color: var(--base700);
- display: flex;
- justify-content: space-between;
- gap: 10px;
- margin: 10px 0;
- text-transform: lowercase;
-}
-
-.visitors {
- color: var(--base900);
- font-size: 24px;
- font-weight: 900;
- margin-right: 10px;
-}
-
-.percent {
- font-size: 20px;
- font-weight: 700;
- align-self: flex-end;
-}
diff --git a/src/app/(main)/reports/funnel/FunnelChart.tsx b/src/app/(main)/reports/funnel/FunnelChart.tsx
deleted file mode 100644
index be3da614..00000000
--- a/src/app/(main)/reports/funnel/FunnelChart.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useContext } from 'react';
-import classNames from 'classnames';
-import { useMessages } from '@/components/hooks';
-import { ReportContext } from '../[reportId]/Report';
-import { formatLongNumber } from '@/lib/format';
-import styles from './FunnelChart.module.css';
-
-export interface FunnelChartProps {
- className?: string;
- isLoading?: boolean;
-}
-
-export function FunnelChart({ className }: FunnelChartProps) {
- const { report } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
-
- const { data } = report || {};
-
- return (
-
- {data?.map(({ type, value, visitors, dropped, dropoff, remaining }, index: number) => {
- return (
-
-
{index + 1}
-
-
-
- {formatMessage(type === 'url' ? labels.viewedPage : labels.triggeredEvent)}
-
- {value}
-
-
-
- {formatLongNumber(visitors)}
- {formatMessage(labels.visitors)}
-
-
{(remaining * 100).toFixed(2)}%
-
-
- {dropoff > 0 && (
-
- {formatLongNumber(dropped)} {formatMessage(labels.visitorsDroppedOff)} (
- {(dropoff * 100).toFixed(2)}%)
-
- )}
-
-
- );
- })}
-
- );
-}
-
-export default FunnelChart;
diff --git a/src/app/(main)/reports/funnel/FunnelParameters.module.css b/src/app/(main)/reports/funnel/FunnelParameters.module.css
deleted file mode 100644
index 0f27d515..00000000
--- a/src/app/(main)/reports/funnel/FunnelParameters.module.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.item {
- display: flex;
- align-items: center;
- gap: 10px;
- width: 100%;
-}
-
-.value {
- display: flex;
- align-self: center;
- gap: 20px;
-}
diff --git a/src/app/(main)/reports/funnel/FunnelParameters.tsx b/src/app/(main)/reports/funnel/FunnelParameters.tsx
deleted file mode 100644
index 3db57135..00000000
--- a/src/app/(main)/reports/funnel/FunnelParameters.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { useContext } from 'react';
-import { useMessages } from '@/components/hooks';
-import {
- Icon,
- Form,
- FormButtons,
- FormInput,
- FormRow,
- PopupTrigger,
- Popup,
- SubmitButton,
- TextField,
- Button,
-} from 'react-basics';
-import Icons from '@/components/icons';
-import FunnelStepAddForm from './FunnelStepAddForm';
-import { ReportContext } from '../[reportId]/Report';
-import BaseParameters from '../[reportId]/BaseParameters';
-import ParameterList from '../[reportId]/ParameterList';
-import PopupForm from '../[reportId]/PopupForm';
-import styles from './FunnelParameters.module.css';
-
-export function FunnelParameters() {
- const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
-
- const { id, parameters } = report || {};
- const { websiteId, dateRange, steps } = parameters || {};
- const queryDisabled = !websiteId || !dateRange || steps?.length < 2;
-
- const handleSubmit = (data: any, e: any) => {
- e.stopPropagation();
- e.preventDefault();
-
- if (!queryDisabled) {
- runReport(data);
- }
- };
-
- const handleAddStep = (step: { type: string; value: string }) => {
- updateReport({ parameters: { steps: parameters.steps.concat(step) } });
- };
-
- const handleUpdateStep = (
- close: () => void,
- index: number,
- step: { type: string; value: string },
- ) => {
- const steps = [...parameters.steps];
- steps[index] = step;
- updateReport({ parameters: { steps } });
- close();
- };
-
- const handleRemoveStep = (index: number) => {
- const steps = [...parameters.steps];
- delete steps[index];
- updateReport({ parameters: { steps: steps.filter(n => n) } });
- };
-
- const AddStepButton = () => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
- };
-
- return (
-
- );
-}
-
-export default FunnelParameters;
diff --git a/src/app/(main)/reports/funnel/FunnelReport.module.css b/src/app/(main)/reports/funnel/FunnelReport.module.css
deleted file mode 100644
index aed66b74..00000000
--- a/src/app/(main)/reports/funnel/FunnelReport.module.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.filters {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- border: 1px solid var(--base400);
- border-radius: var(--border-radius);
- line-height: 32px;
- padding: 10px;
- overflow: hidden;
-}
diff --git a/src/app/(main)/reports/funnel/FunnelReport.tsx b/src/app/(main)/reports/funnel/FunnelReport.tsx
deleted file mode 100644
index e0c90e4a..00000000
--- a/src/app/(main)/reports/funnel/FunnelReport.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import FunnelChart from './FunnelChart';
-import FunnelParameters from './FunnelParameters';
-import Report from '../[reportId]/Report';
-import ReportHeader from '../[reportId]/ReportHeader';
-import ReportMenu from '../[reportId]/ReportMenu';
-import ReportBody from '../[reportId]/ReportBody';
-import Funnel from '@/assets/funnel.svg';
-import { REPORT_TYPES } from '@/lib/constants';
-
-const defaultParameters = {
- type: REPORT_TYPES.funnel,
- parameters: { window: 60, steps: [] },
-};
-
-export default function FunnelReport({ reportId }: { reportId?: string }) {
- return (
-
- } />
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/reports/funnel/FunnelReportPage.tsx b/src/app/(main)/reports/funnel/FunnelReportPage.tsx
deleted file mode 100644
index a114a8cc..00000000
--- a/src/app/(main)/reports/funnel/FunnelReportPage.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-'use client';
-import FunnelReport from './FunnelReport';
-
-export default function FunnelReportPage() {
- return ;
-}
diff --git a/src/app/(main)/reports/funnel/FunnelStepAddForm.module.css b/src/app/(main)/reports/funnel/FunnelStepAddForm.module.css
deleted file mode 100644
index a254ff08..00000000
--- a/src/app/(main)/reports/funnel/FunnelStepAddForm.module.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.dropdown {
- width: 140px;
-}
-
-.input {
- width: 200px;
-}
diff --git a/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx b/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx
deleted file mode 100644
index d7917d7d..00000000
--- a/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { useState } from 'react';
-import { useMessages } from '@/components/hooks';
-import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics';
-import styles from './FunnelStepAddForm.module.css';
-
-export interface FunnelStepAddFormProps {
- type?: string;
- value?: string;
- onChange?: (step: { type: string; value: string }) => void;
-}
-
-export function FunnelStepAddForm({
- type: defaultType = 'url',
- value: defaultValue = '',
- onChange,
-}: FunnelStepAddFormProps) {
- const [type, setType] = useState(defaultType);
- const [value, setValue] = useState(defaultValue);
- const { formatMessage, labels } = useMessages();
- const items = [
- { label: formatMessage(labels.url), value: 'url' },
- { label: formatMessage(labels.event), value: 'event' },
- ];
- const isDisabled = !type || !value;
-
- const handleSave = () => {
- onChange({ type, value });
- setValue('');
- };
-
- const handleChange = e => {
- setValue(e.target.value);
- };
-
- const handleKeyDown = e => {
- if (e.key === 'Enter') {
- e.stopPropagation();
- handleSave();
- }
- };
-
- const renderTypeValue = (value: any) => {
- return items.find(item => item.value === value)?.label;
- };
-
- return (
-
-
-
- setType(value)}
- >
- {({ value, label }) => {
- return - {label}
;
- }}
-
-
-
-
-
-
- {formatMessage(defaultValue ? labels.update : labels.add)}
-
-
-
- );
-}
-
-export default FunnelStepAddForm;
diff --git a/src/app/(main)/reports/funnel/page.tsx b/src/app/(main)/reports/funnel/page.tsx
deleted file mode 100644
index 40270bba..00000000
--- a/src/app/(main)/reports/funnel/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import FunnelReportPage from './FunnelReportPage';
-import { Metadata } from 'next';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Funnel Report',
-};
diff --git a/src/app/(main)/reports/goals/GoalsAddForm.module.css b/src/app/(main)/reports/goals/GoalsAddForm.module.css
deleted file mode 100644
index a254ff08..00000000
--- a/src/app/(main)/reports/goals/GoalsAddForm.module.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.dropdown {
- width: 140px;
-}
-
-.input {
- width: 200px;
-}
diff --git a/src/app/(main)/reports/goals/GoalsAddForm.tsx b/src/app/(main)/reports/goals/GoalsAddForm.tsx
deleted file mode 100644
index b7354533..00000000
--- a/src/app/(main)/reports/goals/GoalsAddForm.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { useMessages } from '@/components/hooks';
-import { useState } from 'react';
-import { Button, Dropdown, Flexbox, FormRow, Item, TextField } from 'react-basics';
-import styles from './GoalsAddForm.module.css';
-
-export function GoalsAddForm({
- type: defaultType = 'url',
- value: defaultValue = '',
- property: defaultProperty = '',
- operator: defaultAggregae = null,
- goal: defaultGoal = 10,
- onChange,
-}: {
- type?: string;
- value?: string;
- operator?: string;
- property?: string;
- goal?: number;
- onChange?: (step: {
- type: string;
- value: string;
- goal: number;
- operator?: string;
- property?: string;
- }) => void;
-}) {
- const [type, setType] = useState(defaultType);
- const [value, setValue] = useState(defaultValue);
- const [operator, setOperator] = useState(defaultAggregae);
- const [property, setProperty] = useState(defaultProperty);
- const [goal, setGoal] = useState(defaultGoal);
- const { formatMessage, labels } = useMessages();
- const items = [
- { label: formatMessage(labels.url), value: 'url' },
- { label: formatMessage(labels.event), value: 'event' },
- { label: formatMessage(labels.eventData), value: 'event-data' },
- ];
- const operators = [
- { label: formatMessage(labels.count), value: 'count' },
- { label: formatMessage(labels.average), value: 'average' },
- { label: formatMessage(labels.sum), value: 'sum' },
- ];
- const isDisabled = !type || !value;
-
- const handleSave = () => {
- onChange(
- type === 'event-data' ? { type, value, goal, operator, property } : { type, value, goal },
- );
- setValue('');
- setProperty('');
- setGoal(10);
- };
-
- const handleChange = (e, set) => {
- set(e.target.value);
- };
-
- const handleKeyDown = e => {
- if (e.key === 'Enter') {
- e.stopPropagation();
- handleSave();
- }
- };
-
- const renderTypeValue = (value: any) => {
- return items.find(item => item.value === value)?.label;
- };
-
- const renderoperatorValue = (value: any) => {
- return operators.find(item => item.value === value)?.label;
- };
-
- return (
-
-
-
- setType(value)}
- >
- {({ value, label }) => {
- return - {label}
;
- }}
-
- handleChange(e, setValue)}
- autoFocus={true}
- autoComplete="off"
- onKeyDown={handleKeyDown}
- />
-
-
- {type === 'event-data' && (
-
-
- setOperator(value)}
- >
- {({ value, label }) => {
- return - {label}
;
- }}
-
- handleChange(e, setProperty)}
- autoFocus={true}
- autoComplete="off"
- onKeyDown={handleKeyDown}
- />
-
-
- )}
-
-
- handleChange(e, setGoal)}
- autoComplete="off"
- onKeyDown={handleKeyDown}
- />
-
-
-
-
- {formatMessage(defaultValue ? labels.update : labels.add)}
-
-
-
- );
-}
-
-export default GoalsAddForm;
diff --git a/src/app/(main)/reports/goals/GoalsChart.module.css b/src/app/(main)/reports/goals/GoalsChart.module.css
deleted file mode 100644
index 799f5fdd..00000000
--- a/src/app/(main)/reports/goals/GoalsChart.module.css
+++ /dev/null
@@ -1,95 +0,0 @@
-.chart {
- display: grid;
- gap: 30px;
-}
-
-.goal {
- padding-bottom: 40px;
- border-bottom: 1px solid var(--base400);
-}
-
-.goal:last-child {
- border: 0;
-}
-
-.card {
- display: grid;
- gap: 20px;
- margin-top: 14px;
-}
-
-.header {
- display: flex;
- flex-direction: column;
- gap: 20px;
-}
-
-.label {
- color: var(--base600);
- font-weight: 700;
- text-transform: uppercase;
-}
-
-.item {
- font-size: 20px;
- color: var(--base900);
- font-weight: 700;
-}
-
-.metric {
- color: var(--base700);
- display: flex;
- justify-content: space-between;
- gap: 10px;
- margin: 10px 0;
- text-transform: lowercase;
-}
-
-.value {
- color: var(--base900);
- font-size: 24px;
- font-weight: 900;
- margin-right: 10px;
-}
-
-.percent {
- font-size: 20px;
- font-weight: 700;
- align-self: flex-end;
-}
-
-.total {
- color: var(--base700);
-}
-
-.bar {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- background: var(--base900);
- height: 10px;
- border-radius: 5px;
- overflow: hidden;
- position: relative;
-}
-
-.bar.level1 {
- background: var(--red800);
-}
-.bar.level2 {
- background: var(--orange200);
-}
-.bar.level3 {
- background: var(--orange400);
-}
-.bar.level4 {
- background: var(--orange600);
-}
-.bar.level5 {
- background: var(--green600);
-}
-
-.track {
- background-color: var(--base100);
- border-radius: 5px;
-}
diff --git a/src/app/(main)/reports/goals/GoalsChart.tsx b/src/app/(main)/reports/goals/GoalsChart.tsx
deleted file mode 100644
index 34ea485e..00000000
--- a/src/app/(main)/reports/goals/GoalsChart.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { useContext } from 'react';
-import classNames from 'classnames';
-import { useMessages } from '@/components/hooks';
-import { ReportContext } from '../[reportId]/Report';
-import { formatLongNumber } from '@/lib/format';
-import styles from './GoalsChart.module.css';
-
-export function GoalsChart({ className }: { className?: string; isLoading?: boolean }) {
- const { report } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
-
- const { data } = report || {};
-
- const getLabel = type => {
- let label = '';
- switch (type) {
- case 'url':
- label = labels.viewedPage;
- break;
- case 'event':
- label = labels.triggeredEvent;
- break;
- default:
- label = labels.collectedData;
- break;
- }
-
- return label;
- };
-
- return (
-
- {data?.map(({ type, value, goal, result, property, operator }, index: number) => {
- const percent = result > goal ? 100 : (result / goal) * 100;
-
- return (
-
-
-
- {formatMessage(getLabel(type))}
- {`${value}${
- type === 'event-data' ? `:(${operator}):${property}` : ''
- }`}
-
-
-
20 && percent <= 40,
- [styles.level3]: percent > 40 && percent <= 60,
- [styles.level4]: percent > 60 && percent <= 80,
- [styles.level5]: percent > 80,
- }),
- )}
- style={{ width: `${percent}%` }}
- >
-
-
-
- {formatLongNumber(result)}
- / {formatLongNumber(goal)}
-
-
{((result / goal) * 100).toFixed(2)}%
-
-
-
- );
- })}
-
- );
-}
-
-export default GoalsChart;
diff --git a/src/app/(main)/reports/goals/GoalsParameters.module.css b/src/app/(main)/reports/goals/GoalsParameters.module.css
deleted file mode 100644
index cd72f524..00000000
--- a/src/app/(main)/reports/goals/GoalsParameters.module.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.value {
- width: 100%;
- margin-bottom: 8px;
- font-weight: 600;
-}
-
-.eventData {
- color: var(--orange900);
- background-color: var(--orange100);
- font-size: 12px;
- font-weight: 900;
- padding: 2px 8px;
- border-radius: 5px;
- width: fit-content;
-}
-
-.goal {
- color: var(--blue900);
- background-color: var(--blue100);
- font-size: 12px;
- font-weight: 900;
- padding: 2px 8px;
- border-radius: 5px;
- width: fit-content;
-}
diff --git a/src/app/(main)/reports/goals/GoalsParameters.tsx b/src/app/(main)/reports/goals/GoalsParameters.tsx
deleted file mode 100644
index 51866645..00000000
--- a/src/app/(main)/reports/goals/GoalsParameters.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import { useMessages } from '@/components/hooks';
-import Icons from '@/components/icons';
-import { formatNumber } from '@/lib/format';
-import { useContext } from 'react';
-import {
- Button,
- Flexbox,
- Form,
- FormButtons,
- FormRow,
- Icon,
- Popup,
- PopupTrigger,
- SubmitButton,
-} from 'react-basics';
-import BaseParameters from '../[reportId]/BaseParameters';
-import ParameterList from '../[reportId]/ParameterList';
-import PopupForm from '../[reportId]/PopupForm';
-import { ReportContext } from '../[reportId]/Report';
-import GoalsAddForm from './GoalsAddForm';
-import styles from './GoalsParameters.module.css';
-
-export function GoalsParameters() {
- const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
-
- const { id, parameters } = report || {};
- const { websiteId, dateRange, goals } = parameters || {};
- const queryDisabled = !websiteId || !dateRange || goals?.length < 1;
-
- const handleSubmit = (data: any, e: any) => {
- e.stopPropagation();
- e.preventDefault();
-
- if (!queryDisabled) {
- runReport(data);
- }
- };
-
- const handleAddGoals = (goal: { type: string; value: string }) => {
- updateReport({ parameters: { goals: parameters.goals.concat(goal) } });
- };
-
- const handleUpdateGoals = (
- close: () => void,
- index: number,
- goal: { type: string; value: string },
- ) => {
- const goals = [...parameters.goals];
- goals[index] = goal;
- updateReport({ parameters: { goals } });
- close();
- };
-
- const handleRemoveGoals = (index: number) => {
- const goals = [...parameters.goals];
- delete goals[index];
- updateReport({ parameters: { goals: goals.filter(n => n) } });
- };
-
- const AddGoalsButton = () => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
- };
-
- return (
-
- );
-}
-
-export default GoalsParameters;
diff --git a/src/app/(main)/reports/goals/GoalsReport.module.css b/src/app/(main)/reports/goals/GoalsReport.module.css
deleted file mode 100644
index aed66b74..00000000
--- a/src/app/(main)/reports/goals/GoalsReport.module.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.filters {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- border: 1px solid var(--base400);
- border-radius: var(--border-radius);
- line-height: 32px;
- padding: 10px;
- overflow: hidden;
-}
diff --git a/src/app/(main)/reports/goals/GoalsReport.tsx b/src/app/(main)/reports/goals/GoalsReport.tsx
deleted file mode 100644
index ae540f3b..00000000
--- a/src/app/(main)/reports/goals/GoalsReport.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import GoalsChart from './GoalsChart';
-import GoalsParameters from './GoalsParameters';
-import Report from '../[reportId]/Report';
-import ReportHeader from '../[reportId]/ReportHeader';
-import ReportMenu from '../[reportId]/ReportMenu';
-import ReportBody from '../[reportId]/ReportBody';
-import Target from '@/assets/target.svg';
-import { REPORT_TYPES } from '@/lib/constants';
-
-const defaultParameters = {
- type: REPORT_TYPES.goals,
- parameters: { goals: [] },
-};
-
-export default function GoalsReport({ reportId }: { reportId?: string }) {
- return (
-
- } />
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/reports/goals/GoalsReportPage.tsx b/src/app/(main)/reports/goals/GoalsReportPage.tsx
deleted file mode 100644
index cbab8bd0..00000000
--- a/src/app/(main)/reports/goals/GoalsReportPage.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-'use client';
-import GoalReport from './GoalsReport';
-
-export default function GoalReportPage() {
- return ;
-}
diff --git a/src/app/(main)/reports/goals/page.tsx b/src/app/(main)/reports/goals/page.tsx
deleted file mode 100644
index 112ae47c..00000000
--- a/src/app/(main)/reports/goals/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import GoalsReportPage from './GoalsReportPage';
-import { Metadata } from 'next';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Goals Report',
-};
diff --git a/src/app/(main)/reports/insights/InsightsParameters.tsx b/src/app/(main)/reports/insights/InsightsParameters.tsx
deleted file mode 100644
index 6b3402fb..00000000
--- a/src/app/(main)/reports/insights/InsightsParameters.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useMessages } from '@/components/hooks';
-import { useContext } from 'react';
-import { Form, FormButtons, SubmitButton } from 'react-basics';
-import BaseParameters from '../[reportId]/BaseParameters';
-import { ReportContext } from '../[reportId]/Report';
-import FieldParameters from '../[reportId]/FieldParameters';
-import FilterParameters from '../[reportId]/FilterParameters';
-
-export function InsightsParameters() {
- const { report, runReport, isRunning } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
- const { id, parameters } = report || {};
- const { websiteId, dateRange, fields, filters } = parameters || {};
- const { startDate, endDate } = dateRange || {};
- const parametersSelected = websiteId && startDate && endDate;
- const queryEnabled = websiteId && dateRange && (fields?.length || filters?.length);
-
- const handleSubmit = (values: any) => {
- runReport(values);
- };
-
- return (
-
- );
-}
-
-export default InsightsParameters;
diff --git a/src/app/(main)/reports/insights/InsightsReport.tsx b/src/app/(main)/reports/insights/InsightsReport.tsx
deleted file mode 100644
index d43576fa..00000000
--- a/src/app/(main)/reports/insights/InsightsReport.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import Report from '../[reportId]/Report';
-import ReportHeader from '../[reportId]/ReportHeader';
-import ReportMenu from '../[reportId]/ReportMenu';
-import ReportBody from '../[reportId]/ReportBody';
-import InsightsParameters from './InsightsParameters';
-import InsightsTable from './InsightsTable';
-import Lightbulb from '@/assets/lightbulb.svg';
-import { REPORT_TYPES } from '@/lib/constants';
-
-const defaultParameters = {
- type: REPORT_TYPES.insights,
- parameters: { fields: [], filters: [] },
-};
-
-export default function InsightsReport({ reportId }: { reportId?: string }) {
- return (
-
- } />
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/reports/insights/InsightsReportPage.tsx b/src/app/(main)/reports/insights/InsightsReportPage.tsx
deleted file mode 100644
index 7525b767..00000000
--- a/src/app/(main)/reports/insights/InsightsReportPage.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-'use client';
-import InsightsReport from './InsightsReport';
-
-export default function InsightsReportPage() {
- return ;
-}
diff --git a/src/app/(main)/reports/insights/InsightsTable.tsx b/src/app/(main)/reports/insights/InsightsTable.tsx
deleted file mode 100644
index 6864d919..00000000
--- a/src/app/(main)/reports/insights/InsightsTable.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { useContext, useEffect, useState } from 'react';
-import { GridTable, GridColumn } from 'react-basics';
-import { useFormat, useMessages } from '@/components/hooks';
-import { ReportContext } from '../[reportId]/Report';
-import EmptyPlaceholder from '@/components/common/EmptyPlaceholder';
-import { formatShortTime } from '@/lib/format';
-
-export function InsightsTable() {
- const [fields, setFields] = useState([]);
- const { report } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
- const { formatValue } = useFormat();
-
- useEffect(
- () => {
- setFields(report?.parameters?.fields);
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [report?.data],
- );
-
- if (!fields || !report?.parameters) {
- return ;
- }
-
- return (
-
- {fields.map(({ name, label }) => {
- return (
-
- {row => formatValue(row[name], name)}
-
- );
- })}
-
- {row => row?.views?.toLocaleString()}
-
-
- {row => row?.visits?.toLocaleString()}
-
-
- {row => row?.visitors?.toLocaleString()}
-
-
- {row => {
- const n = (Math.min(row?.visits, row?.bounces) / row?.visits) * 100;
- return Math.round(+n) + '%';
- }}
-
-
- {row => {
- const n = row?.totaltime / row?.visits;
- return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
- }}
-
-
- );
-}
-
-export default InsightsTable;
diff --git a/src/app/(main)/reports/insights/page.tsx b/src/app/(main)/reports/insights/page.tsx
deleted file mode 100644
index 1e9e0ea6..00000000
--- a/src/app/(main)/reports/insights/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import InsightsReportPage from './InsightsReportPage';
-import { Metadata } from 'next';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Insights Report',
-};
diff --git a/src/app/(main)/reports/journey/JourneyParameters.tsx b/src/app/(main)/reports/journey/JourneyParameters.tsx
deleted file mode 100644
index ffa5df89..00000000
--- a/src/app/(main)/reports/journey/JourneyParameters.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { useContext } from 'react';
-import { useMessages } from '@/components/hooks';
-import {
- Dropdown,
- Form,
- FormButtons,
- FormInput,
- FormRow,
- Item,
- SubmitButton,
- TextField,
-} from 'react-basics';
-import { ReportContext } from '../[reportId]/Report';
-import BaseParameters from '../[reportId]/BaseParameters';
-
-export function JourneyParameters() {
- const { report, runReport, isRunning } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
-
- const { id, parameters } = report || {};
- const { websiteId, dateRange, steps } = parameters || {};
- const queryDisabled = !websiteId || !dateRange || !steps;
-
- const handleSubmit = (data: any, e: any) => {
- e.stopPropagation();
- e.preventDefault();
-
- if (!queryDisabled) {
- runReport(data);
- }
- };
-
- return (
-
- );
-}
-
-export default JourneyParameters;
diff --git a/src/app/(main)/reports/journey/JourneyReport.tsx b/src/app/(main)/reports/journey/JourneyReport.tsx
deleted file mode 100644
index 4322fa2a..00000000
--- a/src/app/(main)/reports/journey/JourneyReport.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-'use client';
-import Report from '../[reportId]/Report';
-import ReportHeader from '../[reportId]/ReportHeader';
-import ReportMenu from '../[reportId]/ReportMenu';
-import ReportBody from '../[reportId]/ReportBody';
-import JourneyParameters from './JourneyParameters';
-import JourneyView from './JourneyView';
-import Path from '@/assets/path.svg';
-import { REPORT_TYPES } from '@/lib/constants';
-
-const defaultParameters = {
- type: REPORT_TYPES.journey,
- parameters: { steps: 5 },
-};
-
-export default function JourneyReport({ reportId }: { reportId?: string }) {
- return (
-
- } />
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/reports/journey/JourneyReportPage.tsx b/src/app/(main)/reports/journey/JourneyReportPage.tsx
deleted file mode 100644
index 0f4b78ca..00000000
--- a/src/app/(main)/reports/journey/JourneyReportPage.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import JourneyReport from './JourneyReport';
-
-export default function JourneyReportPage() {
- return ;
-}
diff --git a/src/app/(main)/reports/journey/JourneyView.tsx b/src/app/(main)/reports/journey/JourneyView.tsx
deleted file mode 100644
index abddf023..00000000
--- a/src/app/(main)/reports/journey/JourneyView.tsx
+++ /dev/null
@@ -1,253 +0,0 @@
-import { useContext, useMemo, useState } from 'react';
-import { TextOverflow, TooltipPopup } from 'react-basics';
-import { firstBy } from 'thenby';
-import classNames from 'classnames';
-import { useEscapeKey, useMessages } from '@/components/hooks';
-import { objectToArray } from '@/lib/data';
-import { ReportContext } from '../[reportId]/Report';
-import styles from './JourneyView.module.css';
-import { formatLongNumber } from '@/lib/format';
-
-const NODE_HEIGHT = 60;
-const NODE_GAP = 10;
-const LINE_WIDTH = 3;
-
-export default function JourneyView() {
- const [selectedNode, setSelectedNode] = useState(null);
- const [activeNode, setActiveNode] = useState(null);
- const { report } = useContext(ReportContext);
- const { data, parameters } = report || {};
- const { formatMessage, labels } = useMessages();
-
- useEscapeKey(() => setSelectedNode(null));
-
- const columns = useMemo(() => {
- if (!data) {
- return [];
- }
-
- const selectedPaths = selectedNode?.paths ?? [];
- const activePaths = activeNode?.paths ?? [];
- const columns = [];
-
- for (let columnIndex = 0; columnIndex < +parameters.steps; columnIndex++) {
- const nodes = {};
-
- data.forEach(({ items, count }: any, nodeIndex: any) => {
- const name = items[columnIndex];
-
- if (name) {
- const selected = !!selectedPaths.find(({ items }) => items[columnIndex] === name);
- const active = selected && !!activePaths.find(({ items }) => items[columnIndex] === name);
-
- if (!nodes[name]) {
- const paths = data.filter(({ items }) => items[columnIndex] === name);
-
- nodes[name] = {
- name,
- count,
- totalCount: count,
- nodeIndex,
- columnIndex,
- selected,
- active,
- paths,
- pathMap: paths.map(({ items, count }) => ({
- [`${columnIndex}:${items.join(':')}`]: count,
- })),
- };
- } else {
- nodes[name].totalCount += count;
- }
- }
- });
-
- columns.push({
- nodes: objectToArray(nodes).sort(firstBy('total', -1)),
- });
- }
-
- columns.forEach((column, columnIndex) => {
- const nodes = column.nodes.map((currentNode, currentNodeIndex) => {
- const previousNodes = columns[columnIndex - 1]?.nodes;
- let selectedCount = previousNodes ? 0 : currentNode.totalCount;
- let activeCount = selectedCount;
-
- const lines =
- previousNodes?.reduce((arr: any[][], previousNode: any, previousNodeIndex: number) => {
- const fromCount = selectedNode?.paths.reduce((sum, path) => {
- if (
- previousNode.name === path.items[columnIndex - 1] &&
- currentNode.name === path.items[columnIndex]
- ) {
- sum += path.count;
- }
- return sum;
- }, 0);
-
- if (currentNode.selected && previousNode.selected && fromCount) {
- arr.push([previousNodeIndex, currentNodeIndex]);
- selectedCount += fromCount;
-
- if (previousNode.active) {
- activeCount += fromCount;
- }
- }
-
- return arr;
- }, []) || [];
-
- return { ...currentNode, selectedCount, activeCount, lines };
- });
-
- const visitorCount = nodes.reduce(
- (sum: number, { selected, selectedCount, active, activeCount, totalCount }) => {
- if (!selectedNode) {
- sum += totalCount;
- } else if (!activeNode && selectedNode && selected) {
- sum += selectedCount;
- } else if (activeNode && active) {
- sum += activeCount;
- }
- return sum;
- },
- 0,
- );
-
- const previousTotal = columns[columnIndex - 1]?.visitorCount ?? 0;
- const dropOff =
- previousTotal > 0 ? ((visitorCount - previousTotal) / previousTotal) * 100 : 0;
-
- Object.assign(column, { nodes, visitorCount, dropOff });
- });
-
- return columns;
- }, [data, selectedNode, activeNode]);
-
- const handleClick = (name: string, columnIndex: number, paths: any[]) => {
- if (name !== selectedNode?.name || columnIndex !== selectedNode?.columnIndex) {
- setSelectedNode({ name, columnIndex, paths });
- } else {
- setSelectedNode(null);
- }
- setActiveNode(null);
- };
-
- if (!data) {
- return null;
- }
-
- return (
-
-
- {columns.map((column, columnIndex) => {
- const dropOffPercent = `${~~column.dropOff}%`;
- return (
-
-
-
{columnIndex + 1}
-
-
- {formatLongNumber(column.visitorCount)} {formatMessage(labels.visitors)}
-
- {columnIndex > 0 &&
{dropOffPercent}
}
-
-
-
- {column.nodes.map(
- ({
- name,
- totalCount,
- selected,
- active,
- paths,
- activeCount,
- selectedCount,
- lines,
- }) => {
- const nodeCount = selected
- ? active
- ? activeCount
- : selectedCount
- : totalCount;
-
- return (
-
selected && setActiveNode({ name, columnIndex, paths })}
- onMouseLeave={() => selected && setActiveNode(null)}
- >
-
handleClick(name, columnIndex, paths)}
- >
-
- {name}
-
-
-
- {formatLongNumber(nodeCount)}
-
-
- {columnIndex < columns.length &&
- lines.map(([fromIndex, nodeIndex], i) => {
- const height =
- (Math.abs(nodeIndex - fromIndex) + 1) * (NODE_HEIGHT + NODE_GAP) -
- NODE_GAP;
- const midHeight =
- (Math.abs(nodeIndex - fromIndex) - 1) * (NODE_HEIGHT + NODE_GAP) +
- NODE_GAP +
- LINE_WIDTH;
- const nodeName = columns[columnIndex - 1]?.nodes[fromIndex].name;
-
- return (
-
- path.items[columnIndex] === name &&
- path.items[columnIndex - 1] === nodeName,
- ),
- [styles.up]: fromIndex < nodeIndex,
- [styles.down]: fromIndex > nodeIndex,
- [styles.flat]: fromIndex === nodeIndex,
- })}
- style={{ height }}
- >
-
-
-
-
- );
- })}
-
-
- );
- },
- )}
-
-
- );
- })}
-
-
- );
-}
diff --git a/src/app/(main)/reports/journey/page.tsx b/src/app/(main)/reports/journey/page.tsx
deleted file mode 100644
index 447747cc..00000000
--- a/src/app/(main)/reports/journey/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Metadata } from 'next';
-import JourneyReportPage from './JourneyReportPage';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Journey Report',
-};
diff --git a/src/app/(main)/reports/page.tsx b/src/app/(main)/reports/page.tsx
deleted file mode 100644
index ef4e56ad..00000000
--- a/src/app/(main)/reports/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import ReportsPage from './ReportsPage';
-import { Metadata } from 'next';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Reports',
-};
diff --git a/src/app/(main)/reports/retention/RetentionParameters.tsx b/src/app/(main)/reports/retention/RetentionParameters.tsx
deleted file mode 100644
index 56cbdbd3..00000000
--- a/src/app/(main)/reports/retention/RetentionParameters.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useContext } from 'react';
-import { useMessages } from '@/components/hooks';
-import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics';
-import { ReportContext } from '../[reportId]/Report';
-import { MonthSelect } from '@/components/input/MonthSelect';
-import BaseParameters from '../[reportId]/BaseParameters';
-import { parseDateRange } from '@/lib/date';
-
-export function RetentionParameters() {
- const { report, runReport, isRunning, updateReport } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
-
- const { id, parameters } = report || {};
- const { websiteId, dateRange } = parameters || {};
- const { startDate } = dateRange || {};
- const queryDisabled = !websiteId || !dateRange;
-
- const handleSubmit = (data: any, e: any) => {
- e.stopPropagation();
- e.preventDefault();
-
- if (!queryDisabled) {
- runReport(data);
- }
- };
-
- const handleDateChange = value => {
- updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } });
- };
-
- return (
-
- );
-}
-
-export default RetentionParameters;
diff --git a/src/app/(main)/reports/retention/RetentionReport.module.css b/src/app/(main)/reports/retention/RetentionReport.module.css
deleted file mode 100644
index aed66b74..00000000
--- a/src/app/(main)/reports/retention/RetentionReport.module.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.filters {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- border: 1px solid var(--base400);
- border-radius: var(--border-radius);
- line-height: 32px;
- padding: 10px;
- overflow: hidden;
-}
diff --git a/src/app/(main)/reports/retention/RetentionReport.tsx b/src/app/(main)/reports/retention/RetentionReport.tsx
deleted file mode 100644
index 054a1a66..00000000
--- a/src/app/(main)/reports/retention/RetentionReport.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import RetentionTable from './RetentionTable';
-import RetentionParameters from './RetentionParameters';
-import Report from '../[reportId]/Report';
-import ReportHeader from '../[reportId]/ReportHeader';
-import ReportMenu from '../[reportId]/ReportMenu';
-import ReportBody from '../[reportId]/ReportBody';
-import Magnet from '@/assets/magnet.svg';
-import { REPORT_TYPES } from '@/lib/constants';
-import { parseDateRange } from '@/lib/date';
-import { endOfMonth, startOfMonth } from 'date-fns';
-
-const defaultParameters = {
- type: REPORT_TYPES.retention,
- parameters: {
- dateRange: parseDateRange(
- `range:${startOfMonth(new Date()).getTime()}:${endOfMonth(new Date()).getTime()}`,
- ),
- },
-};
-
-export default function RetentionReport({ reportId }: { reportId?: string }) {
- return (
-
- } />
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/reports/retention/RetentionReportPage.tsx b/src/app/(main)/reports/retention/RetentionReportPage.tsx
deleted file mode 100644
index 4d3e19e9..00000000
--- a/src/app/(main)/reports/retention/RetentionReportPage.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-'use client';
-import { Metadata } from 'next';
-import RetentionReport from './RetentionReport';
-
-export default function RetentionReportPage() {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Retention Report',
-};
diff --git a/src/app/(main)/reports/retention/RetentionTable.module.css b/src/app/(main)/reports/retention/RetentionTable.module.css
deleted file mode 100644
index bfe3ac1c..00000000
--- a/src/app/(main)/reports/retention/RetentionTable.module.css
+++ /dev/null
@@ -1,52 +0,0 @@
-.table {
- display: flex;
- flex-direction: column;
-}
-
-.header {
- font-weight: 700;
-}
-
-.row {
- display: flex;
- flex-direction: row;
- gap: 1px;
- margin-bottom: 1px;
-}
-
-.cell {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 60px;
- height: 60px;
- background: var(--blue200);
- border-radius: var(--border-radius);
-}
-
-.date {
- display: flex;
- align-items: center;
- min-width: 160px;
-}
-
-.visitors {
- display: flex;
- align-items: center;
- min-width: 80px;
-}
-
-.day {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 60px;
- height: 60px;
- text-align: center;
- font-size: var(--font-size-sm);
- font-weight: 400;
-}
-
-.empty {
- background: var(--blue100);
-}
diff --git a/src/app/(main)/reports/retention/RetentionTable.tsx b/src/app/(main)/reports/retention/RetentionTable.tsx
deleted file mode 100644
index 23f0a8b0..00000000
--- a/src/app/(main)/reports/retention/RetentionTable.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { useContext } from 'react';
-import classNames from 'classnames';
-import { ReportContext } from '../[reportId]/Report';
-import EmptyPlaceholder from '@/components/common/EmptyPlaceholder';
-import { useMessages, useLocale } from '@/components/hooks';
-import { formatDate } from '@/lib/date';
-import styles from './RetentionTable.module.css';
-
-const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
-
-export function RetentionTable({ days = DAYS }) {
- const { formatMessage, labels } = useMessages();
- const { locale } = useLocale();
- const { report } = useContext(ReportContext);
- const { data } = report || {};
-
- if (!data) {
- return ;
- }
-
- const rows = data.reduce((arr: any[], row: { date: any; visitors: any; day: any }) => {
- const { date, visitors, day } = row;
- if (day === 0) {
- return arr.concat({
- date,
- visitors,
- records: days
- .reduce((arr, day) => {
- arr[day] = data.find(x => x.date === date && x.day === day);
- return arr;
- }, [])
- .filter(n => n),
- });
- }
- return arr;
- }, []);
-
- const totalDays = rows.length;
-
- return (
- <>
-
-
-
{formatMessage(labels.date)}
-
{formatMessage(labels.visitors)}
- {days.map(n => (
-
- {formatMessage(labels.day)} {n}
-
- ))}
-
- {rows.map(({ date, visitors, records }, rowIndex) => {
- return (
-
-
{formatDate(date, 'PP', locale)}
-
{visitors}
- {days.map(day => {
- if (totalDays - rowIndex < day) {
- return null;
- }
- const percentage = records.filter(a => a.day === day)[0]?.percentage;
- return (
-
- {percentage ? `${Number(percentage).toFixed(2)}%` : ''}
-
- );
- })}
-
- );
- })}
-
- >
- );
-}
-
-export default RetentionTable;
diff --git a/src/app/(main)/reports/retention/page.tsx b/src/app/(main)/reports/retention/page.tsx
deleted file mode 100644
index 0f04fe98..00000000
--- a/src/app/(main)/reports/retention/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Metadata } from 'next';
-import RetentionReportPage from './RetentionReportPage';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Retention Report',
-};
diff --git a/src/app/(main)/reports/revenue/RevenueParameters.tsx b/src/app/(main)/reports/revenue/RevenueParameters.tsx
deleted file mode 100644
index 5cee14de..00000000
--- a/src/app/(main)/reports/revenue/RevenueParameters.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useMessages } from '@/components/hooks';
-import useRevenueValues from '@/components/hooks/queries/useRevenueValues';
-import { useContext } from 'react';
-import { Dropdown, Form, FormButtons, FormInput, FormRow, Item, SubmitButton } from 'react-basics';
-import BaseParameters from '../[reportId]/BaseParameters';
-import { ReportContext } from '../[reportId]/Report';
-
-export function RevenueParameters() {
- const { report, runReport, isRunning } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
- const { id, parameters } = report || {};
- const { websiteId, dateRange } = parameters || {};
- const queryEnabled = websiteId && dateRange;
- const { data: values = [] } = useRevenueValues(
- websiteId,
- dateRange?.startDate,
- dateRange?.endDate,
- );
-
- const handleSubmit = (data: any, e: any) => {
- e.stopPropagation();
- e.preventDefault();
-
- runReport(data);
- };
-
- return (
-
- );
-}
-
-export default RevenueParameters;
diff --git a/src/app/(main)/reports/revenue/RevenueReport.tsx b/src/app/(main)/reports/revenue/RevenueReport.tsx
deleted file mode 100644
index 8400c651..00000000
--- a/src/app/(main)/reports/revenue/RevenueReport.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import Money from '@/assets/money.svg';
-import { REPORT_TYPES } from '@/lib/constants';
-import Report from '../[reportId]/Report';
-import ReportBody from '../[reportId]/ReportBody';
-import ReportHeader from '../[reportId]/ReportHeader';
-import ReportMenu from '../[reportId]/ReportMenu';
-import RevenueParameters from './RevenueParameters';
-import RevenueView from './RevenueView';
-
-const defaultParameters = {
- type: REPORT_TYPES.revenue,
- parameters: {},
-};
-
-export default function RevenueReport({ reportId }: { reportId?: string }) {
- return (
-
- } />
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/reports/revenue/RevenueReportPage.tsx b/src/app/(main)/reports/revenue/RevenueReportPage.tsx
deleted file mode 100644
index e48c29d2..00000000
--- a/src/app/(main)/reports/revenue/RevenueReportPage.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-'use client';
-import RevenueReport from './RevenueReport';
-
-export default function RevenueReportPage() {
- return ;
-}
diff --git a/src/app/(main)/reports/revenue/RevenueTable.tsx b/src/app/(main)/reports/revenue/RevenueTable.tsx
deleted file mode 100644
index 184797e9..00000000
--- a/src/app/(main)/reports/revenue/RevenueTable.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import EmptyPlaceholder from '@/components/common/EmptyPlaceholder';
-import { useMessages } from '@/components/hooks';
-import { useContext } from 'react';
-import { GridColumn, GridTable } from 'react-basics';
-import { ReportContext } from '../[reportId]/Report';
-import { formatLongCurrency } from '@/lib/format';
-
-export function RevenueTable() {
- const { report } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
- const { data } = report || {};
-
- if (!data) {
- return ;
- }
-
- return (
-
-
- {row => row.currency}
-
-
- {row => formatLongCurrency(row.sum, row.currency)}
-
-
- {row => formatLongCurrency(row.count ? row.sum / row.count : 0, row.currency)}
-
-
- {row => row.count}
-
-
- {row => row.unique_count}
-
-
- );
-}
-
-export default RevenueTable;
diff --git a/src/app/(main)/reports/revenue/RevenueView.module.css b/src/app/(main)/reports/revenue/RevenueView.module.css
deleted file mode 100644
index 9b35260e..00000000
--- a/src/app/(main)/reports/revenue/RevenueView.module.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.container {
- display: grid;
- gap: 20px;
- margin-bottom: 40px;
-}
-
-.row {
- display: flex;
- align-items: center;
- gap: 10px;
-}
diff --git a/src/app/(main)/reports/revenue/RevenueView.tsx b/src/app/(main)/reports/revenue/RevenueView.tsx
deleted file mode 100644
index bd3d6c63..00000000
--- a/src/app/(main)/reports/revenue/RevenueView.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import classNames from 'classnames';
-import { colord } from 'colord';
-import BarChart from '@/components/charts/BarChart';
-import PieChart from '@/components/charts/PieChart';
-import TypeIcon from '@/components/common/TypeIcon';
-import { useCountryNames, useLocale, useMessages } from '@/components/hooks';
-import { GridRow } from '@/components/layout/Grid';
-import ListTable from '@/components/metrics/ListTable';
-import MetricCard from '@/components/metrics/MetricCard';
-import MetricsBar from '@/components/metrics/MetricsBar';
-import { renderDateLabels } from '@/lib/charts';
-import { CHART_COLORS } from '@/lib/constants';
-import { formatLongCurrency, formatLongNumber } from '@/lib/format';
-import { useCallback, useContext, useMemo } from 'react';
-import { ReportContext } from '../[reportId]/Report';
-import RevenueTable from './RevenueTable';
-import styles from './RevenueView.module.css';
-
-export interface RevenueViewProps {
- isLoading?: boolean;
-}
-
-export function RevenueView({ isLoading }: RevenueViewProps) {
- const { formatMessage, labels } = useMessages();
- const { locale } = useLocale();
- const { countryNames } = useCountryNames(locale);
- const { report } = useContext(ReportContext);
- const {
- data,
- parameters: { dateRange, currency },
- } = report || {};
- const showTable = data?.table.length > 1;
-
- const renderCountryName = useCallback(
- ({ x: code }) => (
-
-
- {countryNames[code]}
-
- ),
- [countryNames, locale],
- );
-
- const chartData = useMemo(() => {
- if (!data) return [];
-
- const map = (data.chart as any[]).reduce((obj, { x, t, y }) => {
- if (!obj[x]) {
- obj[x] = [];
- }
-
- obj[x].push({ x: t, y });
-
- return obj;
- }, {});
-
- return {
- datasets: Object.keys(map).map((key, index) => {
- const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
- return {
- label: key,
- data: map[key],
- lineTension: 0,
- backgroundColor: color.alpha(0.6).toRgbString(),
- borderColor: color.alpha(0.7).toRgbString(),
- borderWidth: 1,
- };
- }),
- };
- }, [data]);
-
- const countryData = useMemo(() => {
- if (!data) return [];
-
- const labels = data.country.map(({ name }) => name);
- const datasets = [
- {
- data: data.country.map(({ value }) => value),
- backgroundColor: CHART_COLORS,
- borderWidth: 0,
- },
- ];
-
- return { labels, datasets };
- }, [data]);
-
- const metricData = useMemo(() => {
- if (!data) return [];
-
- const { sum, count, unique_count } = data.total;
-
- return [
- {
- value: sum,
- label: formatMessage(labels.total),
- formatValue: n => formatLongCurrency(n, currency),
- },
- {
- value: count ? sum / count : 0,
- label: formatMessage(labels.average),
- formatValue: n => formatLongCurrency(n, currency),
- },
- {
- value: count,
- label: formatMessage(labels.transactions),
- formatValue: formatLongNumber,
- },
- {
- value: unique_count,
- label: formatMessage(labels.uniqueCustomers),
- formatValue: formatLongNumber,
- },
- ] as any;
- }, [data, locale]);
-
- return (
- <>
-
-
- {metricData?.map(({ label, value, formatValue }) => {
- return ;
- })}
-
- {data && (
- <>
-
-
- ({
- x: name,
- y: Number(value),
- z: (value / data?.total.sum) * 100,
- }))}
- renderLabel={renderCountryName}
- />
-
-
- >
- )}
- {showTable &&
}
-
- >
- );
-}
-
-export default RevenueView;
diff --git a/src/app/(main)/reports/revenue/page.tsx b/src/app/(main)/reports/revenue/page.tsx
deleted file mode 100644
index a8b79f08..00000000
--- a/src/app/(main)/reports/revenue/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import RevenueReportPage from './RevenueReportPage';
-import { Metadata } from 'next';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Revenue Report',
-};
diff --git a/src/app/(main)/reports/utm/UTMParameters.tsx b/src/app/(main)/reports/utm/UTMParameters.tsx
deleted file mode 100644
index 5ae6017f..00000000
--- a/src/app/(main)/reports/utm/UTMParameters.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useContext } from 'react';
-import { useMessages } from '@/components/hooks';
-import { Form, FormButtons, SubmitButton } from 'react-basics';
-import { ReportContext } from '../[reportId]/Report';
-import BaseParameters from '../[reportId]/BaseParameters';
-
-export function UTMParameters() {
- const { report, runReport, isRunning } = useContext(ReportContext);
- const { formatMessage, labels } = useMessages();
-
- const { id, parameters } = report || {};
- const { websiteId, dateRange } = parameters || {};
- const queryDisabled = !websiteId || !dateRange;
-
- const handleSubmit = (data: any, e: any) => {
- e.stopPropagation();
- e.preventDefault();
-
- if (!queryDisabled) {
- runReport(data);
- }
- };
-
- return (
-
- );
-}
-
-export default UTMParameters;
diff --git a/src/app/(main)/reports/utm/UTMReport.tsx b/src/app/(main)/reports/utm/UTMReport.tsx
deleted file mode 100644
index d9d2f579..00000000
--- a/src/app/(main)/reports/utm/UTMReport.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-'use client';
-import Report from '../[reportId]/Report';
-import ReportHeader from '../[reportId]/ReportHeader';
-import ReportMenu from '../[reportId]/ReportMenu';
-import ReportBody from '../[reportId]/ReportBody';
-import UTMParameters from './UTMParameters';
-import UTMView from './UTMView';
-import Tag from '@/assets/tag.svg';
-import { REPORT_TYPES } from '@/lib/constants';
-
-const defaultParameters = {
- type: REPORT_TYPES.utm,
- parameters: {},
-};
-
-export default function UTMReport({ reportId }: { reportId?: string }) {
- return (
-
- } />
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(main)/reports/utm/UTMReportPage.tsx b/src/app/(main)/reports/utm/UTMReportPage.tsx
deleted file mode 100644
index 926a4263..00000000
--- a/src/app/(main)/reports/utm/UTMReportPage.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import UTMReport from './UTMReport';
-
-export default function UTMReportPage() {
- return ;
-}
diff --git a/src/app/(main)/reports/utm/UTMView.module.css b/src/app/(main)/reports/utm/UTMView.module.css
deleted file mode 100644
index fa7cc0b4..00000000
--- a/src/app/(main)/reports/utm/UTMView.module.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.title {
- font-size: 24px;
- line-height: 36px;
- font-weight: 700;
-}
-
-.row {
- display: grid;
- grid-template-columns: 50% 50%;
- gap: 20px;
- border-bottom: 1px solid var(--base300);
- padding-bottom: 30px;
- margin-bottom: 30px;
-}
diff --git a/src/app/(main)/reports/utm/UTMView.tsx b/src/app/(main)/reports/utm/UTMView.tsx
deleted file mode 100644
index ba025824..00000000
--- a/src/app/(main)/reports/utm/UTMView.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { useContext } from 'react';
-import { firstBy } from 'thenby';
-import { ReportContext } from '../[reportId]/Report';
-import { CHART_COLORS, UTM_PARAMS } from '@/lib/constants';
-import PieChart from '@/components/charts/PieChart';
-import ListTable from '@/components/metrics/ListTable';
-import styles from './UTMView.module.css';
-import { useMessages } from '@/components/hooks';
-
-function toArray(data: { [key: string]: number } = {}) {
- return Object.keys(data)
- .map(key => {
- return { name: key, value: data[key] };
- })
- .sort(firstBy('value', -1));
-}
-
-export default function UTMView() {
- const { formatMessage, labels } = useMessages();
- const { report } = useContext(ReportContext);
- const { data } = report || {};
-
- if (!data) {
- return null;
- }
-
- return (
-
- {UTM_PARAMS.map(param => {
- const items = toArray(data[param]);
- const chartData = {
- labels: items.map(({ name }) => name),
- datasets: [
- {
- data: items.map(({ value }) => value),
- backgroundColor: CHART_COLORS,
- borderWidth: 0,
- },
- ],
- };
- const total = items.reduce((sum, { value }) => {
- return +sum + +value;
- }, 0);
-
- return (
-
-
-
{param.replace(/^utm_/, '')}
-
({
- x: name,
- y: value,
- z: (value / total) * 100,
- }))}
- />
-
-
-
- );
- })}
-
- );
-}
diff --git a/src/app/(main)/reports/utm/page.tsx b/src/app/(main)/reports/utm/page.tsx
deleted file mode 100644
index 7fa50660..00000000
--- a/src/app/(main)/reports/utm/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Metadata } from 'next';
-import UTMReportPage from './UTMReportPage';
-
-export default function () {
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Goals Report',
-};
diff --git a/src/app/(main)/settings/SettingsLayout.tsx b/src/app/(main)/settings/SettingsLayout.tsx
index 08dcc3eb..f6588721 100644
--- a/src/app/(main)/settings/SettingsLayout.tsx
+++ b/src/app/(main)/settings/SettingsLayout.tsx
@@ -1,25 +1,26 @@
'use client';
-import { ReactNode } from 'react';
-import { useLogin, useMessages } from '@/components/hooks';
-import MenuLayout from '@/components/layout/MenuLayout';
+import { Column, Grid } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { PageBody } from '@/components/common/PageBody';
+import { SettingsNav } from './SettingsNav';
-export default function SettingsLayout({ children }: { children: ReactNode }) {
- const { user } = useLogin();
- const { formatMessage, labels } = useMessages();
-
- const items = [
- {
- key: 'websites',
- label: formatMessage(labels.websites),
- url: '/settings/websites',
- },
- { key: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' },
- user.isAdmin && {
- key: 'users',
- label: formatMessage(labels.users),
- url: '/settings/users',
- },
- ].filter(n => n);
-
- return {children} ;
+export function SettingsLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+
+
+
+ {children}
+
+
+ );
}
diff --git a/src/app/(main)/settings/SettingsNav.tsx b/src/app/(main)/settings/SettingsNav.tsx
new file mode 100644
index 00000000..4b35c82b
--- /dev/null
+++ b/src/app/(main)/settings/SettingsNav.tsx
@@ -0,0 +1,53 @@
+import { SideMenu } from '@/components/common/SideMenu';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { Settings2, UserCircle, Users } from '@/components/icons';
+
+export function SettingsNav({ onItemClick }: { onItemClick?: () => void }) {
+ const { formatMessage, labels } = useMessages();
+ const { renderUrl, pathname } = useNavigation();
+
+ const items = [
+ {
+ label: formatMessage(labels.application),
+ items: [
+ {
+ id: 'preferences',
+ label: formatMessage(labels.preferences),
+ path: renderUrl('/settings/preferences'),
+ icon: ,
+ },
+ ],
+ },
+ {
+ label: formatMessage(labels.account),
+ items: [
+ {
+ id: 'profile',
+ label: formatMessage(labels.profile),
+ path: renderUrl('/settings/profile'),
+ icon: ,
+ },
+ {
+ id: 'teams',
+ label: formatMessage(labels.teams),
+ path: renderUrl('/settings/teams'),
+ icon: ,
+ },
+ ],
+ },
+ ];
+
+ const selectedKey = items
+ .flatMap(e => e.items)
+ .find(({ path }) => path && pathname.includes(path.split('?')[0]))?.id;
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/settings/layout.tsx b/src/app/(main)/settings/layout.tsx
index 573897d5..4e773a37 100644
--- a/src/app/(main)/settings/layout.tsx
+++ b/src/app/(main)/settings/layout.tsx
@@ -1,5 +1,5 @@
-import SettingsLayout from './SettingsLayout';
-import { Metadata } from 'next';
+import type { Metadata } from 'next';
+import { SettingsLayout } from './SettingsLayout';
export default function ({ children }) {
if (process.env.cloudMode) {
diff --git a/src/app/(main)/settings/preferences/DateRangeSetting.tsx b/src/app/(main)/settings/preferences/DateRangeSetting.tsx
new file mode 100644
index 00000000..3f5e6647
--- /dev/null
+++ b/src/app/(main)/settings/preferences/DateRangeSetting.tsx
@@ -0,0 +1,28 @@
+import { Button, Row } from '@umami/react-zen';
+import { useState } from 'react';
+import { useMessages } from '@/components/hooks';
+import { DateFilter } from '@/components/input/DateFilter';
+import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
+import { getItem, setItem } from '@/lib/storage';
+
+export function DateRangeSetting() {
+ const { formatMessage, labels } = useMessages();
+ const [date, setDate] = useState(getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE);
+
+ const handleChange = (value: string) => {
+ setItem(DATE_RANGE_CONFIG, value);
+ setDate(value);
+ };
+
+ const handleReset = () => {
+ setItem(DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE);
+ setDate(DEFAULT_DATE_RANGE_VALUE);
+ };
+
+ return (
+
+
+ {formatMessage(labels.reset)}
+
+ );
+}
diff --git a/src/app/(main)/profile/LanguageSetting.tsx b/src/app/(main)/settings/preferences/LanguageSetting.tsx
similarity index 57%
rename from src/app/(main)/profile/LanguageSetting.tsx
rename to src/app/(main)/settings/preferences/LanguageSetting.tsx
index a47394b3..00a2d74f 100644
--- a/src/app/(main)/profile/LanguageSetting.tsx
+++ b/src/app/(main)/settings/preferences/LanguageSetting.tsx
@@ -1,15 +1,14 @@
+import { Button, ListItem, Row, Select } from '@umami/react-zen';
import { useState } from 'react';
-import { Button, Dropdown, Item, Flexbox } from 'react-basics';
import { useLocale, useMessages } from '@/components/hooks';
import { DEFAULT_LOCALE } from '@/lib/constants';
import { languages } from '@/lib/lang';
-import styles from './LanguageSetting.module.css';
export function LanguageSetting() {
const [search, setSearch] = useState('');
const { formatMessage, labels } = useMessages();
const { locale, saveLocale } = useLocale();
- const options = search
+ const items = search
? Object.keys(languages).filter(n => {
return (
n.toLowerCase().includes(search.toLowerCase()) ||
@@ -20,24 +19,30 @@ export function LanguageSetting() {
const handleReset = () => saveLocale(DEFAULT_LOCALE);
- const renderValue = (value: string | number) => languages?.[value]?.label;
+ const handleOpen = (isOpen: boolean) => {
+ if (isOpen) {
+ setSearch('');
+ }
+ };
return (
-
-
+ saveLocale(val as string)}
- allowSearch={true}
+ allowSearch
onSearch={setSearch}
- menuProps={{ className: styles.menu }}
+ onOpenChange={handleOpen}
+ listProps={{ style: { maxHeight: 300 } }}
>
- {item => - {languages[item].label}
}
-
- {formatMessage(labels.reset)}
-
+ {items.map(item => (
+
+ {languages[item].label}
+
+ ))}
+ {!items.length && }
+
+ {formatMessage(labels.reset)}
+
);
}
-
-export default LanguageSetting;
diff --git a/src/app/(main)/settings/preferences/PreferenceSettings.tsx b/src/app/(main)/settings/preferences/PreferenceSettings.tsx
new file mode 100644
index 00000000..a2890ce9
--- /dev/null
+++ b/src/app/(main)/settings/preferences/PreferenceSettings.tsx
@@ -0,0 +1,36 @@
+import { Column, Label } from '@umami/react-zen';
+import { useLoginQuery, useMessages } from '@/components/hooks';
+import { DateRangeSetting } from './DateRangeSetting';
+import { LanguageSetting } from './LanguageSetting';
+import { ThemeSetting } from './ThemeSetting';
+import { TimezoneSetting } from './TimezoneSetting';
+
+export function PreferenceSettings() {
+ const { user } = useLoginQuery();
+ const { formatMessage, labels } = useMessages();
+
+ if (!user) {
+ return null;
+ }
+
+ return (
+
+
+ {formatMessage(labels.defaultDateRange)}
+
+
+
+ {formatMessage(labels.timezone)}
+
+
+
+ {formatMessage(labels.language)}
+
+
+
+ {formatMessage(labels.theme)}
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/preferences/PreferencesPage.tsx b/src/app/(main)/settings/preferences/PreferencesPage.tsx
new file mode 100644
index 00000000..61e26694
--- /dev/null
+++ b/src/app/(main)/settings/preferences/PreferencesPage.tsx
@@ -0,0 +1,22 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { PageBody } from '@/components/common/PageBody';
+import { PageHeader } from '@/components/common/PageHeader';
+import { Panel } from '@/components/common/Panel';
+import { useMessages } from '@/components/hooks';
+import { PreferenceSettings } from './PreferenceSettings';
+
+export function PreferencesPage() {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/preferences/ThemeSetting.tsx b/src/app/(main)/settings/preferences/ThemeSetting.tsx
new file mode 100644
index 00000000..03bd6a6e
--- /dev/null
+++ b/src/app/(main)/settings/preferences/ThemeSetting.tsx
@@ -0,0 +1,21 @@
+import { Button, Icon, Row, useTheme } from '@umami/react-zen';
+import { Moon, Sun } from '@/components/icons';
+
+export function ThemeSetting() {
+ const { theme, setTheme } = useTheme();
+
+ return (
+
+ setTheme('light')}>
+
+
+
+
+ setTheme('dark')}>
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/profile/TimezoneSetting.tsx b/src/app/(main)/settings/preferences/TimezoneSetting.tsx
similarity index 50%
rename from src/app/(main)/profile/TimezoneSetting.tsx
rename to src/app/(main)/settings/preferences/TimezoneSetting.tsx
index 56c85813..cf20b20d 100644
--- a/src/app/(main)/profile/TimezoneSetting.tsx
+++ b/src/app/(main)/settings/preferences/TimezoneSetting.tsx
@@ -1,8 +1,7 @@
+import { Button, ListItem, Row, Select } from '@umami/react-zen';
import { useState } from 'react';
-import { Dropdown, Item, Button, Flexbox } from 'react-basics';
-import { useTimezone, useMessages } from '@/components/hooks';
+import { useMessages, useTimezone } from '@/components/hooks';
import { getTimezone } from '@/lib/date';
-import styles from './TimezoneSetting.module.css';
const timezones = Intl.supportedValuesOf('timeZone');
@@ -10,28 +9,36 @@ export function TimezoneSetting() {
const [search, setSearch] = useState('');
const { formatMessage, labels } = useMessages();
const { timezone, saveTimezone } = useTimezone();
- const options = search
+ const items = search
? timezones.filter(n => n.toLowerCase().includes(search.toLowerCase()))
: timezones;
const handleReset = () => saveTimezone(getTimezone());
+ const handleOpen = isOpen => {
+ if (isOpen) {
+ setSearch('');
+ }
+ };
+
return (
-
-
+ saveTimezone(value)}
- menuProps={{ className: styles.menu }}
allowSearch={true}
onSearch={setSearch}
+ onOpenChange={handleOpen}
+ listProps={{ style: { maxHeight: 300 } }}
>
- {item => - {item}
}
-
- {formatMessage(labels.reset)}
-
+ {items.map((item: any) => (
+
+ {item}
+
+ ))}
+ {!items.length && }
+
+ {formatMessage(labels.reset)}
+
);
}
-
-export default TimezoneSetting;
diff --git a/src/app/(main)/settings/preferences/page.tsx b/src/app/(main)/settings/preferences/page.tsx
new file mode 100644
index 00000000..dd16870e
--- /dev/null
+++ b/src/app/(main)/settings/preferences/page.tsx
@@ -0,0 +1,10 @@
+import type { Metadata } from 'next';
+import { PreferencesPage } from './PreferencesPage';
+
+export default function () {
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Preferences',
+};
diff --git a/src/app/(main)/settings/profile/PasswordChangeButton.tsx b/src/app/(main)/settings/profile/PasswordChangeButton.tsx
new file mode 100644
index 00000000..6ce8ef84
--- /dev/null
+++ b/src/app/(main)/settings/profile/PasswordChangeButton.tsx
@@ -0,0 +1,29 @@
+import { Button, Dialog, DialogTrigger, Icon, Modal, Text, useToast } from '@umami/react-zen';
+import { useMessages } from '@/components/hooks';
+import { LockKeyhole } from '@/components/icons';
+import { PasswordEditForm } from './PasswordEditForm';
+
+export function PasswordChangeButton() {
+ const { formatMessage, labels, messages } = useMessages();
+ const { toast } = useToast();
+
+ const handleSave = () => {
+ toast(formatMessage(messages.saved));
+ };
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.changePassword)}
+
+
+
+ {({ close }) => }
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/profile/PasswordEditForm.tsx b/src/app/(main)/settings/profile/PasswordEditForm.tsx
new file mode 100644
index 00000000..6f782e44
--- /dev/null
+++ b/src/app/(main)/settings/profile/PasswordEditForm.tsx
@@ -0,0 +1,67 @@
+import {
+ Button,
+ Form,
+ FormButtons,
+ FormField,
+ FormSubmitButton,
+ PasswordField,
+} from '@umami/react-zen';
+import { useMessages, useUpdateQuery } from '@/components/hooks';
+
+export function PasswordEditForm({ onSave, onClose }) {
+ const { formatMessage, labels, messages, getErrorMessage } = useMessages();
+ const { mutateAsync, error, isPending } = useUpdateQuery('/me/password');
+
+ const handleSubmit = async (data: any) => {
+ await mutateAsync(data, {
+ onSuccess: async () => {
+ onSave();
+ onClose();
+ },
+ });
+ };
+
+ const samePassword = (value: string, values: Record) => {
+ if (value !== values.newPassword) {
+ return formatMessage(messages.noMatchPassword);
+ }
+ return true;
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/settings/profile/ProfileHeader.tsx b/src/app/(main)/settings/profile/ProfileHeader.tsx
new file mode 100644
index 00000000..05f79963
--- /dev/null
+++ b/src/app/(main)/settings/profile/ProfileHeader.tsx
@@ -0,0 +1,8 @@
+import { SectionHeader } from '@/components/common/SectionHeader';
+import { useMessages } from '@/components/hooks';
+
+export function ProfileHeader() {
+ const { formatMessage, labels } = useMessages();
+
+ return ;
+}
diff --git a/src/app/(main)/settings/profile/ProfilePage.tsx b/src/app/(main)/settings/profile/ProfilePage.tsx
new file mode 100644
index 00000000..f03499a3
--- /dev/null
+++ b/src/app/(main)/settings/profile/ProfilePage.tsx
@@ -0,0 +1,22 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { PageBody } from '@/components/common/PageBody';
+import { PageHeader } from '@/components/common/PageHeader';
+import { Panel } from '@/components/common/Panel';
+import { useMessages } from '@/components/hooks';
+import { ProfileSettings } from './ProfileSettings';
+
+export function ProfilePage() {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/profile/ProfileSettings.tsx b/src/app/(main)/settings/profile/ProfileSettings.tsx
new file mode 100644
index 00000000..fae73a53
--- /dev/null
+++ b/src/app/(main)/settings/profile/ProfileSettings.tsx
@@ -0,0 +1,51 @@
+import { Column, Label, Row } from '@umami/react-zen';
+import { useConfig, useLoginQuery, useMessages } from '@/components/hooks';
+import { ROLES } from '@/lib/constants';
+import { PasswordChangeButton } from './PasswordChangeButton';
+
+export function ProfileSettings() {
+ const { user } = useLoginQuery();
+ const { formatMessage, labels } = useMessages();
+ const { cloudMode } = useConfig();
+
+ if (!user) {
+ return null;
+ }
+
+ const { username, role } = user;
+
+ const renderRole = (value: string) => {
+ if (value === ROLES.user) {
+ return formatMessage(labels.user);
+ }
+ if (value === ROLES.admin) {
+ return formatMessage(labels.admin);
+ }
+ if (value === ROLES.viewOnly) {
+ return formatMessage(labels.viewOnly);
+ }
+
+ return formatMessage(labels.unknown);
+ };
+
+ return (
+
+
+ {formatMessage(labels.username)}
+ {username}
+
+
+ {formatMessage(labels.role)}
+ {renderRole(role)}
+
+ {!cloudMode && (
+
+ {formatMessage(labels.password)}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/profile/page.tsx b/src/app/(main)/settings/profile/page.tsx
similarity index 58%
rename from src/app/(main)/profile/page.tsx
rename to src/app/(main)/settings/profile/page.tsx
index 0c1f0082..6060b91f 100644
--- a/src/app/(main)/profile/page.tsx
+++ b/src/app/(main)/settings/profile/page.tsx
@@ -1,5 +1,5 @@
-import { Metadata } from 'next';
-import ProfilePage from './ProfilePage';
+import type { Metadata } from 'next';
+import { ProfilePage } from './ProfilePage';
export default function () {
return ;
diff --git a/src/app/(main)/settings/teams/TeamAddForm.tsx b/src/app/(main)/settings/teams/TeamAddForm.tsx
deleted file mode 100644
index e940aa17..00000000
--- a/src/app/(main)/settings/teams/TeamAddForm.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { useApi, useMessages } from '@/components/hooks';
-import {
- Button,
- Form,
- FormButtons,
- FormInput,
- FormRow,
- SubmitButton,
- TextField,
-} from 'react-basics';
-
-export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
- const { formatMessage, labels } = useMessages();
- const { post, useMutation } = useApi();
- const { mutate, error, isPending } = useMutation({
- mutationFn: (data: any) => post('/teams', data),
- });
-
- const handleSubmit = async (data: any) => {
- mutate(data, {
- onSuccess: async () => {
- onSave?.();
- onClose?.();
- },
- });
- };
-
- return (
-
- );
-}
-
-export default TeamAddForm;
diff --git a/src/app/(main)/settings/teams/TeamJoinForm.tsx b/src/app/(main)/settings/teams/TeamJoinForm.tsx
deleted file mode 100644
index 78be872c..00000000
--- a/src/app/(main)/settings/teams/TeamJoinForm.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { useRef } from 'react';
-import {
- Form,
- FormRow,
- FormInput,
- FormButtons,
- TextField,
- Button,
- SubmitButton,
-} from 'react-basics';
-import { useApi, useMessages, useModified } from '@/components/hooks';
-
-export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
- const { formatMessage, labels } = useMessages();
- const { post, useMutation } = useApi();
- const { mutate, error } = useMutation({ mutationFn: (data: any) => post('/teams/join', data) });
- const ref = useRef(null);
- const { touch } = useModified();
-
- const handleSubmit = async (data: any) => {
- mutate(data, {
- onSuccess: async () => {
- touch('teams:members');
- onSave?.();
- onClose?.();
- },
- });
- };
-
- return (
-
- );
-}
-
-export default TeamJoinForm;
diff --git a/src/app/(main)/settings/teams/TeamLeaveButton.tsx b/src/app/(main)/settings/teams/TeamLeaveButton.tsx
deleted file mode 100644
index 5f5b54f3..00000000
--- a/src/app/(main)/settings/teams/TeamLeaveButton.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { useLocale, useLogin, useMessages, useModified } from '@/components/hooks';
-import { useRouter } from 'next/navigation';
-import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
-import TeamDeleteForm from './TeamLeaveForm';
-
-export function TeamLeaveButton({ teamId, teamName }: { teamId: string; teamName: string }) {
- const { formatMessage, labels } = useMessages();
- const router = useRouter();
- const { dir } = useLocale();
- const { user } = useLogin();
- const { touch } = useModified();
-
- const handleLeave = async () => {
- touch('teams');
- router.push('/settings/teams');
- };
-
- return (
-
-
-
-
-
- {formatMessage(labels.leave)}
-
-
- {(close: () => void) => (
-
- )}
-
-
- );
-}
-
-export default TeamLeaveButton;
diff --git a/src/app/(main)/settings/teams/TeamLeaveForm.tsx b/src/app/(main)/settings/teams/TeamLeaveForm.tsx
deleted file mode 100644
index 389ba4ea..00000000
--- a/src/app/(main)/settings/teams/TeamLeaveForm.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { useApi, useMessages, useModified } from '@/components/hooks';
-import ConfirmationForm from '@/components/common/ConfirmationForm';
-
-export function TeamLeaveForm({
- teamId,
- userId,
- teamName,
- onSave,
- onClose,
-}: {
- teamId: string;
- userId: string;
- teamName: string;
- onSave: () => void;
- onClose: () => void;
-}) {
- const { formatMessage, labels, messages } = useMessages();
- const { del, useMutation } = useApi();
- const { mutate, error, isPending } = useMutation({
- mutationFn: () => del(`/teams/${teamId}/users/${userId}`),
- });
- const { touch } = useModified();
-
- const handleConfirm = async () => {
- mutate(null, {
- onSuccess: async () => {
- touch('teams:members');
- onSave();
- onClose();
- },
- });
- };
-
- return (
- {teamName},
- })}
- onConfirm={handleConfirm}
- onClose={onClose}
- isLoading={isPending}
- error={error}
- />
- );
-}
-
-export default TeamLeaveForm;
diff --git a/src/app/(main)/settings/teams/TeamsDataTable.tsx b/src/app/(main)/settings/teams/TeamsDataTable.tsx
deleted file mode 100644
index 9b8c9b27..00000000
--- a/src/app/(main)/settings/teams/TeamsDataTable.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import DataTable from '@/components/common/DataTable';
-import TeamsTable from '@/app/(main)/settings/teams/TeamsTable';
-import { useLogin, useTeams } from '@/components/hooks';
-import { ReactNode } from 'react';
-
-export function TeamsDataTable({
- allowEdit,
- showActions,
- children,
-}: {
- allowEdit?: boolean;
- showActions?: boolean;
- children?: ReactNode;
-}) {
- const { user } = useLogin();
- const queryResult = useTeams(user.id);
-
- return (
- children}>
- {({ data }) => {
- return ;
- }}
-
- );
-}
-
-export default TeamsDataTable;
diff --git a/src/app/(main)/settings/teams/TeamsHeader.tsx b/src/app/(main)/settings/teams/TeamsHeader.tsx
deleted file mode 100644
index e1911a19..00000000
--- a/src/app/(main)/settings/teams/TeamsHeader.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Flexbox } from 'react-basics';
-import PageHeader from '@/components/layout/PageHeader';
-import { ROLES } from '@/lib/constants';
-import { useLogin, useMessages } from '@/components/hooks';
-import TeamsJoinButton from './TeamsJoinButton';
-import TeamsAddButton from './TeamsAddButton';
-
-export function TeamsHeader({ allowCreate = true }: { allowCreate?: boolean }) {
- const { formatMessage, labels } = useMessages();
- const { user } = useLogin();
- const cloudMode = !!process.env.cloudMode;
-
- return (
-
-
- {!cloudMode && }
- {allowCreate && user.role !== ROLES.viewOnly && }
-
-
- );
-}
-
-export default TeamsHeader;
diff --git a/src/app/(main)/settings/teams/TeamsJoinButton.tsx b/src/app/(main)/settings/teams/TeamsJoinButton.tsx
deleted file mode 100644
index bbf2d685..00000000
--- a/src/app/(main)/settings/teams/TeamsJoinButton.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
-import Icons from '@/components/icons';
-import { useMessages, useModified } from '@/components/hooks';
-import TeamJoinForm from './TeamJoinForm';
-
-export function TeamsJoinButton() {
- const { formatMessage, labels, messages } = useMessages();
- const { showToast } = useToasts();
- const { touch } = useModified();
-
- const handleJoin = () => {
- showToast({ message: formatMessage(messages.saved), variant: 'success' });
- touch('teams');
- };
-
- return (
-
-
-
-
-
- {formatMessage(labels.joinTeam)}
-
-
- {close => }
-
-
- );
-}
-
-export default TeamsJoinButton;
diff --git a/src/app/(main)/settings/teams/TeamsSettingsPage.tsx b/src/app/(main)/settings/teams/TeamsSettingsPage.tsx
index 9b45845b..dc3e3bc8 100644
--- a/src/app/(main)/settings/teams/TeamsSettingsPage.tsx
+++ b/src/app/(main)/settings/teams/TeamsSettingsPage.tsx
@@ -1,12 +1,16 @@
'use client';
-import TeamsDataTable from './TeamsDataTable';
-import TeamsHeader from './TeamsHeader';
+import { Column } from '@umami/react-zen';
+import { TeamsDataTable } from '@/app/(main)/teams/TeamsDataTable';
+import { TeamsHeader } from '@/app/(main)/teams/TeamsHeader';
+import { Panel } from '@/components/common/Panel';
-export default function TeamsSettingsPage() {
+export function TeamsSettingsPage() {
return (
- <>
+
-
- >
+
+
+
+
);
}
diff --git a/src/app/(main)/settings/teams/TeamsTable.tsx b/src/app/(main)/settings/teams/TeamsTable.tsx
deleted file mode 100644
index 8e7efa27..00000000
--- a/src/app/(main)/settings/teams/TeamsTable.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { GridColumn, GridTable, Icon, Text } from 'react-basics';
-import { useMessages } from '@/components/hooks';
-import Icons from '@/components/icons';
-import { ROLES } from '@/lib/constants';
-import LinkButton from '@/components/common/LinkButton';
-
-export function TeamsTable({
- data = [],
- showActions = true,
-}: {
- data: any[];
- allowEdit?: boolean;
- showActions?: boolean;
-}) {
- const { formatMessage, labels } = useMessages();
-
- return (
-
-
-
- {row => row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username}
-
-
- {row => row._count.website}
-
-
- {row => row._count.teamUser}
-
- {showActions && (
-
- {row => {
- const { id } = row;
-
- return (
-
-
-
-
- {formatMessage(labels.view)}
-
- );
- }}
-
- )}
-
- );
-}
-
-export default TeamsTable;
diff --git a/src/app/(main)/settings/teams/WebsiteTags.module.css b/src/app/(main)/settings/teams/WebsiteTags.module.css
deleted file mode 100644
index 5ca7af51..00000000
--- a/src/app/(main)/settings/teams/WebsiteTags.module.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.filters {
- display: flex;
- justify-content: flex-start;
- align-items: flex-start;
-}
-
-.tag {
- text-align: center;
- margin-bottom: 10px;
- margin-inline-end: 20px;
-}
diff --git a/src/app/(main)/settings/teams/WebsiteTags.tsx b/src/app/(main)/settings/teams/WebsiteTags.tsx
deleted file mode 100644
index 4a0f109d..00000000
--- a/src/app/(main)/settings/teams/WebsiteTags.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Button, Icon, Icons, Text } from 'react-basics';
-import styles from './WebsiteTags.module.css';
-
-export function WebsiteTags({
- items = [],
- websites = [],
- onClick,
-}: {
- items: any[];
- websites: any[];
- onClick: (e: Event) => void;
-}) {
- if (websites.length === 0) {
- return null;
- }
-
- return (
-
- {websites.map(websiteId => {
- const website = items.find(a => a.id === websiteId);
-
- return (
-
- onClick(websiteId)} variant="primary" size="sm">
-
- {`${website.name}`}
-
-
-
-
-
-
- );
- })}
-
- );
-}
-
-export default WebsiteTags;
diff --git a/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx b/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx
new file mode 100644
index 00000000..9539625f
--- /dev/null
+++ b/src/app/(main)/settings/teams/[teamId]/TeamSettingsPage.tsx
@@ -0,0 +1,11 @@
+'use client';
+import { TeamSettings } from '@/app/(main)/teams/[teamId]/TeamSettings';
+import { TeamProvider } from '@/app/(main)/teams/TeamProvider';
+
+export function TeamSettingsPage({ teamId }: { teamId: string }) {
+ return (
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/teams/[teamId]/page.tsx b/src/app/(main)/settings/teams/[teamId]/page.tsx
new file mode 100644
index 00000000..58a380bc
--- /dev/null
+++ b/src/app/(main)/settings/teams/[teamId]/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { TeamSettingsPage } from './TeamSettingsPage';
+
+export default async function ({ params }: { params: Promise<{ teamId: string }> }) {
+ const { teamId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Teams',
+};
diff --git a/src/app/(main)/settings/teams/page.tsx b/src/app/(main)/settings/teams/page.tsx
index 4342b607..a0913f45 100644
--- a/src/app/(main)/settings/teams/page.tsx
+++ b/src/app/(main)/settings/teams/page.tsx
@@ -1,5 +1,5 @@
-import { Metadata } from 'next';
-import TeamsSettingsPage from './TeamsSettingsPage';
+import type { Metadata } from 'next';
+import { TeamsSettingsPage } from './TeamsSettingsPage';
export default function () {
return ;
diff --git a/src/app/(main)/settings/users/UserAddButton.tsx b/src/app/(main)/settings/users/UserAddButton.tsx
deleted file mode 100644
index 674771b6..00000000
--- a/src/app/(main)/settings/users/UserAddButton.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Button, Icon, Text, Modal, Icons, ModalTrigger, useToasts } from 'react-basics';
-import UserAddForm from './UserAddForm';
-import { useMessages, useModified } from '@/components/hooks';
-
-export function UserAddButton({ onSave }: { onSave?: () => void }) {
- const { formatMessage, labels, messages } = useMessages();
- const { showToast } = useToasts();
- const { touch } = useModified();
-
- const handleSave = () => {
- showToast({ message: formatMessage(messages.saved), variant: 'success' });
- touch('users');
- onSave?.();
- };
-
- return (
-
-
-
-
-
- {formatMessage(labels.createUser)}
-
-
- {(close: () => void) => }
-
-
- );
-}
-
-export default UserAddButton;
diff --git a/src/app/(main)/settings/users/UserAddForm.tsx b/src/app/(main)/settings/users/UserAddForm.tsx
deleted file mode 100644
index a6998e5d..00000000
--- a/src/app/(main)/settings/users/UserAddForm.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import {
- Dropdown,
- Item,
- Form,
- FormRow,
- FormButtons,
- FormInput,
- TextField,
- PasswordField,
- SubmitButton,
- Button,
-} from 'react-basics';
-import { useApi, useMessages } from '@/components/hooks';
-import { ROLES } from '@/lib/constants';
-import { messages } from '@/components/messages';
-
-export function UserAddForm({ onSave, onClose }) {
- const { post, useMutation } = useApi();
- const { mutate, error, isPending } = useMutation({
- mutationFn: (data: any) => post(`/users`, data),
- });
- const { formatMessage, labels } = useMessages();
-
- const handleSubmit = async (data: any) => {
- mutate(data, {
- onSuccess: async () => {
- onSave(data);
- onClose();
- },
- });
- };
-
- const renderValue = (value: string) => {
- if (value === ROLES.user) {
- return formatMessage(labels.user);
- }
- if (value === ROLES.admin) {
- return formatMessage(labels.admin);
- }
- if (value === ROLES.viewOnly) {
- return formatMessage(labels.viewOnly);
- }
- };
-
- return (
-
- );
-}
-
-export default UserAddForm;
diff --git a/src/app/(main)/settings/users/UserDeleteButton.tsx b/src/app/(main)/settings/users/UserDeleteButton.tsx
deleted file mode 100644
index d4c3da0a..00000000
--- a/src/app/(main)/settings/users/UserDeleteButton.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
-import { useMessages, useLogin } from '@/components/hooks';
-import UserDeleteForm from './UserDeleteForm';
-
-export function UserDeleteButton({
- userId,
- username,
- onDelete,
-}: {
- userId: string;
- username: string;
- onDelete?: () => void;
-}) {
- const { formatMessage, labels } = useMessages();
- const { user } = useLogin();
-
- return (
-
-
-
-
-
- {formatMessage(labels.delete)}
-
-
- {(close: () => void) => (
-
- )}
-
-
- );
-}
-
-export default UserDeleteButton;
diff --git a/src/app/(main)/settings/users/UserDeleteForm.tsx b/src/app/(main)/settings/users/UserDeleteForm.tsx
deleted file mode 100644
index 5c307cdc..00000000
--- a/src/app/(main)/settings/users/UserDeleteForm.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useApi, useMessages, useModified } from '@/components/hooks';
-import ConfirmationForm from '@/components/common/ConfirmationForm';
-
-export function UserDeleteForm({ userId, username, onSave, onClose }) {
- const { messages, labels, formatMessage } = useMessages();
- const { del, useMutation } = useApi();
- const { mutate, error, isPending } = useMutation({ mutationFn: () => del(`/users/${userId}`) });
- const { touch } = useModified();
-
- const handleConfirm = async () => {
- mutate(null, {
- onSuccess: async () => {
- touch('users');
- onSave?.();
- onClose?.();
- },
- });
- };
-
- return (
- {username},
- })}
- onConfirm={handleConfirm}
- onClose={onClose}
- buttonLabel={formatMessage(labels.delete)}
- isLoading={isPending}
- error={error}
- />
- );
-}
-
-export default UserDeleteForm;
diff --git a/src/app/(main)/settings/users/UsersDataTable.tsx b/src/app/(main)/settings/users/UsersDataTable.tsx
deleted file mode 100644
index 867f4090..00000000
--- a/src/app/(main)/settings/users/UsersDataTable.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import DataTable from '@/components/common/DataTable';
-import { useUsers } from '@/components/hooks';
-import UsersTable from './UsersTable';
-import { ReactNode } from 'react';
-
-export function UsersDataTable({
- showActions,
- children,
-}: {
- showActions?: boolean;
- children?: ReactNode;
-}) {
- const queryResult = useUsers();
-
- return (
- children}>
- {({ data }) => }
-
- );
-}
-
-export default UsersDataTable;
diff --git a/src/app/(main)/settings/users/UsersHeader.tsx b/src/app/(main)/settings/users/UsersHeader.tsx
deleted file mode 100644
index d07a159f..00000000
--- a/src/app/(main)/settings/users/UsersHeader.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import PageHeader from '@/components/layout/PageHeader';
-import { useMessages } from '@/components/hooks';
-import UserAddButton from './UserAddButton';
-
-export function UsersHeader({ onAdd }: { onAdd?: () => void }) {
- const { formatMessage, labels } = useMessages();
-
- return (
-
-
-
- );
-}
-
-export default UsersHeader;
diff --git a/src/app/(main)/settings/users/UsersSettingsPage.tsx b/src/app/(main)/settings/users/UsersSettingsPage.tsx
deleted file mode 100644
index 664f58d1..00000000
--- a/src/app/(main)/settings/users/UsersSettingsPage.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-'use client';
-import UsersDataTable from './UsersDataTable';
-import UsersHeader from './UsersHeader';
-
-export default function UsersSettingsPage() {
- return (
- <>
-
-
- >
- );
-}
diff --git a/src/app/(main)/settings/users/UsersTable.tsx b/src/app/(main)/settings/users/UsersTable.tsx
deleted file mode 100644
index 197995f0..00000000
--- a/src/app/(main)/settings/users/UsersTable.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { Text, Icon, Icons, GridTable, GridColumn } from 'react-basics';
-import { formatDistance } from 'date-fns';
-import { ROLES } from '@/lib/constants';
-import { useMessages, useLocale } from '@/components/hooks';
-import UserDeleteButton from './UserDeleteButton';
-import LinkButton from '@/components/common/LinkButton';
-
-export function UsersTable({
- data = [],
- showActions = true,
-}: {
- data: any[];
- showActions?: boolean;
-}) {
- const { formatMessage, labels } = useMessages();
- const { dateLocale } = useLocale();
-
- return (
-
-
-
- {row =>
- formatMessage(
- labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown,
- )
- }
-
-
- {row =>
- formatDistance(new Date(row.createdAt), new Date(), {
- addSuffix: true,
- locale: dateLocale,
- })
- }
-
-
- {row => row._count.website}
-
- {showActions && (
-
- {row => {
- const { id, username } = row;
- return (
- <>
-
-
-
-
-
- {formatMessage(labels.edit)}
-
- >
- );
- }}
-
- )}
-
- );
-}
-
-export default UsersTable;
diff --git a/src/app/(main)/settings/users/[userId]/UserEditForm.tsx b/src/app/(main)/settings/users/[userId]/UserEditForm.tsx
deleted file mode 100644
index 89bd5c02..00000000
--- a/src/app/(main)/settings/users/[userId]/UserEditForm.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import {
- Dropdown,
- Item,
- Form,
- FormRow,
- FormButtons,
- FormInput,
- TextField,
- SubmitButton,
- PasswordField,
-} from 'react-basics';
-import { useApi, useLogin, useMessages } from '@/components/hooks';
-import { ROLES } from '@/lib/constants';
-import { useContext, useRef } from 'react';
-import { UserContext } from './UserProvider';
-
-export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () => void }) {
- const { formatMessage, labels, messages } = useMessages();
- const { post, useMutation } = useApi();
- const { mutate, error } = useMutation({
- mutationFn: ({
- username,
- password,
- role,
- }: {
- username: string;
- password: string;
- role: string;
- }) => post(`/users/${userId}`, { username, password, role }),
- });
- const ref = useRef(null);
- const user = useContext(UserContext);
- const { user: login } = useLogin();
-
- const handleSubmit = async (data: any) => {
- mutate(data, {
- onSuccess: async () => {
- ref.current.reset(data);
- onSave?.();
- },
- });
- };
-
- const renderValue = (value: string) => {
- if (value === ROLES.user) {
- return formatMessage(labels.user);
- }
- if (value === ROLES.admin) {
- return formatMessage(labels.admin);
- }
- if (value === ROLES.viewOnly) {
- return formatMessage(labels.viewOnly);
- }
- };
-
- return (
-
- );
-}
-
-export default UserEditForm;
diff --git a/src/app/(main)/settings/users/[userId]/UserPage.tsx b/src/app/(main)/settings/users/[userId]/UserPage.tsx
deleted file mode 100644
index 50d5ab7e..00000000
--- a/src/app/(main)/settings/users/[userId]/UserPage.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-'use client';
-import UserSettings from './UserSettings';
-import UserProvider from './UserProvider';
-
-export default function ({ userId }: { userId: string }) {
- return (
-
-
-
- );
-}
diff --git a/src/app/(main)/settings/users/[userId]/UserProvider.tsx b/src/app/(main)/settings/users/[userId]/UserProvider.tsx
deleted file mode 100644
index ed559c91..00000000
--- a/src/app/(main)/settings/users/[userId]/UserProvider.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { createContext, ReactNode, useEffect } from 'react';
-import { useModified, useUser } from '@/components/hooks';
-import { Loading } from 'react-basics';
-
-export const UserContext = createContext(null);
-
-export function UserProvider({ userId, children }: { userId: string; children: ReactNode }) {
- const { modified } = useModified(`user:${userId}`);
- const { data: user, isFetching, isLoading, refetch } = useUser(userId);
-
- useEffect(() => {
- if (modified) {
- refetch();
- }
- }, [modified]);
-
- if (isFetching && isLoading) {
- return ;
- }
-
- return {children} ;
-}
-
-export default UserProvider;
diff --git a/src/app/(main)/settings/users/[userId]/UserSettings.tsx b/src/app/(main)/settings/users/[userId]/UserSettings.tsx
deleted file mode 100644
index 0d98205f..00000000
--- a/src/app/(main)/settings/users/[userId]/UserSettings.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Key, useContext, useState } from 'react';
-import { Item, Tabs, useToasts } from 'react-basics';
-import Icons from '@/components/icons';
-import UserEditForm from './UserEditForm';
-import PageHeader from '@/components/layout/PageHeader';
-import { useMessages } from '@/components/hooks';
-import UserWebsites from './UserWebsites';
-import { UserContext } from './UserProvider';
-import Breadcrumb from '@/components/common/Breadcrumb';
-
-export function UserSettings({ userId }: { userId: string }) {
- const { formatMessage, labels, messages } = useMessages();
- const [tab, setTab] = useState('details');
- const user = useContext(UserContext);
- const { showToast } = useToasts();
-
- const handleSave = () => {
- showToast({ message: formatMessage(messages.saved), variant: 'success' });
- };
-
- const breadcrumb = (
-
- );
-
- return (
- <>
- } breadcrumb={breadcrumb} />
-
- - {formatMessage(labels.details)}
- - {formatMessage(labels.websites)}
-
- {tab === 'details' && }
- {tab === 'websites' && }
- >
- );
-}
-
-export default UserSettings;
diff --git a/src/app/(main)/settings/users/[userId]/UserWebsites.tsx b/src/app/(main)/settings/users/[userId]/UserWebsites.tsx
deleted file mode 100644
index 15521b17..00000000
--- a/src/app/(main)/settings/users/[userId]/UserWebsites.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import WebsitesTable from '@/app/(main)/settings/websites/WebsitesTable';
-import DataTable from '@/components/common/DataTable';
-import { useWebsites } from '@/components/hooks';
-
-export function UserWebsites({ userId }) {
- const queryResult = useWebsites({ userId });
-
- return (
-
- {({ data }) => (
-
- )}
-
- );
-}
-
-export default UserWebsites;
diff --git a/src/app/(main)/settings/users/[userId]/page.tsx b/src/app/(main)/settings/users/[userId]/page.tsx
deleted file mode 100644
index 3b3a3fac..00000000
--- a/src/app/(main)/settings/users/[userId]/page.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import UserPage from './UserPage';
-import { Metadata } from 'next';
-
-export default async function ({ params }: { params: { userId: string } }) {
- const { userId } = await params;
-
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'User Settings',
-};
diff --git a/src/app/(main)/settings/users/page.tsx b/src/app/(main)/settings/users/page.tsx
deleted file mode 100644
index 01d5156f..00000000
--- a/src/app/(main)/settings/users/page.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Metadata } from 'next';
-import UsersSettingsPage from './UsersSettingsPage';
-
-export default function () {
- return ;
-}
-export const metadata: Metadata = {
- title: 'Users',
-};
diff --git a/src/app/(main)/settings/websites/WebsiteAddButton.tsx b/src/app/(main)/settings/websites/WebsiteAddButton.tsx
deleted file mode 100644
index 6f32fc9f..00000000
--- a/src/app/(main)/settings/websites/WebsiteAddButton.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useMessages, useModified } from '@/components/hooks';
-import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
-import WebsiteAddForm from './WebsiteAddForm';
-
-export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?: () => void }) {
- const { formatMessage, labels, messages } = useMessages();
- const { showToast } = useToasts();
- const { touch } = useModified();
-
- const handleSave = async () => {
- showToast({ message: formatMessage(messages.saved), variant: 'success' });
- touch('websites');
- onSave?.();
- };
-
- return (
-
-
-
-
-
- {formatMessage(labels.addWebsite)}
-
-
- {(close: () => void) => (
-
- )}
-
-
- );
-}
-
-export default WebsiteAddButton;
diff --git a/src/app/(main)/settings/websites/WebsiteAddForm.tsx b/src/app/(main)/settings/websites/WebsiteAddForm.tsx
deleted file mode 100644
index 90672412..00000000
--- a/src/app/(main)/settings/websites/WebsiteAddForm.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import {
- Form,
- FormRow,
- FormInput,
- FormButtons,
- TextField,
- Button,
- SubmitButton,
-} from 'react-basics';
-import { useApi } from '@/components/hooks';
-import { DOMAIN_REGEX } from '@/lib/constants';
-import { useMessages } from '@/components/hooks';
-
-export function WebsiteAddForm({
- teamId,
- onSave,
- onClose,
-}: {
- teamId?: string;
- onSave?: () => void;
- onClose?: () => void;
-}) {
- const { formatMessage, labels, messages } = useMessages();
- const { post, useMutation } = useApi();
- const { mutate, error, isPending } = useMutation({
- mutationFn: (data: any) => post('/websites', { ...data, teamId }),
- });
-
- const handleSubmit = async (data: any) => {
- mutate(data, {
- onSuccess: async () => {
- onSave?.();
- onClose?.();
- },
- });
- };
-
- return (
-
- );
-}
-
-export default WebsiteAddForm;
diff --git a/src/app/(main)/settings/websites/Websites.module.css b/src/app/(main)/settings/websites/Websites.module.css
deleted file mode 100644
index 2a7cc6e7..00000000
--- a/src/app/(main)/settings/websites/Websites.module.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.website {
- padding-bottom: 30px;
- border-bottom: 1px solid var(--base300);
- margin-bottom: 30px;
- align-self: stretch;
-}
-
-.website:last-child {
- border-bottom: 0;
- margin-bottom: 20px;
-}
diff --git a/src/app/(main)/settings/websites/WebsitesDataTable.tsx b/src/app/(main)/settings/websites/WebsitesDataTable.tsx
deleted file mode 100644
index 023df857..00000000
--- a/src/app/(main)/settings/websites/WebsitesDataTable.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { ReactNode } from 'react';
-import WebsitesTable from '@/app/(main)/settings/websites/WebsitesTable';
-import DataTable from '@/components/common/DataTable';
-import { useWebsites } from '@/components/hooks';
-
-export function WebsitesDataTable({
- teamId,
- allowEdit = true,
- allowView = true,
- showActions = true,
- children,
-}: {
- teamId?: string;
- allowEdit?: boolean;
- allowView?: boolean;
- showActions?: boolean;
- children?: ReactNode;
-}) {
- const queryResult = useWebsites({ teamId });
-
- return (
- children}>
- {({ data }) => (
-
- )}
-
- );
-}
-
-export default WebsitesDataTable;
diff --git a/src/app/(main)/settings/websites/WebsitesHeader.tsx b/src/app/(main)/settings/websites/WebsitesHeader.tsx
deleted file mode 100644
index 34e87a13..00000000
--- a/src/app/(main)/settings/websites/WebsitesHeader.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useMessages } from '@/components/hooks';
-import PageHeader from '@/components/layout/PageHeader';
-import WebsiteAddButton from './WebsiteAddButton';
-
-export interface WebsitesHeaderProps {
- teamId?: string;
- allowCreate?: boolean;
-}
-
-export function WebsitesHeader({ teamId, allowCreate = true }: WebsitesHeaderProps) {
- const { formatMessage, labels } = useMessages();
-
- return (
-
- {allowCreate && }
-
- );
-}
-
-export default WebsitesHeader;
diff --git a/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx b/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx
index 61909a9e..5009ec6c 100644
--- a/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx
+++ b/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx
@@ -1,17 +1,16 @@
'use client';
-import { useLogin } from '@/components/hooks';
-import WebsitesDataTable from './WebsitesDataTable';
-import WebsitesHeader from './WebsitesHeader';
-import { ROLES } from '@/lib/constants';
+import { Column } from '@umami/react-zen';
+import { WebsitesDataTable } from '@/app/(main)/websites/WebsitesDataTable';
+import { SectionHeader } from '@/components/common/SectionHeader';
+import { useMessages } from '@/components/hooks';
-export default function WebsitesSettingsPage({ teamId }: { teamId: string }) {
- const { user } = useLogin();
- const canCreate = user.role !== ROLES.viewOnly;
+export function WebsitesSettingsPage({ teamId }: { teamId: string }) {
+ const { formatMessage, labels } = useMessages();
return (
- <>
-
+
+
- >
+
);
}
diff --git a/src/app/(main)/settings/websites/WebsitesTable.tsx b/src/app/(main)/settings/websites/WebsitesTable.tsx
deleted file mode 100644
index 79749b97..00000000
--- a/src/app/(main)/settings/websites/WebsitesTable.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { ReactNode } from 'react';
-import { Text, Icon, Icons, GridTable, GridColumn } from 'react-basics';
-import { useMessages, useTeamUrl } from '@/components/hooks';
-import LinkButton from '@/components/common/LinkButton';
-
-export interface WebsitesTableProps {
- data: any[];
- showActions?: boolean;
- allowEdit?: boolean;
- allowView?: boolean;
- teamId?: string;
- children?: ReactNode;
-}
-
-export function WebsitesTable({
- data = [],
- showActions,
- allowEdit,
- allowView,
- children,
-}: WebsitesTableProps) {
- const { formatMessage, labels } = useMessages();
- const { renderTeamUrl } = useTeamUrl();
-
- if (!data?.length) {
- return children;
- }
-
- return (
-
-
-
- {showActions && (
-
- {row => {
- const { id: websiteId } = row;
-
- return (
- <>
- {allowEdit && (
-
-
-
-
- {formatMessage(labels.edit)}
-
- )}
- {allowView && (
-
-
-
-
- {formatMessage(labels.view)}
-
- )}
- >
- );
- }}
-
- )}
-
- );
-}
-
-export default WebsitesTable;
diff --git a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx
deleted file mode 100644
index 318e4e95..00000000
--- a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import {
- Form,
- FormRow,
- FormButtons,
- Flexbox,
- TextField,
- Button,
- Toggle,
- LoadingButton,
-} from 'react-basics';
-import { useContext, useState } from 'react';
-import { getRandomChars } from '@/lib/crypto';
-import { useApi, useMessages, useModified } from '@/components/hooks';
-import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
-
-const generateId = () => getRandomChars(16);
-
-export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () => void }) {
- const website = useContext(WebsiteContext);
- const { domain, shareId } = website;
- const { formatMessage, labels, messages } = useMessages();
- const [id, setId] = useState(shareId);
- const { post, useMutation } = useApi();
- const { mutate, error, isPending } = useMutation({
- mutationFn: (data: any) => post(`/websites/${website.id}`, data),
- });
- const { touch } = useModified();
-
- const url = `${hostUrl || window?.location.origin || ''}${
- process.env.basePath || ''
- }/share/${id}/${domain}`;
-
- const handleGenerate = () => {
- setId(generateId());
- };
-
- const handleCheck = (checked: boolean) => {
- const data = {
- name: website.name,
- domain: website.domain,
- shareId: checked ? generateId() : null,
- };
- mutate(data, {
- onSuccess: async () => {
- touch(`website:${website.id}`);
- onSave?.();
- },
- });
- setId(data.shareId);
- };
-
- const handleSave = () => {
- mutate(
- { name: website.name, domain: website.domain, shareId: id },
- {
- onSuccess: async () => {
- touch(`website:${website.id}`);
- onSave?.();
- },
- },
- );
- };
-
- return (
- <>
-
- {formatMessage(labels.enableShareUrl)}
-
- {id && (
-
- )}
- >
- );
-}
-
-export default ShareUrl;
diff --git a/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx b/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx
deleted file mode 100644
index cacdf689..00000000
--- a/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { TextArea } from 'react-basics';
-import { useMessages, useConfig } from '@/components/hooks';
-
-const SCRIPT_NAME = 'script.js';
-
-export function TrackingCode({ websiteId, hostUrl }: { websiteId: string; hostUrl?: string }) {
- const { formatMessage, messages } = useMessages();
- const config = useConfig();
-
- const trackerScriptName =
- config?.trackerScriptName?.split(',')?.map((n: string) => n.trim())?.[0] || SCRIPT_NAME;
-
- const url = trackerScriptName?.startsWith('http')
- ? trackerScriptName
- : `${hostUrl || window?.location.origin || ''}${
- process.env.basePath || ''
- }/${trackerScriptName}`;
-
- const code = ``;
-
- return (
- <>
- {formatMessage(messages.trackingCode)}
-
- >
- );
-}
-
-export default TrackingCode;
diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx
deleted file mode 100644
index d11f24df..00000000
--- a/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics';
-import { useRouter } from 'next/navigation';
-import { useLogin, useMessages, useModified, useTeams, useTeamUrl } from '@/components/hooks';
-import WebsiteDeleteForm from './WebsiteDeleteForm';
-import WebsiteResetForm from './WebsiteResetForm';
-import WebsiteTransferForm from './WebsiteTransferForm';
-import { ROLES } from '@/lib/constants';
-
-export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
- const { formatMessage, labels, messages } = useMessages();
- const { user } = useLogin();
- const { touch } = useModified();
- const { teamId, renderTeamUrl } = useTeamUrl();
- const router = useRouter();
- const { result } = useTeams(user.id);
- const canTransferWebsite =
- (
- !teamId &&
- result.data.filter(({ teamUser }) =>
- teamUser.find(
- ({ role, userId }) =>
- [ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
- ),
- )
- ).length > 0 ||
- (teamId &&
- !!result?.data
- ?.find(({ id }) => id === teamId)
- ?.teamUser.find(({ role, userId }) => role === ROLES.teamOwner && userId === user.id));
-
- const handleSave = () => {
- touch('websites');
- onSave?.();
- router.push(renderTeamUrl(`/settings/websites`));
- };
-
- const handleReset = async () => {
- onSave?.();
- };
-
- return (
- <>
-
-
-
- {formatMessage(labels.transfer)}
-
-
- {(close: () => void) => (
-
- )}
-
-
-
-
-
- {formatMessage(labels.reset)}
-
- {(close: () => void) => (
-
- )}
-
-
-
-
-
-
- {formatMessage(labels.delete)}
-
-
- {(close: () => void) => (
-
- )}
-
-
-
- >
- );
-}
-
-export default WebsiteData;
diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteEditForm.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteEditForm.tsx
deleted file mode 100644
index aeef7f34..00000000
--- a/src/app/(main)/settings/websites/[websiteId]/WebsiteEditForm.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { useContext, useRef } from 'react';
-import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics';
-import { useApi, useMessages, useModified } from '@/components/hooks';
-import { DOMAIN_REGEX } from '@/lib/constants';
-import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
-
-export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
- const website = useContext(WebsiteContext);
- const { formatMessage, labels, messages } = useMessages();
- const { post, useMutation } = useApi();
- const { mutate, error } = useMutation({
- mutationFn: (data: any) => post(`/websites/${websiteId}`, data),
- });
- const ref = useRef(null);
- const { touch } = useModified();
-
- const handleSubmit = async (data: any) => {
- mutate(data, {
- onSuccess: async () => {
- ref.current.reset(data);
- touch(`website:${website.id}`);
- onSave?.();
- },
- });
- };
-
- return (
-
- );
-}
-
-export default WebsiteEditForm;
diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx
deleted file mode 100644
index 5bea2704..00000000
--- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
-import Breadcrumb from '@/components/common/Breadcrumb';
-import { useMessages } from '@/components/hooks';
-import Icons from '@/components/icons';
-import PageHeader from '@/components/layout/PageHeader';
-import Link from 'next/link';
-import { Key, useContext, useState } from 'react';
-import { Button, Icon, Item, Tabs, Text, useToasts } from 'react-basics';
-import ShareUrl from './ShareUrl';
-import TrackingCode from './TrackingCode';
-import WebsiteData from './WebsiteData';
-import WebsiteEditForm from './WebsiteEditForm';
-
-export function WebsiteSettings({
- websiteId,
- openExternal = false,
-}: {
- websiteId: string;
- openExternal?: boolean;
-}) {
- const website = useContext(WebsiteContext);
- const { formatMessage, labels, messages } = useMessages();
- const [tab, setTab] = useState('details');
- const { showToast } = useToasts();
-
- const handleSave = () => {
- showToast({ message: formatMessage(messages.saved), variant: 'success' });
- };
-
- const breadcrumb = (
-
- );
-
- return (
- <>
- } breadcrumb={breadcrumb}>
-
-
-
-
-
- {formatMessage(labels.view)}
-
-
-
-
- - {formatMessage(labels.details)}
- - {formatMessage(labels.trackingCode)}
- - {formatMessage(labels.shareUrl)}
- - {formatMessage(labels.data)}
-
- {tab === 'details' && }
- {tab === 'tracking' && }
- {tab === 'share' && }
- {tab === 'data' && }
- >
- );
-}
-
-export default WebsiteSettings;
diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx
index 8d7badb8..53b4cd9c 100644
--- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx
+++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx
@@ -1,11 +1,16 @@
'use client';
-import WebsiteProvider from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
-import WebsiteSettings from './WebsiteSettings';
+import { Column } from '@umami/react-zen';
+import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings';
+import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader';
+import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
-export default function WebsiteSettingsPage({ websiteId }: { websiteId: string }) {
+export function WebsiteSettingsPage({ websiteId }: { websiteId: string }) {
return (
-
+
+
+
+
);
}
diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteTransferForm.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteTransferForm.tsx
deleted file mode 100644
index 8214fb16..00000000
--- a/src/app/(main)/settings/websites/[websiteId]/WebsiteTransferForm.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import { Key, useContext, useState } from 'react';
-import {
- Button,
- Form,
- FormButtons,
- FormRow,
- LoadingButton,
- Loading,
- Dropdown,
- Item,
- Flexbox,
-} from 'react-basics';
-import { useApi, useLogin, useMessages, useTeams } from '@/components/hooks';
-import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
-import { ROLES } from '@/lib/constants';
-
-export function WebsiteTransferForm({
- websiteId,
- onSave,
- onClose,
-}: {
- websiteId: string;
- onSave?: () => void;
- onClose?: () => void;
-}) {
- const { user } = useLogin();
- const website = useContext(WebsiteContext);
- const [teamId, setTeamId] = useState(null);
- const { formatMessage, labels, messages } = useMessages();
- const { post, useMutation } = useApi();
- const { mutate, isPending, error } = useMutation({
- mutationFn: (data: any) => post(`/websites/${websiteId}/transfer`, data),
- });
- const { result, query } = useTeams(user.id);
- const isTeamWebsite = !!website?.teamId;
-
- const handleSubmit = async () => {
- mutate(
- {
- userId: website.teamId ? user.id : undefined,
- teamId: website.userId ? teamId : undefined,
- },
- {
- onSuccess: async () => {
- onSave?.();
- onClose?.();
- },
- },
- );
- };
-
- const handleChange = (key: Key) => {
- setTeamId(key as string);
- };
-
- const renderValue = (teamId: string) => result?.data?.find(({ id }) => id === teamId)?.name;
-
- if (query.isLoading) {
- return ;
- }
-
- return (
-
- );
-}
-
-export default WebsiteTransferForm;
diff --git a/src/app/(main)/settings/websites/[websiteId]/page.tsx b/src/app/(main)/settings/websites/[websiteId]/page.tsx
index 7e2feaf2..9adfc915 100644
--- a/src/app/(main)/settings/websites/[websiteId]/page.tsx
+++ b/src/app/(main)/settings/websites/[websiteId]/page.tsx
@@ -1,7 +1,7 @@
-import WebsiteSettingsPage from './WebsiteSettingsPage';
-import { Metadata } from 'next';
+import type { Metadata } from 'next';
+import { WebsiteSettingsPage } from './WebsiteSettingsPage';
-export default async function ({ params }: { params: { websiteId: string } }) {
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params;
return ;
diff --git a/src/app/(main)/settings/websites/page.tsx b/src/app/(main)/settings/websites/page.tsx
index d05be0a5..19c14fd6 100644
--- a/src/app/(main)/settings/websites/page.tsx
+++ b/src/app/(main)/settings/websites/page.tsx
@@ -1,7 +1,7 @@
-import { Metadata } from 'next';
-import WebsitesSettingsPage from './WebsitesSettingsPage';
+import type { Metadata } from 'next';
+import { WebsitesSettingsPage } from './WebsitesSettingsPage';
-export default async function ({ params }: { params: { teamId: string } }) {
+export default async function ({ params }: { params: Promise<{ teamId: string }> }) {
const { teamId } = await params;
return ;
diff --git a/src/app/(main)/teams/TeamAddForm.tsx b/src/app/(main)/teams/TeamAddForm.tsx
new file mode 100644
index 00000000..c95259f4
--- /dev/null
+++ b/src/app/(main)/teams/TeamAddForm.tsx
@@ -0,0 +1,39 @@
+import {
+ Button,
+ Form,
+ FormButtons,
+ FormField,
+ FormSubmitButton,
+ TextField,
+} from '@umami/react-zen';
+import { useMessages, useUpdateQuery } from '@/components/hooks';
+
+export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
+ const { formatMessage, labels, getErrorMessage } = useMessages();
+ const { mutateAsync, error, isPending } = useUpdateQuery('/teams');
+
+ const handleSubmit = async (data: any) => {
+ await mutateAsync(data, {
+ onSuccess: async () => {
+ onSave?.();
+ onClose?.();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/teams/TeamJoinForm.tsx b/src/app/(main)/teams/TeamJoinForm.tsx
new file mode 100644
index 00000000..69780788
--- /dev/null
+++ b/src/app/(main)/teams/TeamJoinForm.tsx
@@ -0,0 +1,40 @@
+import {
+ Button,
+ Form,
+ FormButtons,
+ FormField,
+ FormSubmitButton,
+ TextField,
+} from '@umami/react-zen';
+import { useMessages, useUpdateQuery } from '@/components/hooks';
+
+export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
+ const { formatMessage, labels, getErrorMessage } = useMessages();
+ const { mutateAsync, error, touch } = useUpdateQuery('/teams/join');
+
+ const handleSubmit = async (data: any) => {
+ await mutateAsync(data, {
+ onSuccess: async () => {
+ touch('teams:members');
+ onSave?.();
+ onClose?.();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/teams/TeamLeaveButton.tsx b/src/app/(main)/teams/TeamLeaveButton.tsx
new file mode 100644
index 00000000..2cca76f8
--- /dev/null
+++ b/src/app/(main)/teams/TeamLeaveButton.tsx
@@ -0,0 +1,41 @@
+import { Button, Dialog, DialogTrigger, Icon, Modal, Text } from '@umami/react-zen';
+import { useRouter } from 'next/navigation';
+import { useLoginQuery, useMessages, useModified } from '@/components/hooks';
+import { LogOut } from '@/components/icons';
+import { TeamLeaveForm } from './TeamLeaveForm';
+
+export function TeamLeaveButton({ teamId, teamName }: { teamId: string; teamName: string }) {
+ const { formatMessage, labels } = useMessages();
+ const router = useRouter();
+ const { user } = useLoginQuery();
+ const { touch } = useModified();
+
+ const handleLeave = async () => {
+ touch('teams');
+ router.push('/settings/teams');
+ };
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.leave)}
+
+
+
+ {({ close }) => (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/(main)/teams/TeamLeaveForm.tsx b/src/app/(main)/teams/TeamLeaveForm.tsx
new file mode 100644
index 00000000..b3dcaf58
--- /dev/null
+++ b/src/app/(main)/teams/TeamLeaveForm.tsx
@@ -0,0 +1,48 @@
+import { ConfirmationForm } from '@/components/common/ConfirmationForm';
+import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
+
+export function TeamLeaveForm({
+ teamId,
+ userId,
+ teamName,
+ onSave,
+ onClose,
+}: {
+ teamId: string;
+ userId: string;
+ teamName: string;
+ onSave: () => void;
+ onClose: () => void;
+}) {
+ const { formatMessage, labels, messages, getErrorMessage, FormattedMessage } = useMessages();
+ const { mutateAsync, error, isPending } = useDeleteQuery(`/teams/${teamId}/users/${userId}`);
+ const { touch } = useModified();
+
+ const handleConfirm = async () => {
+ await mutateAsync(null, {
+ onSuccess: async () => {
+ touch('teams:members');
+ onSave();
+ onClose();
+ },
+ });
+ };
+
+ return (
+ {teamName},
+ }}
+ />
+ }
+ onConfirm={handleConfirm}
+ onClose={onClose}
+ isLoading={isPending}
+ error={getErrorMessage(error)}
+ />
+ );
+}
diff --git a/src/app/(main)/teams/TeamProvider.tsx b/src/app/(main)/teams/TeamProvider.tsx
new file mode 100644
index 00000000..cea41614
--- /dev/null
+++ b/src/app/(main)/teams/TeamProvider.tsx
@@ -0,0 +1,21 @@
+'use client';
+import { Loading } from '@umami/react-zen';
+import { createContext, type ReactNode } from 'react';
+import { useTeamQuery } from '@/components/hooks/queries/useTeamQuery';
+import type { Team } from '@/generated/prisma/client';
+
+export const TeamContext = createContext(null);
+
+export function TeamProvider({ teamId, children }: { teamId?: string; children: ReactNode }) {
+ const { data: team, isLoading, isFetching } = useTeamQuery(teamId);
+
+ if (isFetching && isLoading) {
+ return ;
+ }
+
+ if (!team) {
+ return null;
+ }
+
+ return {children} ;
+}
diff --git a/src/app/(main)/settings/teams/TeamsAddButton.tsx b/src/app/(main)/teams/TeamsAddButton.tsx
similarity index 50%
rename from src/app/(main)/settings/teams/TeamsAddButton.tsx
rename to src/app/(main)/teams/TeamsAddButton.tsx
index 58c138a8..578a273a 100644
--- a/src/app/(main)/settings/teams/TeamsAddButton.tsx
+++ b/src/app/(main)/teams/TeamsAddButton.tsx
@@ -1,33 +1,33 @@
-import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
-import Icons from '@/components/icons';
+import { Button, Dialog, DialogTrigger, Icon, Modal, Text, useToast } from '@umami/react-zen';
import { useMessages, useModified } from '@/components/hooks';
-import TeamAddForm from './TeamAddForm';
+import { Plus } from '@/components/icons';
import { messages } from '@/components/messages';
+import { TeamAddForm } from './TeamAddForm';
export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
const { formatMessage, labels } = useMessages();
- const { showToast } = useToasts();
+ const { toast } = useToast();
const { touch } = useModified();
const handleSave = async () => {
- showToast({ message: formatMessage(messages.saved), variant: 'success' });
+ toast(formatMessage(messages.saved));
touch('teams');
onSave?.();
};
return (
-
+
-
+
{formatMessage(labels.createTeam)}
-
- {(close: () => void) => }
+
+
+ {({ close }) => }
+
-
+
);
}
-
-export default TeamsAddButton;
diff --git a/src/app/(main)/teams/TeamsDataTable.tsx b/src/app/(main)/teams/TeamsDataTable.tsx
new file mode 100644
index 00000000..cdce7b93
--- /dev/null
+++ b/src/app/(main)/teams/TeamsDataTable.tsx
@@ -0,0 +1,27 @@
+import Link from 'next/link';
+import { DataGrid } from '@/components/common/DataGrid';
+import { useLoginQuery, useNavigation, useUserTeamsQuery } from '@/components/hooks';
+import { TeamsTable } from './TeamsTable';
+
+export function TeamsDataTable() {
+ const { user } = useLoginQuery();
+ const query = useUserTeamsQuery(user.id);
+ const { pathname } = useNavigation();
+ const isSettings = pathname.includes('/settings');
+
+ const renderLink = (row: any) => {
+ return (
+
+ {row.name}
+
+ );
+ };
+
+ return (
+
+ {({ data }) => {
+ return ;
+ }}
+
+ );
+}
diff --git a/src/app/(main)/teams/TeamsHeader.tsx b/src/app/(main)/teams/TeamsHeader.tsx
new file mode 100644
index 00000000..579ba595
--- /dev/null
+++ b/src/app/(main)/teams/TeamsHeader.tsx
@@ -0,0 +1,26 @@
+import { Row } from '@umami/react-zen';
+import { PageHeader } from '@/components/common/PageHeader';
+import { useLoginQuery, useMessages } from '@/components/hooks';
+import { ROLES } from '@/lib/constants';
+import { TeamsAddButton } from './TeamsAddButton';
+import { TeamsJoinButton } from './TeamsJoinButton';
+
+export function TeamsHeader({
+ allowCreate = true,
+ allowJoin = true,
+}: {
+ allowCreate?: boolean;
+ allowJoin?: boolean;
+}) {
+ const { formatMessage, labels } = useMessages();
+ const { user } = useLoginQuery();
+
+ return (
+
+
+ {allowJoin && }
+ {allowCreate && user.role !== ROLES.viewOnly && }
+
+
+ );
+}
diff --git a/src/app/(main)/teams/TeamsJoinButton.tsx b/src/app/(main)/teams/TeamsJoinButton.tsx
new file mode 100644
index 00000000..017211e2
--- /dev/null
+++ b/src/app/(main)/teams/TeamsJoinButton.tsx
@@ -0,0 +1,31 @@
+import { Button, Dialog, DialogTrigger, Icon, Modal, Text, useToast } from '@umami/react-zen';
+import { useMessages, useModified } from '@/components/hooks';
+import { UserPlus } from '@/components/icons';
+import { TeamJoinForm } from './TeamJoinForm';
+
+export function TeamsJoinButton() {
+ const { formatMessage, labels, messages } = useMessages();
+ const { toast } = useToast();
+ const { touch } = useModified();
+
+ const handleJoin = () => {
+ toast(formatMessage(messages.saved));
+ touch('teams');
+ };
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.joinTeam)}
+
+
+
+ {({ close }) => }
+
+
+
+ );
+}
diff --git a/src/app/(main)/teams/TeamsPage.tsx b/src/app/(main)/teams/TeamsPage.tsx
new file mode 100644
index 00000000..5b11bcf8
--- /dev/null
+++ b/src/app/(main)/teams/TeamsPage.tsx
@@ -0,0 +1,19 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { TeamsDataTable } from '@/app/(main)/teams/TeamsDataTable';
+import { TeamsHeader } from '@/app/(main)/teams/TeamsHeader';
+import { PageBody } from '@/components/common/PageBody';
+import { Panel } from '@/components/common/Panel';
+
+export function TeamsPage() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/teams/TeamsTable.tsx b/src/app/(main)/teams/TeamsTable.tsx
new file mode 100644
index 00000000..754f0b2b
--- /dev/null
+++ b/src/app/(main)/teams/TeamsTable.tsx
@@ -0,0 +1,29 @@
+import { DataColumn, DataTable, type DataTableProps } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { useMessages } from '@/components/hooks';
+import { ROLES } from '@/lib/constants';
+
+export interface TeamsTableProps extends DataTableProps {
+ renderLink?: (row: any) => ReactNode;
+}
+
+export function TeamsTable({ renderLink, ...props }: TeamsTableProps) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+ {renderLink}
+
+
+ {(row: any) => row?.members?.find(({ role }) => role === ROLES.teamOwner)?.user?.username}
+
+
+ {(row: any) => row?._count?.members}
+
+
+ {(row: any) => row?._count?.websites}
+
+
+ );
+}
diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm.tsx b/src/app/(main)/teams/[teamId]/TeamDeleteForm.tsx
similarity index 56%
rename from src/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm.tsx
rename to src/app/(main)/teams/[teamId]/TeamDeleteForm.tsx
index 5e7f5cf8..7adc9b34 100644
--- a/src/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm.tsx
+++ b/src/app/(main)/teams/[teamId]/TeamDeleteForm.tsx
@@ -1,5 +1,5 @@
-import TypeConfirmationForm from '@/components/common/TypeConfirmationForm';
-import { useApi, useMessages } from '@/components/hooks';
+import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
+import { useDeleteQuery, useMessages } from '@/components/hooks';
const CONFIRM_VALUE = 'DELETE';
@@ -12,15 +12,14 @@ export function TeamDeleteForm({
onSave?: () => void;
onClose?: () => void;
}) {
- const { labels, formatMessage } = useMessages();
- const { del, useMutation } = useApi();
- const { mutate, error, isPending } = useMutation({
- mutationFn: () => del(`/teams/${teamId}`),
- });
+ const { labels, formatMessage, getErrorMessage } = useMessages();
+ const { mutateAsync, error, isPending, touch } = useDeleteQuery(`/teams/${teamId}`);
const handleConfirm = async () => {
- mutate(null, {
+ await mutateAsync(null, {
onSuccess: async () => {
+ touch('teams');
+ touch(`teams:${teamId}`);
onSave?.();
onClose?.();
},
@@ -33,11 +32,9 @@ export function TeamDeleteForm({
onConfirm={handleConfirm}
onClose={onClose}
isLoading={isPending}
- error={error}
+ error={getErrorMessage(error)}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
);
}
-
-export default TeamDeleteForm;
diff --git a/src/app/(main)/teams/[teamId]/TeamEditForm.tsx b/src/app/(main)/teams/[teamId]/TeamEditForm.tsx
new file mode 100644
index 00000000..74e038f6
--- /dev/null
+++ b/src/app/(main)/teams/[teamId]/TeamEditForm.tsx
@@ -0,0 +1,89 @@
+import {
+ Button,
+ Form,
+ FormButtons,
+ FormField,
+ FormSubmitButton,
+ IconLabel,
+ Row,
+ TextField,
+} from '@umami/react-zen';
+import { useMessages, useTeam, useUpdateQuery } from '@/components/hooks';
+import { RefreshCw } from '@/components/icons';
+import { getRandomChars } from '@/lib/generate';
+
+const generateId = () => `team_${getRandomChars(16)}`;
+
+export function TeamEditForm({
+ teamId,
+ allowEdit,
+ showAccessCode,
+ onSave,
+}: {
+ teamId: string;
+ allowEdit?: boolean;
+ showAccessCode?: boolean;
+ onSave?: () => void;
+}) {
+ const team = useTeam();
+ const { formatMessage, labels, messages, getErrorMessage } = useMessages();
+
+ const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(`/teams/${teamId}`);
+
+ const handleSubmit = async (data: any) => {
+ await mutateAsync(data, {
+ onSuccess: async () => {
+ toast(formatMessage(messages.saved));
+ touch('teams');
+ touch(`teams:${teamId}`);
+ onSave?.();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamManage.tsx b/src/app/(main)/teams/[teamId]/TeamManage.tsx
similarity index 59%
rename from src/app/(main)/teams/[teamId]/settings/team/TeamManage.tsx
rename to src/app/(main)/teams/[teamId]/TeamManage.tsx
index 24ca93d3..88cbad9e 100644
--- a/src/app/(main)/teams/[teamId]/settings/team/TeamManage.tsx
+++ b/src/app/(main)/teams/[teamId]/TeamManage.tsx
@@ -1,7 +1,8 @@
-import { useMessages, useModified } from '@/components/hooks';
+import { Button, Dialog, DialogTrigger, Modal } from '@umami/react-zen';
import { useRouter } from 'next/navigation';
-import { ActionForm, Button, Modal, ModalTrigger } from 'react-basics';
-import TeamDeleteForm from './TeamDeleteForm';
+import { ActionForm } from '@/components/common/ActionForm';
+import { useMessages, useModified } from '@/components/hooks';
+import { TeamDeleteForm } from './TeamDeleteForm';
export function TeamManage({ teamId }: { teamId: string }) {
const { formatMessage, labels, messages } = useMessages();
@@ -18,16 +19,14 @@ export function TeamManage({ teamId }: { teamId: string }) {
label={formatMessage(labels.deleteTeam)}
description={formatMessage(messages.deleteTeamWarning)}
>
-
+
{formatMessage(labels.delete)}
-
- {(close: () => void) => (
-
- )}
+
+
+ {({ close }) => }
+
-
+
);
}
-
-export default TeamManage;
diff --git a/src/app/(main)/teams/[teamId]/TeamMemberEditButton.tsx b/src/app/(main)/teams/[teamId]/TeamMemberEditButton.tsx
new file mode 100644
index 00000000..f75b6d18
--- /dev/null
+++ b/src/app/(main)/teams/[teamId]/TeamMemberEditButton.tsx
@@ -0,0 +1,46 @@
+import { useToast } from '@umami/react-zen';
+import { useMessages, useModified } from '@/components/hooks';
+import { Edit } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { TeamMemberEditForm } from './TeamMemberEditForm';
+
+export function TeamMemberEditButton({
+ teamId,
+ userId,
+ role,
+ onSave,
+}: {
+ teamId: string;
+ userId: string;
+ role: string;
+ onSave?: () => void;
+}) {
+ const { formatMessage, labels, messages } = useMessages();
+ const { toast } = useToast();
+ const { touch } = useModified();
+
+ const handleSave = () => {
+ touch('teams:members');
+ toast(formatMessage(messages.saved));
+ onSave?.();
+ };
+
+ return (
+ }
+ title={formatMessage(labels.editMember)}
+ variant="quiet"
+ width="400px"
+ >
+ {({ close }) => (
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/teams/[teamId]/TeamMemberEditForm.tsx b/src/app/(main)/teams/[teamId]/TeamMemberEditForm.tsx
new file mode 100644
index 00000000..4826746f
--- /dev/null
+++ b/src/app/(main)/teams/[teamId]/TeamMemberEditForm.tsx
@@ -0,0 +1,62 @@
+import {
+ Button,
+ Form,
+ FormButtons,
+ FormField,
+ FormSubmitButton,
+ ListItem,
+ Select,
+} from '@umami/react-zen';
+import { useMessages, useUpdateQuery } from '@/components/hooks';
+import { ROLES } from '@/lib/constants';
+
+export function TeamMemberEditForm({
+ teamId,
+ userId,
+ role,
+ onSave,
+ onClose,
+}: {
+ teamId: string;
+ userId: string;
+ role: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { mutateAsync, error, isPending } = useUpdateQuery(`/teams/${teamId}/users/${userId}`);
+ const { formatMessage, labels, getErrorMessage } = useMessages();
+
+ const handleSubmit = async (data: any) => {
+ await mutateAsync(data, {
+ onSuccess: async () => {
+ onSave();
+ onClose();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/teams/[teamId]/TeamMemberRemoveButton.tsx b/src/app/(main)/teams/[teamId]/TeamMemberRemoveButton.tsx
new file mode 100644
index 00000000..4d3e8e91
--- /dev/null
+++ b/src/app/(main)/teams/[teamId]/TeamMemberRemoveButton.tsx
@@ -0,0 +1,60 @@
+import { ConfirmationForm } from '@/components/common/ConfirmationForm';
+import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
+import { Trash } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { messages } from '@/components/messages';
+
+export function TeamMemberRemoveButton({
+ teamId,
+ userId,
+ userName,
+ onSave,
+}: {
+ teamId: string;
+ userId: string;
+ userName: string;
+ disabled?: boolean;
+ onSave?: () => void;
+}) {
+ const { formatMessage, labels, FormattedMessage } = useMessages();
+ const { mutateAsync, isPending, error } = useDeleteQuery(`/teams/${teamId}/users/${userId}`);
+ const { touch } = useModified();
+
+ const handleConfirm = async (close: () => void) => {
+ await mutateAsync(null, {
+ onSuccess: () => {
+ touch('teams:members');
+ onSave?.();
+ close();
+ },
+ });
+ };
+
+ return (
+ }
+ title={formatMessage(labels.confirm)}
+ variant="quiet"
+ width="400px"
+ >
+ {({ close }) => (
+ {userName},
+ }}
+ />
+ }
+ isLoading={isPending}
+ error={error}
+ onConfirm={handleConfirm.bind(null, close)}
+ onClose={close}
+ buttonLabel={formatMessage(labels.remove)}
+ buttonVariant="danger"
+ />
+ )}
+
+ );
+}
diff --git a/src/app/(main)/teams/[teamId]/TeamMembersDataTable.tsx b/src/app/(main)/teams/[teamId]/TeamMembersDataTable.tsx
new file mode 100644
index 00000000..52c0fe38
--- /dev/null
+++ b/src/app/(main)/teams/[teamId]/TeamMembersDataTable.tsx
@@ -0,0 +1,19 @@
+import { DataGrid } from '@/components/common/DataGrid';
+import { useTeamMembersQuery } from '@/components/hooks';
+import { TeamMembersTable } from './TeamMembersTable';
+
+export function TeamMembersDataTable({
+ teamId,
+ allowEdit = false,
+}: {
+ teamId: string;
+ allowEdit?: boolean;
+}) {
+ const queryResult = useTeamMembersQuery(teamId);
+
+ return (
+
+ {({ data }) => }
+
+ );
+}
diff --git a/src/app/(main)/teams/[teamId]/TeamMembersTable.tsx b/src/app/(main)/teams/[teamId]/TeamMembersTable.tsx
new file mode 100644
index 00000000..8414908c
--- /dev/null
+++ b/src/app/(main)/teams/[teamId]/TeamMembersTable.tsx
@@ -0,0 +1,55 @@
+import { DataColumn, DataTable, Row } from '@umami/react-zen';
+import { useMessages } from '@/components/hooks';
+import { ROLES } from '@/lib/constants';
+import { TeamMemberEditButton } from './TeamMemberEditButton';
+import { TeamMemberRemoveButton } from './TeamMemberRemoveButton';
+
+export function TeamMembersTable({
+ data = [],
+ teamId,
+ allowEdit = false,
+}: {
+ data: any[];
+ teamId: string;
+ allowEdit: boolean;
+}) {
+ const { formatMessage, labels } = useMessages();
+
+ const roles = {
+ [ROLES.teamOwner]: formatMessage(labels.teamOwner),
+ [ROLES.teamManager]: formatMessage(labels.teamManager),
+ [ROLES.teamMember]: formatMessage(labels.teamMember),
+ [ROLES.teamViewOnly]: formatMessage(labels.viewOnly),
+ };
+
+ return (
+
+
+ {(row: any) => row?.user?.username}
+
+
+ {(row: any) => roles[row?.role]}
+
+ {allowEdit && (
+
+ {(row: any) => {
+ if (row?.role === ROLES.teamOwner) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ );
+ }}
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/teams/[teamId]/TeamProvider.tsx b/src/app/(main)/teams/[teamId]/TeamProvider.tsx
deleted file mode 100644
index ed2d5467..00000000
--- a/src/app/(main)/teams/[teamId]/TeamProvider.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-'use client';
-import { createContext, ReactNode, useEffect } from 'react';
-import { useTeam, useModified } from '@/components/hooks';
-import { Loading } from 'react-basics';
-
-export const TeamContext = createContext(null);
-
-export function TeamProvider({ teamId, children }: { teamId?: string; children: ReactNode }) {
- const { modified } = useModified(`teams`);
- const { data: team, isLoading, isFetching, refetch } = useTeam(teamId);
-
- useEffect(() => {
- if (teamId && modified) {
- refetch();
- }
- }, [teamId, modified]);
-
- if (isFetching && isLoading) {
- return ;
- }
-
- if (teamId && !team) {
- return null;
- }
-
- return {children} ;
-}
-
-export default TeamProvider;
diff --git a/src/app/(main)/teams/[teamId]/TeamSettings.tsx b/src/app/(main)/teams/[teamId]/TeamSettings.tsx
new file mode 100644
index 00000000..3ddbe000
--- /dev/null
+++ b/src/app/(main)/teams/[teamId]/TeamSettings.tsx
@@ -0,0 +1,49 @@
+import { Column } from '@umami/react-zen';
+import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
+import { PageHeader } from '@/components/common/PageHeader';
+import { Panel } from '@/components/common/Panel';
+import { useLoginQuery, useNavigation, useTeam } from '@/components/hooks';
+import { Users } from '@/components/icons';
+import { ROLES } from '@/lib/constants';
+import { TeamEditForm } from './TeamEditForm';
+import { TeamManage } from './TeamManage';
+import { TeamMembersDataTable } from './TeamMembersDataTable';
+
+export function TeamSettings({ teamId }: { teamId: string }) {
+ const team: any = useTeam();
+ const { user } = useLoginQuery();
+ const { pathname } = useNavigation();
+
+ const isAdmin = pathname.includes('/admin');
+
+ const isTeamOwner =
+ !!team?.members?.find(({ userId, role }) => role === ROLES.teamOwner && userId === user.id) &&
+ user.role !== ROLES.viewOnly;
+
+ const canEdit =
+ user.isAdmin ||
+ (!!team?.members?.find(
+ ({ userId, role }) =>
+ (role === ROLES.teamOwner || role === ROLES.teamManager) && userId === user.id,
+ ) &&
+ user.role !== ROLES.viewOnly);
+
+ return (
+
+ }>
+ {!isTeamOwner && !isAdmin && }
+
+
+
+
+
+
+
+ {isTeamOwner && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton.tsx b/src/app/(main)/teams/[teamId]/TeamWebsiteRemoveButton.tsx
similarity index 50%
rename from src/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton.tsx
rename to src/app/(main)/teams/[teamId]/TeamWebsiteRemoveButton.tsx
index fdd76cd2..f2b4ecea 100644
--- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton.tsx
+++ b/src/app/(main)/teams/[teamId]/TeamWebsiteRemoveButton.tsx
@@ -1,15 +1,13 @@
-import { useApi, useMessages } from '@/components/hooks';
-import { Icon, Icons, LoadingButton, Text } from 'react-basics';
+import { Icon, LoadingButton, Text } from '@umami/react-zen';
+import { useDeleteQuery, useMessages } from '@/components/hooks';
+import { X } from '@/components/icons';
export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
const { formatMessage, labels } = useMessages();
- const { del, useMutation } = useApi();
- const { mutate, isPending } = useMutation({
- mutationFn: () => del(`/teams/${teamId}/websites/${websiteId}`),
- });
+ const { mutateAsync } = useDeleteQuery(`/teams/${teamId}/websites/${websiteId}`);
const handleRemoveTeamMember = async () => {
- mutate(null, {
+ await mutateAsync(null, {
onSuccess: () => {
onSave();
},
@@ -17,13 +15,11 @@ export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
};
return (
- handleRemoveTeamMember()} isLoading={isPending}>
+ handleRemoveTeamMember()}>
-
+
{formatMessage(labels.remove)}
);
}
-
-export default TeamWebsiteRemoveButton;
diff --git a/src/app/(main)/teams/[teamId]/TeamWebsitesDataTable.tsx b/src/app/(main)/teams/[teamId]/TeamWebsitesDataTable.tsx
new file mode 100644
index 00000000..6a2e4f45
--- /dev/null
+++ b/src/app/(main)/teams/[teamId]/TeamWebsitesDataTable.tsx
@@ -0,0 +1,19 @@
+import { DataGrid } from '@/components/common/DataGrid';
+import { useTeamWebsitesQuery } from '@/components/hooks';
+import { TeamWebsitesTable } from './TeamWebsitesTable';
+
+export function TeamWebsitesDataTable({
+ teamId,
+ allowEdit = false,
+}: {
+ teamId: string;
+ allowEdit?: boolean;
+}) {
+ const queryResult = useTeamWebsitesQuery(teamId);
+
+ return (
+
+ {({ data }) => }
+
+ );
+}
diff --git a/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx b/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx
new file mode 100644
index 00000000..10f56543
--- /dev/null
+++ b/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx
@@ -0,0 +1,50 @@
+import { DataColumn, DataTable, Row } from '@umami/react-zen';
+import Link from 'next/link';
+import { TeamMemberEditButton } from '@/app/(main)/teams/[teamId]/TeamMemberEditButton';
+import { TeamMemberRemoveButton } from '@/app/(main)/teams/[teamId]/TeamMemberRemoveButton';
+import { useMessages } from '@/components/hooks';
+import { ROLES } from '@/lib/constants';
+
+export function TeamWebsitesTable({
+ teamId,
+ data = [],
+ allowEdit,
+}: {
+ teamId: string;
+ data: any[];
+ allowEdit: boolean;
+}) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+ {(row: any) => {row.name}}
+
+
+
+ {(row: any) => row?.createUser?.username}
+
+ {allowEdit && (
+
+ {(row: any) => {
+ if (row?.role === ROLES.teamOwner) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ );
+ }}
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/teams/[teamId]/layout.tsx b/src/app/(main)/teams/[teamId]/layout.tsx
deleted file mode 100644
index 0452ae97..00000000
--- a/src/app/(main)/teams/[teamId]/layout.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import TeamProvider from './TeamProvider';
-import { Metadata } from 'next';
-import TeamSettingsLayout from './settings/TeamSettingsLayout';
-
-export default async function ({
- children,
- params,
-}: {
- children: any;
- params: { teamId: string };
-}) {
- const { teamId } = await params;
-
- return (
-
- {children}
-
- );
-}
-
-export const metadata: Metadata = {
- title: 'Teams',
-};
diff --git a/src/app/(main)/teams/[teamId]/settings/TeamSettingsLayout.tsx b/src/app/(main)/teams/[teamId]/settings/TeamSettingsLayout.tsx
deleted file mode 100644
index 8c638d29..00000000
--- a/src/app/(main)/teams/[teamId]/settings/TeamSettingsLayout.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-'use client';
-import { ReactNode } from 'react';
-import { useMessages, useTeamUrl } from '@/components/hooks';
-import MenuLayout from '@/components/layout/MenuLayout';
-
-export default function TeamSettingsLayout({ children }: { children: ReactNode }) {
- const { formatMessage, labels } = useMessages();
- const { teamId } = useTeamUrl();
-
- const items = [
- {
- key: 'team',
- label: formatMessage(labels.team),
- url: `/teams/${teamId}/settings/team`,
- },
- {
- key: 'websites',
- label: formatMessage(labels.websites),
- url: `/teams/${teamId}/settings/websites`,
- },
- {
- key: 'members',
- label: formatMessage(labels.members),
- url: `/teams/${teamId}/settings/members`,
- },
- ].filter(n => n);
-
- return {children} ;
-}
diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton.tsx
deleted file mode 100644
index 85292f60..00000000
--- a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { useMessages, useModified } from '@/components/hooks';
-import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
-import TeamMemberEditForm from './TeamMemberEditForm';
-
-export function TeamMemberEditButton({
- teamId,
- userId,
- role,
- onSave,
-}: {
- teamId: string;
- userId: string;
- role: string;
- onSave?: () => void;
-}) {
- const { formatMessage, labels, messages } = useMessages();
- const { showToast } = useToasts();
- const { touch } = useModified();
-
- const handleSave = () => {
- showToast({ message: formatMessage(messages.saved), variant: 'success' });
- touch('teams:members');
- onSave?.();
- };
-
- return (
-
-
-
-
-
- {formatMessage(labels.edit)}
-
-
- {(close: () => void) => (
-
- )}
-
-
- );
-}
-
-export default TeamMemberEditButton;
diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm.tsx
deleted file mode 100644
index 4ce605df..00000000
--- a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { useApi, useMessages } from '@/components/hooks';
-import { ROLES } from '@/lib/constants';
-import {
- Button,
- Dropdown,
- Form,
- FormButtons,
- FormInput,
- FormRow,
- Item,
- SubmitButton,
-} from 'react-basics';
-
-export function TeamMemberEditForm({
- teamId,
- userId,
- role,
- onSave,
- onClose,
-}: {
- teamId: string;
- userId: string;
- role: string;
- onSave?: () => void;
- onClose?: () => void;
-}) {
- const { post, useMutation } = useApi();
- const { mutate, error, isPending } = useMutation({
- mutationFn: (data: any) => post(`/teams/${teamId}/users/${userId}`, data),
- });
- const { formatMessage, labels } = useMessages();
-
- const handleSubmit = async (data: any) => {
- mutate(data, {
- onSuccess: async () => {
- onSave();
- onClose();
- },
- });
- };
-
- const renderValue = (value: string) => {
- if (value === ROLES.teamManager) {
- return formatMessage(labels.manager);
- }
- if (value === ROLES.teamMember) {
- return formatMessage(labels.member);
- }
- if (value === ROLES.teamViewOnly) {
- return formatMessage(labels.viewOnly);
- }
- };
-
- return (
-
- );
-}
-
-export default TeamMemberEditForm;
diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx
deleted file mode 100644
index 0dfe758b..00000000
--- a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import ConfirmationForm from '@/components/common/ConfirmationForm';
-import { useApi, useMessages, useModified } from '@/components/hooks';
-import { messages } from '@/components/messages';
-import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
-
-export function TeamMemberRemoveButton({
- teamId,
- userId,
- userName,
- onSave,
-}: {
- teamId: string;
- userId: string;
- userName: string;
- disabled?: boolean;
- onSave?: () => void;
-}) {
- const { formatMessage, labels } = useMessages();
- const { del, useMutation } = useApi();
- const { mutate, isPending, error } = useMutation({
- mutationFn: () => del(`/teams/${teamId}/users/${userId}`),
- });
- const { touch } = useModified();
-
- const handleConfirm = (close: () => void) => {
- mutate(null, {
- onSuccess: () => {
- touch('teams:members');
- onSave?.();
- close();
- },
- });
- };
-
- return (
-
-
-
-
-
- {formatMessage(labels.remove)}
-
-
- {(close: () => void) => (
- {userName},
- })}
- isLoading={isPending}
- error={error}
- onConfirm={handleConfirm.bind(null, close)}
- onClose={close}
- buttonLabel={formatMessage(labels.remove)}
- />
- )}
-
-
- );
-}
-
-export default TeamMemberRemoveButton;
diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable.tsx
deleted file mode 100644
index 9de26415..00000000
--- a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import DataTable from '@/components/common/DataTable';
-import TeamMembersTable from './TeamMembersTable';
-import { useTeamMembers } from '@/components/hooks';
-
-export function TeamMembersDataTable({
- teamId,
- allowEdit = false,
-}: {
- teamId: string;
- allowEdit?: boolean;
-}) {
- const queryResult = useTeamMembers(teamId);
-
- return (
-
- {({ data }) => }
-
- );
-}
-
-export default TeamMembersDataTable;
diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersPage.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersPage.tsx
deleted file mode 100644
index 557a40ba..00000000
--- a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersPage.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-'use client';
-import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
-import TeamMembersDataTable from './TeamMembersDataTable';
-import PageHeader from '@/components/layout/PageHeader';
-import { useLogin, useMessages } from '@/components/hooks';
-import { ROLES } from '@/lib/constants';
-import { useContext } from 'react';
-
-export function TeamMembersPage({ teamId }: { teamId: string }) {
- const team = useContext(TeamContext);
- const { user } = useLogin();
- const { formatMessage, labels } = useMessages();
-
- const canEdit =
- team?.teamUser?.find(
- ({ userId, role }) =>
- (role === ROLES.teamOwner || role === ROLES.teamManager) && userId === user.id,
- ) && user.role !== ROLES.viewOnly;
-
- return (
- <>
-
-
- >
- );
-}
-
-export default TeamMembersPage;
diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersTable.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersTable.tsx
deleted file mode 100644
index 0054437a..00000000
--- a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersTable.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { GridColumn, GridTable } from 'react-basics';
-import { useMessages, useLogin } from '@/components/hooks';
-import { ROLES } from '@/lib/constants';
-import TeamMemberRemoveButton from './TeamMemberRemoveButton';
-import TeamMemberEditButton from './TeamMemberEditButton';
-
-export function TeamMembersTable({
- data = [],
- teamId,
- allowEdit = false,
-}: {
- data: any[];
- teamId: string;
- allowEdit: boolean;
-}) {
- const { formatMessage, labels } = useMessages();
- const { user } = useLogin();
-
- const roles = {
- [ROLES.teamOwner]: formatMessage(labels.teamOwner),
- [ROLES.teamManager]: formatMessage(labels.teamManager),
- [ROLES.teamMember]: formatMessage(labels.teamMember),
- [ROLES.teamViewOnly]: formatMessage(labels.viewOnly),
- };
-
- return (
-
-
- {row => row?.user?.username}
-
-
- {row => roles[row?.role]}
-
-
- {row => {
- return (
- allowEdit &&
- row?.role !== ROLES.teamOwner &&
- user?.id !== row?.user?.id && (
- <>
-
-
- >
- )
- );
- }}
-
-
- );
-}
-
-export default TeamMembersTable;
diff --git a/src/app/(main)/teams/[teamId]/settings/members/page.tsx b/src/app/(main)/teams/[teamId]/settings/members/page.tsx
deleted file mode 100644
index 9810f7a2..00000000
--- a/src/app/(main)/teams/[teamId]/settings/members/page.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Metadata } from 'next';
-import TeamMembersPage from './TeamMembersPage';
-
-export default async function ({ params }: { params: { teamId: string } }) {
- const { teamId } = await params;
-
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Team Members',
-};
diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamDetails.tsx b/src/app/(main)/teams/[teamId]/settings/team/TeamDetails.tsx
deleted file mode 100644
index f3f258bd..00000000
--- a/src/app/(main)/teams/[teamId]/settings/team/TeamDetails.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
-import { useLogin, useMessages } from '@/components/hooks';
-import Icons from '@/components/icons';
-import PageHeader from '@/components/layout/PageHeader';
-import { ROLES } from '@/lib/constants';
-import { useContext, useState } from 'react';
-import { Flexbox, Item, Tabs } from 'react-basics';
-import TeamLeaveButton from '@/app/(main)/settings/teams/TeamLeaveButton';
-import TeamManage from './TeamManage';
-import TeamEditForm from './TeamEditForm';
-
-export function TeamDetails({ teamId }: { teamId: string }) {
- const team = useContext(TeamContext);
- const { formatMessage, labels } = useMessages();
- const { user } = useLogin();
- const [tab, setTab] = useState('details');
-
- const isTeamOwner =
- !!team?.teamUser?.find(({ userId, role }) => role === ROLES.teamOwner && userId === user.id) &&
- user.role !== ROLES.viewOnly;
-
- const canEdit =
- !!team?.teamUser?.find(
- ({ userId, role }) =>
- (role === ROLES.teamOwner || role === ROLES.teamManager) && userId === user.id,
- ) && user.role !== ROLES.viewOnly;
-
- return (
-
- }>
- {!isTeamOwner && }
-
- setTab(value)} style={{ marginBottom: 30 }}>
- - {formatMessage(labels.details)}
- {isTeamOwner && - {formatMessage(labels.manage)}
}
-
- {tab === 'details' && }
- {tab === 'manage' && }
-
- );
-}
-
-export default TeamDetails;
diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamEditForm.tsx b/src/app/(main)/teams/[teamId]/settings/team/TeamEditForm.tsx
deleted file mode 100644
index ac158fa7..00000000
--- a/src/app/(main)/teams/[teamId]/settings/team/TeamEditForm.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import {
- SubmitButton,
- Form,
- FormInput,
- FormRow,
- FormButtons,
- TextField,
- Button,
- Flexbox,
- useToasts,
-} from 'react-basics';
-import { getRandomChars } from '@/lib/crypto';
-import { useContext, useRef, useState } from 'react';
-import { useApi, useMessages, useModified } from '@/components/hooks';
-import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
-
-const generateId = () => getRandomChars(16);
-
-export function TeamEditForm({ teamId, allowEdit }: { teamId: string; allowEdit?: boolean }) {
- const team = useContext(TeamContext);
- const { formatMessage, labels, messages } = useMessages();
- const { post, useMutation } = useApi();
- const { mutate, error } = useMutation({
- mutationFn: (data: any) => post(`/teams/${teamId}`, data),
- });
- const ref = useRef(null);
- const [accessCode, setAccessCode] = useState(team.accessCode);
- const { showToast } = useToasts();
- const { touch } = useModified();
- const cloudMode = !!process.env.cloudMode;
-
- const handleSubmit = async (data: any) => {
- mutate(data, {
- onSuccess: async () => {
- ref.current.reset(data);
- touch('teams');
- showToast({ message: formatMessage(messages.saved), variant: 'success' });
- },
- });
- };
-
- const handleRegenerate = () => {
- const code = generateId();
- ref.current.setValue('accessCode', code, {
- shouldValidate: true,
- shouldDirty: true,
- });
- setAccessCode(code);
- };
-
- return (
-
- );
-}
-
-export default TeamEditForm;
diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamPage.tsx b/src/app/(main)/teams/[teamId]/settings/team/TeamPage.tsx
deleted file mode 100644
index e6fbd10a..00000000
--- a/src/app/(main)/teams/[teamId]/settings/team/TeamPage.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-'use client';
-import TeamDetails from './TeamDetails';
-
-export function TeamPage({ teamId }: { teamId: string }) {
- return ;
-}
-
-export default TeamPage;
diff --git a/src/app/(main)/teams/[teamId]/settings/team/page.tsx b/src/app/(main)/teams/[teamId]/settings/team/page.tsx
deleted file mode 100644
index f15d5fb6..00000000
--- a/src/app/(main)/teams/[teamId]/settings/team/page.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Metadata } from 'next';
-import TeamPage from './TeamPage';
-
-export default async function ({ params }: { params: { teamId: string } }) {
- const { teamId } = await params;
-
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Teams Details',
-};
diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable.tsx b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable.tsx
deleted file mode 100644
index 63aa47f5..00000000
--- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import DataTable from '@/components/common/DataTable';
-import { useTeamWebsites } from '@/components/hooks';
-import TeamWebsitesTable from './TeamWebsitesTable';
-
-export function TeamWebsitesDataTable({
- teamId,
- allowEdit = false,
-}: {
- teamId: string;
- allowEdit?: boolean;
-}) {
- const queryResult = useTeamWebsites(teamId);
-
- return (
-
- {({ data }) => }
-
- );
-}
-
-export default TeamWebsitesDataTable;
diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesPage.tsx b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesPage.tsx
deleted file mode 100644
index d46d928a..00000000
--- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesPage.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-'use client';
-import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
-import WebsiteAddButton from '@/app/(main)/settings/websites/WebsiteAddButton';
-import { useLogin, useMessages } from '@/components/hooks';
-import PageHeader from '@/components/layout/PageHeader';
-import TeamWebsitesDataTable from './TeamWebsitesDataTable';
-import { ROLES } from '@/lib/constants';
-import { useContext } from 'react';
-
-export function TeamWebsitesPage({ teamId }: { teamId: string }) {
- const team = useContext(TeamContext);
- const { formatMessage, labels } = useMessages();
- const { user } = useLogin();
-
- const canEdit =
- !!team?.teamUser?.find(
- ({ userId, role }) => userId === user.id && role !== ROLES.teamViewOnly,
- ) && user.role !== ROLES.viewOnly;
-
- return (
- <>
-
- {canEdit && }
-
-
- >
- );
-}
-
-export default TeamWebsitesPage;
diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable.tsx b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable.tsx
deleted file mode 100644
index 76c343b1..00000000
--- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { GridColumn, GridTable, Icon, Text } from 'react-basics';
-import { useLogin, useMessages } from '@/components/hooks';
-import Icons from '@/components/icons';
-import LinkButton from '@/components/common/LinkButton';
-
-export function TeamWebsitesTable({
- teamId,
- data = [],
- allowEdit = false,
-}: {
- teamId: string;
- data: any[];
- allowEdit?: boolean;
-}) {
- const { user } = useLogin();
- const { formatMessage, labels } = useMessages();
-
- return (
-
-
-
-
- {row => row?.createUser?.username}
-
-
- {row => {
- const { id: websiteId } = row;
- return (
- <>
- {allowEdit && (teamId || user?.isAdmin) && (
-
-
-
-
- {formatMessage(labels.edit)}
-
- )}
-
-
-
-
- {formatMessage(labels.view)}
-
- >
- );
- }}
-
-
- );
-}
-
-export default TeamWebsitesTable;
diff --git a/src/app/(main)/teams/[teamId]/settings/websites/[websiteId]/page.tsx b/src/app/(main)/teams/[teamId]/settings/websites/[websiteId]/page.tsx
deleted file mode 100644
index a18f8a2e..00000000
--- a/src/app/(main)/teams/[teamId]/settings/websites/[websiteId]/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import Page from '@/app/(main)/settings/websites/[websiteId]/page';
-
-export default function ({ params }) {
- return ;
-}
diff --git a/src/app/(main)/teams/[teamId]/settings/websites/page.tsx b/src/app/(main)/teams/[teamId]/settings/websites/page.tsx
deleted file mode 100644
index 6709eb67..00000000
--- a/src/app/(main)/teams/[teamId]/settings/websites/page.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import TeamWebsitesPage from './TeamWebsitesPage';
-import { Metadata } from 'next';
-
-export default async function ({ params }: { params: { teamId: string } }) {
- const { teamId } = await params;
-
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Teams Websites',
-};
diff --git a/src/app/(main)/teams/page.tsx b/src/app/(main)/teams/page.tsx
new file mode 100644
index 00000000..7344f150
--- /dev/null
+++ b/src/app/(main)/teams/page.tsx
@@ -0,0 +1,10 @@
+import type { Metadata } from 'next';
+import { TeamsPage } from './TeamsPage';
+
+export default function () {
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Teams',
+};
diff --git a/src/app/(main)/websites/WebsiteAddButton.tsx b/src/app/(main)/websites/WebsiteAddButton.tsx
new file mode 100644
index 00000000..76710abb
--- /dev/null
+++ b/src/app/(main)/websites/WebsiteAddButton.tsx
@@ -0,0 +1,28 @@
+import { useToast } from '@umami/react-zen';
+import { useMessages, useModified } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { WebsiteAddForm } from './WebsiteAddForm';
+
+export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?: () => void }) {
+ const { formatMessage, labels, messages } = useMessages();
+ const { toast } = useToast();
+ const { touch } = useModified();
+
+ const handleSave = async () => {
+ toast(formatMessage(messages.saved));
+ touch('websites');
+ onSave?.();
+ };
+
+ return (
+ }
+ label={formatMessage(labels.addWebsite)}
+ variant="primary"
+ width="400px"
+ >
+ {({ close }) => }
+
+ );
+}
diff --git a/src/app/(main)/websites/WebsiteAddForm.tsx b/src/app/(main)/websites/WebsiteAddForm.tsx
new file mode 100644
index 00000000..df17ad5a
--- /dev/null
+++ b/src/app/(main)/websites/WebsiteAddForm.tsx
@@ -0,0 +1,60 @@
+import { Button, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen';
+import { useMessages, useUpdateQuery } from '@/components/hooks';
+import { DOMAIN_REGEX } from '@/lib/constants';
+
+export function WebsiteAddForm({
+ teamId,
+ onSave,
+ onClose,
+}: {
+ teamId?: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { formatMessage, labels, messages } = useMessages();
+ const { mutateAsync, error, isPending } = useUpdateQuery('/websites', { teamId });
+
+ const handleSubmit = async (data: any) => {
+ await mutateAsync(data, {
+ onSuccess: async () => {
+ onSave?.();
+ onClose?.();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/websites/WebsiteProvider.tsx b/src/app/(main)/websites/WebsiteProvider.tsx
new file mode 100644
index 00000000..75e8a358
--- /dev/null
+++ b/src/app/(main)/websites/WebsiteProvider.tsx
@@ -0,0 +1,27 @@
+'use client';
+import { Loading } from '@umami/react-zen';
+import { createContext, type ReactNode } from 'react';
+import { useWebsiteQuery } from '@/components/hooks/queries/useWebsiteQuery';
+import type { Website } from '@/generated/prisma/client';
+
+export const WebsiteContext = createContext(null);
+
+export function WebsiteProvider({
+ websiteId,
+ children,
+}: {
+ websiteId: string;
+ children: ReactNode;
+}) {
+ const { data: website, isFetching, isLoading } = useWebsiteQuery(websiteId);
+
+ if (isFetching && isLoading) {
+ return ;
+ }
+
+ if (!website) {
+ return null;
+ }
+
+ return {children} ;
+}
diff --git a/src/app/(main)/websites/WebsitesDataTable.tsx b/src/app/(main)/websites/WebsitesDataTable.tsx
new file mode 100644
index 00000000..3f0a6b9c
--- /dev/null
+++ b/src/app/(main)/websites/WebsitesDataTable.tsx
@@ -0,0 +1,47 @@
+import Link from 'next/link';
+import { DataGrid } from '@/components/common/DataGrid';
+import { useLoginQuery, useNavigation, useUserWebsitesQuery } from '@/components/hooks';
+import { Favicon } from '@/index';
+import { Icon, Row } from '@umami/react-zen';
+import { WebsitesTable } from './WebsitesTable';
+
+export function WebsitesDataTable({
+ userId,
+ teamId,
+ allowEdit = true,
+ allowView = true,
+ showActions = true,
+}: {
+ userId?: string;
+ teamId?: string;
+ allowEdit?: boolean;
+ allowView?: boolean;
+ showActions?: boolean;
+}) {
+ const { user } = useLoginQuery();
+ const queryResult = useUserWebsitesQuery({ userId: userId || user?.id, teamId });
+ const { renderUrl } = useNavigation();
+
+ const renderLink = (row: any) => (
+
+
+
+
+ {row.name}
+
+ );
+
+ return (
+
+ {({ data }) => (
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/WebsitesHeader.tsx b/src/app/(main)/websites/WebsitesHeader.tsx
new file mode 100644
index 00000000..889b6025
--- /dev/null
+++ b/src/app/(main)/websites/WebsitesHeader.tsx
@@ -0,0 +1,18 @@
+import { PageHeader } from '@/components/common/PageHeader';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { WebsiteAddButton } from './WebsiteAddButton';
+
+export interface WebsitesHeaderProps {
+ allowCreate?: boolean;
+}
+
+export function WebsitesHeader({ allowCreate = true }: WebsitesHeaderProps) {
+ const { formatMessage, labels } = useMessages();
+ const { teamId } = useNavigation();
+
+ return (
+
+ {allowCreate && }
+
+ );
+}
diff --git a/src/app/(main)/websites/WebsitesPage.tsx b/src/app/(main)/websites/WebsitesPage.tsx
index b5e40b30..31de7047 100644
--- a/src/app/(main)/websites/WebsitesPage.tsx
+++ b/src/app/(main)/websites/WebsitesPage.tsx
@@ -1,15 +1,26 @@
'use client';
-import WebsitesHeader from '@/app/(main)/settings/websites/WebsitesHeader';
-import WebsitesDataTable from '@/app/(main)/settings/websites/WebsitesDataTable';
-import { useTeamUrl } from '@/components/hooks';
+import { Column } from '@umami/react-zen';
+import { PageBody } from '@/components/common/PageBody';
+import { PageHeader } from '@/components/common/PageHeader';
+import { Panel } from '@/components/common/Panel';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { WebsiteAddButton } from './WebsiteAddButton';
+import { WebsitesDataTable } from './WebsitesDataTable';
-export default function WebsitesPage() {
- const { teamId } = useTeamUrl();
+export function WebsitesPage() {
+ const { teamId } = useNavigation();
+ const { formatMessage, labels } = useMessages();
return (
- <>
-
-
- >
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/app/(main)/websites/WebsitesTable.tsx b/src/app/(main)/websites/WebsitesTable.tsx
new file mode 100644
index 00000000..70648ed7
--- /dev/null
+++ b/src/app/(main)/websites/WebsitesTable.tsx
@@ -0,0 +1,41 @@
+import { DataColumn, DataTable, type DataTableProps, Icon } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { LinkButton } from '@/components/common/LinkButton';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { SquarePen } from '@/components/icons';
+
+export interface WebsitesTableProps extends DataTableProps {
+ showActions?: boolean;
+ allowEdit?: boolean;
+ allowView?: boolean;
+ renderLink?: (row: any) => ReactNode;
+}
+
+export function WebsitesTable({ showActions, renderLink, ...props }: WebsitesTableProps) {
+ const { formatMessage, labels } = useMessages();
+ const { renderUrl } = useNavigation();
+
+ return (
+
+
+ {renderLink}
+
+
+ {showActions && (
+
+ {(row: any) => {
+ const websiteId = row.id;
+
+ return (
+
+
+
+
+
+ );
+ }}
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/attribution/Attribution.tsx b/src/app/(main)/websites/[websiteId]/(reports)/attribution/Attribution.tsx
new file mode 100644
index 00000000..264923a6
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/attribution/Attribution.tsx
@@ -0,0 +1,128 @@
+import { Column, Grid } from '@umami/react-zen';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { Panel } from '@/components/common/Panel';
+import { SectionHeader } from '@/components/common/SectionHeader';
+import { useMessages, useResultQuery } from '@/components/hooks';
+import { ListTable } from '@/components/metrics/ListTable';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
+import { percentFilter } from '@/lib/filters';
+import { formatLongNumber } from '@/lib/format';
+
+export interface AttributionProps {
+ websiteId: string;
+ startDate: Date;
+ endDate: Date;
+ model: string;
+ type: string;
+ step: string;
+ currency?: string;
+}
+
+export function Attribution({
+ websiteId,
+ startDate,
+ endDate,
+ model,
+ type,
+ step,
+ currency,
+}: AttributionProps) {
+ const { data, error, isLoading } = useResultQuery('attribution', {
+ websiteId,
+ startDate,
+ endDate,
+ model,
+ type,
+ step,
+ });
+
+ const { formatMessage, labels } = useMessages();
+
+ const { pageviews, visitors, visits } = data?.total || {};
+
+ const metrics = data
+ ? [
+ {
+ value: visitors,
+ label: formatMessage(labels.visitors),
+ formatValue: formatLongNumber,
+ },
+ {
+ value: visits,
+ label: formatMessage(labels.visits),
+ formatValue: formatLongNumber,
+ },
+ {
+ value: pageviews,
+ label: formatMessage(labels.views),
+ formatValue: formatLongNumber,
+ },
+ ]
+ : [];
+
+ function AttributionTable({ data = [], title }: { data: any; title: string }) {
+ const attributionData = percentFilter(
+ data.map(({ name, value }) => ({
+ x: name,
+ y: Number(value),
+ })),
+ );
+
+ return (
+ ({
+ label: x,
+ count: y,
+ percent: z,
+ }))}
+ />
+ );
+ }
+
+ return (
+
+ {data && (
+
+
+ {metrics?.map(({ label, value, formatValue }) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx
new file mode 100644
index 00000000..48611c46
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx
@@ -0,0 +1,63 @@
+'use client';
+import { Column, Grid, ListItem, SearchField, Select } from '@umami/react-zen';
+import { useState } from 'react';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { Attribution } from './Attribution';
+
+export function AttributionPage({ websiteId }: { websiteId: string }) {
+ const [model, setModel] = useState('first-click');
+ const [type, setType] = useState('path');
+ const [step, setStep] = useState('/');
+ const { formatMessage, labels } = useMessages();
+ const {
+ dateRange: { startDate, endDate },
+ } = useDateRange();
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.firstClick)}
+ {formatMessage(labels.lastClick)}
+
+
+
+
+ {formatMessage(labels.viewedPage)}
+ {formatMessage(labels.triggeredEvent)}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/attribution/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/attribution/page.tsx
new file mode 100644
index 00000000..1368d4bc
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/attribution/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { AttributionPage } from './AttributionPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Attribution',
+};
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/Breakdown.tsx b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/Breakdown.tsx
new file mode 100644
index 00000000..4532d972
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/Breakdown.tsx
@@ -0,0 +1,91 @@
+import { Column, DataColumn, DataTable, Text } from '@umami/react-zen';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useFields, useFormat, useMessages, useResultQuery } from '@/components/hooks';
+import { formatShortTime } from '@/lib/format';
+
+export interface BreakdownProps {
+ websiteId: string;
+ startDate: Date;
+ endDate: Date;
+ selectedFields: string[];
+}
+
+export function Breakdown({ websiteId, selectedFields = [], startDate, endDate }: BreakdownProps) {
+ const { formatMessage, labels } = useMessages();
+ const { formatValue } = useFormat();
+ const { fields } = useFields();
+ const { data, error, isLoading } = useResultQuery(
+ 'breakdown',
+ {
+ websiteId,
+ startDate,
+ endDate,
+ fields: selectedFields,
+ },
+ { enabled: !!selectedFields.length },
+ );
+
+ return (
+
+
+
+ {selectedFields.map(field => {
+ return (
+ f.name === field)?.label}
+ width="minmax(120px, 1fr)"
+ >
+ {row => {
+ const value = formatValue(row[field], field);
+ return (
+
+ {value}
+
+ );
+ }}
+
+ );
+ })}
+
+ {row => row?.visitors?.toLocaleString()}
+
+
+ {row => row?.visits?.toLocaleString()}
+
+
+ {row => row?.views?.toLocaleString()}
+
+
+ {row => {
+ const n = (Math.min(row?.visits, row?.bounces) / row?.visits) * 100;
+ return `${Math.round(+n)}%`;
+ }}
+
+
+ {row => {
+ const n = row?.totaltime / row?.visits;
+ return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
+ }}
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx
new file mode 100644
index 00000000..fdead9fb
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx
@@ -0,0 +1,51 @@
+'use client';
+import { Column, Row } from '@umami/react-zen';
+import { useState } from 'react';
+import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { Panel } from '@/components/common/Panel';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { ListCheck } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { Breakdown } from './Breakdown';
+
+export function BreakdownPage({ websiteId }: { websiteId: string }) {
+ const {
+ dateRange: { startDate, endDate },
+ } = useDateRange();
+ const [fields, setFields] = useState(['path']);
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const FieldsButton = ({ value, onChange }) => {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ }
+ label={formatMessage(labels.fields)}
+ width="400px"
+ minHeight="300px"
+ variant="outline"
+ >
+ {({ close }) => {
+ return ;
+ }}
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm.tsx b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm.tsx
new file mode 100644
index 00000000..28e33682
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm.tsx
@@ -0,0 +1,46 @@
+import { Button, Column, Grid, List, ListItem } from '@umami/react-zen';
+import { useState } from 'react';
+import { useFields, useMessages } from '@/components/hooks';
+
+export function FieldSelectForm({
+ selectedFields = [],
+ onChange,
+ onClose,
+}: {
+ selectedFields?: string[];
+ onChange: (values: string[]) => void;
+ onClose?: () => void;
+}) {
+ const [selected, setSelected] = useState(selectedFields);
+ const { formatMessage, labels } = useMessages();
+ const { fields } = useFields();
+
+ const handleChange = (value: string[]) => {
+ setSelected(value);
+ };
+
+ const handleApply = () => {
+ onChange?.(selected);
+ onClose();
+ };
+
+ return (
+
+
+ {fields.map(({ name, label }) => {
+ return (
+
+ {label}
+
+ );
+ })}
+
+
+ {formatMessage(labels.cancel)}
+
+ {formatMessage(labels.apply)}
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/page.tsx
new file mode 100644
index 00000000..841d8635
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { BreakdownPage } from './BreakdownPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Insights',
+};
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx
new file mode 100644
index 00000000..e336a3db
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx
@@ -0,0 +1,134 @@
+import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useMessages, useResultQuery } from '@/components/hooks';
+import { File, User } from '@/components/icons';
+import { ReportEditButton } from '@/components/input/ReportEditButton';
+import { ChangeLabel } from '@/components/metrics/ChangeLabel';
+import { Lightning } from '@/components/svg';
+import { formatLongNumber } from '@/lib/format';
+import { FunnelEditForm } from './FunnelEditForm';
+
+type FunnelResult = {
+ type: string;
+ value: string;
+ visitors: number;
+ previous: number;
+ dropped: number;
+ dropoff: number;
+ remaining: number;
+};
+
+export function Funnel({ id, name, type, parameters, websiteId }) {
+ const { formatMessage, labels } = useMessages();
+ const { data, error, isLoading } = useResultQuery(type, {
+ websiteId,
+ ...parameters,
+ });
+
+ return (
+
+
+
+
+
+
+ {name}
+
+
+
+
+
+ {({ close }) => {
+ return (
+
+
+
+ );
+ }}
+
+
+
+ {data?.map(
+ (
+ { type, value, visitors, previous, dropped, dropoff, remaining }: FunnelResult,
+ index: number,
+ ) => {
+ const isPage = type === 'path';
+ return (
+
+
+
+
+ {index + 1}
+
+
+ {index > 0 && (
+
+ )}
+
+
+
+
+ {formatMessage(isPage ? labels.viewedPage : labels.triggeredEvent)}
+
+ {formatMessage(labels.conversionRate)}
+
+
+
+ {type === 'path' ? : }
+ {value}
+
+
+ {index > 0 && (
+
+ {formatLongNumber(dropped)}
+
+ )}
+
+
+
+
+ {`${formatLongNumber(visitors)} ${formatMessage(labels.visitors)}`}
+
+
+
+
+
+
+
+ {Math.round(remaining * 100)}%
+
+
+
+
+
+ );
+ },
+ )}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelAddButton.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelAddButton.tsx
new file mode 100644
index 00000000..29b54803
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelAddButton.tsx
@@ -0,0 +1,28 @@
+import { Button, Dialog, DialogTrigger, Icon, Modal, Text } from '@umami/react-zen';
+import { useMessages } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+import { FunnelEditForm } from './FunnelEditForm';
+
+export function FunnelAddButton({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.funnel)}
+
+
+
+ {({ close }) => }
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx
new file mode 100644
index 00000000..5d950ea6
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx
@@ -0,0 +1,141 @@
+import {
+ Button,
+ Column,
+ Form,
+ FormButtons,
+ FormField,
+ FormFieldArray,
+ FormSubmitButton,
+ Grid,
+ Icon,
+ Loading,
+ Row,
+ Text,
+ TextField,
+} from '@umami/react-zen';
+import { useMessages, useReportQuery, useUpdateQuery } from '@/components/hooks';
+import { Plus, X } from '@/components/icons';
+import { ActionSelect } from '@/components/input/ActionSelect';
+import { LookupField } from '@/components/input/LookupField';
+
+const FUNNEL_STEPS_MAX = 8;
+
+export function FunnelEditForm({
+ id,
+ websiteId,
+ onSave,
+ onClose,
+}: {
+ id?: string;
+ websiteId: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { formatMessage, labels } = useMessages();
+ const { data } = useReportQuery(id);
+ const { mutateAsync, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`);
+
+ const handleSubmit = async ({ name, ...parameters }) => {
+ await mutateAsync(
+ { ...data, id, name, type: 'funnel', websiteId, parameters },
+ {
+ onSuccess: async () => {
+ touch('reports:funnel');
+ touch(`report:${id}`);
+ onSave?.();
+ onClose?.();
+ },
+ },
+ );
+ };
+
+ if (id && !data) {
+ return ;
+ }
+
+ const defaultValues = {
+ name: data?.name || '',
+ window: data?.parameters?.window || 60,
+ steps: data?.parameters?.steps || [{ type: 'path', value: '' }],
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx
new file mode 100644
index 00000000..57bce52f
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage.tsx
@@ -0,0 +1,36 @@
+'use client';
+import { Column, Grid } from '@umami/react-zen';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { Panel } from '@/components/common/Panel';
+import { SectionHeader } from '@/components/common/SectionHeader';
+import { useDateRange, useReportsQuery } from '@/components/hooks';
+import { Funnel } from './Funnel';
+import { FunnelAddButton } from './FunnelAddButton';
+
+export function FunnelsPage({ websiteId }: { websiteId: string }) {
+ const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'funnel' });
+ const {
+ dateRange: { startDate, endDate },
+ } = useDateRange();
+
+ return (
+
+
+
+
+
+
+ {data && (
+
+ {data.data?.map((report: any) => (
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/page.tsx
new file mode 100644
index 00000000..2fdcf3b7
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { FunnelsPage } from './FunnelsPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Funnels',
+};
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx
new file mode 100644
index 00000000..b6c4a11d
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx
@@ -0,0 +1,99 @@
+import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useMessages, useResultQuery } from '@/components/hooks';
+import { File, User } from '@/components/icons';
+import { ReportEditButton } from '@/components/input/ReportEditButton';
+import { Lightning } from '@/components/svg';
+import { formatLongNumber } from '@/lib/format';
+import { GoalEditForm } from './GoalEditForm';
+
+export interface GoalProps {
+ id: string;
+ name: string;
+ type: string;
+ parameters: {
+ name: string;
+ type: string;
+ value: string;
+ };
+ websiteId: string;
+ startDate: Date;
+ endDate: Date;
+}
+
+export type GoalData = { num: number; total: number };
+
+export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) {
+ const { formatMessage, labels } = useMessages();
+ const { data, error, isLoading, isFetching } = useResultQuery(type, {
+ websiteId,
+ startDate,
+ endDate,
+ ...parameters,
+ });
+ const isPage = parameters?.type === 'path';
+
+ return (
+
+ {data && (
+
+
+
+
+
+ {name}
+
+
+
+
+
+ {({ close }) => {
+ return (
+
+
+
+ );
+ }}
+
+
+
+
+
+ {formatMessage(isPage ? labels.viewedPage : labels.triggeredEvent)}
+
+ {formatMessage(labels.conversionRate)}
+
+
+
+ {parameters.type === 'path' ? : }
+ {parameters.value}
+
+
+
+
+
+ {`${formatLongNumber(
+ data?.num,
+ )} / ${formatLongNumber(data?.total)}`}
+
+
+
+
+
+ {data?.total ? Math.round((+data?.num / +data?.total) * 100) : '0'}%
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalAddButton.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalAddButton.tsx
new file mode 100644
index 00000000..c85b79c5
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalAddButton.tsx
@@ -0,0 +1,28 @@
+import { Button, Dialog, DialogTrigger, Icon, Modal, Text } from '@umami/react-zen';
+import { useMessages } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+import { GoalEditForm } from './GoalEditForm';
+
+export function GoalAddButton({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.goal)}
+
+
+
+ {({ close }) => }
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalEditForm.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalEditForm.tsx
new file mode 100644
index 00000000..7f68047c
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalEditForm.tsx
@@ -0,0 +1,104 @@
+import {
+ Button,
+ Column,
+ Form,
+ FormButtons,
+ FormField,
+ FormSubmitButton,
+ Grid,
+ Label,
+ Loading,
+ TextField,
+} from '@umami/react-zen';
+import { useMessages, useReportQuery, useUpdateQuery } from '@/components/hooks';
+import { ActionSelect } from '@/components/input/ActionSelect';
+import { LookupField } from '@/components/input/LookupField';
+
+export function GoalEditForm({
+ id,
+ websiteId,
+ onSave,
+ onClose,
+}: {
+ id?: string;
+ websiteId: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { formatMessage, labels } = useMessages();
+ const { data } = useReportQuery(id);
+ const { mutateAsync, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`);
+
+ const handleSubmit = async (formData: Record) => {
+ await mutateAsync(
+ { ...formData, type: 'goal', websiteId },
+ {
+ onSuccess: async () => {
+ if (id) touch(`report:${id}`);
+ touch('reports:goal');
+ onSave?.();
+ onClose?.();
+ },
+ },
+ );
+ };
+
+ if (id && !data) {
+ return ;
+ }
+
+ const defaultValues = {
+ name: '',
+ parameters: { type: 'path', value: '' },
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx
new file mode 100644
index 00000000..ff7b49fb
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage.tsx
@@ -0,0 +1,36 @@
+'use client';
+import { Column, Grid } from '@umami/react-zen';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { Panel } from '@/components/common/Panel';
+import { SectionHeader } from '@/components/common/SectionHeader';
+import { useDateRange, useReportsQuery } from '@/components/hooks';
+import { Goal } from './Goal';
+import { GoalAddButton } from './GoalAddButton';
+
+export function GoalsPage({ websiteId }: { websiteId: string }) {
+ const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'goal' });
+ const {
+ dateRange: { startDate, endDate },
+ } = useDateRange();
+
+ return (
+
+
+
+
+
+
+ {data && (
+
+ {data.data.map((report: any) => (
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/page.tsx
new file mode 100644
index 00000000..b1ab691a
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { GoalsPage } from './GoalsPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Goals',
+};
diff --git a/src/app/(main)/reports/journey/JourneyView.module.css b/src/app/(main)/websites/[websiteId]/(reports)/journeys/Journey.module.css
similarity index 82%
rename from src/app/(main)/reports/journey/JourneyView.module.css
rename to src/app/(main)/websites/[websiteId]/(reports)/journeys/Journey.module.css
index 908f02cb..63643f13 100644
--- a/src/app/(main)/reports/journey/JourneyView.module.css
+++ b/src/app/(main)/websites/[websiteId]/(reports)/journeys/Journey.module.css
@@ -3,9 +3,9 @@
height: 100%;
position: relative;
- --journey-line-color: var(--base600);
- --journey-active-color: var(--primary400);
- --journey-faded-color: var(--base300);
+ --journey-line-color: var(--base-color-6);
+ --journey-active-color: var(--primary-color);
+ --journey-faded-color: var(--base-color-3);
}
.view {
@@ -28,11 +28,11 @@
.stats {
display: flex;
+ flex-direction: column;
align-items: center;
- justify-content: center;
- gap: 20px;
+ justify-content: flex-start;
+ gap: 10px;
width: 100%;
- height: 40px;
}
.visitors {
@@ -43,8 +43,8 @@
.dropoff {
font-weight: 600;
- color: var(--blue800);
- background: var(--blue100);
+ color: var(--font-color-muted);
+ background: var(--base-color-2);
padding: 4px 8px;
border-radius: 5px;
}
@@ -58,8 +58,8 @@
height: 50px;
font-size: 16px;
font-weight: 700;
- color: var(--base100);
- background: var(--base800);
+ color: var(--base-color-1);
+ background: var(--base-color-12);
z-index: 1;
margin: 0 auto 20px;
}
@@ -84,7 +84,7 @@
position: relative;
cursor: pointer;
padding: 10px 20px;
- background: var(--base75);
+ background: var(--base-color-3);
border-radius: 5px;
display: flex;
align-items: center;
@@ -96,40 +96,33 @@
}
.node:hover:not(.selected) {
- color: var(--base900);
- background: var(--base100);
+ background: var(--base-color-4);
}
.node.selected {
- color: var(--base75);
- background: var(--base900);
- font-weight: 400;
+ color: var(--base-color-1);
+ background: var(--base-color-12);
}
.node.active {
- color: var(--light50);
- background: var(--primary400);
+ color: var(--primary-font-color);
+ background: var(--primary-color);
}
.node.selected .count {
- color: var(--base50);
- background: var(--base800);
+ color: var(--base-color-1);
+ background: var(--base-color-12);
}
.node.selected.active .count {
- background: var(--primary600);
+ color: var(--primary-font-color);
+ background: var(--primary-color);
}
.name {
max-width: 200px;
}
-.count {
- border-radius: 4px;
- padding: 5px 10px;
- background: var(--base200);
-}
-
.line {
position: absolute;
bottom: 0;
@@ -215,11 +208,11 @@
.start:before,
.end:before {
- content: '';
+ content: "";
position: absolute;
border-radius: 100%;
border: 3px solid var(--journey-line-color);
- background: var(--light50);
+ background: var(--base-color-1);
width: 14px;
height: 14px;
}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/journeys/Journey.tsx b/src/app/(main)/websites/[websiteId]/(reports)/journeys/Journey.tsx
new file mode 100644
index 00000000..3327a425
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/journeys/Journey.tsx
@@ -0,0 +1,294 @@
+import { Column, Focusable, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
+import classNames from 'classnames';
+import { useMemo, useState } from 'react';
+import { firstBy } from 'thenby';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useEscapeKey, useMessages, useResultQuery } from '@/components/hooks';
+import { File } from '@/components/icons';
+import { Lightning } from '@/components/svg';
+import { objectToArray } from '@/lib/data';
+import { formatLongNumber } from '@/lib/format';
+import styles from './Journey.module.css';
+
+const NODE_HEIGHT = 60;
+const NODE_GAP = 10;
+const LINE_WIDTH = 3;
+
+export interface JourneyProps {
+ websiteId: string;
+ startDate: Date;
+ endDate: Date;
+ steps: number;
+ startStep?: string;
+ endStep?: string;
+}
+
+export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps) {
+ const [selectedNode, setSelectedNode] = useState(null);
+ const [activeNode, setActiveNode] = useState(null);
+ const { formatMessage, labels } = useMessages();
+ const { data, error, isLoading } = useResultQuery('journey', {
+ websiteId,
+ steps,
+ startStep,
+ endStep,
+ });
+
+ useEscapeKey(() => setSelectedNode(null));
+
+ const columns = useMemo(() => {
+ if (!data) {
+ return [];
+ }
+
+ const selectedPaths = selectedNode?.paths ?? [];
+ const activePaths = activeNode?.paths ?? [];
+ const columns = [];
+
+ for (let columnIndex = 0; columnIndex < +steps; columnIndex++) {
+ const nodes = {};
+
+ data.forEach(({ items, count }: any, nodeIndex: any) => {
+ const name = items[columnIndex];
+
+ if (name) {
+ const selected = !!selectedPaths.find(({ items }) => items[columnIndex] === name);
+ const active = selected && !!activePaths.find(({ items }) => items[columnIndex] === name);
+
+ if (!nodes[name]) {
+ const paths = data.filter(({ items }) => items[columnIndex] === name);
+
+ nodes[name] = {
+ name,
+ count,
+ totalCount: count,
+ nodeIndex,
+ columnIndex,
+ selected,
+ active,
+ paths,
+ pathMap: paths.map(({ items, count }) => ({
+ [`${columnIndex}:${items.join(':')}`]: count,
+ })),
+ };
+ } else {
+ nodes[name].totalCount += count;
+ }
+ }
+ });
+
+ columns.push({
+ nodes: objectToArray(nodes).sort(firstBy('total', -1)),
+ });
+ }
+
+ columns.forEach((column, columnIndex) => {
+ const nodes = column.nodes.map(
+ (
+ currentNode: { totalCount: number; name: string; selected: boolean },
+ currentNodeIndex: any,
+ ) => {
+ const previousNodes = columns[columnIndex - 1]?.nodes;
+ let selectedCount = previousNodes ? 0 : currentNode.totalCount;
+ let activeCount = selectedCount;
+
+ const lines =
+ previousNodes?.reduce((arr: any[][], previousNode: any, previousNodeIndex: number) => {
+ const fromCount = selectedNode?.paths.reduce((sum, path) => {
+ if (
+ previousNode.name === path.items[columnIndex - 1] &&
+ currentNode.name === path.items[columnIndex]
+ ) {
+ sum += path.count;
+ }
+ return sum;
+ }, 0);
+
+ if (currentNode.selected && previousNode.selected && fromCount) {
+ arr.push([previousNodeIndex, currentNodeIndex]);
+ selectedCount += fromCount;
+
+ if (previousNode.active) {
+ activeCount += fromCount;
+ }
+ }
+
+ return arr;
+ }, []) || [];
+
+ return { ...currentNode, selectedCount, activeCount, lines };
+ },
+ );
+
+ const visitorCount = nodes.reduce(
+ (sum: number, { selected, selectedCount, active, activeCount, totalCount }) => {
+ if (!selectedNode) {
+ sum += totalCount;
+ } else if (!activeNode && selectedNode && selected) {
+ sum += selectedCount;
+ } else if (activeNode && active) {
+ sum += activeCount;
+ }
+ return sum;
+ },
+ 0,
+ );
+
+ const previousTotal = columns[columnIndex - 1]?.visitorCount ?? 0;
+ const dropOff =
+ previousTotal > 0 ? ((visitorCount - previousTotal) / previousTotal) * 100 : 0;
+
+ Object.assign(column, { nodes, visitorCount, dropOff });
+ });
+
+ return columns;
+ }, [data, selectedNode, activeNode]);
+
+ const handleClick = (name: string, columnIndex: number, paths: any[]) => {
+ if (name !== selectedNode?.name || columnIndex !== selectedNode?.columnIndex) {
+ setSelectedNode({ name, columnIndex, paths });
+ } else {
+ setSelectedNode(null);
+ }
+ setActiveNode(null);
+ };
+
+ return (
+
+
+
+ {columns.map(({ visitorCount, nodes }, columnIndex) => {
+ return (
+
+
+
{columnIndex + 1}
+
+
+ {formatLongNumber(visitorCount)} {formatMessage(labels.visitors)}
+
+
+
+
+ {nodes.map(
+ ({
+ name,
+ totalCount,
+ selected,
+ active,
+ paths,
+ activeCount,
+ selectedCount,
+ lines,
+ }) => {
+ const nodeCount = selected
+ ? active
+ ? activeCount
+ : selectedCount
+ : totalCount;
+
+ const remaining =
+ columnIndex > 0
+ ? Math.round((nodeCount / columns[columnIndex - 1]?.visitorCount) * 100)
+ : 0;
+
+ const dropped = 100 - remaining;
+
+ return (
+
+ selected && setActiveNode({ name, columnIndex, paths })
+ }
+ onMouseLeave={() => selected && setActiveNode(null)}
+ >
+
handleClick(name, columnIndex, paths)}
+ >
+
+ {name.startsWith('/') ? : }
+ {name}
+
+
+
+
+ {formatLongNumber(nodeCount)}
+
+
+
+ {`${dropped}% ${formatMessage(labels.dropoff)}`}
+
+
+
+ {`${remaining}% ${formatMessage(labels.conversion)}`}
+
+
+
+
+
+ {columnIndex < columns.length &&
+ lines.map(([fromIndex, nodeIndex], i) => {
+ const height =
+ (Math.abs(nodeIndex - fromIndex) + 1) * (NODE_HEIGHT + NODE_GAP) -
+ NODE_GAP;
+ const midHeight =
+ (Math.abs(nodeIndex - fromIndex) - 1) * (NODE_HEIGHT + NODE_GAP) +
+ NODE_GAP +
+ LINE_WIDTH;
+ const nodeName = columns[columnIndex - 1]?.nodes[fromIndex].name;
+
+ return (
+
+ path.items[columnIndex] === name &&
+ path.items[columnIndex - 1] === nodeName,
+ ),
+ [styles.up]: fromIndex < nodeIndex,
+ [styles.down]: fromIndex > nodeIndex,
+ [styles.flat]: fromIndex === nodeIndex,
+ })}
+ style={{ height }}
+ >
+
+
+
+
+ );
+ })}
+
+
+ );
+ },
+ )}
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx
new file mode 100644
index 00000000..14b8341d
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx
@@ -0,0 +1,67 @@
+'use client';
+import { Column, Grid, ListItem, SearchField, Select } from '@umami/react-zen';
+import { useState } from 'react';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { Panel } from '@/components/common/Panel';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { Journey } from './Journey';
+
+const JOURNEY_STEPS = [2, 3, 4, 5, 6, 7];
+const DEFAULT_STEP = 3;
+
+export function JourneysPage({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+ const {
+ dateRange: { startDate, endDate },
+ } = useDateRange();
+ const [steps, setSteps] = useState(DEFAULT_STEP);
+ const [startStep, setStartStep] = useState('');
+ const [endStep, setEndStep] = useState('');
+
+ return (
+
+
+
+
+ {JOURNEY_STEPS.map(step => (
+
+ {step}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/journeys/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/journeys/page.tsx
new file mode 100644
index 00000000..f6062a61
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/journeys/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { JourneysPage } from './JourneysPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Journeys',
+};
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx b/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx
new file mode 100644
index 00000000..fdd8a146
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx
@@ -0,0 +1,140 @@
+import { Column, Grid, Icon, Row, Text } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { Panel } from '@/components/common/Panel';
+import { useLocale, useMessages, useResultQuery } from '@/components/hooks';
+import { Users } from '@/components/icons';
+import { formatDate } from '@/lib/date';
+import { formatLongNumber } from '@/lib/format';
+
+const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
+
+export interface RetentionProps {
+ websiteId: string;
+ startDate: Date;
+ endDate: Date;
+ days?: number[];
+}
+
+export function Retention({ websiteId, days = DAYS, startDate, endDate }: RetentionProps) {
+ const { formatMessage, labels } = useMessages();
+ const { locale } = useLocale();
+ const { data, error, isLoading } = useResultQuery('retention', {
+ websiteId,
+ startDate,
+ endDate,
+ });
+
+ const rows =
+ data?.reduce((arr: any[], row: { date: any; visitors: any; day: any }) => {
+ const { date, visitors, day } = row;
+ if (day === 0) {
+ return arr.concat({
+ date,
+ visitors,
+ records: days
+ .reduce((arr, day) => {
+ arr[day] = data.find(
+ (x: { date: any; day: number }) => x.date === date && x.day === day,
+ );
+ return arr;
+ }, [])
+ .filter(n => n),
+ });
+ }
+ return arr;
+ }, []) || [];
+
+ const totalDays = rows.length;
+
+ return (
+
+ {data && (
+
+
+
+
+
+
+ {formatMessage(labels.cohort)}
+
+
+ {days.map(n => (
+
+
+ {formatMessage(labels.day)} {n}
+
+
+ ))}
+
+ {rows.map(({ date, visitors, records }: any, rowIndex: number) => {
+ return (
+
+
+ {formatDate(date, 'PP', locale)}
+
+
+
+
+ {formatLongNumber(visitors)}
+
+
+ {days.map(day => {
+ if (totalDays - rowIndex < day) {
+ return null;
+ }
+ const percentage = records.filter(a => a.day === day)[0]?.percentage;
+ return (
+
+ {percentage ? `${Number(percentage).toFixed(2)}%` : ''}
+ |
+ );
+ })}
+
+ );
+ })}
+
+
+
+ )}
+
+ );
+}
+
+const Cell = ({ children }: { children: ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/retention/RetentionPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/retention/RetentionPage.tsx
new file mode 100644
index 00000000..0ec6e95e
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/retention/RetentionPage.tsx
@@ -0,0 +1,22 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { endOfMonth, startOfMonth } from 'date-fns';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { useDateRange } from '@/components/hooks';
+import { Retention } from './Retention';
+
+export function RetentionPage({ websiteId }: { websiteId: string }) {
+ const {
+ dateRange: { startDate },
+ } = useDateRange();
+
+ const monthStartDate = startOfMonth(startDate);
+ const monthEndDate = endOfMonth(startDate);
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/retention/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/retention/page.tsx
new file mode 100644
index 00000000..2fbbc0ac
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/retention/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { RetentionPage } from './RetentionPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Retention',
+};
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx
new file mode 100644
index 00000000..0e782a16
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/Revenue.tsx
@@ -0,0 +1,152 @@
+import { Column, Grid, Row, Text } from '@umami/react-zen';
+import classNames from 'classnames';
+import { colord } from 'colord';
+import { useCallback, useMemo, useState } from 'react';
+import { BarChart } from '@/components/charts/BarChart';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { Panel } from '@/components/common/Panel';
+import { TypeIcon } from '@/components/common/TypeIcon';
+import { useCountryNames, useLocale, useMessages, useResultQuery } from '@/components/hooks';
+import { CurrencySelect } from '@/components/input/CurrencySelect';
+import { ListTable } from '@/components/metrics/ListTable';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
+import { renderDateLabels } from '@/lib/charts';
+import { CHART_COLORS } from '@/lib/constants';
+import { generateTimeSeries } from '@/lib/date';
+import { formatLongCurrency, formatLongNumber } from '@/lib/format';
+
+export interface RevenueProps {
+ websiteId: string;
+ startDate: Date;
+ endDate: Date;
+ unit: string;
+}
+
+export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
+ const [currency, setCurrency] = useState('USD');
+ const { formatMessage, labels } = useMessages();
+ const { locale, dateLocale } = useLocale();
+ const { countryNames } = useCountryNames(locale);
+ const { data, error, isLoading } = useResultQuery('revenue', {
+ websiteId,
+ startDate,
+ endDate,
+ currency,
+ });
+
+ const renderCountryName = useCallback(
+ ({ label: code }) => (
+
+
+ {countryNames[code] || formatMessage(labels.unknown)}
+
+ ),
+ [countryNames, locale],
+ );
+
+ const chartData: any = useMemo(() => {
+ if (!data) return [];
+
+ const map = (data.chart as any[]).reduce((obj, { x, t, y }) => {
+ if (!obj[x]) {
+ obj[x] = [];
+ }
+
+ obj[x].push({ x: t, y });
+
+ return obj;
+ }, {});
+
+ return {
+ datasets: Object.keys(map).map((key, index) => {
+ const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
+ return {
+ label: key,
+ data: generateTimeSeries(map[key], startDate, endDate, unit, dateLocale),
+ lineTension: 0,
+ backgroundColor: color.alpha(0.6).toRgbString(),
+ borderColor: color.alpha(0.7).toRgbString(),
+ borderWidth: 1,
+ };
+ }),
+ };
+ }, [data, startDate, endDate, unit]);
+
+ const metrics = useMemo(() => {
+ if (!data) return [];
+
+ const { sum, count, unique_count } = data.total;
+
+ return [
+ {
+ value: sum,
+ label: formatMessage(labels.total),
+ formatValue: n => formatLongCurrency(n, currency),
+ },
+ {
+ value: count ? sum / count : 0,
+ label: formatMessage(labels.average),
+ formatValue: n => formatLongCurrency(n, currency),
+ },
+ {
+ value: count,
+ label: formatMessage(labels.transactions),
+ formatValue: formatLongNumber,
+ },
+ {
+ value: unique_count,
+ label: formatMessage(labels.uniqueCustomers),
+ formatValue: formatLongNumber,
+ },
+ ] as any;
+ }, [data, locale]);
+
+ const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]);
+
+ return (
+
+
+
+
+
+ {data && (
+
+
+ {metrics?.map(({ label, value, formatValue }) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+ ({
+ label: name,
+ count: Number(value),
+ percent: (value / data?.total.sum) * 100,
+ }))}
+ currency={currency}
+ renderLabel={renderCountryName}
+ />
+
+
+ )}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenuePage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenuePage.tsx
new file mode 100644
index 00000000..3e429c18
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenuePage.tsx
@@ -0,0 +1,18 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { useDateRange } from '@/components/hooks';
+import { Revenue } from './Revenue';
+
+export function RevenuePage({ websiteId }: { websiteId: string }) {
+ const {
+ dateRange: { startDate, endDate, unit },
+ } = useDateRange();
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueTable.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueTable.tsx
new file mode 100644
index 00000000..e30d54c7
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/RevenueTable.tsx
@@ -0,0 +1,21 @@
+import { DataColumn, DataTable } from '@umami/react-zen';
+import { useMessages } from '@/components/hooks';
+import { formatLongCurrency } from '@/lib/format';
+
+export function RevenueTable({ data = [] }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+ {(row: any) => formatLongCurrency(row.sum, row.currency)}
+
+
+ {(row: any) => formatLongCurrency(row.count ? row.sum / row.count : 0, row.currency)}
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/revenue/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/revenue/page.tsx
new file mode 100644
index 00000000..fba10f15
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/revenue/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { RevenuePage } from './RevenuePage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Revenue',
+};
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/utm/UTM.tsx b/src/app/(main)/websites/[websiteId]/(reports)/utm/UTM.tsx
new file mode 100644
index 00000000..1399174a
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/utm/UTM.tsx
@@ -0,0 +1,71 @@
+import { Column, Grid, Heading, Text } from '@umami/react-zen';
+import { PieChart } from '@/components/charts/PieChart';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { Panel } from '@/components/common/Panel';
+import { useMessages, useResultQuery } from '@/components/hooks';
+import { ListTable } from '@/components/metrics/ListTable';
+import { CHART_COLORS, UTM_PARAMS } from '@/lib/constants';
+
+export interface UTMProps {
+ websiteId: string;
+ startDate: Date;
+ endDate: Date;
+}
+
+export function UTM({ websiteId, startDate, endDate }: UTMProps) {
+ const { formatMessage, labels } = useMessages();
+ const { data, error, isLoading } = useResultQuery('utm', {
+ websiteId,
+ startDate,
+ endDate,
+ });
+
+ return (
+
+ {data && (
+
+ {UTM_PARAMS.map(param => {
+ const items = data?.[param];
+
+ const chartData = {
+ labels: items.map(({ utm }) => utm),
+ datasets: [
+ {
+ data: items.map(({ views }) => views),
+ backgroundColor: CHART_COLORS,
+ borderWidth: 0,
+ },
+ ],
+ };
+ const total = items.reduce((sum, { views }) => {
+ return +sum + +views;
+ }, 0);
+
+ return (
+
+
+
+
+ {param.replace(/^utm_/, '')}
+
+ ({
+ label: utm,
+ count: views,
+ percent: (views / total) * 100,
+ }))}
+ />
+
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx
new file mode 100644
index 00000000..0d2a7329
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage.tsx
@@ -0,0 +1,18 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { useDateRange } from '@/components/hooks';
+import { UTM } from './UTM';
+
+export function UTMPage({ websiteId }: { websiteId: string }) {
+ const {
+ dateRange: { startDate, endDate },
+ } = useDateRange();
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/utm/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/utm/page.tsx
new file mode 100644
index 00000000..8b8fd6af
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/utm/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { UTMPage } from './UTMPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'UTM Parameters',
+};
diff --git a/src/app/(main)/websites/[websiteId]/ExpandedViewModal.tsx b/src/app/(main)/websites/[websiteId]/ExpandedViewModal.tsx
new file mode 100644
index 00000000..36638121
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/ExpandedViewModal.tsx
@@ -0,0 +1,52 @@
+import { Dialog, Modal } from '@umami/react-zen';
+import { WebsiteExpandedView } from '@/app/(main)/websites/[websiteId]/WebsiteExpandedView';
+import { useMobile, useNavigation } from '@/components/hooks';
+
+export function ExpandedViewModal({
+ websiteId,
+ excludedIds,
+}: {
+ websiteId: string;
+ excludedIds?: string[];
+}) {
+ const {
+ router,
+ query: { view },
+ updateParams,
+ } = useNavigation();
+ const { isMobile } = useMobile();
+
+ const handleClose = (close: () => void) => {
+ router.push(updateParams({ view: undefined }));
+ close();
+ };
+
+ const handleOpenChange = (isOpen: boolean) => {
+ if (!isOpen) {
+ router.push(updateParams({ view: undefined }));
+ }
+ };
+
+ return (
+
+
+ {({ close }) => {
+ return (
+ handleClose(close)}
+ />
+ );
+ }}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.module.css b/src/app/(main)/websites/[websiteId]/WebsiteChart.module.css
deleted file mode 100644
index b795047a..00000000
--- a/src/app/(main)/websites/[websiteId]/WebsiteChart.module.css
+++ /dev/null
@@ -1,17 +0,0 @@
-.container {
- position: relative;
- display: flex;
- flex-direction: column;
- align-self: stretch;
-}
-
-.chart {
- position: relative;
- overflow: hidden;
-}
-
-.title {
- font-size: var(--font-size-lg);
- line-height: 60px;
- font-weight: 600;
-}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
index 68192307..b2ea2a83 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
@@ -1,18 +1,23 @@
import { useMemo } from 'react';
-import PageviewsChart from '@/components/metrics/PageviewsChart';
-import useWebsitePageviews from '@/components/hooks/queries/useWebsitePageviews';
-import { useDateRange } from '@/components/hooks';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useDateRange, useTimezone } from '@/components/hooks';
+import { useWebsitePageviewsQuery } from '@/components/hooks/queries/useWebsitePageviewsQuery';
+import { PageviewsChart } from '@/components/metrics/PageviewsChart';
export function WebsiteChart({
websiteId,
- compareMode = false,
+ compareMode,
}: {
websiteId: string;
compareMode?: boolean;
}) {
- const { dateRange, dateCompare } = useDateRange(websiteId);
+ const { timezone } = useTimezone();
+ const { dateRange, dateCompare } = useDateRange({ timezone: timezone });
const { startDate, endDate, unit, value } = dateRange;
- const { data, isLoading } = useWebsitePageviews(websiteId, compareMode ? dateCompare : undefined);
+ const { data, isLoading, isFetching, error } = useWebsitePageviewsQuery({
+ websiteId,
+ compare: compareMode ? dateCompare?.compare : undefined,
+ });
const { pageviews, sessions, compare } = (data || {}) as any;
const chartData = useMemo(() => {
@@ -23,7 +28,7 @@ export function WebsiteChart({
};
if (compare) {
- result['compare'] = {
+ result.compare = {
pageviews: result.pageviews.map(({ x }, i) => ({
x,
y: compare.pageviews[i]?.y,
@@ -43,15 +48,14 @@ export function WebsiteChart({
}, [data, startDate, endDate, unit]);
return (
-
+
+
+
);
}
-
-export default WebsiteChart;
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx
deleted file mode 100644
index b27f9870..00000000
--- a/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { Button, Text, Icon, Icons } from 'react-basics';
-import { useMemo } from 'react';
-import { firstBy } from 'thenby';
-import Link from 'next/link';
-import WebsiteChart from './WebsiteChart';
-import useDashboard from '@/store/dashboard';
-import WebsiteHeader from './WebsiteHeader';
-import { WebsiteMetricsBar } from './WebsiteMetricsBar';
-import { useMessages, useLocale, useTeamUrl } from '@/components/hooks';
-
-export default function WebsiteChartList({
- websites,
- showCharts,
- limit,
-}: {
- websites: any[];
- showCharts?: boolean;
- limit?: number;
-}) {
- const { formatMessage, labels } = useMessages();
- const { websiteOrder, websiteActive } = useDashboard();
- const { renderTeamUrl } = useTeamUrl();
- const { dir } = useLocale();
-
- const ordered = useMemo(() => {
- return websites
- .filter(website => (websiteActive.length ? websiteActive.includes(website.id) : true))
- .map(website => ({ ...website, order: websiteOrder.indexOf(website.id) || 0 }))
- .sort(firstBy('order'));
- }, [websites, websiteOrder, websiteActive]);
-
- return (
-
- {ordered.map(({ id }, index) => {
- return index < limit ? (
-
-
-
-
- {formatMessage(labels.viewDetails)}
-
-
-
-
-
-
-
-
-
- {showCharts && }
-
- ) : null;
- })}
-
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx
new file mode 100644
index 00000000..6223dbc0
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx
@@ -0,0 +1,40 @@
+import { Column, Grid, Row } from '@umami/react-zen';
+import { ExportButton } from '@/components/input/ExportButton';
+import { FilterBar } from '@/components/input/FilterBar';
+import { MonthFilter } from '@/components/input/MonthFilter';
+import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
+import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
+
+export function WebsiteControls({
+ websiteId,
+ allowFilter = true,
+ allowDateFilter = true,
+ allowMonthFilter,
+ allowDownload = false,
+ allowCompare = false,
+}: {
+ websiteId: string;
+ allowFilter?: boolean;
+ allowDateFilter?: boolean;
+ allowMonthFilter?: boolean;
+ allowDownload?: boolean;
+ allowCompare?: boolean;
+}) {
+ return (
+
+
+
+ {allowFilter ? :
}
+
+
+ {allowDateFilter && (
+
+ )}
+ {allowDownload && }
+ {allowMonthFilter && }
+
+
+ {allowFilter && }
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx b/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx
deleted file mode 100644
index 460792ef..00000000
--- a/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-'use client';
-import { usePathname } from 'next/navigation';
-import FilterTags from '@/components/metrics/FilterTags';
-import { useNavigation } from '@/components/hooks';
-import WebsiteChart from './WebsiteChart';
-import WebsiteExpandedView from './WebsiteExpandedView';
-import WebsiteHeader from './WebsiteHeader';
-import WebsiteMetricsBar from './WebsiteMetricsBar';
-import WebsiteTableView from './WebsiteTableView';
-import { FILTER_COLUMNS } from '@/lib/constants';
-
-export default function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
- const pathname = usePathname();
- const { query } = useNavigation();
-
- const showLinks = !pathname.includes('/share/');
- const { view } = query;
-
- const params = Object.keys(query).reduce((obj, key) => {
- if (FILTER_COLUMNS[key]) {
- obj[key] = query[key];
- }
- return obj;
- }, {});
-
- return (
- <>
-
-
-
-
- {!view && }
- {view && }
- >
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx b/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx
new file mode 100644
index 00000000..29c3954f
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedMenu.tsx
@@ -0,0 +1,183 @@
+import { SideMenu } from '@/components/common/SideMenu';
+import { useMessages, useNavigation } from '@/components/hooks';
+import {
+ AppWindow,
+ Cpu,
+ Earth,
+ Globe,
+ Landmark,
+ Languages,
+ Laptop,
+ LogIn,
+ LogOut,
+ MapPin,
+ Megaphone,
+ Monitor,
+ Network,
+ Search,
+ Share2,
+ SquareSlash,
+ Tag,
+ Type,
+} from '@/components/icons';
+import { Lightning } from '@/components/svg';
+
+export function WebsiteExpandedMenu({
+ excludedIds = [],
+ onItemClick,
+}: {
+ excludedIds?: string[];
+ onItemClick?: () => void;
+}) {
+ const { formatMessage, labels } = useMessages();
+ const {
+ updateParams,
+ query: { view },
+ } = useNavigation();
+
+ const filterExcluded = (item: { id: string }) => !excludedIds.includes(item.id);
+
+ const items = [
+ {
+ label: 'URL',
+ items: [
+ {
+ id: 'path',
+ label: formatMessage(labels.path),
+ path: updateParams({ view: 'path' }),
+ icon: ,
+ },
+ {
+ id: 'entry',
+ label: formatMessage(labels.entry),
+ path: updateParams({ view: 'entry' }),
+ icon: ,
+ },
+ {
+ id: 'exit',
+ label: formatMessage(labels.exit),
+ path: updateParams({ view: 'exit' }),
+ icon: ,
+ },
+ {
+ id: 'title',
+ label: formatMessage(labels.title),
+ path: updateParams({ view: 'title' }),
+ icon: ,
+ },
+ {
+ id: 'query',
+ label: formatMessage(labels.query),
+ path: updateParams({ view: 'query' }),
+ icon: ,
+ },
+ ].filter(filterExcluded),
+ },
+ {
+ label: formatMessage(labels.sources),
+ items: [
+ {
+ id: 'referrer',
+ label: formatMessage(labels.referrer),
+ path: updateParams({ view: 'referrer' }),
+ icon: ,
+ },
+ {
+ id: 'channel',
+ label: formatMessage(labels.channel),
+ path: updateParams({ view: 'channel' }),
+ icon: ,
+ },
+ {
+ id: 'domain',
+ label: formatMessage(labels.domain),
+ path: updateParams({ view: 'domain' }),
+ icon: ,
+ },
+ ].filter(filterExcluded),
+ },
+ {
+ label: formatMessage(labels.location),
+ items: [
+ {
+ id: 'country',
+ label: formatMessage(labels.country),
+ path: updateParams({ view: 'country' }),
+ icon: ,
+ },
+ {
+ id: 'region',
+ label: formatMessage(labels.region),
+ path: updateParams({ view: 'region' }),
+ icon: ,
+ },
+ {
+ id: 'city',
+ label: formatMessage(labels.city),
+ path: updateParams({ view: 'city' }),
+ icon: ,
+ },
+ ].filter(filterExcluded),
+ },
+ {
+ label: formatMessage(labels.environment),
+ items: [
+ {
+ id: 'browser',
+ label: formatMessage(labels.browser),
+ path: updateParams({ view: 'browser' }),
+ icon: ,
+ },
+ {
+ id: 'os',
+ label: formatMessage(labels.os),
+ path: updateParams({ view: 'os' }),
+ icon: ,
+ },
+ {
+ id: 'device',
+ label: formatMessage(labels.device),
+ path: updateParams({ view: 'device' }),
+ icon: ,
+ },
+ {
+ id: 'language',
+ label: formatMessage(labels.language),
+ path: updateParams({ view: 'language' }),
+ icon: ,
+ },
+ {
+ id: 'screen',
+ label: formatMessage(labels.screen),
+ path: updateParams({ view: 'screen' }),
+ icon: ,
+ },
+ ].filter(filterExcluded),
+ },
+ {
+ label: formatMessage(labels.other),
+ items: [
+ {
+ id: 'event',
+ label: formatMessage(labels.event),
+ path: updateParams({ view: 'event' }),
+ icon: ,
+ },
+ {
+ id: 'hostname',
+ label: formatMessage(labels.hostname),
+ path: updateParams({ view: 'hostname' }),
+ icon: ,
+ },
+ {
+ id: 'tag',
+ label: formatMessage(labels.tag),
+ path: updateParams({ view: 'tag' }),
+ icon: ,
+ },
+ ].filter(filterExcluded),
+ },
+ ];
+
+ return ;
+}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.module.css b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.module.css
deleted file mode 100644
index afe2028a..00000000
--- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.module.css
+++ /dev/null
@@ -1,64 +0,0 @@
-.layout {
- display: grid;
- grid-template-columns: 300px 1fr;
- border-top: 1px solid var(--base300);
-}
-
-.menu {
- display: flex;
- flex-direction: column;
- position: relative;
- padding: 20px 20px 20px 0;
-}
-
-.back {
- display: inline-flex;
- align-items: center;
- align-self: center;
- margin-bottom: 20px;
-}
-
-.content {
- min-height: 800px;
- padding: 20px 0 20px 20px;
- border-left: 1px solid var(--base300);
-}
-
-.dropdown {
- display: none;
-}
-
-@media screen and (max-width: 992px) {
- .layout {
- grid-template-columns: 1fr;
- }
-
- .content {
- border: 0;
- }
-
- .back {
- align-self: flex-start;
- margin: 0;
- }
-
- .nav {
- display: none;
- }
-
- .dropdown {
- display: flex;
- width: 200px;
- align-self: flex-end;
- }
-
- .menu {
- display: flex;
- flex-direction: row;
- gap: 20px;
- align-items: center;
- justify-content: space-between;
- padding-inline-end: 0;
- z-index: 10;
- }
-}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx
index 4858ec73..2c670df1 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx
@@ -1,184 +1,57 @@
-import { Dropdown, Icon, Icons, Item, Text } from 'react-basics';
-import LinkButton from '@/components/common/LinkButton';
-import { useLocale, useMessages, useNavigation } from '@/components/hooks';
-import SideNav from '@/components/layout/SideNav';
-import BrowsersTable from '@/components/metrics/BrowsersTable';
-import CitiesTable from '@/components/metrics/CitiesTable';
-import CountriesTable from '@/components/metrics/CountriesTable';
-import DevicesTable from '@/components/metrics/DevicesTable';
-import EventsTable from '@/components/metrics/EventsTable';
-import HostsTable from '@/components/metrics/HostsTable';
-import LanguagesTable from '@/components/metrics/LanguagesTable';
-import OSTable from '@/components/metrics/OSTable';
-import PagesTable from '@/components/metrics/PagesTable';
-import QueryParametersTable from '@/components/metrics/QueryParametersTable';
-import ReferrersTable from '@/components/metrics/ReferrersTable';
-import RegionsTable from '@/components/metrics/RegionsTable';
-import ScreenTable from '@/components/metrics/ScreenTable';
-import TagsTable from '@/components/metrics/TagsTable';
-import ChannelsTable from '@/components/metrics/ChannelsTable';
-import styles from './WebsiteExpandedView.module.css';
+import { Column, Grid, Row } from '@umami/react-zen';
+import { WebsiteExpandedMenu } from '@/app/(main)/websites/[websiteId]/WebsiteExpandedMenu';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { MobileMenuButton } from '@/components/input/MobileMenuButton';
+import { MetricsExpandedTable } from '@/components/metrics/MetricsExpandedTable';
-const views = {
- url: PagesTable,
- entry: PagesTable,
- exit: PagesTable,
- title: PagesTable,
- referrer: ReferrersTable,
- grouped: ReferrersTable,
- host: HostsTable,
- browser: BrowsersTable,
- os: OSTable,
- device: DevicesTable,
- screen: ScreenTable,
- country: CountriesTable,
- region: RegionsTable,
- city: CitiesTable,
- language: LanguagesTable,
- event: EventsTable,
- query: QueryParametersTable,
- tag: TagsTable,
- channel: ChannelsTable,
-};
-
-export default function WebsiteExpandedView({
+export function WebsiteExpandedView({
websiteId,
- domainName,
+ excludedIds = [],
+ onClose,
}: {
websiteId: string;
- domainName?: string;
+ excludedIds?: string[];
+ onClose?: () => void;
}) {
- const { dir } = useLocale();
const { formatMessage, labels } = useMessages();
const {
- router,
- renderUrl,
query: { view },
} = useNavigation();
- const items = [
- {
- key: 'url',
- label: formatMessage(labels.pages),
- url: renderUrl({ view: 'url' }),
- },
- {
- key: 'referrer',
- label: formatMessage(labels.referrers),
- url: renderUrl({ view: 'referrer' }),
- },
- {
- key: 'channel',
- label: formatMessage(labels.channels),
- url: renderUrl({ view: 'channel' }),
- },
- {
- key: 'browser',
- label: formatMessage(labels.browsers),
- url: renderUrl({ view: 'browser' }),
- },
- {
- key: 'os',
- label: formatMessage(labels.os),
- url: renderUrl({ view: 'os' }),
- },
- {
- key: 'device',
- label: formatMessage(labels.devices),
- url: renderUrl({ view: 'device' }),
- },
- {
- key: 'country',
- label: formatMessage(labels.countries),
- url: renderUrl({ view: 'country' }),
- },
- {
- key: 'region',
- label: formatMessage(labels.regions),
- url: renderUrl({ view: 'region' }),
- },
- {
- key: 'city',
- label: formatMessage(labels.cities),
- url: renderUrl({ view: 'city' }),
- },
- {
- key: 'language',
- label: formatMessage(labels.languages),
- url: renderUrl({ view: 'language' }),
- },
- {
- key: 'screen',
- label: formatMessage(labels.screens),
- url: renderUrl({ view: 'screen' }),
- },
- {
- key: 'event',
- label: formatMessage(labels.events),
- url: renderUrl({ view: 'event' }),
- },
- {
- key: 'query',
- label: formatMessage(labels.queryParameters),
- url: renderUrl({ view: 'query' }),
- },
- {
- key: 'host',
- label: formatMessage(labels.hosts),
- url: renderUrl({ view: 'host' }),
- },
- {
- key: 'tag',
- label: formatMessage(labels.tags),
- url: renderUrl({ view: 'tag' }),
- },
- ];
-
- const DetailsComponent = views[view] || (() => null);
-
- const handleChange = (view: any) => {
- router.push(renderUrl({ view }));
- };
-
- const renderValue = (value: string) => items.find(({ key }) => key === value)?.label;
-
return (
-
-
-
+
+
+ {({ close }) => {
+ return (
+
+
+
+ );
+ }}
+
+
+
+
-
-
-
- {formatMessage(labels.back)}
-
-
-
- {({ key, label }) => - {label}
}
-
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.module.css b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.module.css
deleted file mode 100644
index 80f101e3..00000000
--- a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.module.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.button {
- font-weight: 700;
-}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx
deleted file mode 100644
index 02b74418..00000000
--- a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { Button, Icon, Icons, Popup, PopupTrigger, Text } from 'react-basics';
-import PopupForm from '@/app/(main)/reports/[reportId]/PopupForm';
-import FilterSelectForm from '@/app/(main)/reports/[reportId]/FilterSelectForm';
-import { useFields, useMessages, useNavigation, useDateRange } from '@/components/hooks';
-import { OPERATOR_PREFIXES } from '@/lib/constants';
-import styles from './WebsiteFilterButton.module.css';
-
-export function WebsiteFilterButton({
- websiteId,
- className,
- position = 'bottom',
- alignment = 'end',
- showText = true,
-}: {
- websiteId: string;
- className?: string;
- position?: 'bottom' | 'top' | 'left' | 'right';
- alignment?: 'end' | 'center' | 'start';
- showText?: boolean;
-}) {
- const { formatMessage, labels } = useMessages();
- const { renderUrl, router } = useNavigation();
- const { fields } = useFields();
- const {
- dateRange: { startDate, endDate },
- } = useDateRange(websiteId);
-
- const handleAddFilter = ({ name, operator, value }) => {
- const prefix = OPERATOR_PREFIXES[operator];
-
- router.push(renderUrl({ [name]: prefix + value }));
- };
-
- return (
-
-
-
-
-
- {showText && {formatMessage(labels.filter)} }
-
-
- {(close: () => void) => {
- return (
-
- {
- handleAddFilter(value);
- close();
- }}
- />
-
- );
- }}
-
-
- );
-}
-
-export default WebsiteFilterButton;
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.module.css b/src/app/(main)/websites/[websiteId]/WebsiteHeader.module.css
deleted file mode 100644
index 90c3f5cb..00000000
--- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.module.css
+++ /dev/null
@@ -1,62 +0,0 @@
-.header {
- display: flex;
- gap: 10px;
- align-items: center;
- flex-wrap: wrap;
- padding: 20px 0px;
-}
-
-.title {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 10px;
- font-size: 24px;
- font-weight: 700;
- overflow: hidden;
- height: 60px;
-}
-
-.actions {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: flex-end;
- gap: 30px;
- min-height: 0;
- margin-left: auto;
-}
-
-.selected {
- font-weight: bold;
-}
-
-.links {
- display: flex;
- flex-direction: row;
- align-items: center;
-}
-
-@media only screen and (max-width: 768px) {
- .header {
- grid-template-columns: 1fr;
- }
-
- .links {
- justify-content: space-evenly;
- flex: 1;
- border-bottom: 1px solid var(--base300);
- padding-bottom: 10px;
- margin-bottom: 10px;
- }
-
- .label {
- display: none;
- }
-
- .icon,
- .icon svg {
- width: 20px;
- height: 20px;
- }
-}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
index b568dd3d..7dd1d771 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
@@ -1,102 +1,57 @@
-import classNames from 'classnames';
-import Favicon from '@/components/common/Favicon';
-import { useMessages, useTeamUrl, useWebsite } from '@/components/hooks';
-import Icons from '@/components/icons';
-import ActiveUsers from '@/components/metrics/ActiveUsers';
-import Link from 'next/link';
-import { usePathname } from 'next/navigation';
-import { ReactNode } from 'react';
-import { Button, Icon, Text } from 'react-basics';
-import Lightning from '@/assets/lightning.svg';
-import styles from './WebsiteHeader.module.css';
+import { Icon, Row, Text } from '@umami/react-zen';
+import { WebsiteShareForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteShareForm';
+import { Favicon } from '@/components/common/Favicon';
+import { LinkButton } from '@/components/common/LinkButton';
+import { PageHeader } from '@/components/common/PageHeader';
+import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
+import { Edit, Share } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { ActiveUsers } from '@/components/metrics/ActiveUsers';
+
+export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
+ const website = useWebsite();
+ const { renderUrl, pathname } = useNavigation();
+ const isSettings = pathname.endsWith('/settings');
-export function WebsiteHeader({
- websiteId,
- showLinks = true,
- children,
-}: {
- websiteId: string;
- showLinks?: boolean;
- children?: ReactNode;
-}) {
const { formatMessage, labels } = useMessages();
- const { renderTeamUrl } = useTeamUrl();
- const pathname = usePathname();
- const { data: website } = useWebsite(websiteId);
- const { name, domain } = website || {};
- const links = [
- {
- label: formatMessage(labels.overview),
- icon: ,
- path: '',
- },
- {
- label: formatMessage(labels.events),
- icon: ,
- path: '/events',
- },
- {
- label: formatMessage(labels.sessions),
- icon: ,
- path: '/sessions',
- },
- {
- label: formatMessage(labels.realtime),
- icon: ,
- path: '/realtime',
- },
- {
- label: formatMessage(labels.compare),
- icon: ,
- path: '/compare',
- },
- {
- label: formatMessage(labels.reports),
- icon: ,
- path: '/reports',
- },
- ];
+ if (isSettings) {
+ return null;
+ }
return (
-
-
-
- {showLinks && (
-
- {links.map(({ label, icon, path }) => {
- const selected = path
- ? pathname.includes(path)
- : pathname.match(/^\/websites\/[\w-]+$/);
+
}
+ titleHref={renderUrl(`/websites/${website.id}`, false)}
+ >
+
+
- return (
-
-
- {icon}
- {label}
-
-
- );
- })}
-
+ {showActions && (
+
+
+
+
+
+
+ {formatMessage(labels.edit)}
+
+
)}
- {children}
-
-
+
+
);
}
-export default WebsiteHeader;
+const ShareButton = ({ websiteId, shareId }) => {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ } label={formatMessage(labels.share)} width="800px">
+ {({ close }) => {
+ return ;
+ }}
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx
new file mode 100644
index 00000000..7260a7ea
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx
@@ -0,0 +1,30 @@
+'use client';
+import { Column, Grid } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
+import { PageBody } from '@/components/common/PageBody';
+import { WebsiteHeader } from './WebsiteHeader';
+import { WebsiteNav } from './WebsiteNav';
+
+export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMenu.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMenu.tsx
new file mode 100644
index 00000000..30189534
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/WebsiteMenu.tsx
@@ -0,0 +1,56 @@
+import {
+ Button,
+ Icon,
+ Menu,
+ MenuItem,
+ MenuSeparator,
+ MenuTrigger,
+ Popover,
+ Text,
+} from '@umami/react-zen';
+import { Fragment } from 'react';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { Edit, More, Share } from '@/components/icons';
+
+export function WebsiteMenu({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+ const { router, updateParams, renderUrl } = useNavigation();
+
+ const menuItems = [
+ { id: 'share', label: formatMessage(labels.share), icon: },
+ { id: 'edit', label: formatMessage(labels.edit), icon: , seperator: true },
+ ];
+
+ const handleAction = (id: any) => {
+ if (id === 'compare') {
+ router.push(updateParams({ compare: 'prev' }));
+ } else if (id === 'edit') {
+ router.push(renderUrl(`/websites/${websiteId}`));
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {menuItems.map(({ id, label, icon, seperator }, index) => {
+ return (
+
+ {seperator && }
+
+ {icon}
+ {label}
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.module.css b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.module.css
deleted file mode 100644
index 6c5a0e56..00000000
--- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.module.css
+++ /dev/null
@@ -1,52 +0,0 @@
-.container {
- display: grid;
- grid-template-columns: 2fr 1fr;
- justify-content: space-between;
- align-items: center;
- background: var(--base50);
- z-index: var(--z-index-above);
- min-height: 120px;
- padding-bottom: 20px;
-}
-
-.actions {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- gap: 10px;
- flex-wrap: wrap;
-}
-
-.vs {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- flex-basis: 100%;
- gap: 10px;
-}
-
-.dropdown {
- min-width: 200px;
-}
-
-@media screen and (max-width: 1200px) {
- .container {
- grid-template-columns: 1fr;
- }
-
- .actions {
- margin: 20px 0;
- }
-}
-
-@media screen and (min-width: 992px) {
- .sticky {
- position: sticky;
- top: -1px;
- }
-
- .isSticky {
- padding: 10px 0;
- border-bottom: 1px solid var(--base300);
- }
-}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
index f206d3c9..6c91ba6d 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
@@ -1,136 +1,88 @@
-import { Dropdown, Item } from 'react-basics';
-import classNames from 'classnames';
-import { useDateRange, useMessages, useSticky } from '@/components/hooks';
-import WebsiteDateFilter from '@/components/input/WebsiteDateFilter';
-import MetricCard from '@/components/metrics/MetricCard';
-import MetricsBar from '@/components/metrics/MetricsBar';
-import { formatShortTime, formatLongNumber } from '@/lib/format';
-import useWebsiteStats from '@/components/hooks/queries/useWebsiteStats';
-import useStore, { setWebsiteDateCompare } from '@/store/websites';
-import WebsiteFilterButton from './WebsiteFilterButton';
-import styles from './WebsiteMetricsBar.module.css';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
+import { formatLongNumber, formatShortTime } from '@/lib/format';
export function WebsiteMetricsBar({
websiteId,
- sticky,
- showChange = false,
- compareMode = false,
- showFilter = false,
}: {
websiteId: string;
- sticky?: boolean;
showChange?: boolean;
compareMode?: boolean;
- showFilter?: boolean;
}) {
- const { dateRange } = useDateRange(websiteId);
- const { formatMessage, labels } = useMessages();
- const dateCompare = useStore(state => state[websiteId]?.dateCompare);
- const { ref, isSticky } = useSticky({ enabled: sticky });
- const { data, isLoading, isFetched, error } = useWebsiteStats(
- websiteId,
- compareMode && dateCompare,
- );
- const isAllTime = dateRange.value === 'all';
+ const { isAllTime } = useDateRange();
+ const { formatMessage, labels, getErrorMessage } = useMessages();
+ const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
- const { pageviews, visitors, visits, bounces, totaltime } = data || {};
+ const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};
const metrics = data
? [
{
- ...pageviews,
- label: formatMessage(labels.views),
- change: pageviews.value - pageviews.prev,
- formatValue: formatLongNumber,
- },
- {
- ...visits,
- label: formatMessage(labels.visits),
- change: visits.value - visits.prev,
- formatValue: formatLongNumber,
- },
- {
- ...visitors,
+ value: visitors,
label: formatMessage(labels.visitors),
- change: visitors.value - visitors.prev,
+ change: visitors - comparison.visitors,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: visits,
+ label: formatMessage(labels.visits),
+ change: visits - comparison.visits,
+ formatValue: formatLongNumber,
+ },
+ {
+ value: pageviews,
+ label: formatMessage(labels.views),
+ change: pageviews - comparison.pageviews,
formatValue: formatLongNumber,
},
{
label: formatMessage(labels.bounceRate),
- value: (Math.min(visits.value, bounces.value) / visits.value) * 100,
- prev: (Math.min(visits.prev, bounces.prev) / visits.prev) * 100,
+ value: (Math.min(visits, bounces) / visits) * 100,
+ prev: (Math.min(comparison.visits, comparison.bounces) / comparison.visits) * 100,
change:
- (Math.min(visits.value, bounces.value) / visits.value) * 100 -
- (Math.min(visits.prev, bounces.prev) / visits.prev) * 100,
- formatValue: n => Math.round(+n) + '%',
+ (Math.min(visits, bounces) / visits) * 100 -
+ (Math.min(comparison.visits, comparison.bounces) / comparison.visits) * 100,
+ formatValue: n => `${Math.round(+n)}%`,
reverseColors: true,
},
{
label: formatMessage(labels.visitDuration),
- value: totaltime.value / visits.value,
- prev: totaltime.prev / visits.prev,
- change: totaltime.value / visits.value - totaltime.prev / visits.prev,
+ value: totaltime / visits,
+ prev: comparison.totaltime / comparison.visits,
+ change: totaltime / visits - comparison.totaltime / comparison.visits,
formatValue: n =>
`${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`,
},
]
- : [];
-
- const items = [
- { label: formatMessage(labels.previousPeriod), value: 'prev' },
- { label: formatMessage(labels.previousYear), value: 'yoy' },
- ];
+ : null;
return (
-
-
-
- {metrics.map(({ label, value, prev, change, formatValue, reverseColors }) => {
- return (
-
- );
- })}
-
-
-
- {showFilter &&
}
-
- {compareMode && (
-
- VS
- items.find(i => i.value === value)?.label}
- alignment="end"
- onChange={(value: any) => setWebsiteDateCompare(websiteId, value)}
- >
- {items.map(({ label, value }) => (
- - {label}
- ))}
-
-
- )}
-
-
+
+ {metrics?.map(({ label, value, prev, change, formatValue, reverseColors }) => {
+ return (
+
+ );
+ })}
+
+
);
}
-
-export default WebsiteMetricsBar;
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx
new file mode 100644
index 00000000..ad05b706
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx
@@ -0,0 +1,180 @@
+import { Column, Text } from '@umami/react-zen';
+import { SideMenu } from '@/components/common/SideMenu';
+import { useMessages, useNavigation } from '@/components/hooks';
+import {
+ AlignEndHorizontal,
+ ChartPie,
+ Clock,
+ Eye,
+ Sheet,
+ Tag,
+ User,
+ UserPlus,
+} from '@/components/icons';
+import { WebsiteSelect } from '@/components/input/WebsiteSelect';
+import { Funnel, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg';
+
+export function WebsiteNav({
+ websiteId,
+ onItemClick,
+}: {
+ websiteId: string;
+ onItemClick?: () => void;
+}) {
+ const { formatMessage, labels } = useMessages();
+ const { pathname, renderUrl, teamId, router } = useNavigation();
+
+ const renderPath = (path: string) =>
+ renderUrl(`/websites/${websiteId}${path}`, {
+ event: undefined,
+ compare: undefined,
+ view: undefined,
+ });
+
+ const items = [
+ {
+ label: formatMessage(labels.traffic),
+ items: [
+ {
+ id: 'overview',
+ label: formatMessage(labels.overview),
+ icon: ,
+ path: renderPath(''),
+ },
+ {
+ id: 'events',
+ label: formatMessage(labels.events),
+ icon: ,
+ path: renderPath('/events'),
+ },
+ {
+ id: 'sessions',
+ label: formatMessage(labels.sessions),
+ icon: ,
+ path: renderPath('/sessions'),
+ },
+ {
+ id: 'realtime',
+ label: formatMessage(labels.realtime),
+ icon: ,
+ path: renderPath('/realtime'),
+ },
+ {
+ id: 'compare',
+ label: formatMessage(labels.compare),
+ icon: ,
+ path: renderPath('/compare'),
+ },
+ {
+ id: 'breakdown',
+ label: formatMessage(labels.breakdown),
+ icon: ,
+ path: renderPath('/breakdown'),
+ },
+ ],
+ },
+ {
+ label: formatMessage(labels.behavior),
+ items: [
+ {
+ id: 'goals',
+ label: formatMessage(labels.goals),
+ icon: ,
+ path: renderPath('/goals'),
+ },
+ {
+ id: 'funnel',
+ label: formatMessage(labels.funnels),
+ icon: ,
+ path: renderPath('/funnels'),
+ },
+ {
+ id: 'journeys',
+ label: formatMessage(labels.journeys),
+ icon: ,
+ path: renderPath('/journeys'),
+ },
+ {
+ id: 'retention',
+ label: formatMessage(labels.retention),
+ icon: ,
+ path: renderPath('/retention'),
+ },
+ ],
+ },
+ {
+ label: formatMessage(labels.audience),
+ items: [
+ {
+ id: 'segments',
+ label: formatMessage(labels.segments),
+ icon: ,
+ path: renderPath('/segments'),
+ },
+ {
+ id: 'cohorts',
+ label: formatMessage(labels.cohorts),
+ icon: ,
+ path: renderPath('/cohorts'),
+ },
+ ],
+ },
+ {
+ label: formatMessage(labels.growth),
+ items: [
+ {
+ id: 'utm',
+ label: formatMessage(labels.utm),
+ icon: ,
+ path: renderPath('/utm'),
+ },
+ {
+ id: 'revenue',
+ label: formatMessage(labels.revenue),
+ icon: ,
+ path: renderPath('/revenue'),
+ },
+ {
+ id: 'attribution',
+ label: formatMessage(labels.attribution),
+ icon: ,
+ path: renderPath('/attribution'),
+ },
+ ],
+ },
+ ];
+
+ const handleChange = (value: string) => {
+ router.push(renderUrl(`/websites/${value}`));
+ };
+
+ const renderValue = (value: any) => {
+ return (
+
+ {value?.selectedItem?.name}
+
+ );
+ };
+
+ const selectedKey = items
+ .flatMap(e => e.items)
+ .find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx
new file mode 100644
index 00000000..f587e112
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx
@@ -0,0 +1,22 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
+import { Panel } from '@/components/common/Panel';
+import { WebsiteChart } from './WebsiteChart';
+import { WebsiteControls } from './WebsiteControls';
+import { WebsiteMetricsBar } from './WebsiteMetricsBar';
+import { WebsitePanels } from './WebsitePanels';
+
+export function WebsitePage({ websiteId }: { websiteId: string }) {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx b/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx
new file mode 100644
index 00000000..a91d562e
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/WebsitePanels.tsx
@@ -0,0 +1,140 @@
+import { Grid, Heading, Row, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
+import { GridRow } from '@/components/common/GridRow';
+import { Panel } from '@/components/common/Panel';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { EventsChart } from '@/components/metrics/EventsChart';
+import { MetricsTable } from '@/components/metrics/MetricsTable';
+import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic';
+import { WorldMap } from '@/components/metrics/WorldMap';
+
+export function WebsitePanels({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+ const { pathname } = useNavigation();
+ const tableProps = {
+ websiteId,
+ limit: 10,
+ allowDownload: false,
+ showMore: true,
+ metric: formatMessage(labels.visitors),
+ };
+ const rowProps = { minHeight: '570px' };
+ const isSharePage = pathname.includes('/share/');
+
+ return (
+
+
+
+ {formatMessage(labels.pages)}
+
+
+ {formatMessage(labels.path)}
+ {formatMessage(labels.entry)}
+ {formatMessage(labels.exit)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.sources)}
+
+
+ {formatMessage(labels.referrers)}
+ {formatMessage(labels.channels)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.environment)}
+
+
+ {formatMessage(labels.browsers)}
+ {formatMessage(labels.os)}
+ {formatMessage(labels.devices)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.location)}
+
+
+ {formatMessage(labels.countries)}
+ {formatMessage(labels.regions)}
+ {formatMessage(labels.cities)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.traffic)}
+
+
+
+
+ {isSharePage && (
+
+
+ {formatMessage(labels.events)}
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx b/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx
deleted file mode 100644
index 198ad030..00000000
--- a/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-'use client';
-import { createContext, ReactNode, useEffect } from 'react';
-import { useModified, useWebsite } from '@/components/hooks';
-import { Loading } from 'react-basics';
-
-export const WebsiteContext = createContext(null);
-
-export function WebsiteProvider({
- websiteId,
- children,
-}: {
- websiteId: string;
- children: ReactNode;
-}) {
- const { modified } = useModified(`website:${websiteId}`);
- const { data: website, isFetching, isLoading, refetch } = useWebsite(websiteId);
-
- useEffect(() => {
- if (modified) {
- refetch();
- }
- }, [modified]);
-
- if (isFetching && isLoading) {
- return ;
- }
-
- return {children} ;
-}
-
-export default WebsiteProvider;
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx
deleted file mode 100644
index 02422075..00000000
--- a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Grid, GridRow } from '@/components/layout/Grid';
-import PagesTable from '@/components/metrics/PagesTable';
-import ReferrersTable from '@/components/metrics/ReferrersTable';
-import BrowsersTable from '@/components/metrics/BrowsersTable';
-import OSTable from '@/components/metrics/OSTable';
-import DevicesTable from '@/components/metrics/DevicesTable';
-import WorldMap from '@/components/metrics/WorldMap';
-import CountriesTable from '@/components/metrics/CountriesTable';
-import EventsTable from '@/components/metrics/EventsTable';
-import EventsChart from '@/components/metrics/EventsChart';
-import { usePathname } from 'next/navigation';
-
-export default function WebsiteTableView({ websiteId }: { websiteId: string }) {
- const pathname = usePathname();
- const tableProps = {
- websiteId,
- limit: 10,
- };
- const isSharePage = pathname.includes('/share/');
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isSharePage && (
-
-
-
-
- )}
-
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteTabs.tsx b/src/app/(main)/websites/[websiteId]/WebsiteTabs.tsx
new file mode 100644
index 00000000..ac978a23
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/WebsiteTabs.tsx
@@ -0,0 +1,64 @@
+import { Icon, Row, Tab, TabList, Tabs, Text } from '@umami/react-zen';
+import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
+import { ChartPie, Clock, Eye, User } from '@/components/icons';
+import { Lightning } from '@/components/svg';
+
+export function WebsiteTabs() {
+ const website = useWebsite();
+ const { pathname, renderUrl } = useNavigation();
+ const { formatMessage, labels } = useMessages();
+
+ const links = [
+ {
+ id: 'overview',
+ label: formatMessage(labels.overview),
+ icon: ,
+ path: '',
+ },
+ {
+ id: 'events',
+ label: formatMessage(labels.events),
+ icon: ,
+ path: '/events',
+ },
+ {
+ id: 'sessions',
+ label: formatMessage(labels.sessions),
+ icon: ,
+ path: '/sessions',
+ },
+ {
+ id: 'realtime',
+ label: formatMessage(labels.realtime),
+ icon: ,
+ path: '/realtime',
+ },
+ {
+ id: 'reports',
+ label: formatMessage(labels.reports),
+ icon: ,
+ path: '/reports',
+ },
+ ];
+
+ const selectedKey = links.find(({ path }) => path && pathname.includes(path))?.id || 'overview';
+
+ return (
+
+
+
+ {links.map(({ id, label, icon, path }) => {
+ return (
+
+
+ {icon}
+ {label}
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortAddButton.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortAddButton.tsx
new file mode 100644
index 00000000..3f7f8723
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortAddButton.tsx
@@ -0,0 +1,21 @@
+import { useMessages } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { CohortEditForm } from './CohortEditForm';
+
+export function CohortAddButton({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ }
+ label={formatMessage(labels.cohort)}
+ variant="primary"
+ width="800px"
+ >
+ {({ close }) => {
+ return ;
+ }}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton.tsx
new file mode 100644
index 00000000..94d62ff2
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton.tsx
@@ -0,0 +1,60 @@
+import { ConfirmationForm } from '@/components/common/ConfirmationForm';
+import { useDeleteQuery, useMessages } from '@/components/hooks';
+import { Trash } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { messages } from '@/components/messages';
+
+export function CohortDeleteButton({
+ cohortId,
+ websiteId,
+ name,
+ onSave,
+}: {
+ cohortId: string;
+ websiteId: string;
+ name: string;
+ onSave?: () => void;
+}) {
+ const { formatMessage, labels, FormattedMessage } = useMessages();
+ const { mutateAsync, isPending, error, touch } = useDeleteQuery(
+ `/websites/${websiteId}/segments/${cohortId}`,
+ );
+
+ const handleConfirm = async (close: () => void) => {
+ await mutateAsync(null, {
+ onSuccess: () => {
+ touch('cohorts');
+ onSave?.();
+ close();
+ },
+ });
+ };
+
+ return (
+ }
+ variant="quiet"
+ title={formatMessage(labels.confirm)}
+ width="400px"
+ >
+ {({ close }) => (
+ {name},
+ }}
+ />
+ }
+ isLoading={isPending}
+ error={error}
+ onConfirm={handleConfirm.bind(null, close)}
+ onClose={close}
+ buttonLabel={formatMessage(labels.delete)}
+ buttonVariant="danger"
+ />
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortEditButton.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortEditButton.tsx
new file mode 100644
index 00000000..07990712
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortEditButton.tsx
@@ -0,0 +1,37 @@
+import { CohortEditForm } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditForm';
+import { useMessages } from '@/components/hooks';
+import { Edit } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import type { Filter } from '@/lib/types';
+
+export function CohortEditButton({
+ cohortId,
+ websiteId,
+ filters,
+}: {
+ cohortId: string;
+ websiteId: string;
+ filters: Filter[];
+}) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ }
+ variant="quiet"
+ title={formatMessage(labels.cohort)}
+ width="800px"
+ >
+ {({ close }) => {
+ return (
+
+ );
+ }}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortEditForm.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortEditForm.tsx
new file mode 100644
index 00000000..c7550352
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortEditForm.tsx
@@ -0,0 +1,135 @@
+import {
+ Button,
+ Column,
+ Form,
+ FormButtons,
+ FormField,
+ FormSubmitButton,
+ Grid,
+ Label,
+ Loading,
+ TextField,
+} from '@umami/react-zen';
+import { useMessages, useUpdateQuery, useWebsiteCohortQuery } from '@/components/hooks';
+import { ActionSelect } from '@/components/input/ActionSelect';
+import { DateFilter } from '@/components/input/DateFilter';
+import { FieldFilters } from '@/components/input/FieldFilters';
+import { LookupField } from '@/components/input/LookupField';
+
+export function CohortEditForm({
+ cohortId,
+ websiteId,
+ filters = [],
+ onSave,
+ onClose,
+}: {
+ cohortId?: string;
+ websiteId: string;
+ filters?: any[];
+ showFilters?: boolean;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { data } = useWebsiteCohortQuery(websiteId, cohortId);
+ const { formatMessage, labels, messages, getErrorMessage } = useMessages();
+
+ const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(
+ `/websites/${websiteId}/segments${cohortId ? `/${cohortId}` : ''}`,
+ {
+ type: 'cohort',
+ },
+ );
+
+ const handleSubmit = async (formData: any) => {
+ await mutateAsync(formData, {
+ onSuccess: async () => {
+ toast(formatMessage(messages.saved));
+ touch('cohorts');
+ onSave?.();
+ onClose?.();
+ },
+ });
+ };
+
+ if (cohortId && !data) {
+ return ;
+ }
+
+ const defaultValues = {
+ parameters: { filters, dateRange: '30day', action: { type: 'path', value: '' } },
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortsDataTable.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortsDataTable.tsx
new file mode 100644
index 00000000..6734384e
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortsDataTable.tsx
@@ -0,0 +1,24 @@
+import { DataGrid } from '@/components/common/DataGrid';
+import { useWebsiteCohortsQuery } from '@/components/hooks';
+import { CohortAddButton } from './CohortAddButton';
+import { CohortsTable } from './CohortsTable';
+
+export function CohortsDataTable({ websiteId }: { websiteId?: string }) {
+ const query = useWebsiteCohortsQuery(websiteId, { type: 'cohort' });
+
+ const renderActions = () => {
+ return ;
+ };
+
+ return (
+
+ {({ data }) => }
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortsPage.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortsPage.tsx
new file mode 100644
index 00000000..14f366e5
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortsPage.tsx
@@ -0,0 +1,16 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { Panel } from '@/components/common/Panel';
+import { CohortsDataTable } from './CohortsDataTable';
+
+export function CohortsPage({ websiteId }) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx
new file mode 100644
index 00000000..5c7ac03f
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx
@@ -0,0 +1,41 @@
+import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
+import Link from 'next/link';
+import { CohortDeleteButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton';
+import { CohortEditButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditButton';
+import { DateDistance } from '@/components/common/DateDistance';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { filtersObjectToArray } from '@/lib/params';
+
+export function CohortsTable(props: DataTableProps) {
+ const { formatMessage, labels } = useMessages();
+ const { websiteId, renderUrl } = useNavigation();
+
+ return (
+
+
+ {(row: any) => (
+ {row.name}
+ )}
+
+
+ {(row: any) => }
+
+
+ {(row: any) => {
+ const { id, name, parameters } = row;
+
+ return (
+
+
+
+
+ );
+ }}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/cohorts/page.tsx b/src/app/(main)/websites/[websiteId]/cohorts/page.tsx
new file mode 100644
index 00000000..9946f602
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/cohorts/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { CohortsPage } from './CohortsPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Cohorts',
+};
diff --git a/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx
new file mode 100644
index 00000000..bca8d244
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx
@@ -0,0 +1,20 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { WebsiteMetricsBar } from '@/app/(main)/websites/[websiteId]/WebsiteMetricsBar';
+import { Panel } from '@/components/common/Panel';
+import { CompareTables } from './CompareTables';
+
+export function ComparePage({ websiteId }: { websiteId: string }) {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx
new file mode 100644
index 00000000..13c05160
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/compare/CompareTables.tsx
@@ -0,0 +1,171 @@
+import { Column, Grid, Heading, ListItem, Row, Select } from '@umami/react-zen';
+import { useState } from 'react';
+import { DateDisplay } from '@/components/common/DateDisplay';
+import { Panel } from '@/components/common/Panel';
+import { useDateRange, useMessages, useNavigation } from '@/components/hooks';
+import { ChangeLabel } from '@/components/metrics/ChangeLabel';
+import { MetricsTable } from '@/components/metrics/MetricsTable';
+import { formatNumber } from '@/lib/format';
+
+export function CompareTables({ websiteId }: { websiteId: string }) {
+ const [data, setData] = useState([]);
+ const { dateRange, dateCompare } = useDateRange();
+ const { formatMessage, labels } = useMessages();
+ const {
+ router,
+ updateParams,
+ query: { view = 'path' },
+ } = useNavigation();
+ const { startDate, endDate } = dateCompare;
+
+ const params = {
+ startAt: startDate.getTime(),
+ endAt: endDate.getTime(),
+ };
+
+ const renderPath = (view: string) => {
+ return updateParams({ view });
+ };
+
+ const items = [
+ {
+ id: 'path',
+ label: formatMessage(labels.path),
+ path: renderPath('path'),
+ },
+ {
+ id: 'channel',
+ label: formatMessage(labels.channels),
+ path: renderPath('channel'),
+ },
+ {
+ id: 'referrer',
+ label: formatMessage(labels.referrers),
+ path: renderPath('referrer'),
+ },
+ {
+ id: 'browser',
+ label: formatMessage(labels.browsers),
+ path: renderPath('browser'),
+ },
+ {
+ id: 'os',
+ label: formatMessage(labels.os),
+ path: renderPath('os'),
+ },
+ {
+ id: 'device',
+ label: formatMessage(labels.devices),
+ path: renderPath('device'),
+ },
+ {
+ id: 'country',
+ label: formatMessage(labels.countries),
+ path: renderPath('country'),
+ },
+ {
+ id: 'region',
+ label: formatMessage(labels.regions),
+ path: renderPath('region'),
+ },
+ {
+ id: 'city',
+ label: formatMessage(labels.cities),
+ path: renderPath('city'),
+ },
+ {
+ id: 'language',
+ label: formatMessage(labels.languages),
+ path: renderPath('language'),
+ },
+ {
+ id: 'screen',
+ label: formatMessage(labels.screens),
+ path: renderPath('screen'),
+ },
+ {
+ id: 'event',
+ label: formatMessage(labels.events),
+ path: renderPath('event'),
+ },
+ {
+ id: 'hostname',
+ label: formatMessage(labels.hostname),
+ path: renderPath('hostname'),
+ },
+ {
+ id: 'tag',
+ label: formatMessage(labels.tags),
+ path: renderPath('tag'),
+ },
+ ];
+
+ const renderChange = ({ label, count }) => {
+ const prev = data.find(d => d.x === label)?.y;
+ const value = count - prev;
+ const change = Math.abs(((count - prev) / prev) * 100);
+
+ return (
+ !Number.isNaN(change) && (
+
+ {formatNumber(change)}%
+
+ )
+ );
+ };
+
+ const handleChange = (id: any) => {
+ router.push(renderPath(id));
+ };
+
+ return (
+ <>
+
+
+ {items.map(({ id, label }) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
+
+ {formatMessage(labels.previous)}
+
+
+
+
+
+
+ {formatMessage(labels.current)}
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx b/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx
deleted file mode 100644
index 10a2eed1..00000000
--- a/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-'use client';
-import WebsiteHeader from '../WebsiteHeader';
-import WebsiteMetricsBar from '../WebsiteMetricsBar';
-import FilterTags from '@/components/metrics/FilterTags';
-import { useNavigation } from '@/components/hooks';
-import { FILTER_COLUMNS } from '@/lib/constants';
-import WebsiteChart from '../WebsiteChart';
-import WebsiteCompareTables from './WebsiteCompareTables';
-
-export function WebsiteComparePage({ websiteId }) {
- const { query } = useNavigation();
-
- const params = Object.keys(query).reduce((obj, key) => {
- if (FILTER_COLUMNS[key]) {
- obj[key] = query[key];
- }
- return obj;
- }, {});
-
- return (
- <>
-
-
-
-
-
- >
- );
-}
-
-export default WebsiteComparePage;
diff --git a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.module.css b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.module.css
deleted file mode 100644
index c4821e88..00000000
--- a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.module.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.container {
- margin-bottom: 60px;
-}
-
-.nav {
- width: 200px;
- margin-top: 40px;
-}
-
-.title {
- color: var(--base800);
- text-align: center;
- font-weight: 700;
-}
diff --git a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx
deleted file mode 100644
index ce7f5b47..00000000
--- a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-import { useDateRange, useMessages, useNavigation } from '@/components/hooks';
-import { Grid, GridRow } from '@/components/layout/Grid';
-import SideNav from '@/components/layout/SideNav';
-import BrowsersTable from '@/components/metrics/BrowsersTable';
-import ChangeLabel from '@/components/metrics/ChangeLabel';
-import CitiesTable from '@/components/metrics/CitiesTable';
-import CountriesTable from '@/components/metrics/CountriesTable';
-import DevicesTable from '@/components/metrics/DevicesTable';
-import EventsTable from '@/components/metrics/EventsTable';
-import LanguagesTable from '@/components/metrics/LanguagesTable';
-import MetricsTable from '@/components/metrics/MetricsTable';
-import OSTable from '@/components/metrics/OSTable';
-import PagesTable from '@/components/metrics/PagesTable';
-import QueryParametersTable from '@/components/metrics/QueryParametersTable';
-import ReferrersTable from '@/components/metrics/ReferrersTable';
-import RegionsTable from '@/components/metrics/RegionsTable';
-import ScreenTable from '@/components/metrics/ScreenTable';
-import TagsTable from '@/components/metrics/TagsTable';
-import { getCompareDate } from '@/lib/date';
-import { formatNumber } from '@/lib/format';
-import { useState } from 'react';
-import useStore from '@/store/websites';
-import styles from './WebsiteCompareTables.module.css';
-
-const views = {
- url: PagesTable,
- title: PagesTable,
- referrer: ReferrersTable,
- browser: BrowsersTable,
- os: OSTable,
- device: DevicesTable,
- screen: ScreenTable,
- country: CountriesTable,
- region: RegionsTable,
- city: CitiesTable,
- language: LanguagesTable,
- event: EventsTable,
- query: QueryParametersTable,
- tag: TagsTable,
-};
-
-export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
- const [data, setData] = useState([]);
- const { dateRange } = useDateRange(websiteId);
- const dateCompare = useStore(state => state[websiteId]?.dateCompare);
- const { formatMessage, labels } = useMessages();
- const {
- renderUrl,
- query: { view },
- } = useNavigation();
- const Component: typeof MetricsTable = views[view || 'url'] || (() => null);
-
- const items = [
- {
- key: 'url',
- label: formatMessage(labels.pages),
- url: renderUrl({ view: 'url' }),
- },
- {
- key: 'referrer',
- label: formatMessage(labels.referrers),
- url: renderUrl({ view: 'referrer' }),
- },
- {
- key: 'browser',
- label: formatMessage(labels.browsers),
- url: renderUrl({ view: 'browser' }),
- },
- {
- key: 'os',
- label: formatMessage(labels.os),
- url: renderUrl({ view: 'os' }),
- },
- {
- key: 'device',
- label: formatMessage(labels.devices),
- url: renderUrl({ view: 'device' }),
- },
- {
- key: 'country',
- label: formatMessage(labels.countries),
- url: renderUrl({ view: 'country' }),
- },
- {
- key: 'region',
- label: formatMessage(labels.regions),
- url: renderUrl({ view: 'region' }),
- },
- {
- key: 'city',
- label: formatMessage(labels.cities),
- url: renderUrl({ view: 'city' }),
- },
- {
- key: 'language',
- label: formatMessage(labels.languages),
- url: renderUrl({ view: 'language' }),
- },
- {
- key: 'screen',
- label: formatMessage(labels.screens),
- url: renderUrl({ view: 'screen' }),
- },
- {
- key: 'event',
- label: formatMessage(labels.events),
- url: renderUrl({ view: 'event' }),
- },
- {
- key: 'query',
- label: formatMessage(labels.queryParameters),
- url: renderUrl({ view: 'query' }),
- },
- {
- key: 'host',
- label: formatMessage(labels.hosts),
- url: renderUrl({ view: 'host' }),
- },
- {
- key: 'tag',
- label: formatMessage(labels.tags),
- url: renderUrl({ view: 'tag' }),
- },
- ];
-
- const renderChange = ({ x, y }) => {
- const prev = data.find(d => d.x === x)?.y;
- const value = y - prev;
- const change = Math.abs(((y - prev) / prev) * 100);
-
- return !isNaN(change) && {formatNumber(change)}% ;
- };
-
- const { startDate, endDate } = getCompareDate(
- dateCompare,
- dateRange.startDate,
- dateRange.endDate,
- );
-
- const params = {
- startAt: startDate.getTime(),
- endAt: endDate.getTime(),
- };
-
- return (
-
-
-
-
-
{formatMessage(labels.previous)}
-
-
-
-
{formatMessage(labels.current)}
-
-
-
-
- );
-}
-
-export default WebsiteCompareTables;
diff --git a/src/app/(main)/websites/[websiteId]/compare/page.tsx b/src/app/(main)/websites/[websiteId]/compare/page.tsx
index bdd29bd5..1b2899b2 100644
--- a/src/app/(main)/websites/[websiteId]/compare/page.tsx
+++ b/src/app/(main)/websites/[websiteId]/compare/page.tsx
@@ -1,12 +1,12 @@
-import WebsiteComparePage from './WebsiteComparePage';
-import { Metadata } from 'next';
+import type { Metadata } from 'next';
+import { ComparePage } from './ComparePage';
-export default async function ({ params }: { params: { websiteId: string } }) {
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params;
- return ;
+ return ;
}
export const metadata: Metadata = {
- title: 'Website Comparison',
+ title: 'Compare',
};
diff --git a/src/app/(main)/websites/[websiteId]/events/EventProperties.module.css b/src/app/(main)/websites/[websiteId]/events/EventProperties.module.css
deleted file mode 100644
index 0b9c011d..00000000
--- a/src/app/(main)/websites/[websiteId]/events/EventProperties.module.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.container {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
- gap: 60px;
- margin-bottom: 40px;
-}
-
-.table {
- align-self: flex-start;
-}
-
-.link:hover {
- cursor: pointer;
- color: var(--primary400);
-}
-
-.title {
- text-align: center;
- font-weight: bold;
- margin: 20px 0;
-}
-
-.chart {
- min-height: 620px;
-}
diff --git a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx
index 453aa9a8..c3b1325d 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx
@@ -1,65 +1,127 @@
-import { GridColumn, GridTable } from 'react-basics';
-import { useEventDataProperties, useEventDataValues, useMessages } from '@/components/hooks';
+import { Column, Grid, ListItem, Select } from '@umami/react-zen';
+import { useMemo, useState } from 'react';
+import { PieChart } from '@/components/charts/PieChart';
import { LoadingPanel } from '@/components/common/LoadingPanel';
-import PieChart from '@/components/charts/PieChart';
-import { useState } from 'react';
+import {
+ useEventDataPropertiesQuery,
+ useEventDataValuesQuery,
+ useMessages,
+} from '@/components/hooks';
+import { ListTable } from '@/components/metrics/ListTable';
import { CHART_COLORS } from '@/lib/constants';
-import styles from './EventProperties.module.css';
export function EventProperties({ websiteId }: { websiteId: string }) {
const [propertyName, setPropertyName] = useState('');
const [eventName, setEventName] = useState('');
- const { formatMessage, labels } = useMessages();
- const { data, isLoading, isFetched, error } = useEventDataProperties(websiteId);
- const { data: values } = useEventDataValues(websiteId, eventName, propertyName);
- const chartData =
- propertyName && values
- ? {
- labels: values.map(({ value }) => value),
- datasets: [
- {
- data: values.map(({ total }) => total),
- backgroundColor: CHART_COLORS,
- borderWidth: 0,
- },
- ],
- }
- : null;
- const handleRowClick = row => {
- setEventName(row.eventName);
- setPropertyName(row.propertyName);
- };
+ const { formatMessage, labels } = useMessages();
+ const { data, isLoading, isFetching, error } = useEventDataPropertiesQuery(websiteId);
+
+ const events: string[] = data
+ ? data.reduce((arr: string | any[], e: { eventName: any }) => {
+ return !arr.includes(e.eventName) ? arr.concat(e.eventName) : arr;
+ }, [])
+ : [];
+ const properties: string[] = eventName
+ ? data?.filter(e => e.eventName === eventName).map(e => e.propertyName)
+ : [];
return (
-
-
-
-
- {row => (
- handleRowClick(row)}>
- {row.eventName}
-
- )}
-
-
- {row => (
- handleRowClick(row)}>
- {row.propertyName}
-
- )}
-
-
-
- {propertyName && (
-
+
+
+ {data && (
+
+
+ {events?.map(p => (
+
+ {p}
+
+ ))}
+
+
+ {properties?.map(p => (
+
+ {p}
+
+ ))}
+
+
)}
-
+ {eventName && propertyName && (
+
+ )}
+
);
}
-export default EventProperties;
+const EventValues = ({ websiteId, eventName, propertyName }) => {
+ const {
+ data: values,
+ isLoading,
+ isFetching,
+ error,
+ } = useEventDataValuesQuery(websiteId, eventName, propertyName);
+
+ const propertySum = useMemo(() => {
+ return values?.reduce((sum, { total }) => sum + total, 0) ?? 0;
+ }, [values]);
+
+ const chartData = useMemo(() => {
+ if (!propertyName || !values) return null;
+ return {
+ labels: values.map(({ value }) => value),
+ datasets: [
+ {
+ data: values.map(({ total }) => total),
+ backgroundColor: CHART_COLORS,
+ borderWidth: 0,
+ },
+ ],
+ };
+ }, [propertyName, values]);
+
+ const tableData = useMemo(() => {
+ if (!propertyName || !values || propertySum === 0) return [];
+ return values.map(({ value, total }) => ({
+ label: value,
+ count: total,
+ percent: 100 * (total / propertySum),
+ }));
+ }, [propertyName, values, propertySum]);
+
+ return (
+
+ {values && (
+
+
+
+
+ )}
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx
index ce9048d3..f686b3fd 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx
@@ -1,20 +1,48 @@
-import { useWebsiteEvents } from '@/components/hooks';
-import EventsTable from './EventsTable';
-import DataTable from '@/components/common/DataTable';
-import { ReactNode } from 'react';
+import { type ReactNode, useState } from 'react';
+import { DataGrid } from '@/components/common/DataGrid';
+import { useMessages, useWebsiteEventsQuery } from '@/components/hooks';
+import { FilterButtons } from '@/components/input/FilterButtons';
+import { EventsTable } from './EventsTable';
-export default function EventsDataTable({
+export function EventsDataTable({
websiteId,
}: {
websiteId?: string;
teamId?: string;
children?: ReactNode;
}) {
- const queryResult = useWebsiteEvents(websiteId);
+ const { formatMessage, labels } = useMessages();
+ const [view, setView] = useState('all');
+ const query = useWebsiteEventsQuery(websiteId, { view });
+
+ const buttons = [
+ {
+ id: 'all',
+ label: formatMessage(labels.all),
+ },
+ {
+ id: 'views',
+ label: formatMessage(labels.views),
+ },
+ {
+ id: 'events',
+ label: formatMessage(labels.events),
+ },
+ ];
+
+ const renderActions = () => {
+ return ;
+ };
return (
-
+
{({ data }) => }
-
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx
index e90a7790..a7ed399c 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx
@@ -1,42 +1,40 @@
+import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useMessages } from '@/components/hooks';
-import useWebsiteSessionStats from '@/components/hooks/queries/useWebsiteSessionStats';
-import WebsiteDateFilter from '@/components/input/WebsiteDateFilter';
-import MetricCard from '@/components/metrics/MetricCard';
-import MetricsBar from '@/components/metrics/MetricsBar';
+import { useWebsiteSessionStatsQuery } from '@/components/hooks/queries/useWebsiteSessionStatsQuery';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
import { formatLongNumber } from '@/lib/format';
-import { Flexbox } from 'react-basics';
export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
- const { data, isLoading, isFetched, error } = useWebsiteSessionStats(websiteId);
+ const { data, isLoading, isFetching, error } = useWebsiteSessionStatsQuery(websiteId);
return (
-
-
-
-
-
-
-
-
-
+
+ {data && (
+
+
+
+
+
+
+ )}
+
);
}
-
-export default EventsMetricsBar;
diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
index 285a230e..55ec0403 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
@@ -1,50 +1,59 @@
'use client';
-import WebsiteHeader from '../WebsiteHeader';
-import EventsDataTable from './EventsDataTable';
-import EventsMetricsBar from './EventsMetricsBar';
-import EventsChart from '@/components/metrics/EventsChart';
-import { GridRow } from '@/components/layout/Grid';
-import EventsTable from '@/components/metrics/EventsTable';
+import { Column, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
+import { type Key, useState } from 'react';
+import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { Panel } from '@/components/common/Panel';
import { useMessages } from '@/components/hooks';
-import { Item, Tabs } from 'react-basics';
-import { useState } from 'react';
-import EventProperties from './EventProperties';
+import { EventsChart } from '@/components/metrics/EventsChart';
+import { MetricsTable } from '@/components/metrics/MetricsTable';
+import { getItem, setItem } from '@/lib/storage';
+import { EventProperties } from './EventProperties';
+import { EventsDataTable } from './EventsDataTable';
-export default function EventsPage({ websiteId }) {
- const [label, setLabel] = useState(null);
- const [tab, setTab] = useState('activity');
+const KEY_NAME = 'umami.events.tab';
+
+export function EventsPage({ websiteId }) {
+ const [tab, setTab] = useState(getItem(KEY_NAME) || 'chart');
const { formatMessage, labels } = useMessages();
- const handleLabelClick = (value: string) => {
- setLabel(value !== label ? value : '');
+ const handleSelect = (value: Key) => {
+ setItem(KEY_NAME, value);
+ setTab(value);
};
return (
- <>
-
-
-
-
-
-
-
-
setTab(value)}
- style={{ marginBottom: 30 }}
- >
- - {formatMessage(labels.activity)}
- - {formatMessage(labels.properties)}
+
+
+
+ handleSelect(key)}>
+
+ {formatMessage(labels.chart)}
+ {formatMessage(labels.activity)}
+ {formatMessage(labels.properties)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- {tab === 'activity' && }
- {tab === 'properties' && }
-
- >
+
+
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
index 8e6cdf76..7fb2eb41 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
@@ -1,44 +1,107 @@
-import { GridTable, GridColumn, Icon } from 'react-basics';
-import { useMessages, useTeamUrl, useTimezone } from '@/components/hooks';
-import Empty from '@/components/common/Empty';
-import Avatar from '@/components/common/Avatar';
+import {
+ Button,
+ DataColumn,
+ DataTable,
+ type DataTableProps,
+ Dialog,
+ DialogTrigger,
+ Icon,
+ IconLabel,
+ Popover,
+ Row,
+ Text,
+} from '@umami/react-zen';
import Link from 'next/link';
-import Icons from '@/components/icons';
+import { Avatar } from '@/components/common/Avatar';
+import { DateDistance } from '@/components/common/DateDistance';
+import { TypeIcon } from '@/components/common/TypeIcon';
+import { useFormat, useMessages, useNavigation } from '@/components/hooks';
+import { Eye, FileText } from '@/components/icons';
+import { EventData } from '@/components/metrics/EventData';
+import { Lightning } from '@/components/svg';
-export function EventsTable({ data = [] }) {
- const { formatTimezoneDate } = useTimezone();
+export function EventsTable(props: DataTableProps) {
const { formatMessage, labels } = useMessages();
- const { renderTeamUrl } = useTeamUrl();
-
- if (data.length === 0) {
- return ;
- }
+ const { updateParams } = useNavigation();
+ const { formatValue } = useFormat();
return (
-
-
- {row => (
-
-
-
- )}
-
-
- {row => {
+
+
+ {(row: any) => {
return (
- <>
- {row.eventName ? : }
- {formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
- {row.eventName || row.urlPath}
- >
+
+
+ : }
+ label={formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
+ />
+
+
+ {row.eventName || row.urlPath}
+
+ {row.hasData > 0 && }
+
);
}}
-
-
- {row => formatTimezoneDate(row.createdAt, 'PPPpp')}
-
-
+
+
+ {(row: any) => {
+ return (
+
+
+
+ );
+ }}
+
+
+ {(row: any) => (
+
+ {row.city ? `${row.city}, ` : ''} {formatValue(row.country, 'country')}
+
+ )}
+
+
+ {(row: any) => (
+
+ {formatValue(row.browser, 'browser')}
+
+ )}
+
+
+ {(row: any) => (
+
+ {formatValue(row.device, 'device')}
+
+ )}
+
+
+ {(row: any) => }
+
+
);
}
-export default EventsTable;
+const PropertiesButton = props => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/events/page.tsx b/src/app/(main)/websites/[websiteId]/events/page.tsx
index 1b888244..d77ba3bd 100644
--- a/src/app/(main)/websites/[websiteId]/events/page.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/page.tsx
@@ -1,12 +1,12 @@
-import { Metadata } from 'next';
-import EventsPage from './EventsPage';
+import type { Metadata } from 'next';
+import { EventsPage } from './EventsPage';
-export default async function ({ params }: { params: { websiteId: string } }) {
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params;
return ;
}
export const metadata: Metadata = {
- title: 'Event Data',
+ title: 'Events',
};
diff --git a/src/app/(main)/websites/[websiteId]/layout.tsx b/src/app/(main)/websites/[websiteId]/layout.tsx
index 2542f65a..67595e9d 100644
--- a/src/app/(main)/websites/[websiteId]/layout.tsx
+++ b/src/app/(main)/websites/[websiteId]/layout.tsx
@@ -1,5 +1,5 @@
-import { Metadata } from 'next';
-import WebsiteProvider from './WebsiteProvider';
+import type { Metadata } from 'next';
+import { WebsiteLayout } from '@/app/(main)/websites/[websiteId]/WebsiteLayout';
export default async function ({
children,
@@ -10,7 +10,7 @@ export default async function ({
}) {
const { websiteId } = await params;
- return {children} ;
+ return {children} ;
}
export const metadata: Metadata = {
diff --git a/src/app/(main)/websites/[websiteId]/page.tsx b/src/app/(main)/websites/[websiteId]/page.tsx
index d3aa1633..d4889c5d 100644
--- a/src/app/(main)/websites/[websiteId]/page.tsx
+++ b/src/app/(main)/websites/[websiteId]/page.tsx
@@ -1,10 +1,10 @@
-import WebsiteDetailsPage from './WebsiteDetailsPage';
-import { Metadata } from 'next';
+import type { Metadata } from 'next';
+import { WebsitePage } from './WebsitePage';
-export default async function WebsitePage({ params }: { params: { websiteId: string } }) {
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params;
- return ;
+ return ;
}
export const metadata: Metadata = {
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.module.css b/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.module.css
deleted file mode 100644
index e55063c3..00000000
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.module.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.row {
- display: flex;
- align-items: center;
- gap: 10px;
-}
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx
index c3a3b8f7..6e2495b5 100644
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx
@@ -1,9 +1,8 @@
+import { IconLabel } from '@umami/react-zen';
import { useCallback } from 'react';
-import ListTable from '@/components/metrics/ListTable';
-import { useLocale, useCountryNames, useMessages } from '@/components/hooks';
-import classNames from 'classnames';
-import styles from './RealtimeCountries.module.css';
-import TypeIcon from '@/components/common/TypeIcon';
+import { TypeIcon } from '@/components/common/TypeIcon';
+import { useCountryNames, useLocale, useMessages } from '@/components/hooks';
+import { ListTable } from '@/components/metrics/ListTable';
export function RealtimeCountries({ data }) {
const { formatMessage, labels } = useMessages();
@@ -11,11 +10,8 @@ export function RealtimeCountries({ data }) {
const { countryNames } = useCountryNames(locale);
const renderCountryName = useCallback(
- ({ x: code }) => (
-
-
- {countryNames[code]}
-
+ ({ label: code }) => (
+ } label={countryNames[code]} />
),
[countryNames, locale],
);
@@ -24,10 +20,12 @@ export function RealtimeCountries({ data }) {
({
+ label: x,
+ count: y,
+ percent: z,
+ }))}
renderLabel={renderCountryName}
/>
);
}
-
-export default RealtimeCountries;
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.module.css b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.module.css
deleted file mode 100644
index f87d86e8..00000000
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.module.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 20px;
-}
-
-.metrics {
- display: flex;
- flex-wrap: wrap;
-}
-
-.card {
- justify-self: flex-start;
-}
-
-@media only screen and (max-width: 992px) {
- .card {
- flex-basis: calc(50% - 20px);
- }
-}
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx
index 6db56b76..2b9d881e 100644
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx
@@ -1,38 +1,17 @@
-import MetricCard from '@/components/metrics/MetricCard';
import { useMessages } from '@/components/hooks';
-import { RealtimeData } from '@/lib/types';
-import styles from './RealtimeHeader.module.css';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
-export function RealtimeHeader({ data }: { data: RealtimeData }) {
+export function RealtimeHeader({ data }: { data: any }) {
const { formatMessage, labels } = useMessages();
const { totals }: any = data || {};
return (
-
+
+
+
+
+
+
);
}
-
-export default RealtimeHeader;
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx
deleted file mode 100644
index 104cf334..00000000
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useEffect } from 'react';
-import { useRouter } from 'next/navigation';
-import Page from '@/components/layout/Page';
-import PageHeader from '@/components/layout/PageHeader';
-import { useApi, useMessages } from '@/components/hooks';
-import EmptyPlaceholder from '@/components/common/EmptyPlaceholder';
-
-export function RealtimeHome() {
- const { formatMessage, labels, messages } = useMessages();
- const { get, useQuery } = useApi();
- const router = useRouter();
- const { data, isLoading, error } = useQuery({
- queryKey: ['websites:me'],
- queryFn: () => get('/me/websites'),
- });
-
- useEffect(() => {
- if (data?.length) {
- router.push(`realtime/${data[0].id}`);
- }
- }, [data, router]);
-
- return (
- 0} error={error}>
-
- {data?.length === 0 && (
-
- )}
-
- );
-}
-
-export default RealtimeHome;
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.module.css b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.module.css
deleted file mode 100644
index 19d02384..00000000
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.module.css
+++ /dev/null
@@ -1,85 +0,0 @@
-.table {
- font-size: var(--font-size-sm);
- overflow: hidden;
- height: 100%;
-}
-
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: var(--font-size-md);
- line-height: 40px;
- font-weight: 700;
-}
-
-.row {
- display: flex;
- align-items: center;
- gap: 10px;
- height: 50px;
- border-bottom: 1px solid var(--base300);
-}
-
-.body {
- overflow: auto;
- height: 100%;
-}
-
-.icon {
- margin-inline-end: 10px;
-}
-
-.time {
- min-width: 60px;
- overflow: hidden;
-}
-
-.detail {
- display: flex;
- align-items: center;
- flex: 1;
- gap: 10px;
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-
-.detail > span {
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
-}
-
-.row .link {
- color: var(--base900);
- text-decoration: none;
-}
-
-.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/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
index 6a2b3c25..10763618 100644
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx
@@ -1,16 +1,24 @@
-import useFormat from '@/components//hooks/useFormat';
-import Empty from '@/components/common/Empty';
-import FilterButtons from '@/components/common/FilterButtons';
-import { useCountryNames, useLocale, useMessages, useTimezone } from '@/components/hooks';
-import Icons from '@/components/icons';
-import { BROWSERS, OS_NAMES } from '@/lib/constants';
-import { stringToColor } from '@/lib/format';
-import { RealtimeData } from '@/lib/types';
-import { useContext, useMemo, useState } from 'react';
-import { Icon, SearchField, StatusLight, Text } from 'react-basics';
+import { Column, Heading, IconLabel, Row, SearchField, Text } from '@umami/react-zen';
+import Link from 'next/link';
+import { useMemo, useState } from 'react';
import { FixedSizeList } from 'react-window';
-import { WebsiteContext } from '../WebsiteProvider';
-import styles from './RealtimeLog.module.css';
+import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
+import { useFormat } from '@/components//hooks/useFormat';
+import { Avatar } from '@/components/common/Avatar';
+import { Empty } from '@/components/common/Empty';
+import {
+ useCountryNames,
+ useLocale,
+ useMessages,
+ useMobile,
+ useNavigation,
+ useTimezone,
+ useWebsite,
+} from '@/components/hooks';
+import { Eye, User } from '@/components/icons';
+import { FilterButtons } from '@/components/input/FilterButtons';
+import { Lightning } from '@/components/svg';
+import { BROWSERS, OS_NAMES } from '@/lib/constants';
const TYPE_ALL = 'all';
const TYPE_PAGEVIEW = 'pageview';
@@ -18,44 +26,44 @@ const TYPE_SESSION = 'session';
const TYPE_EVENT = 'event';
const icons = {
- [TYPE_PAGEVIEW]: ,
- [TYPE_SESSION]: ,
- [TYPE_EVENT]: ,
+ [TYPE_PAGEVIEW]: ,
+ [TYPE_SESSION]: ,
+ [TYPE_EVENT]: ,
};
-export function RealtimeLog({ data }: { data: RealtimeData }) {
- const website = useContext(WebsiteContext);
+export function RealtimeLog({ data }: { data: any }) {
+ const website = useWebsite();
const [search, setSearch] = useState('');
- const { formatMessage, labels, messages } = useMessages();
+ const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { formatValue } = useFormat();
const { locale } = useLocale();
const { formatTimezoneDate } = useTimezone();
const { countryNames } = useCountryNames(locale);
const [filter, setFilter] = useState(TYPE_ALL);
+ const { updateParams } = useNavigation();
+ const { isPhone } = useMobile();
const buttons = [
{
label: formatMessage(labels.all),
- key: TYPE_ALL,
+ id: TYPE_ALL,
},
{
label: formatMessage(labels.views),
- key: TYPE_PAGEVIEW,
+ id: TYPE_PAGEVIEW,
},
{
label: formatMessage(labels.visitors),
- key: TYPE_SESSION,
+ id: TYPE_SESSION,
},
{
label: formatMessage(labels.events),
- key: TYPE_EVENT,
+ id: TYPE_EVENT,
},
];
const getTime = ({ createdAt, firstAt }) => formatTimezoneDate(firstAt || createdAt, 'pp');
- const getColor = ({ id, sessionId }) => stringToColor(sessionId || id);
-
const getIcon = ({ __type }) => icons[__type];
const getDetail = (log: {
@@ -67,61 +75,70 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
country: string;
device: string;
}) => {
- const { __type, eventName, urlPath: url, browser, os, country, device } = log;
+ const { __type, eventName, urlPath, browser, os, country, device } = log;
if (__type === TYPE_EVENT) {
- return formatMessage(messages.eventLog, {
- event: {eventName || formatMessage(labels.unknown)} ,
- url: (
-
- {url}
-
- ),
- });
+ return (
+ {eventName || formatMessage(labels.unknown)},
+ url: (
+
+ {urlPath}
+
+ ),
+ }}
+ />
+ );
}
if (__type === TYPE_PAGEVIEW) {
return (
-
- {url}
+
+ {urlPath}
);
}
if (__type === TYPE_SESSION) {
- return formatMessage(messages.visitorLog, {
- country: {countryNames[country] || formatMessage(labels.unknown)} ,
- browser: {BROWSERS[browser]} ,
- os: {OS_NAMES[os] || os} ,
- device: {formatMessage(labels[device] || labels.unknown)} ,
- });
+ return (
+ {countryNames[country] || formatMessage(labels.unknown)},
+ browser: {BROWSERS[browser]} ,
+ os: {OS_NAMES[os] || os} ,
+ device: {formatMessage(labels[device] || labels.unknown)} ,
+ }}
+ />
+ );
}
};
- const Row = ({ index, style }) => {
+ const TableRow = ({ index, style }) => {
const row = logs[index];
return (
-
-
-
-
-
{getTime(row)}
-
- {getIcon(row)}
- {getDetail(row)}
-
-
+
+
+
+
+
+
+
+ {getTime(row)}
+
+
+
+ {getDetail(row)}
+
+
+
);
};
@@ -157,22 +174,33 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
}, [data, filter, formatValue, search]);
return (
-
-
-
-
-
-
{formatMessage(labels.activity)}
-
+
+ {formatMessage(labels.activity)}
+ {isPhone ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : (
+
+
+
+
+ )}
+
+
{logs?.length === 0 && }
{logs?.length > 0 && (
- {Row}
+ {TableRow}
)}
-
-
+
+
+
);
}
-
-export default RealtimeLog;
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx
new file mode 100644
index 00000000..6220c695
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx
@@ -0,0 +1,58 @@
+'use client';
+import { Grid } from '@umami/react-zen';
+import { firstBy } from 'thenby';
+import { GridRow } from '@/components/common/GridRow';
+import { PageBody } from '@/components/common/PageBody';
+import { Panel } from '@/components/common/Panel';
+import { useMobile, useRealtimeQuery } from '@/components/hooks';
+import { RealtimeChart } from '@/components/metrics/RealtimeChart';
+import { WorldMap } from '@/components/metrics/WorldMap';
+import { percentFilter } from '@/lib/filters';
+import { RealtimeCountries } from './RealtimeCountries';
+import { RealtimeHeader } from './RealtimeHeader';
+import { RealtimeLog } from './RealtimeLog';
+import { RealtimePaths } from './RealtimePaths';
+import { RealtimeReferrers } from './RealtimeReferrers';
+
+export function RealtimePage({ websiteId }: { websiteId: string }) {
+ const { data, isLoading, error } = useRealtimeQuery(websiteId);
+ const { isMobile } = useMobile();
+
+ if (isLoading || error) {
+ return ;
+ }
+
+ const countries = percentFilter(
+ Object.keys(data.countries)
+ .map(key => ({ x: key, y: data.countries[key] }))
+ .sort(firstBy('y', -1)),
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx
new file mode 100644
index 00000000..1f90ad83
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx
@@ -0,0 +1,45 @@
+import thenby from 'thenby';
+import { useMessages, useWebsite } from '@/components/hooks';
+import { ListTable } from '@/components/metrics/ListTable';
+import { percentFilter } from '@/lib/filters';
+
+export function RealtimePaths({ data }: { data: any }) {
+ const website = useWebsite();
+ const { formatMessage, labels } = useMessages();
+ const { urls } = data || {};
+ const limit = 15;
+
+ const renderLink = ({ label: x }) => {
+ const domain = x.startsWith('/') ? website?.domain : '';
+ return (
+
+ {x}
+
+ );
+ };
+
+ const pages = percentFilter(
+ Object.keys(urls)
+ .map(key => {
+ return {
+ x: key,
+ y: urls[key],
+ };
+ })
+ .sort(thenby.firstBy('y', -1))
+ .slice(0, limit),
+ );
+
+ return (
+ ({
+ label: x,
+ count: y,
+ percent: z,
+ }))}
+ />
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx
new file mode 100644
index 00000000..9fd4477b
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx
@@ -0,0 +1,45 @@
+import thenby from 'thenby';
+import { useMessages, useWebsite } from '@/components/hooks';
+import { ListTable } from '@/components/metrics/ListTable';
+import { percentFilter } from '@/lib/filters';
+
+export function RealtimeReferrers({ data }: { data: any }) {
+ const website = useWebsite();
+ const { formatMessage, labels } = useMessages();
+ const { referrers } = data || {};
+ const limit = 15;
+
+ const renderLink = ({ label: x }) => {
+ const domain = x.startsWith('/') ? website?.domain : '';
+ return (
+
+ {x}
+
+ );
+ };
+
+ const domains = percentFilter(
+ Object.keys(referrers)
+ .map(key => {
+ return {
+ x: key,
+ y: referrers[key],
+ };
+ })
+ .sort(thenby.firstBy('y', -1))
+ .slice(0, limit),
+ );
+
+ return (
+ ({
+ label: x,
+ count: y,
+ percent: z,
+ }))}
+ />
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx
deleted file mode 100644
index ce95bf41..00000000
--- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { Key, useContext, useState } from 'react';
-import { ButtonGroup, Button, Flexbox } from 'react-basics';
-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';
-import { RealtimeData } from '@/lib/types';
-import { WebsiteContext } from '../WebsiteProvider';
-
-export function RealtimeUrls({ data }: { data: RealtimeData }) {
- const website = useContext(WebsiteContext);
- const { formatMessage, labels } = useMessages();
- const { referrers, urls } = data || {};
- const [filter, setFilter] = useState(FILTER_REFERRERS);
- const limit = 15;
-
- const buttons = [
- {
- label: formatMessage(labels.referrers),
- key: FILTER_REFERRERS,
- },
- {
- label: formatMessage(labels.pages),
- key: FILTER_PAGES,
- },
- ];
-
- const renderLink = ({ x }) => {
- const domain = x.startsWith('/') ? website?.domain : '';
- return (
-
- {x}
-
- );
- };
-
- const domains = percentFilter(
- Object.keys(referrers)
- .map(key => {
- return {
- x: key,
- y: referrers[key],
- };
- })
- .sort(thenby.firstBy('y', -1))
- .slice(0, limit),
- );
-
- const pages = percentFilter(
- Object.keys(urls)
- .map(key => {
- return {
- x: key,
- y: urls[key],
- };
- })
- .sort(thenby.firstBy('y', -1))
- .slice(0, limit),
- );
-
- return (
- <>
-
-
- {({ key, label }) => {label} }
-
-
- {filter === FILTER_REFERRERS && (
-
- )}
- {filter === FILTER_PAGES && (
-
- )}
- >
- );
-}
-
-export default RealtimeUrls;
diff --git a/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx b/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx
deleted file mode 100644
index e6d8d2ab..00000000
--- a/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-'use client';
-import { firstBy } 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 { useRealtime } from '@/components/hooks';
-import RealtimeLog from './RealtimeLog';
-import RealtimeHeader from './RealtimeHeader';
-import RealtimeUrls from './RealtimeUrls';
-import RealtimeCountries from './RealtimeCountries';
-import WebsiteHeader from '../WebsiteHeader';
-import { percentFilter } from '@/lib/filters';
-
-export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
- const { data, isLoading, error } = useRealtime(websiteId);
-
- if (isLoading || error) {
- return ;
- }
-
- const countries = percentFilter(
- Object.keys(data.countries)
- .map(key => ({ x: key, y: data.countries[key] }))
- .sort(firstBy('y', -1)),
- );
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
-
-export default WebsiteRealtimePage;
diff --git a/src/app/(main)/websites/[websiteId]/realtime/page.tsx b/src/app/(main)/websites/[websiteId]/realtime/page.tsx
index b376062b..1552196c 100644
--- a/src/app/(main)/websites/[websiteId]/realtime/page.tsx
+++ b/src/app/(main)/websites/[websiteId]/realtime/page.tsx
@@ -1,10 +1,10 @@
-import WebsiteRealtimePage from './WebsiteRealtimePage';
-import { Metadata } from 'next';
+import type { Metadata } from 'next';
+import { RealtimePage } from './RealtimePage';
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params;
- return ;
+ return ;
}
export const metadata: Metadata = {
diff --git a/src/app/(main)/websites/[websiteId]/reports/WebsiteReportsPage.tsx b/src/app/(main)/websites/[websiteId]/reports/WebsiteReportsPage.tsx
deleted file mode 100644
index e61aacb1..00000000
--- a/src/app/(main)/websites/[websiteId]/reports/WebsiteReportsPage.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-'use client';
-import Link from 'next/link';
-import { Button, Flexbox, Icon, Icons, Text } from 'react-basics';
-import { useMessages, useTeamUrl } from '@/components/hooks';
-import WebsiteHeader from '../WebsiteHeader';
-import ReportsDataTable from '@/app/(main)/reports/ReportsDataTable';
-
-export function WebsiteReportsPage({ websiteId }) {
- const { formatMessage, labels } = useMessages();
- const { renderTeamUrl } = useTeamUrl();
-
- return (
- <>
-
-
-
-
-
-
-
- {formatMessage(labels.createReport)}
-
-
-
-
- >
- );
-}
-
-export default WebsiteReportsPage;
diff --git a/src/app/(main)/websites/[websiteId]/reports/page.tsx b/src/app/(main)/websites/[websiteId]/reports/page.tsx
deleted file mode 100644
index 15c79de9..00000000
--- a/src/app/(main)/websites/[websiteId]/reports/page.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import WebsiteReportsPage from './WebsiteReportsPage';
-import { Metadata } from 'next';
-
-export default async function ({ params }: { params: { websiteId: string } }) {
- const { websiteId } = await params;
-
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Website Reports',
-};
diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentAddButton.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentAddButton.tsx
new file mode 100644
index 00000000..7b70fee6
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/segments/SegmentAddButton.tsx
@@ -0,0 +1,21 @@
+import { useMessages } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { SegmentEditForm } from './SegmentEditForm';
+
+export function SegmentAddButton({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ }
+ label={formatMessage(labels.segment)}
+ variant="primary"
+ width="800px"
+ >
+ {({ close }) => {
+ return ;
+ }}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx
new file mode 100644
index 00000000..bb52a220
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx
@@ -0,0 +1,60 @@
+import { ConfirmationForm } from '@/components/common/ConfirmationForm';
+import { useDeleteQuery, useMessages } from '@/components/hooks';
+import { Trash } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { messages } from '@/components/messages';
+
+export function SegmentDeleteButton({
+ segmentId,
+ websiteId,
+ name,
+ onSave,
+}: {
+ segmentId: string;
+ websiteId: string;
+ name: string;
+ onSave?: () => void;
+}) {
+ const { formatMessage, labels, FormattedMessage } = useMessages();
+ const { mutateAsync, isPending, error, touch } = useDeleteQuery(
+ `/websites/${websiteId}/segments/${segmentId}`,
+ );
+
+ const handleConfirm = async (close: () => void) => {
+ await mutateAsync(null, {
+ onSuccess: () => {
+ touch('segments');
+ onSave?.();
+ close();
+ },
+ });
+ };
+
+ return (
+ }
+ title={formatMessage(labels.confirm)}
+ variant="quiet"
+ width="600px"
+ >
+ {({ close }) => (
+ {name},
+ }}
+ />
+ }
+ isLoading={isPending}
+ error={error}
+ onConfirm={handleConfirm.bind(null, close)}
+ onClose={close}
+ buttonLabel={formatMessage(labels.delete)}
+ buttonVariant="danger"
+ />
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentEditButton.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentEditButton.tsx
new file mode 100644
index 00000000..5c56cf1e
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/segments/SegmentEditButton.tsx
@@ -0,0 +1,37 @@
+import { useMessages } from '@/components/hooks';
+import { Edit } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import type { Filter } from '@/lib/types';
+import { SegmentEditForm } from './SegmentEditForm';
+
+export function SegmentEditButton({
+ segmentId,
+ websiteId,
+ filters,
+}: {
+ segmentId: string;
+ websiteId: string;
+ filters?: Filter[];
+}) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ }
+ title={formatMessage(labels.segment)}
+ variant="quiet"
+ width="800px"
+ >
+ {({ close }) => {
+ return (
+
+ );
+ }}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx
new file mode 100644
index 00000000..c3529d97
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/segments/SegmentEditForm.tsx
@@ -0,0 +1,86 @@
+import {
+ Button,
+ Form,
+ FormButtons,
+ FormField,
+ FormSubmitButton,
+ Label,
+ Loading,
+ TextField,
+} from '@umami/react-zen';
+import { useMessages, useUpdateQuery, useWebsiteSegmentQuery } from '@/components/hooks';
+import { FieldFilters } from '@/components/input/FieldFilters';
+import { messages } from '@/components/messages';
+
+export function SegmentEditForm({
+ segmentId,
+ websiteId,
+ filters = [],
+ showFilters = true,
+ onSave,
+ onClose,
+}: {
+ segmentId?: string;
+ websiteId: string;
+ filters?: any[];
+ showFilters?: boolean;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { data } = useWebsiteSegmentQuery(websiteId, segmentId);
+ const { formatMessage, labels, getErrorMessage } = useMessages();
+
+ const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(
+ `/websites/${websiteId}/segments${segmentId ? `/${segmentId}` : ''}`,
+ {
+ type: 'segment',
+ },
+ );
+
+ const handleSubmit = async (formData: any) => {
+ await mutateAsync(formData, {
+ onSuccess: async () => {
+ toast(formatMessage(messages.saved));
+ touch('segments');
+ onSave?.();
+ onClose?.();
+ },
+ });
+ };
+
+ if (segmentId && !data) {
+ return ;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentsDataTable.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentsDataTable.tsx
new file mode 100644
index 00000000..c1ba82eb
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/segments/SegmentsDataTable.tsx
@@ -0,0 +1,24 @@
+import { DataGrid } from '@/components/common/DataGrid';
+import { useWebsiteSegmentsQuery } from '@/components/hooks';
+import { SegmentAddButton } from './SegmentAddButton';
+import { SegmentsTable } from './SegmentsTable';
+
+export function SegmentsDataTable({ websiteId }: { websiteId?: string }) {
+ const query = useWebsiteSegmentsQuery(websiteId, { type: 'segment' });
+
+ const renderActions = () => {
+ return ;
+ };
+
+ return (
+
+ {({ data }) => }
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentsPage.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentsPage.tsx
new file mode 100644
index 00000000..cbe7a1c1
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/segments/SegmentsPage.tsx
@@ -0,0 +1,16 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { Panel } from '@/components/common/Panel';
+import { SegmentsDataTable } from './SegmentsDataTable';
+
+export function SegmentsPage({ websiteId }) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx
new file mode 100644
index 00000000..4dbe5114
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/segments/SegmentsTable.tsx
@@ -0,0 +1,38 @@
+import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
+import Link from 'next/link';
+import { SegmentDeleteButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton';
+import { SegmentEditButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditButton';
+import { DateDistance } from '@/components/common/DateDistance';
+import { useMessages, useNavigation } from '@/components/hooks';
+
+export function SegmentsTable(props: DataTableProps) {
+ const { formatMessage, labels } = useMessages();
+ const { websiteId, renderUrl } = useNavigation();
+
+ return (
+
+
+ {(row: any) => (
+
+ {row.name}
+
+ )}
+
+
+ {(row: any) => }
+
+
+ {(row: any) => {
+ const { id, name } = row;
+
+ return (
+
+
+
+
+ );
+ }}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/segments/page.tsx b/src/app/(main)/websites/[websiteId]/segments/page.tsx
new file mode 100644
index 00000000..0d3faacb
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/segments/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { SegmentsPage } from './SegmentsPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Segments',
+};
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx
new file mode 100644
index 00000000..cbb28108
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx
@@ -0,0 +1,94 @@
+import {
+ Button,
+ Column,
+ Dialog,
+ DialogTrigger,
+ Heading,
+ Icon,
+ Popover,
+ Row,
+ StatusLight,
+ Text,
+} from '@umami/react-zen';
+import { isSameDay } from 'date-fns';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useMessages, useMobile, useSessionActivityQuery, useTimezone } from '@/components/hooks';
+import { Eye, FileText } from '@/components/icons';
+import { EventData } from '@/components/metrics/EventData';
+import { Lightning } from '@/components/svg';
+
+export function SessionActivity({
+ websiteId,
+ sessionId,
+ startDate,
+ endDate,
+}: {
+ websiteId: string;
+ sessionId: string;
+ startDate: Date;
+ endDate: Date;
+}) {
+ const { formatMessage, labels } = useMessages();
+ const { formatTimezoneDate } = useTimezone();
+ const { data, isLoading, error } = useSessionActivityQuery(
+ websiteId,
+ sessionId,
+ startDate,
+ endDate,
+ );
+ const { isMobile } = useMobile();
+ let lastDay = null;
+
+ return (
+
+
+ {data?.map(({ eventId, createdAt, urlPath, eventName, visitId, hasData }) => {
+ const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt));
+ lastDay = createdAt;
+
+ return (
+
+ {showHeader && {formatTimezoneDate(createdAt, 'PPPP')} }
+
+
+ {formatTimezoneDate(createdAt, 'pp')}
+
+
+ {eventName ? : }
+
+ {eventName
+ ? formatMessage(labels.triggeredEvent)
+ : formatMessage(labels.viewedPage)}
+
+
+ {eventName || urlPath}
+
+ {hasData > 0 && }
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+const PropertiesButton = props => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionData.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionData.tsx
new file mode 100644
index 00000000..7c82c17a
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionData.tsx
@@ -0,0 +1,32 @@
+import { Box, Column, Label, Row, Text } from '@umami/react-zen';
+import { Empty } from '@/components/common/Empty';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useSessionDataQuery } from '@/components/hooks';
+import { DATA_TYPES } from '@/lib/constants';
+
+export function SessionData({ websiteId, sessionId }: { websiteId: string; sessionId: string }) {
+ const { data, isLoading, error } = useSessionDataQuery(websiteId, sessionId);
+
+ return (
+
+ {!data?.length && }
+
+ {data?.map(({ dataKey, dataType, stringValue }) => {
+ return (
+
+ {dataKey}
+
+ {stringValue}
+
+
+ {DATA_TYPES[dataType]}
+
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx
new file mode 100644
index 00000000..f15e6ee5
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx
@@ -0,0 +1,85 @@
+import { Column, Grid, Icon, Label, Row } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { DateDistance } from '@/components/common/DateDistance';
+import { TypeIcon } from '@/components/common/TypeIcon';
+import { useFormat, useLocale, useMessages, useRegionNames } from '@/components/hooks';
+import { Calendar, KeyRound, Landmark, MapPin } from '@/components/icons';
+
+export function SessionInfo({ data }) {
+ const { locale } = useLocale();
+ const { formatMessage, labels } = useMessages();
+ const { formatValue } = useFormat();
+ const { getRegionName } = useRegionNames(locale);
+
+ return (
+
+ }>
+ {data?.distinctId}
+
+
+ }>
+
+
+
+ }>
+
+
+
+ }
+ >
+ {formatValue(data?.country, 'country')}
+
+
+ }>
+ {getRegionName(data?.region)}
+
+
+ }>
+ {data?.city}
+
+
+ }
+ >
+ {formatValue(data?.browser, 'browser')}
+
+
+ }
+ >
+ {formatValue(data?.os, 'os')}
+
+
+ }
+ >
+ {formatValue(data?.device, 'device')}
+
+
+ );
+}
+
+const Info = ({
+ label,
+ icon,
+ children,
+}: {
+ label: string;
+ icon?: ReactNode;
+ children: ReactNode;
+}) => {
+ return (
+
+ {label}
+
+ {icon && {icon} }
+ {children || '—'}
+
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx
new file mode 100644
index 00000000..d6580388
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx
@@ -0,0 +1,41 @@
+import { Column, Dialog, Modal, type ModalProps } from '@umami/react-zen';
+import { SessionProfile } from '@/app/(main)/websites/[websiteId]/sessions/SessionProfile';
+import { useNavigation } from '@/components/hooks';
+
+export interface SessionModalProps extends ModalProps {
+ websiteId: string;
+}
+
+export function SessionModal({ websiteId, ...props }: SessionModalProps) {
+ const {
+ router,
+ query: { session },
+ updateParams,
+ } = useNavigation();
+ const handleOpenChange = (isOpen: boolean) => {
+ if (!isOpen) {
+ router.push(updateParams({ session: undefined }));
+ }
+ };
+
+ return (
+
+
+
+ {({ close }) => (
+
+ close()} />
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx
new file mode 100644
index 00000000..6624d439
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx
@@ -0,0 +1,84 @@
+import {
+ Button,
+ Column,
+ Icon,
+ Row,
+ Tab,
+ TabList,
+ TabPanel,
+ Tabs,
+ TextField,
+} from '@umami/react-zen';
+import { X } from 'lucide-react';
+import { Avatar } from '@/components/common/Avatar';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useMessages, useWebsiteSessionQuery } from '@/components/hooks';
+import { SessionActivity } from './SessionActivity';
+import { SessionData } from './SessionData';
+import { SessionInfo } from './SessionInfo';
+import { SessionStats } from './SessionStats';
+
+export function SessionProfile({
+ websiteId,
+ sessionId,
+ onClose,
+}: {
+ websiteId: string;
+ sessionId: string;
+ onClose?: () => void;
+}) {
+ const { data, isLoading, error } = useWebsiteSessionQuery(websiteId, sessionId);
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+ {data && (
+
+ {onClose && (
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.activity)}
+ {formatMessage(labels.properties)}
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.module.css b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.module.css
deleted file mode 100644
index 0b9c011d..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.module.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.container {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
- gap: 60px;
- margin-bottom: 40px;
-}
-
-.table {
- align-self: flex-start;
-}
-
-.link:hover {
- cursor: pointer;
- color: var(--primary400);
-}
-
-.title {
- text-align: center;
- font-weight: bold;
- margin: 20px 0;
-}
-
-.chart {
- min-height: 620px;
-}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx
index a0b47bc9..1693d057 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx
@@ -1,52 +1,97 @@
-import { GridColumn, GridTable } from 'react-basics';
-import { useSessionDataProperties, useSessionDataValues, useMessages } from '@/components/hooks';
+import { Column, Grid, ListItem, Select } from '@umami/react-zen';
+import { useMemo, useState } from 'react';
+import { PieChart } from '@/components/charts/PieChart';
import { LoadingPanel } from '@/components/common/LoadingPanel';
-import PieChart from '@/components/charts/PieChart';
-import { useState } from 'react';
+import {
+ useMessages,
+ useSessionDataPropertiesQuery,
+ useSessionDataValuesQuery,
+} from '@/components/hooks';
+import { ListTable } from '@/components/metrics/ListTable';
import { CHART_COLORS } from '@/lib/constants';
-import styles from './SessionProperties.module.css';
export function SessionProperties({ websiteId }: { websiteId: string }) {
const [propertyName, setPropertyName] = useState('');
const { formatMessage, labels } = useMessages();
- const { data, isLoading, isFetched, error } = useSessionDataProperties(websiteId);
- const { data: values } = useSessionDataValues(websiteId, propertyName);
- const chartData =
- propertyName && values
- ? {
- labels: values.map(({ value }) => value),
- datasets: [
- {
- data: values.map(({ total }) => total),
- backgroundColor: CHART_COLORS,
- borderWidth: 0,
- },
- ],
- }
- : null;
+ const { data, isLoading, isFetching, error } = useSessionDataPropertiesQuery(websiteId);
+
+ const properties: string[] = data?.map(e => e.propertyName);
return (
-
-
-
-
- {row => (
- setPropertyName(row.propertyName)}>
- {row.propertyName}
-
- )}
-
-
-
- {propertyName && (
-
+
+
+ {data && (
+
+
+ {properties?.map(p => (
+
+ {p}
+
+ ))}
+
+
)}
-
+ {propertyName && }
+
);
}
-export default SessionProperties;
+const SessionValues = ({ websiteId, propertyName }) => {
+ const { data, isLoading, isFetching, error } = useSessionDataValuesQuery(websiteId, propertyName);
+
+ const propertySum = useMemo(() => {
+ return data?.reduce((sum, { total }) => sum + total, 0) ?? 0;
+ }, [data]);
+
+ const chartData = useMemo(() => {
+ if (!propertyName || !data) return null;
+ return {
+ labels: data.map(({ value }) => value),
+ datasets: [
+ {
+ data: data.map(({ total }) => total),
+ backgroundColor: CHART_COLORS,
+ borderWidth: 0,
+ },
+ ],
+ };
+ }, [propertyName, data]);
+
+ const tableData = useMemo(() => {
+ if (!propertyName || !data || propertySum === 0) return [];
+ return data.map(({ value, total }) => ({
+ label: value,
+ count: total,
+ percent: 100 * (total / propertySum),
+ }));
+ }, [propertyName, data, propertySum]);
+
+ return (
+
+ {data && (
+
+
+
+
+ )}
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionStats.tsx
similarity index 82%
rename from src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx
rename to src/app/(main)/websites/[websiteId]/sessions/SessionStats.tsx
index eb385e9b..e25be9ad 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionStats.tsx
@@ -1,13 +1,13 @@
import { useMessages } from '@/components/hooks';
-import MetricCard from '@/components/metrics/MetricCard';
-import MetricsBar from '@/components/metrics/MetricsBar';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
import { formatShortTime } from '@/lib/format';
export function SessionStats({ data }) {
const { formatMessage, labels } = useMessages();
return (
-
+
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx
index 0f193a97..b1b9f658 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx
@@ -1,21 +1,15 @@
-import { useWebsiteSessions } from '@/components/hooks';
-import SessionsTable from './SessionsTable';
-import DataTable from '@/components/common/DataTable';
-import { ReactNode } from 'react';
+import { DataGrid } from '@/components/common/DataGrid';
+import { useWebsiteSessionsQuery } from '@/components/hooks';
+import { SessionsTable } from './SessionsTable';
-export default function SessionsDataTable({
- websiteId,
- children,
-}: {
- websiteId?: string;
- teamId?: string;
- children?: ReactNode;
-}) {
- const queryResult = useWebsiteSessions(websiteId);
+export function SessionsDataTable({ websiteId }: { websiteId?: string; teamId?: string }) {
+ const queryResult = useWebsiteSessionsQuery(websiteId);
return (
- children}>
- {({ data }) => }
-
+
+ {({ data }) => {
+ return ;
+ }}
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx
index 62d60de8..c8317a2b 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx
@@ -1,42 +1,40 @@
+import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useMessages } from '@/components/hooks';
-import useWebsiteSessionStats from '@/components/hooks/queries/useWebsiteSessionStats';
-import WebsiteDateFilter from '@/components/input/WebsiteDateFilter';
-import MetricCard from '@/components/metrics/MetricCard';
-import MetricsBar from '@/components/metrics/MetricsBar';
+import { useWebsiteSessionStatsQuery } from '@/components/hooks/queries/useWebsiteSessionStatsQuery';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
import { formatLongNumber } from '@/lib/format';
-import { Flexbox } from 'react-basics';
export function SessionsMetricsBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
- const { data, isLoading, isFetched, error } = useWebsiteSessionStats(websiteId);
+ const { data, isLoading, isFetching, error } = useWebsiteSessionStatsQuery(websiteId);
return (
-
-
-
-
-
-
-
-
-
+
+ {data && (
+
+
+
+
+
+
+ )}
+
);
}
-
-export default SessionsMetricsBar;
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx
index 2ee044db..8e9d2f21 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx
@@ -1,35 +1,43 @@
'use client';
-import WebsiteHeader from '../WebsiteHeader';
-import SessionsDataTable from './SessionsDataTable';
-import SessionsMetricsBar from './SessionsMetricsBar';
-import SessionProperties from './SessionProperties';
-import WorldMap from '@/components/metrics/WorldMap';
-import { GridRow } from '@/components/layout/Grid';
-import { Item, Tabs } from 'react-basics';
-import { useState } from 'react';
+import { Column, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
+import { type Key, useState } from 'react';
+import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { Panel } from '@/components/common/Panel';
import { useMessages } from '@/components/hooks';
-import SessionsWeekly from './SessionsWeekly';
+import { getItem, setItem } from '@/lib/storage';
+import { SessionProperties } from './SessionProperties';
+import { SessionsDataTable } from './SessionsDataTable';
+
+const KEY_NAME = 'umami.sessions.tab';
export function SessionsPage({ websiteId }) {
- const [tab, setTab] = useState('activity');
+ const [tab, setTab] = useState(getItem(KEY_NAME) || 'activity');
const { formatMessage, labels } = useMessages();
+ const handleSelect = (value: Key) => {
+ setItem(KEY_NAME, value);
+ setTab(value);
+ };
+
return (
- <>
-
-
-
-
-
-
- setTab(value)} style={{ marginBottom: 30 }}>
- - {formatMessage(labels.activity)}
- - {formatMessage(labels.properties)}
-
- {tab === 'activity' && }
- {tab === 'properties' && }
- >
+
+
+
+
+
+ {formatMessage(labels.activity)}
+ {formatMessage(labels.properties)}
+
+
+
+
+
+
+
+
+
+
+
);
}
-
-export default SessionsPage;
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.module.css b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.module.css
deleted file mode 100644
index 140ad0bb..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.module.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.link {
- display: flex;
- align-items: center;
- gap: 20px;
-}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx
index ddb3ed65..5d3bb374 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx
@@ -1,60 +1,58 @@
+import { DataColumn, DataTable, type DataTableProps } from '@umami/react-zen';
import Link from 'next/link';
-import { GridColumn, GridTable } from 'react-basics';
-import { useFormat, useMessages, useTimezone } from '@/components/hooks';
-import Avatar from '@/components/common/Avatar';
-import styles from './SessionsTable.module.css';
-import TypeIcon from '@/components/common/TypeIcon';
+import { Avatar } from '@/components/common/Avatar';
+import { DateDistance } from '@/components/common/DateDistance';
+import { TypeIcon } from '@/components/common/TypeIcon';
+import { useFormat, useMessages, useNavigation } from '@/components/hooks';
-export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean }) {
- const { formatTimezoneDate } = useTimezone();
+export function SessionsTable(props: DataTableProps) {
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();
+ const { updateParams } = useNavigation();
return (
-
-
- {row => (
-
-
+
+
+ {(row: any) => (
+
+
)}
-
-
-
-
- {row => (
+
+
+
+
+ {(row: any) => (
{formatValue(row.country, 'country')}
)}
-
-
-
- {row => (
+
+
+
+ {(row: any) => (
{formatValue(row.browser, 'browser')}
)}
-
-
- {row => (
+
+
+ {(row: any) => (
{formatValue(row.os, 'os')}
)}
-
-
- {row => (
+
+
+ {(row: any) => (
{formatValue(row.device, 'device')}
)}
-
-
- {row => formatTimezoneDate(row.createdAt, 'PPPpp')}
-
-
+
+
+ {(row: any) => }
+
+
);
}
-
-export default SessionsTable;
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.module.css b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.module.css
deleted file mode 100644
index 35361643..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.module.css
+++ /dev/null
@@ -1,43 +0,0 @@
-.week {
- display: flex;
- justify-content: space-between;
- position: relative;
-}
-
-.header {
- text-align: center;
- font-weight: 700;
- margin-bottom: 10px;
-}
-
-.day {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- justify-content: flex-start;
- gap: 1px;
- position: relative;
-}
-
-.cell {
- display: flex;
- background-color: var(--base75);
- width: 20px;
- height: 20px;
- margin: auto;
- border-radius: 100%;
- align-items: flex-start;
-}
-
-.hour {
- font-weight: 700;
- color: var(--font-color300);
- height: 20px;
-}
-
-.block {
- background-color: var(--primary400);
- width: 20px;
- height: 20px;
- border-radius: 100%;
-}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx
deleted file mode 100644
index 283a7ea0..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { format, startOfDay, addHours } from 'date-fns';
-import { useLocale, useMessages, useWebsiteSessionsWeekly } from '@/components/hooks';
-import { LoadingPanel } from '@/components/common/LoadingPanel';
-import { getDayOfWeekAsDate } from '@/lib/date';
-import styles from './SessionsWeekly.module.css';
-import classNames from 'classnames';
-import { TooltipPopup } from 'react-basics';
-
-export function SessionsWeekly({ websiteId }: { websiteId: string }) {
- const { data, ...props } = useWebsiteSessionsWeekly(websiteId);
- const { dateLocale } = useLocale();
- const { labels, formatMessage } = useMessages();
- const { weekStartsOn } = dateLocale.options;
- const daysOfWeek = Array(7)
- .fill(weekStartsOn)
- .map((d, i) => (d + i) % 7);
-
- const [, max] = data
- ? data.reduce((arr: number[], hours: number[], index: number) => {
- const min = Math.min(...hours);
- const max = Math.max(...hours);
-
- if (index === 0) {
- return [min, max];
- }
-
- if (min < arr[0]) {
- arr[0] = min;
- }
-
- if (max > arr[1]) {
- arr[1] = max;
- }
-
- return arr;
- }, [])
- : [];
-
- return (
-
-
-
-
- {Array(24)
- .fill(null)
- .map((_, i) => {
- const label = format(addHours(startOfDay(new Date()), i), 'p', { locale: dateLocale })
- .replace(/\D00 ?/, '')
- .toLowerCase();
- return (
-
- {label}
-
- );
- })}
-
- {data &&
- daysOfWeek.map((index: number) => {
- const day = data[index];
- return (
-
-
- {format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })}
-
- {day?.map((hour: number, j) => {
- const pct = hour / max;
- return (
-
- {hour > 0 && (
-
-
-
- )}
-
- );
- })}
-
- );
- })}
-
-
- );
-}
-
-export default SessionsWeekly;
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.module.css b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.module.css
deleted file mode 100644
index fb830d38..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.module.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.timeline {
- display: flex;
- flex-direction: column;
- gap: 20px;
-}
-
-.row {
- display: grid;
- grid-template-columns: max-content max-content 1fr;
- align-items: center;
- gap: 20px;
-}
-
-.time {
- color: var(--font-color200);
- width: 150px;
-}
-
-.header {
- font-weight: bold;
-}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx
deleted file mode 100644
index 6a847fb4..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { isSameDay } from 'date-fns';
-import { Loading, Icon, StatusLight } from 'react-basics';
-import Icons from '@/components/icons';
-import { useSessionActivity, useTimezone } from '@/components/hooks';
-import styles from './SessionActivity.module.css';
-import { Fragment } from 'react';
-
-export function SessionActivity({
- websiteId,
- sessionId,
- startDate,
- endDate,
-}: {
- websiteId: string;
- sessionId: string;
- startDate: Date;
- endDate: Date;
-}) {
- const { formatTimezoneDate } = useTimezone();
- const { data, isLoading } = useSessionActivity(websiteId, sessionId, startDate, endDate);
-
- if (isLoading) {
- return ;
- }
-
- let lastDay = null;
-
- return (
-
- {data.map(({ id, createdAt, urlPath, eventName, visitId }) => {
- const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt));
- lastDay = createdAt;
-
- return (
-
- {showHeader && (
- {formatTimezoneDate(createdAt, 'PPPP')}
- )}
-
-
-
- {formatTimezoneDate(createdAt, 'pp')}
-
-
-
{eventName ? : }
-
{eventName || urlPath}
-
-
- );
- })}
-
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.module.css b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.module.css
deleted file mode 100644
index 4794803d..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.module.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.data {
- display: flex;
- flex-direction: column;
- gap: 20px;
- position: relative;
-}
-
-.header {
- font-weight: bold;
- margin-bottom: 20px;
-}
-
-.empty {
- color: var(--font-color300);
- text-align: center;
-}
-
-.label {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.type {
- font-size: 11px;
- padding: 0 6px;
- border-radius: 4px;
- border: 1px solid var(--base400);
-}
-
-.name {
- color: var(--font-color200);
- font-weight: bold;
-}
-
-.value {
- margin: 5px 0;
-}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx
deleted file mode 100644
index 56d4a0d9..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { TextOverflow } from 'react-basics';
-import { useMessages, useSessionData } from '@/components/hooks';
-import Empty from '@/components/common/Empty';
-import { DATA_TYPES } from '@/lib/constants';
-import styles from './SessionData.module.css';
-import { LoadingPanel } from '@/components/common/LoadingPanel';
-
-export function SessionData({ websiteId, sessionId }: { websiteId: string; sessionId: string }) {
- const { formatMessage, labels } = useMessages();
- const { data, ...query } = useSessionData(websiteId, sessionId);
-
- return (
- <>
- {formatMessage(labels.properties)}
-
- {!data?.length && }
- {data?.map(({ dataKey, dataType, stringValue }) => {
- return (
-
-
-
- {dataKey}
-
-
{DATA_TYPES[dataType]}
-
-
{stringValue}
-
- );
- })}
-
- >
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.module.css b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.module.css
deleted file mode 100644
index 7058cd79..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.module.css
+++ /dev/null
@@ -1,47 +0,0 @@
-.page {
- display: grid;
- grid-template-columns: max-content 1fr max-content;
- margin-bottom: 40px;
- position: relative;
-}
-
-.sidebar {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: flex-start;
- gap: 20px;
- width: 300px;
- padding-right: 20px;
- border-right: 1px solid var(--base300);
- position: relative;
-}
-
-.content {
- display: flex;
- flex-direction: column;
- gap: 30px;
- padding: 0 20px;
- position: relative;
-}
-
-.data {
- width: 300px;
- border-left: 1px solid var(--base300);
- padding-left: 20px;
- position: relative;
- transition: width 200ms ease-in-out;
-}
-
-@media screen and (max-width: 992px) {
- .page {
- grid-template-columns: 1fr;
- gap: 30px;
- }
-
- .sidebar,
- .data {
- border: 0;
- width: auto;
- }
-}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx
deleted file mode 100644
index 9ccf275f..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-'use client';
-import Avatar from '@/components/common/Avatar';
-import { LoadingPanel } from '@/components/common/LoadingPanel';
-import { useWebsiteSession } from '@/components/hooks';
-import WebsiteHeader from '../../WebsiteHeader';
-import { SessionActivity } from './SessionActivity';
-import { SessionData } from './SessionData';
-import styles from './SessionDetailsPage.module.css';
-import SessionInfo from './SessionInfo';
-import { SessionStats } from './SessionStats';
-
-export default function SessionDetailsPage({
- websiteId,
- sessionId,
-}: {
- websiteId: string;
- sessionId: string;
-}) {
- const { data, ...query } = useWebsiteSession(websiteId, sessionId);
-
- return (
-
-
-
-
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.module.css b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.module.css
deleted file mode 100644
index de6e796f..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.module.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.info {
- display: grid;
- gap: 10px;
-}
-
-.info dl {
- width: 100%;
-}
-
-.info dt {
- color: var(--font-color200);
- font-weight: bold;
-}
-
-.info dd {
- display: flex;
- gap: 10px;
- align-items: center;
- margin: 5px 0 28px;
- text-align: left;
-}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.tsx
deleted file mode 100644
index fc69494f..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { useFormat, useLocale, useMessages, useRegionNames, useTimezone } from '@/components/hooks';
-import TypeIcon from '@/components/common/TypeIcon';
-import { Icon, CopyIcon } from 'react-basics';
-import Icons from '@/components/icons';
-import styles from './SessionInfo.module.css';
-
-export default function SessionInfo({ data }) {
- const { locale } = useLocale();
- const { formatTimezoneDate } = useTimezone();
- const { formatMessage, labels } = useMessages();
- const { formatValue } = useFormat();
- const { getRegionName } = useRegionNames(locale);
-
- return (
-
-
- ID
-
- {data?.id}
-
- {formatMessage(labels.distinctId)}
- {data?.distinctId}
- {formatMessage(labels.lastSeen)}
- {formatTimezoneDate(data?.lastAt, 'PPPPpp')}
-
- {formatMessage(labels.firstSeen)}
- {formatTimezoneDate(data?.firstAt, 'PPPPpp')}
-
- {formatMessage(labels.country)}
-
-
- {formatValue(data?.country, 'country')}
-
-
- {formatMessage(labels.region)}
-
-
-
-
- {getRegionName(data?.region)}
-
-
- {formatMessage(labels.city)}
-
-
-
-
- {data?.city}
-
-
- {formatMessage(labels.os)}
-
-
- {formatValue(data?.os, 'os')}
-
-
- {formatMessage(labels.device)}
-
-
- {formatValue(data?.device, 'device')}
-
-
- {formatMessage(labels.browser)}
-
-
- {formatValue(data?.browser, 'browser')}
-
-
-
- );
-}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/page.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/page.tsx
deleted file mode 100644
index f4882880..00000000
--- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/page.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import SessionDetailsPage from './SessionDetailsPage';
-import { Metadata } from 'next';
-
-export default async function WebsitePage({
- params,
-}: {
- params: { websiteId: string; sessionId: string };
-}) {
- const { websiteId, sessionId } = await params;
-
- return ;
-}
-
-export const metadata: Metadata = {
- title: 'Websites',
-};
diff --git a/src/app/(main)/websites/[websiteId]/sessions/page.tsx b/src/app/(main)/websites/[websiteId]/sessions/page.tsx
index d1ff96f5..221ab71c 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/page.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/page.tsx
@@ -1,7 +1,7 @@
-import SessionsPage from './SessionsPage';
-import { Metadata } from 'next';
+import type { Metadata } from 'next';
+import { SessionsPage } from './SessionsPage';
-export default async function ({ params }: { params: { websiteId: string } }) {
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params;
return ;
diff --git a/src/app/(main)/websites/[websiteId]/settings/SettingsPage.tsx b/src/app/(main)/websites/[websiteId]/settings/SettingsPage.tsx
new file mode 100644
index 00000000..468f250d
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/SettingsPage.tsx
@@ -0,0 +1,6 @@
+'use client';
+import { WebsiteSettingsPage } from '@/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage';
+
+export function SettingsPage({ websiteId }: { websiteId: string }) {
+ return ;
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteData.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteData.tsx
new file mode 100644
index 00000000..21cd6137
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteData.tsx
@@ -0,0 +1,104 @@
+import { Button, Column, Dialog, DialogTrigger, Modal } from '@umami/react-zen';
+import { ActionForm } from '@/components/common/ActionForm';
+import {
+ useLoginQuery,
+ useMessages,
+ useModified,
+ useNavigation,
+ useUserTeamsQuery,
+} from '@/components/hooks';
+import { ROLES } from '@/lib/constants';
+import { WebsiteDeleteForm } from './WebsiteDeleteForm';
+import { WebsiteResetForm } from './WebsiteResetForm';
+import { WebsiteTransferForm } from './WebsiteTransferForm';
+
+export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
+ const { formatMessage, labels, messages } = useMessages();
+ const { user } = useLoginQuery();
+ const { touch } = useModified();
+ const { router, pathname, teamId, renderUrl } = useNavigation();
+ const { data: teams } = useUserTeamsQuery(user.id);
+ const isAdmin = pathname.startsWith('/admin');
+
+ const canTransferWebsite =
+ (
+ (!teamId &&
+ teams?.data?.filter(({ members }) =>
+ members.find(
+ ({ role, userId }) =>
+ [ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
+ ),
+ )) ||
+ []
+ ).length > 0 ||
+ (teamId &&
+ !!teams?.data
+ ?.find(({ id }) => id === teamId)
+ ?.members.find(({ role, userId }) => role === ROLES.teamOwner && userId === user.id));
+
+ const handleSave = () => {
+ touch('websites');
+ onSave?.();
+ router.push(renderUrl(`/websites`));
+ };
+
+ const handleReset = async () => {
+ onSave?.();
+ };
+
+ return (
+
+ {!isAdmin && (
+
+
+ {formatMessage(labels.transfer)}
+
+
+ {({ close }) => (
+
+ )}
+
+
+
+
+ )}
+
+
+
+ {formatMessage(labels.reset)}
+
+
+ {({ close }) => (
+
+ )}
+
+
+
+
+
+
+
+
+ {formatMessage(labels.delete)}
+
+
+
+ {({ close }) => (
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm.tsx
similarity index 65%
rename from src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx
rename to src/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm.tsx
index 5eef3544..2fc02768 100644
--- a/src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm.tsx
@@ -1,5 +1,5 @@
-import { useApi, useMessages } from '@/components/hooks';
-import TypeConfirmationForm from '@/components/common/TypeConfirmationForm';
+import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
+import { useDeleteQuery, useMessages } from '@/components/hooks';
const CONFIRM_VALUE = 'DELETE';
@@ -13,14 +13,13 @@ export function WebsiteDeleteForm({
onClose?: () => void;
}) {
const { formatMessage, labels } = useMessages();
- const { del, useMutation } = useApi();
- const { mutate, isPending, error } = useMutation({
- mutationFn: () => del(`/websites/${websiteId}`),
- });
+ const { mutateAsync, isPending, error, touch } = useDeleteQuery(`/websites/${websiteId}`);
const handleConfirm = async () => {
- mutate(null, {
+ await mutateAsync(null, {
onSuccess: async () => {
+ touch('websites');
+ touch(`websites:${websiteId}`);
onSave?.();
onClose?.();
},
@@ -39,5 +38,3 @@ export function WebsiteDeleteForm({
/>
);
}
-
-export default WebsiteDeleteForm;
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx
new file mode 100644
index 00000000..4ae819ee
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteEditForm.tsx
@@ -0,0 +1,55 @@
+import { Form, FormButtons, FormField, FormSubmitButton, TextField } from '@umami/react-zen';
+import { useMessages, useUpdateQuery, useWebsite } from '@/components/hooks';
+import { DOMAIN_REGEX } from '@/lib/constants';
+
+export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
+ const website = useWebsite();
+ const { formatMessage, labels, messages, getErrorMessage } = useMessages();
+ const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
+
+ const handleSubmit = async (data: any) => {
+ await mutateAsync(data, {
+ onSuccess: async () => {
+ toast(formatMessage(messages.saved));
+ touch('websites');
+ touch(`website:${website.id}`);
+ onSave?.();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteResetForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteResetForm.tsx
similarity index 62%
rename from src/app/(main)/settings/websites/[websiteId]/WebsiteResetForm.tsx
rename to src/app/(main)/websites/[websiteId]/settings/WebsiteResetForm.tsx
index 73886aa9..d791bc96 100644
--- a/src/app/(main)/settings/websites/[websiteId]/WebsiteResetForm.tsx
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteResetForm.tsx
@@ -1,5 +1,5 @@
-import { useApi, useMessages } from '@/components/hooks';
-import TypeConfirmationForm from '@/components/common/TypeConfirmationForm';
+import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
+import { useMessages, useUpdateQuery } from '@/components/hooks';
const CONFIRM_VALUE = 'RESET';
@@ -13,13 +13,10 @@ export function WebsiteResetForm({
onClose?: () => void;
}) {
const { formatMessage, labels } = useMessages();
- const { post, useMutation } = useApi();
- const { mutate, isPending, error } = useMutation({
- mutationFn: (data: any) => post(`/websites/${websiteId}/reset`, data),
- });
+ const { mutateAsync, isPending, error } = useUpdateQuery(`/websites/${websiteId}/reset`);
const handleConfirm = async () => {
- mutate(null, {
+ await mutateAsync(null, {
onSuccess: async () => {
onSave?.();
onClose?.();
@@ -38,5 +35,3 @@ export function WebsiteResetForm({
/>
);
}
-
-export default WebsiteResetForm;
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx
new file mode 100644
index 00000000..3970cdbd
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx
@@ -0,0 +1,28 @@
+import { Column } from '@umami/react-zen';
+import { Panel } from '@/components/common/Panel';
+import { useWebsite } from '@/components/hooks';
+import { WebsiteData } from './WebsiteData';
+import { WebsiteEditForm } from './WebsiteEditForm';
+import { WebsiteShareForm } from './WebsiteShareForm';
+import { WebsiteTrackingCode } from './WebsiteTrackingCode';
+
+export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) {
+ const website = useWebsite();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx
new file mode 100644
index 00000000..99977a0b
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader.tsx
@@ -0,0 +1,22 @@
+import { IconLabel, Row } from '@umami/react-zen';
+import Link from 'next/link';
+import { PageHeader } from '@/components/common/PageHeader';
+import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
+import { ArrowLeft, Globe } from '@/components/icons';
+
+export function WebsiteSettingsHeader() {
+ const website = useWebsite();
+ const { formatMessage, labels } = useMessages();
+ const { renderUrl } = useNavigation();
+
+ return (
+ <>
+
+
+ } label={formatMessage(labels.website)} />
+
+
+ } />
+ >
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx
new file mode 100644
index 00000000..56c6f436
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx
@@ -0,0 +1,93 @@
+import {
+ Button,
+ Column,
+ Form,
+ FormButtons,
+ FormSubmitButton,
+ IconLabel,
+ Label,
+ Row,
+ Switch,
+ TextField,
+} from '@umami/react-zen';
+import { RefreshCcw } from 'lucide-react';
+import { useState } from 'react';
+import { useConfig, useMessages, useUpdateQuery } from '@/components/hooks';
+import { getRandomChars } from '@/lib/generate';
+
+const generateId = () => getRandomChars(16);
+
+export interface WebsiteShareFormProps {
+ websiteId: string;
+ shareId?: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}
+
+export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
+ const { formatMessage, labels, messages, getErrorMessage } = useMessages();
+ const [currentId, setCurrentId] = useState(shareId);
+ const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
+ const { cloudMode } = useConfig();
+
+ const getUrl = (shareId: string) => {
+ if (cloudMode) {
+ return `${process.env.cloudUrl}/share/${shareId}`;
+ }
+
+ return `${window?.location.origin}${process.env.basePath || ''}/share/${shareId}`;
+ };
+
+ const url = getUrl(currentId);
+
+ const handleGenerate = () => {
+ setCurrentId(generateId());
+ };
+
+ const handleSwitch = () => {
+ setCurrentId(currentId ? null : generateId());
+ };
+
+ const handleSave = async () => {
+ const data = {
+ shareId: currentId,
+ };
+ await mutateAsync(data, {
+ onSuccess: async () => {
+ toast(formatMessage(messages.saved));
+ touch(`website:${websiteId}`);
+ onSave?.();
+ onClose?.();
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteTrackingCode.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteTrackingCode.tsx
new file mode 100644
index 00000000..d24f9485
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteTrackingCode.tsx
@@ -0,0 +1,40 @@
+import { Column, Label, Text, TextField } from '@umami/react-zen';
+import { useConfig, useMessages } from '@/components/hooks';
+
+const SCRIPT_NAME = 'script.js';
+
+export function WebsiteTrackingCode({
+ websiteId,
+ hostUrl,
+}: {
+ websiteId: string;
+ hostUrl?: string;
+}) {
+ const { formatMessage, messages, labels } = useMessages();
+ const config = useConfig();
+
+ const trackerScriptName =
+ config?.trackerScriptName?.split(',')?.map((n: string) => n.trim())?.[0] || SCRIPT_NAME;
+
+ const getUrl = () => {
+ if (config?.cloudMode) {
+ return `${process.env.cloudUrl}/${trackerScriptName}`;
+ }
+
+ return `${hostUrl || window?.location?.origin || ''}${
+ process.env.basePath || ''
+ }/${trackerScriptName}`;
+ };
+
+ const url = trackerScriptName?.startsWith('http') ? trackerScriptName : getUrl();
+
+ const code = ``;
+
+ return (
+
+ {formatMessage(labels.trackingCode)}
+ {formatMessage(messages.trackingCode)}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteTransferForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteTransferForm.tsx
new file mode 100644
index 00000000..8af4f05c
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteTransferForm.tsx
@@ -0,0 +1,102 @@
+import {
+ Button,
+ Form,
+ FormButtons,
+ FormField,
+ FormSubmitButton,
+ ListItem,
+ Loading,
+ Select,
+ Text,
+} from '@umami/react-zen';
+import { type Key, useState } from 'react';
+import {
+ useLoginQuery,
+ useMessages,
+ useUpdateQuery,
+ useUserTeamsQuery,
+ useWebsite,
+} from '@/components/hooks';
+import { ROLES } from '@/lib/constants';
+
+export function WebsiteTransferForm({
+ websiteId,
+ onSave,
+ onClose,
+}: {
+ websiteId: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { user } = useLoginQuery();
+ const website = useWebsite();
+ const [teamId, setTeamId] = useState(null);
+ const { formatMessage, labels, messages, getErrorMessage } = useMessages();
+ const { mutateAsync, error, isPending } = useUpdateQuery(`/websites/${websiteId}/transfer`);
+ const { data: teams, isLoading } = useUserTeamsQuery(user.id);
+ const isTeamWebsite = !!website?.teamId;
+
+ const items =
+ teams?.data?.filter(({ members }) =>
+ members.some(
+ ({ role, userId }) =>
+ [ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
+ ),
+ ) || [];
+
+ const handleSubmit = async () => {
+ await mutateAsync(
+ {
+ userId: website.teamId ? user.id : undefined,
+ teamId: website.userId ? teamId : undefined,
+ },
+ {
+ onSuccess: async () => {
+ onSave?.();
+ onClose?.();
+ },
+ },
+ );
+ };
+
+ const handleChange = (key: Key) => {
+ setTeamId(key as string);
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/page.tsx b/src/app/(main)/websites/[websiteId]/settings/page.tsx
new file mode 100644
index 00000000..a26d14f7
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { SettingsPage } from './SettingsPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Settings',
+};
diff --git a/src/app/(main)/websites/page.tsx b/src/app/(main)/websites/page.tsx
index 859516c9..cefaf809 100644
--- a/src/app/(main)/websites/page.tsx
+++ b/src/app/(main)/websites/page.tsx
@@ -1,5 +1,5 @@
-import WebsitesPage from './WebsitesPage';
-import { Metadata } from 'next';
+import type { Metadata } from 'next';
+import { WebsitesPage } from './WebsitesPage';
export default function () {
return ;
diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx
index 66884c2f..ae1a0007 100644
--- a/src/app/Providers.tsx
+++ b/src/app/Providers.tsx
@@ -1,17 +1,19 @@
'use client';
-import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { ReactBasicsProvider } from 'react-basics';
-import ErrorBoundary from '@/components/common/ErrorBoundary';
+import { RouterProvider, ZenProvider } from '@umami/react-zen';
+import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
+import { IntlProvider } from 'react-intl';
+import { ErrorBoundary } from '@/components/common/ErrorBoundary';
import { useLocale } from '@/components/hooks';
import 'chartjs-adapter-date-fns';
-import { useEffect } from 'react';
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
+ staleTime: 1000 * 60,
},
},
});
@@ -32,15 +34,29 @@ function MessagesProvider({ children }) {
}
export function Providers({ children }) {
+ const router = useRouter();
+
+ function navigate(url: string) {
+ if (shouldUseNativeLink(url)) {
+ window.location.href = url;
+ } else {
+ router.push(url);
+ }
+ }
+
+ function shouldUseNativeLink(url: string) {
+ return url.startsWith('http');
+ }
+
return (
-
-
-
- {children}
-
-
-
+
+
+
+
+ {children}
+
+
+
+
);
}
-
-export default Providers;
diff --git a/src/app/actions/getConfig.ts b/src/app/actions/getConfig.ts
deleted file mode 100644
index bb892f01..00000000
--- a/src/app/actions/getConfig.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-'use server';
-
-export async function getConfig() {
- return {
- telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
- trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
- uiDisabled: !!process.env.DISABLE_UI,
- updatesDisabled: !!process.env.DISABLE_UPDATES,
- };
-}
diff --git a/src/app/api/admin/teams/route.ts b/src/app/api/admin/teams/route.ts
new file mode 100644
index 00000000..ceb16ab1
--- /dev/null
+++ b/src/app/api/admin/teams/route.ts
@@ -0,0 +1,58 @@
+import { z } from 'zod';
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams, searchParams } from '@/lib/schema';
+import { canViewAllTeams } from '@/permissions';
+import { getTeams } from '@/queries/prisma/team';
+
+export async function GET(request: Request) {
+ const schema = z.object({
+ ...pagingParams,
+ ...searchParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ if (!(await canViewAllTeams(auth))) {
+ return unauthorized();
+ }
+
+ const teams = await getTeams(
+ {
+ include: {
+ members: {
+ include: {
+ user: {
+ select: {
+ id: true,
+ username: true,
+ },
+ },
+ },
+ },
+ _count: {
+ select: {
+ websites: {
+ where: { deletedAt: null },
+ },
+ members: {
+ where: {
+ user: { deletedAt: null },
+ },
+ },
+ },
+ },
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ },
+ query,
+ );
+
+ return json(teams);
+}
diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts
index 2185e03e..2e522615 100644
--- a/src/app/api/admin/users/route.ts
+++ b/src/app/api/admin/users/route.ts
@@ -1,13 +1,14 @@
import { z } from 'zod';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
-import { pagingParams } from '@/lib/schema';
-import { canViewUsers } from '@/lib/auth';
+import { pagingParams, searchParams } from '@/lib/schema';
+import { canViewUsers } from '@/permissions';
import { getUsers } from '@/queries/prisma/user';
export async function GET(request: Request) {
const schema = z.object({
...pagingParams,
+ ...searchParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -25,12 +26,18 @@ export async function GET(request: Request) {
include: {
_count: {
select: {
- websiteUser: {
+ websites: {
where: { deletedAt: null },
},
},
},
},
+ omit: {
+ password: true,
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
},
query,
);
diff --git a/src/app/api/admin/websites/route.ts b/src/app/api/admin/websites/route.ts
index 3f35ea49..09b2ef98 100644
--- a/src/app/api/admin/websites/route.ts
+++ b/src/app/api/admin/websites/route.ts
@@ -1,17 +1,15 @@
import { z } from 'zod';
+import { ROLES } from '@/lib/constants';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
-import { pagingParams } from '@/lib/schema';
-import { canViewAllWebsites } from '@/lib/auth';
+import { pagingParams, searchParams } from '@/lib/schema';
+import { canViewAllWebsites } from '@/permissions';
import { getWebsites } from '@/queries/prisma/website';
-import { ROLES } from '@/lib/constants';
export async function GET(request: Request) {
const schema = z.object({
- userId: z.string().uuid(),
- includeOwnedTeams: z.string().optional(),
- includeAllTeams: z.string().optional(),
...pagingParams,
+ ...searchParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -24,46 +22,13 @@ export async function GET(request: Request) {
return unauthorized();
}
- const { userId, includeOwnedTeams, includeAllTeams } = query;
-
const websites = await getWebsites(
{
- where: {
- OR: [
- ...(userId && [{ userId }]),
- ...(userId && includeOwnedTeams
- ? [
- {
- team: {
- deletedAt: null,
- teamUser: {
- some: {
- role: ROLES.teamOwner,
- userId,
- },
- },
- },
- },
- ]
- : []),
- ...(userId && includeAllTeams
- ? [
- {
- team: {
- deletedAt: null,
- teamUser: {
- some: {
- userId,
- },
- },
- },
- },
- ]
- : []),
- ],
- },
include: {
user: {
+ where: {
+ deletedAt: null,
+ },
select: {
username: true,
id: true,
@@ -74,7 +39,7 @@ export async function GET(request: Request) {
deletedAt: null,
},
include: {
- teamUser: {
+ members: {
where: {
role: ROLES.teamOwner,
},
@@ -82,6 +47,9 @@ export async function GET(request: Request) {
},
},
},
+ orderBy: {
+ createdAt: 'desc',
+ },
},
query,
);
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
index bfac5548..17ca2f7d 100644
--- a/src/app/api/auth/login/route.ts
+++ b/src/app/api/auth/login/route.ts
@@ -1,13 +1,13 @@
import { z } from 'zod';
-import { checkPassword } from '@/lib/auth';
-import { createSecureToken } from '@/lib/jwt';
-import redis from '@/lib/redis';
-import { getUserByUsername } from '@/queries';
-import { json, unauthorized } from '@/lib/response';
-import { parseRequest } from '@/lib/request';
import { saveAuth } from '@/lib/auth';
-import { secret } from '@/lib/crypto';
import { ROLES } from '@/lib/constants';
+import { secret } from '@/lib/crypto';
+import { createSecureToken } from '@/lib/jwt';
+import { checkPassword } from '@/lib/password';
+import redis from '@/lib/redis';
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { getAllUserTeams, getUserByUsername } from '@/queries/prisma';
export async function POST(request: Request) {
const schema = z.object({
@@ -26,7 +26,7 @@ export async function POST(request: Request) {
const user = await getUserByUsername(username, { includePassword: true });
if (!user || !checkPassword(password, user.password)) {
- return unauthorized('message.incorrect-username-password');
+ return unauthorized({ code: 'incorrect-username-password' });
}
const { id, role, createdAt } = user;
@@ -39,8 +39,10 @@ export async function POST(request: Request) {
token = createSecureToken({ userId: user.id, role }, secret());
}
+ const teams = await getAllUserTeams(id);
+
return json({
token,
- user: { id, username, role, createdAt, isAdmin: role === ROLES.admin },
+ user: { id, username, role, createdAt, isAdmin: role === ROLES.admin, teams },
});
}
diff --git a/src/app/api/auth/sso/route.ts b/src/app/api/auth/sso/route.ts
index fc8fb9bf..bba3dde3 100644
--- a/src/app/api/auth/sso/route.ts
+++ b/src/app/api/auth/sso/route.ts
@@ -1,7 +1,7 @@
-import redis from '@/lib/redis';
-import { json } from '@/lib/response';
-import { parseRequest } from '@/lib/request';
import { saveAuth } from '@/lib/auth';
+import redis from '@/lib/redis';
+import { parseRequest } from '@/lib/request';
+import { json } from '@/lib/response';
export async function POST(request: Request) {
const { auth, error } = await parseRequest(request);
diff --git a/src/app/api/auth/verify/route.ts b/src/app/api/auth/verify/route.ts
index 5f8543a5..b308b7b6 100644
--- a/src/app/api/auth/verify/route.ts
+++ b/src/app/api/auth/verify/route.ts
@@ -1,5 +1,6 @@
import { parseRequest } from '@/lib/request';
import { json } from '@/lib/response';
+import { getAllUserTeams } from '@/queries/prisma';
export async function POST(request: Request) {
const { auth, error } = await parseRequest(request);
@@ -8,5 +9,7 @@ export async function POST(request: Request) {
return error();
}
- return json(auth.user);
+ const teams = await getAllUserTeams(auth.user.id);
+
+ return json({ ...auth.user, teams });
}
diff --git a/src/app/api/batch/route.ts b/src/app/api/batch/route.ts
index 87e04110..46e8b3c3 100644
--- a/src/app/api/batch/route.ts
+++ b/src/app/api/batch/route.ts
@@ -2,8 +2,9 @@ import { z } from 'zod';
import * as send from '@/app/api/send/route';
import { parseRequest } from '@/lib/request';
import { json, serverError } from '@/lib/response';
+import { anyObjectParam } from '@/lib/schema';
-const schema = z.array(z.object({}).passthrough());
+const schema = z.array(anyObjectParam);
export async function POST(request: Request) {
try {
@@ -16,12 +17,29 @@ export async function POST(request: Request) {
const errors = [];
let index = 0;
+ let cache = null;
for (const data of body) {
- const newRequest = new Request(request, { body: JSON.stringify(data) });
+ // Recreate a fresh Request since `new Request(request)` will have the following error:
+ // > Cannot read private member #state from an object whose class did not declare it
+
+ // Copy headers we received, ensure JSON content type, and avoid conflicting content-length
+ const headers = new Headers(request.headers);
+ headers.set('content-type', 'application/json');
+ headers.delete('content-length');
+
+ const newRequest = new Request(request.url, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(data),
+ });
+
const response = await send.POST(newRequest);
+ const responseJson = await response.json();
if (!response.ok) {
- errors.push({ index, response: await response.json() });
+ errors.push({ index, response: responseJson });
+ } else {
+ cache ??= responseJson.cache;
}
index++;
@@ -32,6 +50,7 @@ export async function POST(request: Request) {
processed: body.length - errors.length,
errors: errors.length,
details: errors,
+ cache,
});
} catch (e) {
return serverError(e);
diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts
new file mode 100644
index 00000000..4e40caa4
--- /dev/null
+++ b/src/app/api/config/route.ts
@@ -0,0 +1,21 @@
+import { parseRequest } from '@/lib/request';
+import { json } from '@/lib/response';
+
+export async function GET(request: Request) {
+ const { error } = await parseRequest(request, null, { skipAuth: true });
+
+ if (error) {
+ return error();
+ }
+
+ return json({
+ cloudMode: !!process.env.CLOUD_MODE,
+ faviconUrl: process.env.FAVICON_URL,
+ linksUrl: process.env.LINKS_URL,
+ pixelsUrl: process.env.PIXELS_URL,
+ privateMode: !!process.env.PRIVATE_MODE,
+ telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
+ trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
+ updatesDisabled: !!process.env.DISABLE_UPDATES,
+ });
+}
diff --git a/src/app/api/links/[linkId]/route.ts b/src/app/api/links/[linkId]/route.ts
new file mode 100644
index 00000000..92f572c4
--- /dev/null
+++ b/src/app/api/links/[linkId]/route.ts
@@ -0,0 +1,77 @@
+import { z } from 'zod';
+import { parseRequest } from '@/lib/request';
+import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
+import { canDeleteLink, canUpdateLink, canViewLink } from '@/permissions';
+import { deleteLink, getLink, updateLink } from '@/queries/prisma';
+
+export async function GET(request: Request, { params }: { params: Promise<{ linkId: string }> }) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { linkId } = await params;
+
+ if (!(await canViewLink(auth, linkId))) {
+ return unauthorized();
+ }
+
+ const website = await getLink(linkId);
+
+ return json(website);
+}
+
+export async function POST(request: Request, { params }: { params: Promise<{ linkId: string }> }) {
+ const schema = z.object({
+ name: z.string().optional(),
+ url: z.string().optional(),
+ slug: z.string().min(8).optional(),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { linkId } = await params;
+ const { name, url, slug } = body;
+
+ if (!(await canUpdateLink(auth, linkId))) {
+ return unauthorized();
+ }
+
+ try {
+ const result = await updateLink(linkId, { name, url, slug });
+
+ return Response.json(result);
+ } catch (e: any) {
+ if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('slug')) {
+ return badRequest({ message: 'That slug is already taken.' });
+ }
+
+ return serverError(e);
+ }
+}
+
+export async function DELETE(
+ request: Request,
+ { params }: { params: Promise<{ linkId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { linkId } = await params;
+
+ if (!(await canDeleteLink(auth, linkId))) {
+ return unauthorized();
+ }
+
+ await deleteLink(linkId);
+
+ return ok();
+}
diff --git a/src/app/api/links/route.ts b/src/app/api/links/route.ts
new file mode 100644
index 00000000..a639888b
--- /dev/null
+++ b/src/app/api/links/route.ts
@@ -0,0 +1,64 @@
+import { z } from 'zod';
+import { uuid } from '@/lib/crypto';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams, searchParams } from '@/lib/schema';
+import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions';
+import { createLink, getUserLinks } from '@/queries/prisma';
+
+export async function GET(request: Request) {
+ const schema = z.object({
+ ...pagingParams,
+ ...searchParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const filters = await getQueryFilters(query);
+
+ const links = await getUserLinks(auth.user.id, filters);
+
+ return json(links);
+}
+
+export async function POST(request: Request) {
+ const schema = z.object({
+ name: z.string().max(100),
+ url: z.string().max(500),
+ slug: z.string().max(100),
+ teamId: z.string().nullable().optional(),
+ id: z.uuid().nullable().optional(),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { id, name, url, slug, teamId } = body;
+
+ if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
+ return unauthorized();
+ }
+
+ const data: any = {
+ id: id ?? uuid(),
+ name,
+ url,
+ slug,
+ teamId,
+ };
+
+ if (!teamId) {
+ data.userId = auth.user.id;
+ }
+
+ const result = await createLink(data);
+
+ return json(result);
+}
diff --git a/src/app/api/me/password/route.ts b/src/app/api/me/password/route.ts
index 69bef49b..24c73705 100644
--- a/src/app/api/me/password/route.ts
+++ b/src/app/api/me/password/route.ts
@@ -1,7 +1,7 @@
import { z } from 'zod';
-import { checkPassword, hashPassword } from '@/lib/auth';
+import { checkPassword, hashPassword } from '@/lib/password';
import { parseRequest } from '@/lib/request';
-import { json, badRequest } from '@/lib/response';
+import { badRequest, json } from '@/lib/response';
import { getUser, updateUser } from '@/queries/prisma/user';
export async function POST(request: Request) {
@@ -22,7 +22,7 @@ export async function POST(request: Request) {
const user = await getUser(userId, { includePassword: true });
if (!checkPassword(currentPassword, user.password)) {
- return badRequest('Current password is incorrect');
+ return badRequest({ message: 'Current password is incorrect' });
}
const password = hashPassword(newPassword);
diff --git a/src/app/api/me/teams/route.ts b/src/app/api/me/teams/route.ts
index 2ea6575e..555bf300 100644
--- a/src/app/api/me/teams/route.ts
+++ b/src/app/api/me/teams/route.ts
@@ -1,8 +1,8 @@
import { z } from 'zod';
-import { pagingParams } from '@/lib/schema';
-import { getUserTeams } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
import { json } from '@/lib/response';
-import { parseRequest } from '@/lib/request';
+import { pagingParams } from '@/lib/schema';
+import { getUserTeams } from '@/queries/prisma';
export async function GET(request: Request) {
const schema = z.object({
@@ -15,7 +15,9 @@ export async function GET(request: Request) {
return error();
}
- const teams = await getUserTeams(auth.user.id, query);
+ const filters = await getQueryFilters(query);
+
+ const teams = await getUserTeams(auth.user.id, filters);
return json(teams);
}
diff --git a/src/app/api/me/websites/route.ts b/src/app/api/me/websites/route.ts
index a8df856a..9ec39c78 100644
--- a/src/app/api/me/websites/route.ts
+++ b/src/app/api/me/websites/route.ts
@@ -1,12 +1,13 @@
import { z } from 'zod';
-import { pagingParams } from '@/lib/schema';
-import { getUserWebsites } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
import { json } from '@/lib/response';
-import { parseRequest } from '@/lib/request';
+import { pagingParams } from '@/lib/schema';
+import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma';
export async function GET(request: Request) {
const schema = z.object({
...pagingParams,
+ includeTeams: z.string().optional(),
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -15,7 +16,11 @@ export async function GET(request: Request) {
return error();
}
- const websites = await getUserWebsites(auth.user.id, query);
+ const filters = await getQueryFilters(query);
- return json(websites);
+ if (query.includeTeams) {
+ return json(await getAllUserWebsitesIncludingTeamOwner(auth.user.id, filters));
+ }
+
+ return json(await getUserWebsites(auth.user.id, filters));
}
diff --git a/src/app/api/pixels/[pixelId]/route.ts b/src/app/api/pixels/[pixelId]/route.ts
new file mode 100644
index 00000000..ecaf1fdf
--- /dev/null
+++ b/src/app/api/pixels/[pixelId]/route.ts
@@ -0,0 +1,76 @@
+import { z } from 'zod';
+import { parseRequest } from '@/lib/request';
+import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
+import { canDeletePixel, canUpdatePixel, canViewPixel } from '@/permissions';
+import { deletePixel, getPixel, updatePixel } from '@/queries/prisma';
+
+export async function GET(request: Request, { params }: { params: Promise<{ pixelId: string }> }) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { pixelId } = await params;
+
+ if (!(await canViewPixel(auth, pixelId))) {
+ return unauthorized();
+ }
+
+ const pixel = await getPixel(pixelId);
+
+ return json(pixel);
+}
+
+export async function POST(request: Request, { params }: { params: Promise<{ pixelId: string }> }) {
+ const schema = z.object({
+ name: z.string().optional(),
+ slug: z.string().min(8).optional(),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { pixelId } = await params;
+ const { name, slug } = body;
+
+ if (!(await canUpdatePixel(auth, pixelId))) {
+ return unauthorized();
+ }
+
+ try {
+ const pixel = await updatePixel(pixelId, { name, slug });
+
+ return Response.json(pixel);
+ } catch (e: any) {
+ if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('slug')) {
+ return badRequest({ message: 'That slug is already taken.' });
+ }
+
+ return serverError(e);
+ }
+}
+
+export async function DELETE(
+ request: Request,
+ { params }: { params: Promise<{ pixelId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { pixelId } = await params;
+
+ if (!(await canDeletePixel(auth, pixelId))) {
+ return unauthorized();
+ }
+
+ await deletePixel(pixelId);
+
+ return ok();
+}
diff --git a/src/app/api/pixels/route.ts b/src/app/api/pixels/route.ts
new file mode 100644
index 00000000..8baae4f3
--- /dev/null
+++ b/src/app/api/pixels/route.ts
@@ -0,0 +1,62 @@
+import { z } from 'zod';
+import { uuid } from '@/lib/crypto';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams, searchParams } from '@/lib/schema';
+import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions';
+import { createPixel, getUserPixels } from '@/queries/prisma';
+
+export async function GET(request: Request) {
+ const schema = z.object({
+ ...pagingParams,
+ ...searchParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const filters = await getQueryFilters(query);
+
+ const links = await getUserPixels(auth.user.id, filters);
+
+ return json(links);
+}
+
+export async function POST(request: Request) {
+ const schema = z.object({
+ name: z.string().max(100),
+ slug: z.string().max(100),
+ teamId: z.string().nullable().optional(),
+ id: z.uuid().nullable().optional(),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { id, name, slug, teamId } = body;
+
+ if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
+ return unauthorized();
+ }
+
+ const data: any = {
+ id: id ?? uuid(),
+ name,
+ slug,
+ teamId,
+ };
+
+ if (!teamId) {
+ data.userId = auth.user.id;
+ }
+
+ const result = await createPixel(data);
+
+ return json(result);
+}
diff --git a/src/app/api/realtime/[websiteId]/route.ts b/src/app/api/realtime/[websiteId]/route.ts
index 7f9c1a9a..32b7a16c 100644
--- a/src/app/api/realtime/[websiteId]/route.ts
+++ b/src/app/api/realtime/[websiteId]/route.ts
@@ -1,9 +1,9 @@
-import { json, unauthorized } from '@/lib/response';
-import { getRealtimeData } from '@/queries';
-import { canViewWebsite } from '@/lib/auth';
import { startOfMinute, subMinutes } from 'date-fns';
import { REALTIME_RANGE } from '@/lib/constants';
-import { parseRequest } from '@/lib/request';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getRealtimeData } from '@/queries/sql';
export async function GET(
request: Request,
@@ -16,15 +16,21 @@ export async function GET(
}
const { websiteId } = await params;
- const { timezone } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
+ const filters = await getQueryFilters(
+ {
+ ...query,
+ startAt: subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(),
+ endAt: Date.now(),
+ },
+ websiteId,
+ );
- const data = await getRealtimeData(websiteId, { startDate, timezone });
+ const data = await getRealtimeData(websiteId, filters);
return json(data);
}
diff --git a/src/app/api/reports/[reportId]/route.ts b/src/app/api/reports/[reportId]/route.ts
index ba90ee08..1f22c62f 100644
--- a/src/app/api/reports/[reportId]/route.ts
+++ b/src/app/api/reports/[reportId]/route.ts
@@ -1,9 +1,8 @@
-import { z } from 'zod';
import { parseRequest } from '@/lib/request';
-import { deleteReport, getReport, updateReport } from '@/queries';
-import { canDeleteReport, canUpdateReport, canViewReport } from '@/lib/auth';
-import { unauthorized, json, notFound, ok } from '@/lib/response';
-import { reportTypeParam } from '@/lib/schema';
+import { json, notFound, ok, unauthorized } from '@/lib/response';
+import { reportSchema } from '@/lib/schema';
+import { canDeleteWebsite, canUpdateWebsite, canViewReport } from '@/permissions';
+import { deleteReport, getReport, updateReport } from '@/queries/prisma';
export async function GET(request: Request, { params }: { params: Promise<{ reportId: string }> }) {
const { auth, error } = await parseRequest(request);
@@ -20,8 +19,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ repo
return unauthorized();
}
- report.parameters = JSON.parse(report.parameters);
-
return json(report);
}
@@ -29,15 +26,7 @@ export async function POST(
request: Request,
{ params }: { params: Promise<{ reportId: string }> },
) {
- const schema = z.object({
- websiteId: z.string().uuid(),
- type: reportTypeParam,
- name: z.string().max(200),
- description: z.string().max(500),
- parameters: z.object({}).passthrough(),
- });
-
- const { auth, body, error } = await parseRequest(request, schema);
+ const { auth, body, error } = await parseRequest(request, reportSchema);
if (error) {
return error();
@@ -52,7 +41,7 @@ export async function POST(
return notFound();
}
- if (!(await canUpdateReport(auth, report))) {
+ if (!(await canUpdateWebsite(auth, websiteId))) {
return unauthorized();
}
@@ -62,7 +51,7 @@ export async function POST(
type,
name,
description,
- parameters: JSON.stringify(parameters),
+ parameters,
} as any);
return json(result);
@@ -81,7 +70,7 @@ export async function DELETE(
const { reportId } = await params;
const report = await getReport(reportId);
- if (!(await canDeleteReport(auth, report))) {
+ if (!(await canDeleteWebsite(auth, report.websiteId))) {
return unauthorized();
}
diff --git a/src/app/api/reports/attribution/route.ts b/src/app/api/reports/attribution/route.ts
index a1f7992d..bd7d86dc 100644
--- a/src/app/api/reports/attribution/route.ts
+++ b/src/app/api/reports/attribution/route.ts
@@ -1,50 +1,26 @@
-import { canViewWebsite } from '@/lib/auth';
-import { parseRequest } from '@/lib/request';
+import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
-import { reportParms } from '@/lib/schema';
-import { getAttribution } from '@/queries/sql/reports/getAttribution';
-import { z } from 'zod';
+import { reportResultSchema } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { type AttributionParameters, getAttribution } from '@/queries/sql/reports/getAttribution';
export async function POST(request: Request) {
- const schema = z.object({
- ...reportParms,
- model: z.string().regex(/firstClick|lastClick/i),
- steps: z
- .array(
- z.object({
- type: z.string(),
- value: z.string(),
- }),
- )
- .min(1),
- currency: z.string().optional(),
- });
-
- const { auth, body, error } = await parseRequest(request, schema);
+ const { auth, body, error } = await parseRequest(request, reportResultSchema);
if (error) {
return error();
}
- const {
- websiteId,
- model,
- steps,
- currency,
- dateRange: { startDate, endDate },
- } = body;
+ const { websiteId } = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const data = await getAttribution(websiteId, {
- startDate: new Date(startDate),
- endDate: new Date(endDate),
- model: model,
- steps,
- currency,
- });
+ const parameters = await setWebsiteDate(websiteId, body.parameters);
+ const filters = await getQueryFilters(body.filters, websiteId);
+
+ const data = await getAttribution(websiteId, parameters as AttributionParameters, filters);
return json(data);
}
diff --git a/src/app/api/reports/breakdown/route.ts b/src/app/api/reports/breakdown/route.ts
new file mode 100644
index 00000000..3c593145
--- /dev/null
+++ b/src/app/api/reports/breakdown/route.ts
@@ -0,0 +1,26 @@
+import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { reportResultSchema } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { type BreakdownParameters, getBreakdown } from '@/queries/sql';
+
+export async function POST(request: Request) {
+ const { auth, body, error } = await parseRequest(request, reportResultSchema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = body;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const parameters = await setWebsiteDate(websiteId, body.parameters);
+ const filters = await getQueryFilters(body.filters, websiteId);
+
+ const data = await getBreakdown(websiteId, parameters as BreakdownParameters, filters);
+
+ return json(data);
+}
diff --git a/src/app/api/reports/funnel/route.ts b/src/app/api/reports/funnel/route.ts
index 6033c633..c13f6f1c 100644
--- a/src/app/api/reports/funnel/route.ts
+++ b/src/app/api/reports/funnel/route.ts
@@ -1,47 +1,26 @@
-import { z } from 'zod';
-import { canViewWebsite } from '@/lib/auth';
-import { unauthorized, json } from '@/lib/response';
-import { parseRequest } from '@/lib/request';
-import { getFunnel } from '@/queries';
-import { reportParms } from '@/lib/schema';
+import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { reportResultSchema } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { type FunnelParameters, getFunnel } from '@/queries/sql';
export async function POST(request: Request) {
- const schema = z.object({
- ...reportParms,
- window: z.coerce.number().positive(),
- steps: z
- .array(
- z.object({
- type: z.string(),
- value: z.string(),
- }),
- )
- .min(2),
- });
-
- const { auth, body, error } = await parseRequest(request, schema);
+ const { auth, body, error } = await parseRequest(request, reportResultSchema);
if (error) {
return error();
}
- const {
- websiteId,
- steps,
- window,
- dateRange: { startDate, endDate },
- } = body;
+ const { websiteId } = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const data = await getFunnel(websiteId, {
- startDate: new Date(startDate),
- endDate: new Date(endDate),
- steps,
- windowMinutes: +window,
- });
+ const parameters = await setWebsiteDate(websiteId, body.parameters);
+ const filters = await getQueryFilters(body.filters, websiteId);
+
+ const data = await getFunnel(websiteId, parameters as FunnelParameters, filters);
return json(data);
}
diff --git a/src/app/api/reports/goal/route.ts b/src/app/api/reports/goal/route.ts
new file mode 100644
index 00000000..3bd0415d
--- /dev/null
+++ b/src/app/api/reports/goal/route.ts
@@ -0,0 +1,26 @@
+import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { reportResultSchema } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { type GoalParameters, getGoal } from '@/queries/sql/reports/getGoal';
+
+export async function POST(request: Request) {
+ const { auth, body, error } = await parseRequest(request, reportResultSchema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = body;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const parameters = await setWebsiteDate(websiteId, body.parameters);
+ const filters = await getQueryFilters(body.filters, websiteId);
+
+ const data = await getGoal(websiteId, parameters as GoalParameters, filters);
+
+ return json(data);
+}
diff --git a/src/app/api/reports/goals/route.ts b/src/app/api/reports/goals/route.ts
deleted file mode 100644
index 5a2f6bd0..00000000
--- a/src/app/api/reports/goals/route.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { z } from 'zod';
-import { canViewWebsite } from '@/lib/auth';
-import { unauthorized, json } from '@/lib/response';
-import { parseRequest } from '@/lib/request';
-import { getGoals } from '@/queries/sql/reports/getGoals';
-import { reportParms } from '@/lib/schema';
-
-export async function POST(request: Request) {
- const schema = z.object({
- ...reportParms,
- goals: z
- .array(
- z
- .object({
- type: z.string().regex(/url|event|event-data/),
- value: z.string(),
- goal: z.coerce.number(),
- operator: z
- .string()
- .regex(/count|sum|average/)
- .optional(),
- property: z.string().optional(),
- })
- .refine(data => {
- if (data['type'] === 'event-data') {
- return data['operator'] && data['property'];
- }
- return true;
- }),
- )
- .min(1),
- });
-
- const { auth, body, error } = await parseRequest(request, schema);
-
- if (error) {
- return error();
- }
-
- const {
- websiteId,
- dateRange: { startDate, endDate },
- goals,
- } = body;
-
- if (!(await canViewWebsite(auth, websiteId))) {
- return unauthorized();
- }
-
- const data = await getGoals(websiteId, {
- startDate: new Date(startDate),
- endDate: new Date(endDate),
- goals,
- });
-
- return json(data);
-}
diff --git a/src/app/api/reports/insights/route.ts b/src/app/api/reports/insights/route.ts
deleted file mode 100644
index b3569cba..00000000
--- a/src/app/api/reports/insights/route.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { z } from 'zod';
-import { canViewWebsite } from '@/lib/auth';
-import { unauthorized, json } from '@/lib/response';
-import { parseRequest } from '@/lib/request';
-import { getInsights } from '@/queries';
-import { reportParms } from '@/lib/schema';
-
-function convertFilters(filters: any[]) {
- return filters.reduce((obj, filter) => {
- obj[filter.name] = filter;
-
- return obj;
- }, {});
-}
-
-export async function POST(request: Request) {
- const schema = z.object({
- ...reportParms,
- fields: z
- .array(
- z.object({
- name: z.string(),
- type: z.string(),
- label: z.string(),
- }),
- )
- .min(1),
- filters: z.array(
- z.object({
- name: z.string(),
- type: z.string(),
- operator: z.string(),
- value: z.string(),
- }),
- ),
- });
-
- const { auth, body, error } = await parseRequest(request, schema);
-
- if (error) {
- return error();
- }
-
- const {
- websiteId,
- dateRange: { startDate, endDate },
- fields,
- filters,
- } = body;
-
- if (!(await canViewWebsite(auth, websiteId))) {
- return unauthorized();
- }
-
- const data = await getInsights(websiteId, fields, {
- ...convertFilters(filters),
- startDate: new Date(startDate),
- endDate: new Date(endDate),
- });
-
- return json(data);
-}
diff --git a/src/app/api/reports/journey/route.ts b/src/app/api/reports/journey/route.ts
index 19ad98fa..29e85319 100644
--- a/src/app/api/reports/journey/route.ts
+++ b/src/app/api/reports/journey/route.ts
@@ -1,43 +1,25 @@
-import { z } from 'zod';
-import { canViewWebsite } from '@/lib/auth';
-import { unauthorized, json } from '@/lib/response';
-import { parseRequest } from '@/lib/request';
-import { getJourney } from '@/queries';
-import { reportParms } from '@/lib/schema';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { reportResultSchema } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getJourney } from '@/queries/sql';
export async function POST(request: Request) {
- const schema = z.object({
- ...reportParms,
- steps: z.coerce.number().min(3).max(7),
- startStep: z.string().optional(),
- endStep: z.string().optional(),
- });
-
- const { auth, body, error } = await parseRequest(request, schema);
+ const { auth, body, error } = await parseRequest(request, reportResultSchema);
if (error) {
return error();
}
- const {
- websiteId,
- dateRange: { startDate, endDate },
- steps,
- startStep,
- endStep,
- } = body;
+ const { websiteId, parameters, filters } = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const data = await getJourney(websiteId, {
- startDate: new Date(startDate),
- endDate: new Date(endDate),
- steps,
- startStep,
- endStep,
- });
+ const queryFilters = await getQueryFilters(filters, websiteId);
+
+ const data = await getJourney(websiteId, parameters, queryFilters);
return json(data);
}
diff --git a/src/app/api/reports/retention/route.ts b/src/app/api/reports/retention/route.ts
index 83220bb4..d1a7d698 100644
--- a/src/app/api/reports/retention/route.ts
+++ b/src/app/api/reports/retention/route.ts
@@ -1,37 +1,26 @@
-import { z } from 'zod';
-import { canViewWebsite } from '@/lib/auth';
-import { unauthorized, json } from '@/lib/response';
-import { parseRequest } from '@/lib/request';
-import { getRetention } from '@/queries';
-import { reportParms, timezoneParam } from '@/lib/schema';
+import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { reportResultSchema } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getRetention, type RetentionParameters } from '@/queries/sql';
export async function POST(request: Request) {
- const schema = z.object({
- ...reportParms,
- timezone: timezoneParam,
- });
-
- const { auth, body, error } = await parseRequest(request, schema);
+ const { auth, body, error } = await parseRequest(request, reportResultSchema);
if (error) {
return error();
}
- const {
- websiteId,
- dateRange: { startDate, endDate },
- timezone,
- } = body;
+ const { websiteId } = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const data = await getRetention(websiteId, {
- startDate: new Date(startDate),
- endDate: new Date(endDate),
- timezone,
- });
+ const filters = await getQueryFilters(body.filters, websiteId);
+ const parameters = await setWebsiteDate(websiteId, body.parameters);
+
+ const data = await getRetention(websiteId, parameters as RetentionParameters, filters);
return json(data);
}
diff --git a/src/app/api/reports/revenue/route.ts b/src/app/api/reports/revenue/route.ts
index 13a34f38..6a556612 100644
--- a/src/app/api/reports/revenue/route.ts
+++ b/src/app/api/reports/revenue/route.ts
@@ -1,63 +1,26 @@
-import { z } from 'zod';
-import { canViewWebsite } from '@/lib/auth';
-import { unauthorized, json } from '@/lib/response';
-import { parseRequest } from '@/lib/request';
-import { reportParms, timezoneParam } from '@/lib/schema';
-import { getRevenue } from '@/queries/sql/reports/getRevenue';
-import { getRevenueValues } from '@/queries/sql/reports/getRevenueValues';
-
-export async function GET(request: Request) {
- const { auth, query, error } = await parseRequest(request);
-
- if (error) {
- return error();
- }
-
- const { websiteId, startDate, endDate } = query;
-
- if (!(await canViewWebsite(auth, websiteId))) {
- return unauthorized();
- }
-
- const data = await getRevenueValues(websiteId, {
- startDate: new Date(startDate),
- endDate: new Date(endDate),
- });
-
- return json(data);
-}
+import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { reportResultSchema } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getRevenue, type RevenuParameters } from '@/queries/sql/reports/getRevenue';
export async function POST(request: Request) {
- const schema = z.object({
- currency: z.string(),
- ...reportParms,
- timezone: timezoneParam,
- });
-
- const { auth, body, error } = await parseRequest(request, schema);
+ const { auth, body, error } = await parseRequest(request, reportResultSchema);
if (error) {
return error();
}
- const {
- websiteId,
- currency,
- timezone,
- dateRange: { startDate, endDate, unit },
- } = body;
+ const { websiteId } = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const data = await getRevenue(websiteId, {
- startDate: new Date(startDate),
- endDate: new Date(endDate),
- unit,
- timezone,
- currency,
- });
+ const parameters = await setWebsiteDate(websiteId, body.parameters);
+ const filters = await getQueryFilters(body.filters, websiteId);
+
+ const data = await getRevenue(websiteId, parameters as RevenuParameters, filters);
return json(data);
}
diff --git a/src/app/api/reports/route.ts b/src/app/api/reports/route.ts
index e50c57bc..b0a41354 100644
--- a/src/app/api/reports/route.ts
+++ b/src/app/api/reports/route.ts
@@ -1,15 +1,15 @@
import { z } from 'zod';
import { uuid } from '@/lib/crypto';
-import { pagingParams, reportTypeParam } from '@/lib/schema';
import { parseRequest } from '@/lib/request';
-import { canViewTeam, canViewWebsite, canUpdateWebsite } from '@/lib/auth';
-import { unauthorized, json } from '@/lib/response';
-import { getReports, createReport } from '@/queries';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams, reportSchema, reportTypeParam } from '@/lib/schema';
+import { canUpdateWebsite, canViewWebsite } from '@/permissions';
+import { createReport, getReports } from '@/queries/prisma';
export async function GET(request: Request) {
const schema = z.object({
- websiteId: z.string().uuid().optional(),
- teamId: z.string().uuid().optional(),
+ websiteId: z.uuid(),
+ type: reportTypeParam.optional(),
...pagingParams,
});
@@ -19,53 +19,24 @@ export async function GET(request: Request) {
return error();
}
- const { page, search, pageSize, websiteId, teamId } = query;
- const userId = auth.user.id;
+ const { page, search, pageSize, websiteId, type } = query;
const filters = {
page,
pageSize,
search,
};
- if (
- (websiteId && !(await canViewWebsite(auth, websiteId))) ||
- (teamId && !(await canViewTeam(auth, teamId)))
- ) {
+ if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getReports(
{
where: {
- OR: [
- ...(websiteId ? [{ websiteId }] : []),
- ...(teamId
- ? [
- {
- website: {
- deletedAt: null,
- teamId,
- },
- },
- ]
- : []),
- ...(userId && !websiteId && !teamId
- ? [
- {
- website: {
- deletedAt: null,
- userId,
- },
- },
- ]
- : []),
- ],
- },
- include: {
+ websiteId,
+ type,
website: {
- select: {
- domain: true,
- },
+ deletedAt: null,
},
},
},
@@ -76,15 +47,7 @@ export async function GET(request: Request) {
}
export async function POST(request: Request) {
- const schema = z.object({
- websiteId: z.string().uuid(),
- name: z.string().max(200),
- type: reportTypeParam,
- description: z.string().max(500),
- parameters: z.object({}).passthrough(),
- });
-
- const { auth, body, error } = await parseRequest(request, schema);
+ const { auth, body, error } = await parseRequest(request, reportSchema);
if (error) {
return error();
@@ -102,9 +65,9 @@ export async function POST(request: Request) {
websiteId,
type,
name,
- description,
- parameters: JSON.stringify(parameters),
- } as any);
+ description: description || '',
+ parameters,
+ });
return json(result);
}
diff --git a/src/app/api/reports/utm/route.ts b/src/app/api/reports/utm/route.ts
index 38e88a6d..577fdab7 100644
--- a/src/app/api/reports/utm/route.ts
+++ b/src/app/api/reports/utm/route.ts
@@ -1,35 +1,37 @@
-import { z } from 'zod';
-import { canViewWebsite } from '@/lib/auth';
-import { unauthorized, json } from '@/lib/response';
-import { parseRequest } from '@/lib/request';
-import { getUTM } from '@/queries';
-import { reportParms } from '@/lib/schema';
+import { UTM_PARAMS } from '@/lib/constants';
+import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { reportResultSchema } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getUTM, type UTMParameters } from '@/queries/sql';
export async function POST(request: Request) {
- const schema = z.object({
- ...reportParms,
- });
-
- const { auth, body, error } = await parseRequest(request, schema);
+ const { auth, body, error } = await parseRequest(request, reportResultSchema);
if (error) {
return error();
}
- const {
- websiteId,
- dateRange: { startDate, endDate, timezone },
- } = body;
+ const { websiteId } = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const data = await getUTM(websiteId, {
- startDate: new Date(startDate),
- endDate: new Date(endDate),
- timezone,
- });
+ const filters = await getQueryFilters(body.filters, websiteId);
+ const parameters = await setWebsiteDate(websiteId, body.parameters);
+
+ const data = {
+ utm_source: [],
+ utm_medium: [],
+ utm_campaign: [],
+ utm_term: [],
+ utm_content: [],
+ };
+
+ for (const key of UTM_PARAMS) {
+ data[key] = await getUTM(websiteId, { column: key, ...parameters } as UTMParameters, filters);
+ }
return json(data);
}
diff --git a/src/app/api/scripts/telemetry/route.ts b/src/app/api/scripts/telemetry/route.ts
index 54cee565..b19e99f1 100644
--- a/src/app/api/scripts/telemetry/route.ts
+++ b/src/app/api/scripts/telemetry/route.ts
@@ -2,25 +2,25 @@ import { CURRENT_VERSION, TELEMETRY_PIXEL } from '@/lib/constants';
export async function GET() {
if (
- process.env.NODE_ENV !== 'production' &&
- process.env.DISABLE_TELEMETRY &&
+ process.env.NODE_ENV !== 'production' ||
+ process.env.DISABLE_TELEMETRY ||
process.env.PRIVATE_MODE
) {
- const script = `
- (()=>{const i=document.createElement('img');
- i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}');
- i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;');
- document.body.appendChild(i);})();
- `;
-
- return new Response(script.replace(/\s\s+/g, ''), {
+ return new Response('/* telemetry disabled */', {
headers: {
'content-type': 'text/javascript',
},
});
}
- return new Response('/* telemetry disabled */', {
+ const script = `
+ (()=>{const i=document.createElement('img');
+ i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}');
+ i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;');
+ document.body.appendChild(i);})();
+ `;
+
+ return new Response(script.replace(/\s\s+/g, ''), {
headers: {
'content-type': 'text/javascript',
},
diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts
index b35a67b7..a0becc2a 100644
--- a/src/app/api/send/route.ts
+++ b/src/app/api/send/route.ts
@@ -1,36 +1,61 @@
-import { z } from 'zod';
-import { isbot } from 'isbot';
import { startOfHour, startOfMonth } from 'date-fns';
+import { isbot } from 'isbot';
+import { serializeError } from 'serialize-error';
+import { z } from 'zod';
import clickhouse from '@/lib/clickhouse';
-import { parseRequest } from '@/lib/request';
-import { badRequest, json, forbidden, serverError } from '@/lib/response';
-import { fetchSession, fetchWebsite } from '@/lib/load';
+import { COLLECTION_TYPE, EVENT_TYPE } from '@/lib/constants';
+import { hash, secret, uuid } from '@/lib/crypto';
import { getClientInfo, hasBlockedIp } from '@/lib/detect';
import { createToken, parseToken } from '@/lib/jwt';
-import { secret, uuid, hash } from '@/lib/crypto';
-import { COLLECTION_TYPE } from '@/lib/constants';
+import { fetchWebsite } from '@/lib/load';
+import { parseRequest } from '@/lib/request';
+import { badRequest, forbidden, json, serverError } from '@/lib/response';
import { anyObjectParam, urlOrPathParam } from '@/lib/schema';
import { safeDecodeURI, safeDecodeURIComponent } from '@/lib/url';
-import { createSession, saveEvent, saveSessionData } from '@/queries';
+import { createSession, saveEvent, saveSessionData } from '@/queries/sql';
+
+interface Cache {
+ websiteId: string;
+ sessionId: string;
+ visitId: string;
+ iat: number;
+}
const schema = z.object({
type: z.enum(['event', 'identify']),
- payload: z.object({
- website: z.string().uuid(),
- data: anyObjectParam.optional(),
- hostname: z.string().max(100).optional(),
- language: z.string().max(35).optional(),
- referrer: urlOrPathParam.optional(),
- screen: z.string().max(11).optional(),
- title: z.string().optional(),
- url: urlOrPathParam.optional(),
- name: z.string().max(50).optional(),
- tag: z.string().max(50).optional(),
- ip: z.string().ip().optional(),
- userAgent: z.string().optional(),
- timestamp: z.coerce.number().int().optional(),
- id: z.string().optional(),
- }),
+ payload: z
+ .object({
+ website: z.uuid().optional(),
+ link: z.uuid().optional(),
+ pixel: z.uuid().optional(),
+ data: anyObjectParam.optional(),
+ hostname: z.string().max(100).optional(),
+ language: z.string().max(35).optional(),
+ referrer: urlOrPathParam.optional(),
+ screen: z.string().max(11).optional(),
+ title: z.string().optional(),
+ url: urlOrPathParam.optional(),
+ name: z.string().max(50).optional(),
+ tag: z.string().max(50).optional(),
+ ip: z.string().optional(),
+ userAgent: z.string().optional(),
+ timestamp: z.coerce.number().int().optional(),
+ id: z.string().optional(),
+ browser: z.string().optional(),
+ os: z.string().optional(),
+ device: z.string().optional(),
+ })
+ .refine(
+ data => {
+ const keys = [data.website, data.link, data.pixel];
+ const count = keys.filter(Boolean).length;
+ return count === 1;
+ },
+ {
+ message: 'Exactly one of website, link, or pixel must be provided',
+ path: ['website'],
+ },
+ ),
});
export async function POST(request: Request) {
@@ -45,6 +70,8 @@ export async function POST(request: Request) {
const {
website: websiteId,
+ pixel: pixelId,
+ link: linkId,
hostname,
screen,
language,
@@ -58,24 +85,29 @@ export async function POST(request: Request) {
id,
} = payload;
+ const sourceId = websiteId || pixelId || linkId;
+
// Cache check
- let cache: { websiteId: string; sessionId: string; visitId: string; iat: number } | null = null;
- const cacheHeader = request.headers.get('x-umami-cache');
+ let cache: Cache | null = null;
- if (cacheHeader) {
- const result = await parseToken(cacheHeader, secret());
+ if (websiteId) {
+ const cacheHeader = request.headers.get('x-umami-cache');
- if (result) {
- cache = result;
+ if (cacheHeader) {
+ const result = await parseToken(cacheHeader, secret());
+
+ if (result) {
+ cache = result;
+ }
}
- }
- // Find website
- if (!cache?.websiteId) {
- const website = await fetchWebsite(websiteId);
+ // Find website
+ if (!cache?.websiteId) {
+ const website = await fetchWebsite(websiteId);
- if (!website) {
- return badRequest('Website not found.');
+ if (!website) {
+ return badRequest({ message: 'Website not found.' });
+ }
}
}
@@ -96,39 +128,29 @@ export async function POST(request: Request) {
}
const createdAt = timestamp ? new Date(timestamp * 1000) : new Date();
- const now = Math.floor(new Date().getTime() / 1000);
+ const now = Math.floor(Date.now() / 1000);
const sessionSalt = hash(startOfMonth(createdAt).toUTCString());
const visitSalt = hash(startOfHour(createdAt).toUTCString());
- const sessionId = id ? uuid(websiteId, id) : uuid(websiteId, ip, userAgent, sessionSalt);
+ const sessionId = id ? uuid(sourceId, id) : uuid(sourceId, ip, userAgent, sessionSalt);
- // Find session
+ // Create a session if not found
if (!clickhouse.enabled && !cache?.sessionId) {
- const session = await fetchSession(websiteId, sessionId);
-
- // Create a session if not found
- if (!session) {
- try {
- await createSession({
- id: sessionId,
- websiteId,
- browser,
- os,
- device,
- screen,
- language,
- country,
- region,
- city,
- distinctId: id,
- });
- } catch (e: any) {
- if (!e.message.toLowerCase().includes('unique constraint')) {
- return serverError(e);
- }
- }
- }
+ await createSession({
+ id: sessionId,
+ websiteId: sourceId,
+ browser,
+ os,
+ device,
+ screen,
+ language,
+ country,
+ region,
+ city,
+ distinctId: id,
+ createdAt,
+ });
}
// Visit info
@@ -145,7 +167,8 @@ export async function POST(request: Request) {
const base = hostname ? `https://${hostname}` : 'https://localhost';
const currentUrl = new URL(url, base);
- let urlPath = currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname;
+ let urlPath =
+ currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname + currentUrl.hash;
const urlQuery = currentUrl.search.substring(1);
const urlDomain = currentUrl.hostname.replace(/^www./, '');
@@ -169,7 +192,7 @@ export async function POST(request: Request) {
const twclid = currentUrl.searchParams.get('twclid');
if (process.env.REMOVE_TRAILING_SLASH) {
- urlPath = urlPath.replace(/(.+)\/$/, '$1');
+ urlPath = urlPath.replace(/\/(?=(#.*)?$)/, '');
}
if (referrer) {
@@ -177,36 +200,35 @@ export async function POST(request: Request) {
referrerPath = referrerUrl.pathname;
referrerQuery = referrerUrl.search.substring(1);
-
- if (referrerUrl.hostname !== 'localhost') {
- referrerDomain = referrerUrl.hostname.replace(/^www\./, '');
- }
+ referrerDomain = referrerUrl.hostname.replace(/^www\./, '');
}
+ const eventType = linkId
+ ? EVENT_TYPE.linkEvent
+ : pixelId
+ ? EVENT_TYPE.pixelEvent
+ : name
+ ? EVENT_TYPE.customEvent
+ : EVENT_TYPE.pageView;
+
await saveEvent({
- websiteId,
+ websiteId: sourceId,
sessionId,
visitId,
+ eventType,
+ createdAt,
+
+ // Page
+ pageTitle: safeDecodeURIComponent(title),
+ hostname: hostname || urlDomain,
urlPath: safeDecodeURI(urlPath),
urlQuery,
- utmSource,
- utmMedium,
- utmCampaign,
- utmContent,
- utmTerm,
referrerPath: safeDecodeURI(referrerPath),
referrerQuery,
referrerDomain,
- pageTitle: safeDecodeURIComponent(title),
- gclid,
- fbclid,
- msclkid,
- ttclid,
- lifatid,
- twclid,
- eventName: name,
- eventData: data,
- hostname: hostname || urlDomain,
+
+ // Session
+ distinctId: id,
browser,
os,
device,
@@ -215,30 +237,48 @@ export async function POST(request: Request) {
country,
region,
city,
+
+ // Events
+ eventName: name,
+ eventData: data,
tag,
- distinctId: id,
- createdAt,
- });
- }
- if (type === COLLECTION_TYPE.identify) {
- if (!data) {
- return badRequest('Data required.');
+ // UTM
+ utmSource,
+ utmMedium,
+ utmCampaign,
+ utmContent,
+ utmTerm,
+
+ // Click IDs
+ gclid,
+ fbclid,
+ msclkid,
+ ttclid,
+ lifatid,
+ twclid,
+ });
+ } else if (type === COLLECTION_TYPE.identify) {
+ if (data) {
+ await saveSessionData({
+ websiteId,
+ sessionId,
+ sessionData: data,
+ distinctId: id,
+ createdAt,
+ });
}
-
- await saveSessionData({
- websiteId,
- sessionId,
- sessionData: data,
- distinctId: id,
- createdAt,
- });
}
const token = createToken({ websiteId, sessionId, visitId, iat }, secret());
return json({ cache: token, sessionId, visitId });
} catch (e) {
- return serverError(e);
+ const error = serializeError(e);
+
+ // eslint-disable-next-line no-console
+ console.log(error);
+
+ return serverError({ errorObject: error });
}
}
diff --git a/src/app/api/share/[shareId]/route.ts b/src/app/api/share/[shareId]/route.ts
index e387938d..bef87c4f 100644
--- a/src/app/api/share/[shareId]/route.ts
+++ b/src/app/api/share/[shareId]/route.ts
@@ -1,9 +1,9 @@
-import { json, notFound } from '@/lib/response';
-import { createToken } from '@/lib/jwt';
import { secret } from '@/lib/crypto';
-import { getSharedWebsite } from '@/queries';
+import { createToken } from '@/lib/jwt';
+import { json, notFound } from '@/lib/response';
+import { getSharedWebsite } from '@/queries/prisma';
-export async function GET(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
+export async function GET(_request: Request, { params }: { params: Promise<{ shareId: string }> }) {
const { shareId } = await params;
const website = await getSharedWebsite(shareId);
diff --git a/src/app/api/teams/[teamId]/links/route.ts b/src/app/api/teams/[teamId]/links/route.ts
new file mode 100644
index 00000000..41e139b3
--- /dev/null
+++ b/src/app/api/teams/[teamId]/links/route.ts
@@ -0,0 +1,29 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams, searchParams } from '@/lib/schema';
+import { canViewTeam } from '@/permissions';
+import { getTeamLinks } from '@/queries/prisma';
+
+export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) {
+ const schema = z.object({
+ ...pagingParams,
+ ...searchParams,
+ });
+ const { teamId } = await params;
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ if (!(await canViewTeam(auth, teamId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query);
+
+ const links = await getTeamLinks(teamId, filters);
+
+ return json(links);
+}
diff --git a/src/app/api/teams/[teamId]/pixels/route.ts b/src/app/api/teams/[teamId]/pixels/route.ts
new file mode 100644
index 00000000..daac2040
--- /dev/null
+++ b/src/app/api/teams/[teamId]/pixels/route.ts
@@ -0,0 +1,29 @@
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams, searchParams } from '@/lib/schema';
+import { canViewTeam } from '@/permissions';
+import { getTeamPixels } from '@/queries/prisma';
+
+export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) {
+ const schema = z.object({
+ ...pagingParams,
+ ...searchParams,
+ });
+ const { teamId } = await params;
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ if (!(await canViewTeam(auth, teamId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query);
+
+ const websites = await getTeamPixels(teamId, filters);
+
+ return json(websites);
+}
diff --git a/src/app/api/teams/[teamId]/route.ts b/src/app/api/teams/[teamId]/route.ts
index 194e7bbb..c334b2af 100644
--- a/src/app/api/teams/[teamId]/route.ts
+++ b/src/app/api/teams/[teamId]/route.ts
@@ -1,8 +1,8 @@
import { z } from 'zod';
-import { unauthorized, json, notFound, ok } from '@/lib/response';
-import { canDeleteTeam, canUpdateTeam, canViewTeam } from '@/lib/auth';
import { parseRequest } from '@/lib/request';
-import { deleteTeam, getTeam, updateTeam } from '@/queries';
+import { json, notFound, ok, unauthorized } from '@/lib/response';
+import { canDeleteTeam, canUpdateTeam, canViewTeam } from '@/permissions';
+import { deleteTeam, getTeam, updateTeam } from '@/queries/prisma';
export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) {
const { auth, error } = await parseRequest(request);
@@ -20,7 +20,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
const team = await getTeam(teamId, { includeMembers: true });
if (!team) {
- return notFound('Team not found.');
+ return notFound({ message: 'Team not found.' });
}
return json(team);
@@ -41,7 +41,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ tea
const { teamId } = await params;
if (!(await canUpdateTeam(auth, teamId))) {
- return unauthorized('You must be the owner of this team.');
+ return unauthorized({ message: 'You must be the owner/manager of this team.' });
}
const team = await updateTeam(teamId, body);
@@ -62,7 +62,7 @@ export async function DELETE(
const { teamId } = await params;
if (!(await canDeleteTeam(auth, teamId))) {
- return unauthorized('You must be the owner of this team.');
+ return unauthorized({ message: 'You must be the owner/manager of this team.' });
}
await deleteTeam(teamId);
diff --git a/src/app/api/teams/[teamId]/users/[userId]/route.ts b/src/app/api/teams/[teamId]/users/[userId]/route.ts
index bf5f4d36..d09af9da 100644
--- a/src/app/api/teams/[teamId]/users/[userId]/route.ts
+++ b/src/app/api/teams/[teamId]/users/[userId]/route.ts
@@ -1,8 +1,9 @@
-import { canDeleteTeamUser, canUpdateTeam } from '@/lib/auth';
+import { z } from 'zod';
import { parseRequest } from '@/lib/request';
import { badRequest, json, ok, unauthorized } from '@/lib/response';
-import { deleteTeamUser, getTeamUser, updateTeamUser } from '@/queries';
-import { z } from 'zod';
+import { teamRoleParam } from '@/lib/schema';
+import { canDeleteTeamUser, canUpdateTeam } from '@/permissions';
+import { deleteTeamUser, getTeamUser, updateTeamUser } from '@/queries/prisma';
export async function GET(
request: Request,
@@ -17,7 +18,7 @@ export async function GET(
const { teamId, userId } = await params;
if (!(await canUpdateTeam(auth, teamId))) {
- return unauthorized('You must be the owner of this team.');
+ return unauthorized({ message: 'You must be the owner/manager of this team.' });
}
const teamUser = await getTeamUser(teamId, userId);
@@ -30,7 +31,7 @@ export async function POST(
{ params }: { params: Promise<{ teamId: string; userId: string }> },
) {
const schema = z.object({
- role: z.string().regex(/team-member|team-view-only|team-manager/),
+ role: teamRoleParam,
});
const { auth, body, error } = await parseRequest(request, schema);
@@ -42,13 +43,13 @@ export async function POST(
const { teamId, userId } = await params;
if (!(await canUpdateTeam(auth, teamId))) {
- return unauthorized('You must be the owner of this team.');
+ return unauthorized({ message: 'You must be the owner/manager of this team.' });
}
const teamUser = await getTeamUser(teamId, userId);
if (!teamUser) {
- return badRequest('The User does not exists on this team.');
+ return badRequest({ message: 'The User does not exists on this team.' });
}
const user = await updateTeamUser(teamUser.id, body);
@@ -69,13 +70,13 @@ export async function DELETE(
const { teamId, userId } = await params;
if (!(await canDeleteTeamUser(auth, teamId, userId))) {
- return unauthorized('You must be the owner of this team.');
+ return unauthorized({ message: 'You must be the owner/manager of this team.' });
}
const teamUser = await getTeamUser(teamId, userId);
if (!teamUser) {
- return badRequest('The User does not exists on this team.');
+ return badRequest({ message: 'The User does not exists on this team.' });
}
await deleteTeamUser(teamId, userId);
diff --git a/src/app/api/teams/[teamId]/users/route.ts b/src/app/api/teams/[teamId]/users/route.ts
index 57460b89..c1297636 100644
--- a/src/app/api/teams/[teamId]/users/route.ts
+++ b/src/app/api/teams/[teamId]/users/route.ts
@@ -1,13 +1,14 @@
import { z } from 'zod';
-import { unauthorized, json, badRequest } from '@/lib/response';
-import { canAddUserToTeam, canViewTeam } from '@/lib/auth';
-import { parseRequest } from '@/lib/request';
-import { pagingParams, roleParam } from '@/lib/schema';
-import { createTeamUser, getTeamUser, getTeamUsers } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { badRequest, json, unauthorized } from '@/lib/response';
+import { pagingParams, searchParams, teamRoleParam } from '@/lib/schema';
+import { canUpdateTeam, canViewTeam } from '@/permissions';
+import { createTeamUser, getTeamUser, getTeamUsers } from '@/queries/prisma';
export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) {
const schema = z.object({
...pagingParams,
+ ...searchParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -19,9 +20,11 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
const { teamId } = await params;
if (!(await canViewTeam(auth, teamId))) {
- return unauthorized('You must be the owner of this team.');
+ return unauthorized({ message: 'You must be a member of this team.' });
}
+ const filters = await getQueryFilters(query);
+
const users = await getTeamUsers(
{
where: {
@@ -38,8 +41,11 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
},
},
},
+ orderBy: {
+ createdAt: 'asc',
+ },
},
- query,
+ filters,
);
return json(users);
@@ -47,8 +53,8 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
export async function POST(request: Request, { params }: { params: Promise<{ teamId: string }> }) {
const schema = z.object({
- userId: z.string().uuid(),
- role: roleParam,
+ userId: z.uuid(),
+ role: teamRoleParam,
});
const { auth, body, error } = await parseRequest(request, schema);
@@ -59,8 +65,8 @@ export async function POST(request: Request, { params }: { params: Promise<{ tea
const { teamId } = await params;
- if (!(await canAddUserToTeam(auth))) {
- return unauthorized();
+ if (!(await canUpdateTeam(auth, teamId))) {
+ return unauthorized({ message: 'You must be the owner/manager of this team.' });
}
const { userId, role } = body;
@@ -68,7 +74,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ tea
const teamUser = await getTeamUser(teamId, userId);
if (teamUser) {
- return badRequest('User is already a member of the Team.');
+ return badRequest({ message: 'User is already a member of the Team.' });
}
const users = await createTeamUser(userId, teamId, role);
diff --git a/src/app/api/teams/[teamId]/websites/route.ts b/src/app/api/teams/[teamId]/websites/route.ts
index f69ab465..05c6d804 100644
--- a/src/app/api/teams/[teamId]/websites/route.ts
+++ b/src/app/api/teams/[teamId]/websites/route.ts
@@ -1,13 +1,14 @@
import { z } from 'zod';
-import { unauthorized, json } from '@/lib/response';
-import { canViewTeam } from '@/lib/auth';
-import { parseRequest } from '@/lib/request';
-import { pagingParams } from '@/lib/schema';
-import { getTeamWebsites } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams, searchParams } from '@/lib/schema';
+import { canViewTeam } from '@/permissions';
+import { getTeamWebsites } from '@/queries/prisma';
export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) {
const schema = z.object({
...pagingParams,
+ ...searchParams,
});
const { teamId } = await params;
const { auth, query, error } = await parseRequest(request, schema);
@@ -20,7 +21,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
return unauthorized();
}
- const websites = await getTeamWebsites(teamId, query);
+ const filters = await getQueryFilters(query);
+
+ const websites = await getTeamWebsites(teamId, filters);
return json(websites);
}
diff --git a/src/app/api/teams/join/route.ts b/src/app/api/teams/join/route.ts
index 3464054c..3ce0913f 100644
--- a/src/app/api/teams/join/route.ts
+++ b/src/app/api/teams/join/route.ts
@@ -1,9 +1,8 @@
import { z } from 'zod';
-import { unauthorized, json, badRequest, notFound } from '@/lib/response';
-import { canCreateTeam } from '@/lib/auth';
-import { parseRequest } from '@/lib/request';
import { ROLES } from '@/lib/constants';
-import { createTeamUser, findTeam, getTeamUser } from '@/queries';
+import { parseRequest } from '@/lib/request';
+import { badRequest, json, notFound } from '@/lib/response';
+import { createTeamUser, findTeam, getTeamUser } from '@/queries/prisma';
export async function POST(request: Request) {
const schema = z.object({
@@ -16,10 +15,6 @@ export async function POST(request: Request) {
return error();
}
- if (!(await canCreateTeam(auth))) {
- return unauthorized();
- }
-
const { accessCode } = body;
const team = await findTeam({
@@ -29,13 +24,13 @@ export async function POST(request: Request) {
});
if (!team) {
- return notFound('Team not found.');
+ return notFound({ message: 'Team not found.', code: 'team-not-found' });
}
const teamUser = await getTeamUser(team.id, auth.user.id);
if (teamUser) {
- return badRequest('User is already a team member.');
+ return badRequest({ message: 'User is already a team member.' });
}
const user = await createTeamUser(auth.user.id, team.id, ROLES.teamMember);
diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts
index d319d87b..53ef5923 100644
--- a/src/app/api/teams/route.ts
+++ b/src/app/api/teams/route.ts
@@ -1,10 +1,29 @@
import { z } from 'zod';
-import { getRandomChars } from '@/lib/crypto';
-import { unauthorized, json } from '@/lib/response';
-import { canCreateTeam } from '@/lib/auth';
import { uuid } from '@/lib/crypto';
-import { parseRequest } from '@/lib/request';
-import { createTeam } from '@/queries';
+import { getRandomChars } from '@/lib/generate';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams } from '@/lib/schema';
+import { canCreateTeam } from '@/permissions';
+import { createTeam, getUserTeams } from '@/queries/prisma';
+
+export async function GET(request: Request) {
+ const schema = z.object({
+ ...pagingParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const filters = await getQueryFilters(query);
+
+ const teams = await getUserTeams(auth.user.id, filters);
+
+ return json(teams);
+}
export async function POST(request: Request) {
const schema = z.object({
diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts
index d011783c..aade8aa8 100644
--- a/src/app/api/users/[userId]/route.ts
+++ b/src/app/api/users/[userId]/route.ts
@@ -1,8 +1,10 @@
-import { canDeleteUser, canUpdateUser, canViewUser, hashPassword } from '@/lib/auth';
+import { z } from 'zod';
+import { hashPassword } from '@/lib/password';
import { parseRequest } from '@/lib/request';
import { badRequest, json, ok, unauthorized } from '@/lib/response';
-import { deleteUser, getUser, getUserByUsername, updateUser } from '@/queries';
-import { z } from 'zod';
+import { userRoleParam } from '@/lib/schema';
+import { canDeleteUser, canUpdateUser, canViewUser } from '@/permissions';
+import { deleteUser, getUser, getUserByUsername, updateUser } from '@/queries/prisma';
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const { auth, error } = await parseRequest(request);
@@ -24,12 +26,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const schema = z.object({
- username: z.string().max(255),
+ username: z.string().max(255).optional(),
password: z.string().max(255).optional(),
- role: z
- .string()
- .regex(/admin|user|view-only/i)
- .optional(),
+ role: userRoleParam.optional(),
});
const { auth, body, error } = await parseRequest(request, schema);
@@ -68,7 +67,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ use
const user = await getUserByUsername(username);
if (user) {
- return badRequest('User already exists');
+ return badRequest({ message: 'User already exists' });
}
}
@@ -94,7 +93,7 @@ export async function DELETE(
}
if (userId === auth.user.id) {
- return badRequest('You cannot delete yourself.');
+ return badRequest({ message: 'You cannot delete yourself.' });
}
await deleteUser(userId);
diff --git a/src/app/api/users/[userId]/teams/route.ts b/src/app/api/users/[userId]/teams/route.ts
index ff659525..7a834a3f 100644
--- a/src/app/api/users/[userId]/teams/route.ts
+++ b/src/app/api/users/[userId]/teams/route.ts
@@ -1,8 +1,8 @@
import { z } from 'zod';
-import { pagingParams } from '@/lib/schema';
-import { getUserTeams } from '@/queries';
-import { unauthorized, json } from '@/lib/response';
import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams } from '@/lib/schema';
+import { getUserTeams } from '@/queries/prisma';
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const schema = z.object({
diff --git a/src/app/api/users/[userId]/usage/route.ts b/src/app/api/users/[userId]/usage/route.ts
deleted file mode 100644
index e6ff217d..00000000
--- a/src/app/api/users/[userId]/usage/route.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { z } from 'zod';
-import { json, unauthorized } from '@/lib/response';
-import { getAllUserWebsitesIncludingTeamOwner } from '@/queries/prisma/website';
-import { getEventUsage } from '@/queries/sql/events/getEventUsage';
-import { getEventDataUsage } from '@/queries/sql/events/getEventDataUsage';
-import { parseRequest } from '@/lib/request';
-
-export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
- const schema = z.object({
- startAt: z.coerce.number().int(),
- endAt: z.coerce.number().int(),
- });
-
- const { auth, query, error } = await parseRequest(request, schema);
-
- if (error) {
- return error();
- }
-
- if (!auth.user.isAdmin) {
- return unauthorized();
- }
-
- const { userId } = await params;
- const { startAt, endAt } = query;
-
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
-
- const websites = await getAllUserWebsitesIncludingTeamOwner(userId);
-
- const websiteIds = websites.map(a => a.id);
-
- const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate);
- const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate);
-
- const websiteUsage = websites.map(a => ({
- websiteId: a.id,
- websiteName: a.name,
- websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0,
- eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0,
- deletedAt: a.deletedAt,
- }));
-
- const usage = websiteUsage.reduce(
- (acc, cv) => {
- acc.websiteEventUsage += cv.websiteEventUsage;
- acc.eventDataUsage += cv.eventDataUsage;
-
- return acc;
- },
- { websiteEventUsage: 0, eventDataUsage: 0 },
- );
-
- const filteredWebsiteUsage = websiteUsage.filter(
- a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0),
- );
-
- return json({
- ...usage,
- websites: filteredWebsiteUsage,
- });
-}
diff --git a/src/app/api/users/[userId]/websites/route.ts b/src/app/api/users/[userId]/websites/route.ts
index 77d41084..1107d8e1 100644
--- a/src/app/api/users/[userId]/websites/route.ts
+++ b/src/app/api/users/[userId]/websites/route.ts
@@ -1,12 +1,14 @@
import { z } from 'zod';
-import { unauthorized, json } from '@/lib/response';
-import { getUserWebsites } from '@/queries/prisma/website';
-import { pagingParams } from '@/lib/schema';
-import { parseRequest } from '@/lib/request';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams, searchParams } from '@/lib/schema';
+import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma/website';
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const schema = z.object({
...pagingParams,
+ ...searchParams,
+ includeTeams: z.string().optional(),
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -21,7 +23,11 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
return unauthorized();
}
- const websites = await getUserWebsites(userId, query);
+ const filters = await getQueryFilters(query);
- return json(websites);
+ if (query.includeTeams) {
+ return json(await getAllUserWebsitesIncludingTeamOwner(userId, filters));
+ }
+
+ return json(await getUserWebsites(userId, filters));
}
diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts
index c5896f89..dbb114cf 100644
--- a/src/app/api/users/route.ts
+++ b/src/app/api/users/route.ts
@@ -1,14 +1,15 @@
import { z } from 'zod';
-import { hashPassword, canCreateUser } from '@/lib/auth';
import { ROLES } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
+import { hashPassword } from '@/lib/password';
import { parseRequest } from '@/lib/request';
-import { unauthorized, json, badRequest } from '@/lib/response';
-import { createUser, getUserByUsername } from '@/queries';
+import { badRequest, json, unauthorized } from '@/lib/response';
+import { canCreateUser } from '@/permissions';
+import { createUser, getUserByUsername } from '@/queries/prisma';
export async function POST(request: Request) {
const schema = z.object({
- id: z.string().uuid().optional(),
+ id: z.uuid().optional(),
username: z.string().max(255),
password: z.string(),
role: z.string().regex(/admin|user|view-only/i),
@@ -29,7 +30,7 @@ export async function POST(request: Request) {
const existingUser = await getUserByUsername(username, { showDeleted: true });
if (existingUser) {
- return badRequest('User already exists');
+ return badRequest({ message: 'User already exists' });
}
const user = await createUser({
diff --git a/src/app/api/websites/[websiteId]/active/route.ts b/src/app/api/websites/[websiteId]/active/route.ts
index 88c0fd17..233b97e5 100644
--- a/src/app/api/websites/[websiteId]/active/route.ts
+++ b/src/app/api/websites/[websiteId]/active/route.ts
@@ -1,7 +1,7 @@
-import { canViewWebsite } from '@/lib/auth';
-import { json, unauthorized } from '@/lib/response';
-import { getActiveVisitors } from '@/queries';
import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getActiveVisitors } from '@/queries/sql';
export async function GET(
request: Request,
@@ -19,7 +19,7 @@ export async function GET(
return unauthorized();
}
- const result = await getActiveVisitors(websiteId);
+ const visitors = await getActiveVisitors(websiteId);
- return json(result);
+ return json(visitors);
}
diff --git a/src/app/api/websites/[websiteId]/daterange/route.ts b/src/app/api/websites/[websiteId]/daterange/route.ts
index ea2d10d2..14a241fd 100644
--- a/src/app/api/websites/[websiteId]/daterange/route.ts
+++ b/src/app/api/websites/[websiteId]/daterange/route.ts
@@ -1,7 +1,7 @@
-import { canViewWebsite } from '@/lib/auth';
-import { getWebsiteDateRange } from '@/queries';
-import { json, unauthorized } from '@/lib/response';
import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteDateRange } from '@/queries/sql';
export async function GET(
request: Request,
@@ -19,7 +19,7 @@ export async function GET(
return unauthorized();
}
- const result = await getWebsiteDateRange(websiteId);
+ const dateRange = await getWebsiteDateRange(websiteId);
- return json(result);
+ return json(dateRange);
}
diff --git a/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts b/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts
new file mode 100644
index 00000000..54afab21
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts
@@ -0,0 +1,25 @@
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getEventData } from '@/queries/sql/events/getEventData';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string; eventId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, eventId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const data = await getEventData(websiteId, eventId);
+
+ return json(data);
+}
diff --git a/src/app/api/websites/[websiteId]/event-data/events/route.ts b/src/app/api/websites/[websiteId]/event-data/events/route.ts
index aec7b471..eb6ee6ed 100644
--- a/src/app/api/websites/[websiteId]/event-data/events/route.ts
+++ b/src/app/api/websites/[websiteId]/event-data/events/route.ts
@@ -1,7 +1,8 @@
import { z } from 'zod';
-import { parseRequest } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
import { getEventDataEvents } from '@/queries/sql/events/getEventDataEvents';
export async function GET(
@@ -12,6 +13,7 @@ export async function GET(
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
event: z.string().optional(),
+ ...filterParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -20,19 +22,15 @@ export async function GET(
}
const { websiteId } = await params;
- const { startAt, endAt, event } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
+ const filters = await getQueryFilters(query, websiteId);
const data = await getEventDataEvents(websiteId, {
- startDate,
- endDate,
- event,
+ ...filters,
});
return json(data);
diff --git a/src/app/api/websites/[websiteId]/event-data/fields/route.ts b/src/app/api/websites/[websiteId]/event-data/fields/route.ts
index 60101e45..bce6a977 100644
--- a/src/app/api/websites/[websiteId]/event-data/fields/route.ts
+++ b/src/app/api/websites/[websiteId]/event-data/fields/route.ts
@@ -1,8 +1,9 @@
import { z } from 'zod';
-import { parseRequest } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
-import { getEventDataFields } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventDataFields } from '@/queries/sql';
export async function GET(
request: Request,
@@ -11,6 +12,7 @@ export async function GET(
const schema = z.object({
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
+ ...filterParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -20,19 +22,14 @@ export async function GET(
}
const { websiteId } = await params;
- const { startAt, endAt } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
+ const filters = await getQueryFilters(query, websiteId);
- const data = await getEventDataFields(websiteId, {
- startDate,
- endDate,
- });
+ const data = await getEventDataFields(websiteId, filters);
return json(data);
}
diff --git a/src/app/api/websites/[websiteId]/event-data/properties/route.ts b/src/app/api/websites/[websiteId]/event-data/properties/route.ts
index fe085f74..52d15cfb 100644
--- a/src/app/api/websites/[websiteId]/event-data/properties/route.ts
+++ b/src/app/api/websites/[websiteId]/event-data/properties/route.ts
@@ -1,8 +1,9 @@
import { z } from 'zod';
-import { parseRequest } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
-import { getEventDataProperties } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventDataProperties } from '@/queries/sql';
export async function GET(
request: Request,
@@ -11,7 +12,7 @@ export async function GET(
const schema = z.object({
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
- propertyName: z.string().optional(),
+ ...filterParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -21,16 +22,14 @@ export async function GET(
}
const { websiteId } = await params;
- const { startAt, endAt, propertyName } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
+ const filters = await getQueryFilters(query, websiteId);
- const data = await getEventDataProperties(websiteId, { startDate, endDate, propertyName });
+ const data = await getEventDataProperties(websiteId, filters);
return json(data);
}
diff --git a/src/app/api/websites/[websiteId]/event-data/stats/route.ts b/src/app/api/websites/[websiteId]/event-data/stats/route.ts
index 6928aa1e..042e989a 100644
--- a/src/app/api/websites/[websiteId]/event-data/stats/route.ts
+++ b/src/app/api/websites/[websiteId]/event-data/stats/route.ts
@@ -1,8 +1,9 @@
import { z } from 'zod';
-import { parseRequest } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
-import { getEventDataStats } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventDataStats } from '@/queries/sql';
export async function GET(
request: Request,
@@ -11,7 +12,7 @@ export async function GET(
const schema = z.object({
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
- propertyName: z.string().optional(),
+ ...filterParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -21,16 +22,14 @@ export async function GET(
}
const { websiteId } = await params;
- const { startAt, endAt } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
+ const filters = await getQueryFilters(query, websiteId);
- const data = await getEventDataStats(websiteId, { startDate, endDate });
+ const data = await getEventDataStats(websiteId, filters);
return json(data);
}
diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts
index 2a912439..12e8f2dc 100644
--- a/src/app/api/websites/[websiteId]/event-data/values/route.ts
+++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts
@@ -1,8 +1,9 @@
import { z } from 'zod';
-import { parseRequest } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
-import { getEventDataValues } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventDataValues } from '@/queries/sql';
export async function GET(
request: Request,
@@ -11,8 +12,9 @@ export async function GET(
const schema = z.object({
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
- eventName: z.string().optional(),
- propertyName: z.string().optional(),
+ event: z.string(),
+ propertyName: z.string(),
+ ...filterParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -22,19 +24,16 @@ export async function GET(
}
const { websiteId } = await params;
- const { startAt, endAt, eventName, propertyName } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
+ const { propertyName } = query;
+ const filters = await getQueryFilters(query, websiteId);
const data = await getEventDataValues(websiteId, {
- startDate,
- endDate,
- eventName,
+ ...filters,
propertyName,
});
diff --git a/src/app/api/websites/[websiteId]/events/route.ts b/src/app/api/websites/[websiteId]/events/route.ts
index 66eaba2c..74ec3ece 100644
--- a/src/app/api/websites/[websiteId]/events/route.ts
+++ b/src/app/api/websites/[websiteId]/events/route.ts
@@ -1,18 +1,20 @@
import { z } from 'zod';
-import { parseRequest } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
-import { pagingParams } from '@/lib/schema';
-import { getWebsiteEvents } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams, pagingParams, searchParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteEvents } from '@/queries/sql';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
- startAt: z.coerce.number().int(),
- endAt: z.coerce.number().int(),
+ startAt: z.coerce.number().optional(),
+ endAt: z.coerce.number().optional(),
+ ...filterParams,
...pagingParams,
+ ...searchParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -22,16 +24,14 @@ export async function GET(
}
const { websiteId } = await params;
- const { startAt, endAt } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
+ const filters = await getQueryFilters(query, websiteId);
- const data = await getWebsiteEvents(websiteId, { startDate, endDate }, query);
+ const data = await getWebsiteEvents(websiteId, filters);
return json(data);
}
diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts
index da4b0d4f..977e9c81 100644
--- a/src/app/api/websites/[websiteId]/events/series/route.ts
+++ b/src/app/api/websites/[websiteId]/events/series/route.ts
@@ -1,9 +1,9 @@
import { z } from 'zod';
-import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
import { filterParams, timezoneParam, unitParam } from '@/lib/schema';
-import { getEventMetrics } from '@/queries';
+import { canViewWebsite } from '@/permissions';
+import { getEventStats } from '@/queries/sql';
export async function GET(
request: Request,
@@ -12,7 +12,7 @@ export async function GET(
const schema = z.object({
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
- unit: unitParam,
+ unit: unitParam.optional(),
timezone: timezoneParam,
...filterParams,
});
@@ -24,22 +24,14 @@ export async function GET(
}
const { websiteId } = await params;
- const { timezone } = query;
- const { startDate, endDate, unit } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const filters = {
- ...getRequestFilters(query),
- startDate,
- endDate,
- timezone,
- unit,
- };
+ const filters = await getQueryFilters(query, websiteId);
- const data = await getEventMetrics(websiteId, filters);
+ const data = await getEventStats(websiteId, filters);
return json(data);
}
diff --git a/src/app/api/websites/[websiteId]/export/route.ts b/src/app/api/websites/[websiteId]/export/route.ts
new file mode 100644
index 00000000..eec81c6d
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/export/route.ts
@@ -0,0 +1,64 @@
+import JSZip from 'jszip';
+import Papa from 'papaparse';
+import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { dateRangeParams, pagingParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getEventMetrics, getPageviewMetrics, getSessionMetrics } from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ ...dateRangeParams,
+ ...pagingParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query, websiteId);
+
+ const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([
+ getEventMetrics(websiteId, { type: 'event' }, filters),
+ getPageviewMetrics(websiteId, { type: 'path' }, filters),
+ getPageviewMetrics(websiteId, { type: 'referrer' }, filters),
+ getSessionMetrics(websiteId, { type: 'browser' }, filters),
+ getSessionMetrics(websiteId, { type: 'os' }, filters),
+ getSessionMetrics(websiteId, { type: 'device' }, filters),
+ getSessionMetrics(websiteId, { type: 'country' }, filters),
+ ]);
+
+ const zip = new JSZip();
+
+ const parse = (data: any) => {
+ return Papa.unparse(data, {
+ header: true,
+ skipEmptyLines: true,
+ });
+ };
+
+ zip.file('events.csv', parse(events));
+ zip.file('pages.csv', parse(pages));
+ zip.file('referrers.csv', parse(referrers));
+ zip.file('browsers.csv', parse(browsers));
+ zip.file('os.csv', parse(os));
+ zip.file('devices.csv', parse(devices));
+ zip.file('countries.csv', parse(countries));
+
+ const content = await zip.generateAsync({ type: 'nodebuffer' });
+ const base64 = content.toString('base64');
+
+ return json({ zip: base64 });
+}
diff --git a/src/app/api/websites/[websiteId]/metrics/expanded/route.ts b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts
new file mode 100644
index 00000000..d52c1773
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts
@@ -0,0 +1,66 @@
+import { z } from 'zod';
+import { EVENT_COLUMNS, EVENT_TYPE, SESSION_COLUMNS } from '@/lib/constants';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { badRequest, json, unauthorized } from '@/lib/response';
+import { dateRangeParams, filterParams, searchParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import {
+ getChannelExpandedMetrics,
+ getEventExpandedMetrics,
+ getPageviewExpandedMetrics,
+ getSessionExpandedMetrics,
+} from '@/queries/sql';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ type: z.string(),
+ limit: z.coerce.number().optional(),
+ offset: z.coerce.number().optional(),
+ ...dateRangeParams,
+ ...searchParams,
+ ...filterParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+
+ if (!(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const { type, limit, offset, search } = query;
+ const filters = await getQueryFilters(query, websiteId);
+
+ if (search) {
+ filters[type] = `c.${search}`;
+ }
+
+ if (SESSION_COLUMNS.includes(type)) {
+ const data = await getSessionExpandedMetrics(websiteId, { type, limit, offset }, filters);
+
+ return json(data);
+ }
+
+ if (EVENT_COLUMNS.includes(type)) {
+ if (type === 'event') {
+ filters.eventType = EVENT_TYPE.customEvent;
+ return json(await getEventExpandedMetrics(websiteId, { type, limit, offset }, filters));
+ } else {
+ return json(await getPageviewExpandedMetrics(websiteId, { type, limit, offset }, filters));
+ }
+ }
+
+ if (type === 'channel') {
+ return json(await getChannelExpandedMetrics(websiteId, filters));
+ }
+
+ return badRequest();
+}
diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts
index 85433904..12784adb 100644
--- a/src/app/api/websites/[websiteId]/metrics/route.ts
+++ b/src/app/api/websites/[websiteId]/metrics/route.ts
@@ -1,22 +1,15 @@
import { z } from 'zod';
-import thenby from 'thenby';
-import { canViewWebsite } from '@/lib/auth';
+import { EVENT_COLUMNS, EVENT_TYPE, SESSION_COLUMNS } from '@/lib/constants';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { badRequest, json, unauthorized } from '@/lib/response';
+import { dateRangeParams, filterParams, searchParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
import {
- SESSION_COLUMNS,
- EVENT_COLUMNS,
- FILTER_COLUMNS,
- OPERATORS,
- SEARCH_DOMAINS,
- SOCIAL_DOMAINS,
- EMAIL_DOMAINS,
- SHOPPING_DOMAINS,
- VIDEO_DOMAINS,
- PAID_AD_PARAMS,
-} from '@/lib/constants';
-import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request';
-import { json, unauthorized, badRequest } from '@/lib/response';
-import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries';
-import { filterParams } from '@/lib/schema';
+ getChannelMetrics,
+ getEventMetrics,
+ getPageviewMetrics,
+ getSessionMetrics,
+} from '@/queries/sql';
export async function GET(
request: Request,
@@ -24,11 +17,10 @@ export async function GET(
) {
const schema = z.object({
type: z.string(),
- startAt: z.coerce.number().int(),
- endAt: z.coerce.number().int(),
limit: z.coerce.number().optional(),
offset: z.coerce.number().optional(),
- search: z.string().optional(),
+ ...dateRangeParams,
+ ...searchParams,
...filterParams,
});
@@ -39,129 +31,36 @@ export async function GET(
}
const { websiteId } = await params;
- const { type, limit, offset, search } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const { startDate, endDate } = await getRequestDateRange(query);
- const column = FILTER_COLUMNS[type] || type;
- const filters = {
- ...getRequestFilters(query),
- startDate,
- endDate,
- };
+ const { type, limit, offset, search } = query;
+ const filters = await getQueryFilters(query, websiteId);
if (search) {
- filters[type] = {
- name: type,
- column,
- operator: OPERATORS.contains,
- value: search,
- };
+ filters[type] = `c.${search}`;
}
if (SESSION_COLUMNS.includes(type)) {
- const data = await getSessionMetrics(websiteId, type, filters, limit, offset);
-
- if (type === 'language') {
- const combined = {};
-
- for (const { x, y } of data) {
- const key = String(x).toLowerCase().split('-')[0];
-
- if (combined[key] === undefined) {
- combined[key] = { x: key, y };
- } else {
- combined[key].y += y;
- }
- }
-
- return json(Object.values(combined));
- }
+ const data = await getSessionMetrics(websiteId, { type, limit, offset }, filters);
return json(data);
}
if (EVENT_COLUMNS.includes(type)) {
- const data = await getPageviewMetrics(websiteId, type, filters, limit, offset);
-
- return json(data);
+ if (type === 'event') {
+ filters.eventType = EVENT_TYPE.customEvent;
+ return json(await getEventMetrics(websiteId, { type, limit, offset }, filters));
+ } else {
+ return json(await getPageviewMetrics(websiteId, { type, limit, offset }, filters));
+ }
}
if (type === 'channel') {
- const data = await getChannelMetrics(websiteId, filters);
-
- const channels = getChannels(data);
-
- return json(
- Object.keys(channels)
- .map(key => ({ x: key, y: channels[key] }))
- .sort(thenby.firstBy('y', -1)),
- );
+ return json(await getChannelMetrics(websiteId, filters));
}
return badRequest();
}
-
-function getChannels(data: { domain: string; query: string; visitors: number }[]) {
- const channels = {
- direct: 0,
- referral: 0,
- affiliate: 0,
- email: 0,
- sms: 0,
- organicSearch: 0,
- organicSocial: 0,
- organicShopping: 0,
- organicVideo: 0,
- paidAds: 0,
- paidSearch: 0,
- paidSocial: 0,
- paidShopping: 0,
- paidVideo: 0,
- };
-
- const match = (value: string) => {
- return (str: string | RegExp) => {
- return typeof str === 'string' ? value?.includes(str) : (str as RegExp).test(value);
- };
- };
-
- for (const { domain, query, visitors } of data) {
- if (!domain && !query) {
- channels.direct += Number(visitors);
- }
-
- const prefix = /utm_medium=(.*cp.*|ppc|retargeting|paid.*)/.test(query) ? 'paid' : 'organic';
-
- if (PAID_AD_PARAMS.some(match(query))) {
- channels.paidAds += Number(visitors);
- } else if (/utm_medium=(referral|app|link)/.test(query)) {
- channels.referral += Number(visitors);
- } else if (/utm_medium=affiliate/.test(query)) {
- channels.affiliate += Number(visitors);
- } else if (/utm_(source|medium)=sms/.test(query)) {
- channels.sms += Number(visitors);
- } else if (SEARCH_DOMAINS.some(match(domain)) || /utm_medium=organic/.test(query)) {
- channels[`${prefix}Search`] += Number(visitors);
- } else if (
- SOCIAL_DOMAINS.some(match(domain)) ||
- /utm_medium=(social|social-network|social-media|sm|social network|social media)/.test(query)
- ) {
- channels[`${prefix}Social`] += Number(visitors);
- } else if (EMAIL_DOMAINS.some(match(domain)) || /utm_medium=(.*e[-_ ]?mail.*)/.test(query)) {
- channels.email += Number(visitors);
- } else if (
- SHOPPING_DOMAINS.some(match(domain)) ||
- /utm_campaign=(.*(([^a-df-z]|^)shop|shopping).*)/.test(query)
- ) {
- channels[`${prefix}Shopping`] += Number(visitors);
- } else if (VIDEO_DOMAINS.some(match(domain)) || /utm_medium=(.*video.*)/.test(query)) {
- channels[`${prefix}Video`] += Number(visitors);
- }
- }
-
- return channels;
-}
diff --git a/src/app/api/websites/[websiteId]/pageviews/route.ts b/src/app/api/websites/[websiteId]/pageviews/route.ts
index e603ae9c..af59bce4 100644
--- a/src/app/api/websites/[websiteId]/pageviews/route.ts
+++ b/src/app/api/websites/[websiteId]/pageviews/route.ts
@@ -1,21 +1,17 @@
import { z } from 'zod';
-import { canViewWebsite } from '@/lib/auth';
-import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request';
-import { unitParam, timezoneParam, filterParams } from '@/lib/schema';
import { getCompareDate } from '@/lib/date';
-import { unauthorized, json } from '@/lib/response';
-import { getPageviewStats, getSessionStats } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { dateRangeParams, filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getPageviewStats, getSessionStats } from '@/queries/sql';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
- startAt: z.coerce.number().int(),
- endAt: z.coerce.number().int(),
- unit: unitParam,
- timezone: timezoneParam,
- compare: z.string().optional(),
+ ...dateRangeParams,
...filterParams,
});
@@ -26,32 +22,23 @@ export async function GET(
}
const { websiteId } = await params;
- const { timezone, compare } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const { startDate, endDate, unit } = await getRequestDateRange(query);
-
- const filters = {
- ...getRequestFilters(query),
- startDate,
- endDate,
- timezone,
- unit,
- };
+ const filters = await getQueryFilters(query, websiteId);
const [pageviews, sessions] = await Promise.all([
getPageviewStats(websiteId, filters),
getSessionStats(websiteId, filters),
]);
- if (compare) {
+ if (filters.compare) {
const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
- compare,
- startDate,
- endDate,
+ filters.compare,
+ filters.startDate,
+ filters.endDate,
);
const [comparePageviews, compareSessions] = await Promise.all([
@@ -70,8 +57,8 @@ export async function GET(
return json({
pageviews,
sessions,
- startDate,
- endDate,
+ startDate: filters.startDate,
+ endDate: filters.endDate,
compare: {
pageviews: comparePageviews,
sessions: compareSessions,
diff --git a/src/app/api/websites/[websiteId]/reports/route.ts b/src/app/api/websites/[websiteId]/reports/route.ts
index c6941f53..93e7ab46 100644
--- a/src/app/api/websites/[websiteId]/reports/route.ts
+++ b/src/app/api/websites/[websiteId]/reports/route.ts
@@ -1,15 +1,17 @@
import { z } from 'zod';
-import { canViewWebsite } from '@/lib/auth';
-import { getWebsiteReports } from '@/queries';
-import { pagingParams } from '@/lib/schema';
import { parseRequest } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams, pagingParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getReports } from '@/queries/prisma';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
+ filters: { type: string },
) {
const schema = z.object({
+ ...filterParams,
...pagingParams,
});
@@ -26,11 +28,19 @@ export async function GET(
return unauthorized();
}
- const data = await getWebsiteReports(websiteId, {
- page: +page,
- pageSize: +pageSize,
- search,
- });
+ const data = await getReports(
+ {
+ where: {
+ websiteId,
+ type: filters.type,
+ },
+ },
+ {
+ page,
+ pageSize,
+ search,
+ },
+ );
return json(data);
}
diff --git a/src/app/api/websites/[websiteId]/reset/route.ts b/src/app/api/websites/[websiteId]/reset/route.ts
index 62edceea..e0be5a53 100644
--- a/src/app/api/websites/[websiteId]/reset/route.ts
+++ b/src/app/api/websites/[websiteId]/reset/route.ts
@@ -1,7 +1,7 @@
-import { canUpdateWebsite } from '@/lib/auth';
-import { resetWebsite } from '@/queries';
-import { unauthorized, ok } from '@/lib/response';
import { parseRequest } from '@/lib/request';
+import { ok, unauthorized } from '@/lib/response';
+import { canUpdateWebsite } from '@/permissions';
+import { resetWebsite } from '@/queries/prisma';
export async function POST(
request: Request,
diff --git a/src/app/api/websites/[websiteId]/route.ts b/src/app/api/websites/[websiteId]/route.ts
index 346e5856..b4c0e7e8 100644
--- a/src/app/api/websites/[websiteId]/route.ts
+++ b/src/app/api/websites/[websiteId]/route.ts
@@ -1,9 +1,9 @@
import { z } from 'zod';
-import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from '@/lib/auth';
import { SHARE_ID_REGEX } from '@/lib/constants';
import { parseRequest } from '@/lib/request';
-import { ok, json, unauthorized, serverError } from '@/lib/response';
-import { deleteWebsite, getWebsite, updateWebsite } from '@/queries';
+import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
+import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions';
+import { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma';
export async function GET(
request: Request,
@@ -31,8 +31,8 @@ export async function POST(
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
- name: z.string(),
- domain: z.string(),
+ name: z.string().optional(),
+ domain: z.string().optional(),
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
});
@@ -54,8 +54,8 @@ export async function POST(
return Response.json(website);
} catch (e: any) {
- if (e.message.includes('Unique constraint') && e.message.includes('share_id')) {
- return serverError(new Error('That share ID is already taken.'));
+ if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('share_id')) {
+ return badRequest({ message: 'That share ID is already taken.' });
}
return serverError(e);
diff --git a/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts b/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts
new file mode 100644
index 00000000..b51f783b
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts
@@ -0,0 +1,92 @@
+import { z } from 'zod';
+import { parseRequest } from '@/lib/request';
+import { json, notFound, ok, unauthorized } from '@/lib/response';
+import { anyObjectParam, segmentTypeParam } from '@/lib/schema';
+import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions';
+import { deleteSegment, getSegment, updateSegment } from '@/queries/prisma';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string; segmentId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, segmentId } = await params;
+
+ const segment = await getSegment(segmentId);
+
+ if (websiteId && !(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ return json(segment);
+}
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string; segmentId: string }> },
+) {
+ const schema = z.object({
+ type: segmentTypeParam,
+ name: z.string().max(200),
+ parameters: anyObjectParam,
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, segmentId } = await params;
+ const { type, name, parameters } = body;
+
+ const segment = await getSegment(segmentId);
+
+ if (!segment) {
+ return notFound();
+ }
+
+ if (!(await canUpdateWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const result = await updateSegment(segmentId, {
+ type,
+ name,
+ parameters,
+ } as any);
+
+ return json(result);
+}
+
+export async function DELETE(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string; segmentId: string }> },
+) {
+ const { auth, error } = await parseRequest(request);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId, segmentId } = await params;
+
+ const segment = await getSegment(segmentId);
+
+ if (!segment) {
+ return notFound();
+ }
+
+ if (!(await canDeleteWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ await deleteSegment(segmentId);
+
+ return ok();
+}
diff --git a/src/app/api/websites/[websiteId]/segments/route.ts b/src/app/api/websites/[websiteId]/segments/route.ts
new file mode 100644
index 00000000..45927656
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/segments/route.ts
@@ -0,0 +1,70 @@
+import { z } from 'zod';
+import { uuid } from '@/lib/crypto';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { anyObjectParam, searchParams, segmentTypeParam } from '@/lib/schema';
+import { canUpdateWebsite, canViewWebsite } from '@/permissions';
+import { createSegment, getWebsiteSegments } from '@/queries/prisma';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ type: segmentTypeParam,
+ ...searchParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { type } = query;
+
+ if (websiteId && !(await canViewWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const filters = await getQueryFilters(query);
+
+ const segments = await getWebsiteSegments(websiteId, type, filters);
+
+ return json(segments);
+}
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ websiteId: string }> },
+) {
+ const schema = z.object({
+ type: segmentTypeParam,
+ name: z.string().max(200),
+ parameters: anyObjectParam,
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { websiteId } = await params;
+ const { type, name, parameters } = body;
+
+ if (!(await canUpdateWebsite(auth, websiteId))) {
+ return unauthorized();
+ }
+
+ const result = await createSegment({
+ id: uuid(),
+ websiteId,
+ type,
+ name,
+ parameters,
+ } as any);
+
+ return json(result);
+}
diff --git a/src/app/api/websites/[websiteId]/session-data/properties/route.ts b/src/app/api/websites/[websiteId]/session-data/properties/route.ts
index a6d9e2a4..2d8db153 100644
--- a/src/app/api/websites/[websiteId]/session-data/properties/route.ts
+++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts
@@ -1,8 +1,9 @@
import { z } from 'zod';
-import { parseRequest } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
-import { getSessionDataProperties } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getSessionDataProperties } from '@/queries/sql';
export async function GET(
request: Request,
@@ -11,7 +12,7 @@ export async function GET(
const schema = z.object({
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
- propertyName: z.string().optional(),
+ ...filterParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -20,17 +21,15 @@ export async function GET(
return error();
}
- const { startAt, endAt, propertyName } = query;
const { websiteId } = await params;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
+ const filters = await getQueryFilters(query, websiteId);
- const data = await getSessionDataProperties(websiteId, { startDate, endDate, propertyName });
+ const data = await getSessionDataProperties(websiteId, filters);
return json(data);
}
diff --git a/src/app/api/websites/[websiteId]/session-data/values/route.ts b/src/app/api/websites/[websiteId]/session-data/values/route.ts
index d950da34..7d06870a 100644
--- a/src/app/api/websites/[websiteId]/session-data/values/route.ts
+++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts
@@ -1,8 +1,9 @@
-import { canViewWebsite } from '@/lib/auth';
-import { parseRequest } from '@/lib/request';
-import { json, unauthorized } from '@/lib/response';
-import { getSessionDataValues } from '@/queries';
import { z } from 'zod';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getSessionDataValues } from '@/queries/sql';
export async function GET(
request: Request,
@@ -12,6 +13,7 @@ export async function GET(
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
propertyName: z.string().optional(),
+ ...filterParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -20,19 +22,17 @@ export async function GET(
return error();
}
- const { startAt, endAt, propertyName } = query;
const { websiteId } = await params;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
+ const { propertyName } = query;
+ const filters = await getQueryFilters(query, websiteId);
const data = await getSessionDataValues(websiteId, {
- startDate,
- endDate,
+ ...filters,
propertyName,
});
diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts
index aac40c38..41b766d0 100644
--- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts
+++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts
@@ -1,8 +1,8 @@
import { z } from 'zod';
-import { parseRequest } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
-import { getSessionActivity } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getSessionActivity } from '@/queries/sql';
export async function GET(
request: Request,
@@ -20,16 +20,14 @@ export async function GET(
}
const { websiteId, sessionId } = await params;
- const { startAt, endAt } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
+ const filters = await getQueryFilters(query, websiteId);
- const data = await getSessionActivity(websiteId, sessionId, startDate, endDate);
+ const data = await getSessionActivity(websiteId, sessionId, filters);
return json(data);
}
diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts
index 9c389c82..6b5c2418 100644
--- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts
+++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts
@@ -1,7 +1,7 @@
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
-import { getSessionData } from '@/queries';
import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getSessionData } from '@/queries/sql';
export async function GET(
request: Request,
diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts
index c4621ef4..10916637 100644
--- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts
+++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts
@@ -1,7 +1,7 @@
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
-import { getWebsiteSession } from '@/queries';
import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteSession } from '@/queries/sql';
export async function GET(
request: Request,
diff --git a/src/app/api/websites/[websiteId]/sessions/route.ts b/src/app/api/websites/[websiteId]/sessions/route.ts
index 5a14f00f..ed4757a1 100644
--- a/src/app/api/websites/[websiteId]/sessions/route.ts
+++ b/src/app/api/websites/[websiteId]/sessions/route.ts
@@ -1,18 +1,19 @@
import { z } from 'zod';
-import { parseRequest } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
-import { pagingParams } from '@/lib/schema';
-import { getWebsiteSessions } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { dateRangeParams, filterParams, pagingParams, searchParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteSessions } from '@/queries/sql';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
- startAt: z.coerce.number().int(),
- endAt: z.coerce.number().int(),
+ ...dateRangeParams,
+ ...filterParams,
...pagingParams,
+ ...searchParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -22,16 +23,14 @@ export async function GET(
}
const { websiteId } = await params;
- const { startAt, endAt } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
+ const filters = await getQueryFilters(query, websiteId);
- const data = await getWebsiteSessions(websiteId, { startDate, endDate }, query);
+ const data = await getWebsiteSessions(websiteId, filters);
return json(data);
}
diff --git a/src/app/api/websites/[websiteId]/sessions/stats/route.ts b/src/app/api/websites/[websiteId]/sessions/stats/route.ts
index e8e8e6c8..459830ed 100644
--- a/src/app/api/websites/[websiteId]/sessions/stats/route.ts
+++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts
@@ -1,9 +1,9 @@
import { z } from 'zod';
-import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
import { filterParams } from '@/lib/schema';
-import { getWebsiteSessionStats } from '@/queries';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteSessionStats } from '@/queries/sql';
export async function GET(
request: Request,
@@ -27,15 +27,9 @@ export async function GET(
return unauthorized();
}
- const { startDate, endDate } = await getRequestDateRange(query);
+ const filters = await getQueryFilters(query, websiteId);
- const filters = getRequestFilters(query);
-
- const metrics = await getWebsiteSessionStats(websiteId, {
- ...filters,
- startDate,
- endDate,
- });
+ const metrics = await getWebsiteSessionStats(websiteId, filters);
const data = Object.keys(metrics[0]).reduce((obj, key) => {
obj[key] = {
diff --git a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts
index 20be378d..b9ccf3ef 100644
--- a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts
+++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts
@@ -1,9 +1,9 @@
import { z } from 'zod';
-import { parseRequest } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
-import { pagingParams, timezoneParam } from '@/lib/schema';
-import { getWebsiteSessionsWeekly } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { filterParams, timezoneParam } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWeeklyTraffic } from '@/queries/sql';
export async function GET(
request: Request,
@@ -13,7 +13,7 @@ export async function GET(
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
timezone: timezoneParam,
- ...pagingParams,
+ ...filterParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -23,16 +23,14 @@ export async function GET(
}
const { websiteId } = await params;
- const { startAt, endAt, timezone } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
+ const filters = await getQueryFilters(query, websiteId);
- const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate, timezone });
+ const data = await getWeeklyTraffic(websiteId, filters);
return json(data);
}
diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts
index c146271f..07c8b969 100644
--- a/src/app/api/websites/[websiteId]/stats/route.ts
+++ b/src/app/api/websites/[websiteId]/stats/route.ts
@@ -1,19 +1,17 @@
import { z } from 'zod';
-import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request';
-import { unauthorized, json } from '@/lib/response';
-import { canViewWebsite } from '@/lib/auth';
import { getCompareDate } from '@/lib/date';
-import { filterParams } from '@/lib/schema';
-import { getWebsiteStats } from '@/queries';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { dateRangeParams, filterParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteStats } from '@/queries/sql';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
- startAt: z.coerce.number().int(),
- endAt: z.coerce.number().int(),
- compare: z.string().optional(),
+ ...dateRangeParams,
...filterParams,
});
@@ -24,40 +22,22 @@ export async function GET(
}
const { websiteId } = await params;
- const { compare } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- const { startDate, endDate } = await getRequestDateRange(query);
- const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
- compare,
- startDate,
- endDate,
- );
+ const filters = await getQueryFilters(query, websiteId);
- const filters = getRequestFilters(query);
+ const data = await getWebsiteStats(websiteId, filters);
- const metrics = await getWebsiteStats(websiteId, {
+ const { startDate, endDate } = getCompareDate('prev', filters.startDate, filters.endDate);
+
+ const comparison = await getWebsiteStats(websiteId, {
...filters,
startDate,
endDate,
});
- const prevPeriod = await getWebsiteStats(websiteId, {
- ...filters,
- startDate: compareStartDate,
- endDate: compareEndDate,
- });
-
- const stats = Object.keys(metrics[0]).reduce((obj, key) => {
- obj[key] = {
- value: Number(metrics[0][key]) || 0,
- prev: Number(prevPeriod[0][key]) || 0,
- };
- return obj;
- }, {});
-
- return json(stats);
+ return json({ ...data, comparison });
}
diff --git a/src/app/api/websites/[websiteId]/transfer/route.ts b/src/app/api/websites/[websiteId]/transfer/route.ts
index 03c0ae7f..df2fed20 100644
--- a/src/app/api/websites/[websiteId]/transfer/route.ts
+++ b/src/app/api/websites/[websiteId]/transfer/route.ts
@@ -1,16 +1,16 @@
import { z } from 'zod';
-import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from '@/lib/auth';
-import { updateWebsite } from '@/queries';
import { parseRequest } from '@/lib/request';
-import { badRequest, unauthorized, json } from '@/lib/response';
+import { badRequest, json, unauthorized } from '@/lib/response';
+import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from '@/permissions';
+import { updateWebsite } from '@/queries/prisma';
export async function POST(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
- userId: z.string().uuid().optional(),
- teamId: z.string().uuid().optional(),
+ userId: z.uuid().optional(),
+ teamId: z.uuid().optional(),
});
const { auth, body, error } = await parseRequest(request, schema);
diff --git a/src/app/api/websites/[websiteId]/values/route.ts b/src/app/api/websites/[websiteId]/values/route.ts
index ed3cfae6..172325e3 100644
--- a/src/app/api/websites/[websiteId]/values/route.ts
+++ b/src/app/api/websites/[websiteId]/values/route.ts
@@ -1,19 +1,20 @@
import { z } from 'zod';
-import { canViewWebsite } from '@/lib/auth';
-import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
-import { getValues } from '@/queries';
-import { parseRequest, getRequestDateRange } from '@/lib/request';
+import { EVENT_COLUMNS, FILTER_COLUMNS, SEGMENT_TYPES, SESSION_COLUMNS } from '@/lib/constants';
+import { getQueryFilters, parseRequest } from '@/lib/request';
import { badRequest, json, unauthorized } from '@/lib/response';
+import { dateRangeParams, fieldsParam, searchParams } from '@/lib/schema';
+import { canViewWebsite } from '@/permissions';
+import { getWebsiteSegments } from '@/queries/prisma';
+import { getValues } from '@/queries/sql';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
- type: z.string(),
- startAt: z.coerce.number().int(),
- endAt: z.coerce.number().int(),
- search: z.string().optional(),
+ type: fieldsParam,
+ ...dateRangeParams,
+ ...searchParams,
});
const { auth, query, error } = await parseRequest(request, schema);
@@ -23,18 +24,27 @@ export async function GET(
}
const { websiteId } = await params;
- const { type, search } = query;
- const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
- if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type)) {
- return badRequest('Invalid type.');
+ const { type } = query;
+
+ if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type) && !SEGMENT_TYPES[type]) {
+ return badRequest();
}
- const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search);
+ let values: any[];
+
+ if (SEGMENT_TYPES[type]) {
+ values = (await getWebsiteSegments(websiteId, type))?.data?.map(segment => ({
+ value: segment.name,
+ }));
+ } else {
+ const filters = await getQueryFilters(query, websiteId);
+ values = await getValues(websiteId, FILTER_COLUMNS[type], filters);
+ }
return json(values.filter(n => n).sort());
}
diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts
index b8fb2a0b..e2b26c10 100644
--- a/src/app/api/websites/route.ts
+++ b/src/app/api/websites/route.ts
@@ -1,13 +1,21 @@
import { z } from 'zod';
-import { canCreateTeamWebsite, canCreateWebsite } from '@/lib/auth';
-import { json, unauthorized } from '@/lib/response';
import { uuid } from '@/lib/crypto';
-import { parseRequest } from '@/lib/request';
-import { createWebsite, getUserWebsites } from '@/queries';
-import { pagingParams } from '@/lib/schema';
+import redis from '@/lib/redis';
+import { getQueryFilters, parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams, searchParams } from '@/lib/schema';
+import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions';
+import { createWebsite, getWebsiteCount } from '@/queries/prisma';
+import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma/website';
+
+const CLOUD_WEBSITE_LIMIT = 3;
export async function GET(request: Request) {
- const schema = z.object({ ...pagingParams });
+ const schema = z.object({
+ ...pagingParams,
+ ...searchParams,
+ includeTeams: z.string().optional(),
+ });
const { auth, query, error } = await parseRequest(request, schema);
@@ -15,9 +23,15 @@ export async function GET(request: Request) {
return error();
}
- const websites = await getUserWebsites(auth.user.id, query);
+ const userId = auth.user.id;
- return json(websites);
+ const filters = await getQueryFilters(query);
+
+ if (query.includeTeams) {
+ return json(await getAllUserWebsitesIncludingTeamOwner(userId, filters));
+ }
+
+ return json(await getUserWebsites(userId, filters));
}
export async function POST(request: Request) {
@@ -25,7 +39,8 @@ export async function POST(request: Request) {
name: z.string().max(100),
domain: z.string().max(500),
shareId: z.string().max(50).nullable().optional(),
- teamId: z.string().nullable().optional(),
+ teamId: z.uuid().nullable().optional(),
+ id: z.uuid().nullable().optional(),
});
const { auth, body, error } = await parseRequest(request, schema);
@@ -34,14 +49,26 @@ export async function POST(request: Request) {
return error();
}
- const { name, domain, shareId, teamId } = body;
+ const { id, name, domain, shareId, teamId } = body;
+
+ if (process.env.CLOUD_MODE && !teamId) {
+ const account = await redis.client.get(`account:${auth.user.id}`);
+
+ if (!account?.hasSubscription) {
+ const count = await getWebsiteCount(auth.user.id);
+
+ if (count >= CLOUD_WEBSITE_LIMIT) {
+ return unauthorized({ message: 'Website limit reached.' });
+ }
+ }
+ }
if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
return unauthorized();
}
const data: any = {
- id: uuid(),
+ id: id ?? uuid(),
createdBy: auth.user.id,
name,
domain,
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index ebe313e6..afcbfc60 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,16 +1,25 @@
-import { Metadata } from 'next';
-import Providers from './Providers';
+import type { Metadata } from 'next';
+import { Suspense } from 'react';
+import { Providers } from './Providers';
import '@fontsource/inter/300.css';
import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/700.css';
-import 'react-basics/dist/styles.css';
-import '@/styles/index.css';
+import '@umami/react-zen/styles.css';
+import '@/styles/global.css';
import '@/styles/variables.css';
export default function ({ children }) {
+ if (process.env.DISABLE_UI) {
+ return (
+
+
+
+ );
+ }
+
return (
-
+
@@ -23,8 +32,10 @@ export default function ({ children }) {
-
- {children}
+
+
+ {children}
+
);
diff --git a/src/app/login/LoginForm.module.css b/src/app/login/LoginForm.module.css
deleted file mode 100644
index 0d3f981f..00000000
--- a/src/app/login/LoginForm.module.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.login {
- width: 400px;
- margin: auto;
- transform: translateY(-25%);
-}
-
-.form {
- display: flex;
- flex-direction: column;
- margin: 0 auto;
- width: 300px;
-}
-
-.title {
- font-size: 24px;
- font-weight: 700;
- text-align: center;
- margin: 30px 0;
-}
-
-.icon {
- width: 100%;
-}
-
-.icon svg {
- width: 32px;
- height: 32px;
-}
-
-.button {
- flex: 1;
- justify-content: center;
-}
diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx
index a808c622..26d78dd5 100644
--- a/src/app/login/LoginForm.tsx
+++ b/src/app/login/LoginForm.tsx
@@ -1,77 +1,70 @@
import {
+ Column,
Form,
- FormRow,
- FormInput,
FormButtons,
- TextField,
- PasswordField,
- SubmitButton,
+ FormField,
+ FormSubmitButton,
+ Heading,
Icon,
-} from 'react-basics';
+ PasswordField,
+ TextField,
+} from '@umami/react-zen';
import { useRouter } from 'next/navigation';
-import { useApi, useMessages } from '@/components/hooks';
-import { setUser } from '@/store/app';
+import { useMessages, useUpdateQuery } from '@/components/hooks';
+import { Logo } from '@/components/svg';
import { setClientAuthToken } from '@/lib/client';
-import Logo from '@/assets/logo.svg';
-import styles from './LoginForm.module.css';
+import { setUser } from '@/store/app';
export function LoginForm() {
- const { formatMessage, labels, getMessage } = useMessages();
+ const { formatMessage, labels, getErrorMessage } = useMessages();
const router = useRouter();
- const { post, useMutation } = useApi();
- const { mutate, error, isPending } = useMutation({
- mutationFn: (data: any) => post('/auth/login', data),
- });
+ const { mutateAsync, error } = useUpdateQuery('/auth/login');
const handleSubmit = async (data: any) => {
- mutate(data, {
+ await mutateAsync(data, {
onSuccess: async ({ token, user }) => {
setClientAuthToken(token);
setUser(user);
-
- router.push('/dashboard');
+ router.push('/');
},
});
};
return (
-
+
);
}
-
-export default LoginForm;
diff --git a/src/app/login/LoginPage.module.css b/src/app/login/LoginPage.module.css
deleted file mode 100644
index 45115d5b..00000000
--- a/src/app/login/LoginPage.module.css
+++ /dev/null
@@ -1,7 +0,0 @@
-.page {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100vh;
- background: var(--base75);
-}
diff --git a/src/app/login/LoginPage.tsx b/src/app/login/LoginPage.tsx
index 8ea0b4e2..6f485e3f 100644
--- a/src/app/login/LoginPage.tsx
+++ b/src/app/login/LoginPage.tsx
@@ -1,17 +1,11 @@
'use client';
-import LoginForm from './LoginForm';
-import styles from './LoginPage.module.css';
+import { Column } from '@umami/react-zen';
+import { LoginForm } from './LoginForm';
export function LoginPage() {
- if (process.env.disableLogin) {
- return null;
- }
-
return (
-
+
-
+
);
}
-
-export default LoginPage;
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index 6f34d987..ea27735a 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -1,7 +1,11 @@
-import { Metadata } from 'next';
-import LoginPage from './LoginPage';
+import type { Metadata } from 'next';
+import { LoginPage } from './LoginPage';
export default async function () {
+ if (process.env.DISABLE_LOGIN || process.env.CLOUD_MODE) {
+ return null;
+ }
+
return ;
}
diff --git a/src/app/logout/LogoutPage.tsx b/src/app/logout/LogoutPage.tsx
index d3dc481a..33e1615d 100644
--- a/src/app/logout/LogoutPage.tsx
+++ b/src/app/logout/LogoutPage.tsx
@@ -1,32 +1,25 @@
'use client';
-import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
+import { useEffect } from 'react';
import { useApi } from '@/components/hooks';
-import { setUser } from '@/store/app';
import { removeClientAuthToken } from '@/lib/client';
+import { setUser } from '@/store/app';
export function LogoutPage() {
- const disabled = !!(process.env.disableLogin || process.env.cloudMode);
const router = useRouter();
const { post } = useApi();
useEffect(() => {
async function logout() {
await post('/auth/logout');
+
+ window.location.href = `${process.env.basePath || ''}/login`;
}
- if (!disabled) {
- removeClientAuthToken();
-
- logout();
-
- router.push('/login');
-
- return () => setUser(null);
- }
- }, [disabled, router, post]);
+ removeClientAuthToken();
+ setUser(null);
+ logout();
+ }, [router, post]);
return null;
}
-
-export default LogoutPage;
diff --git a/src/app/logout/page.tsx b/src/app/logout/page.tsx
index a253049a..20952788 100644
--- a/src/app/logout/page.tsx
+++ b/src/app/logout/page.tsx
@@ -1,7 +1,11 @@
-import LogoutPage from './LogoutPage';
-import { Metadata } from 'next';
+import type { Metadata } from 'next';
+import { LogoutPage } from './LogoutPage';
export default function () {
+ if (process.env.DISABLE_LOGIN || process.env.CLOUD_MODE) {
+ return null;
+ }
+
return ;
}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
index c673e40f..b3761513 100644
--- a/src/app/not-found.tsx
+++ b/src/app/not-found.tsx
@@ -1,12 +1,12 @@
'use client';
-import { Flexbox } from 'react-basics';
+import { Flexbox } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
export default function () {
const { formatMessage, labels } = useMessages();
return (
-
+
{formatMessage(labels.pageNotFound)}
);
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 4977c99b..6f0033df 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,5 +1,19 @@
+'use client';
import { redirect } from 'next/navigation';
+import { useEffect } from 'react';
+import { LAST_TEAM_CONFIG } from '@/lib/constants';
+import { getItem } from '@/lib/storage';
export default function RootPage() {
- redirect('/dashboard');
+ useEffect(() => {
+ const lastTeam = getItem(LAST_TEAM_CONFIG);
+
+ if (lastTeam) {
+ redirect(`/teams/${lastTeam}/websites`);
+ } else {
+ redirect(`/websites`);
+ }
+ }, []);
+
+ return null;
}
diff --git a/src/app/share/[...shareId]/Footer.module.css b/src/app/share/[...shareId]/Footer.module.css
deleted file mode 100644
index 5dc2d584..00000000
--- a/src/app/share/[...shareId]/Footer.module.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.footer {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: flex-end;
- font-size: var(--font-size-sm);
- height: 100px;
-}
-
-.footer a {
- color: var(--font-color100);
-}
diff --git a/src/app/share/[...shareId]/Footer.tsx b/src/app/share/[...shareId]/Footer.tsx
index e1ba9833..f2948628 100644
--- a/src/app/share/[...shareId]/Footer.tsx
+++ b/src/app/share/[...shareId]/Footer.tsx
@@ -1,14 +1,12 @@
+import { Row, Text } from '@umami/react-zen';
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
-import styles from './Footer.module.css';
export function Footer() {
return (
-
+
);
}
-
-export default Footer;
diff --git a/src/app/share/[...shareId]/Header.module.css b/src/app/share/[...shareId]/Header.module.css
deleted file mode 100644
index 9fc946c7..00000000
--- a/src/app/share/[...shareId]/Header.module.css
+++ /dev/null
@@ -1,31 +0,0 @@
-.header {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
- width: 100%;
- height: 100px;
-}
-
-.title {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 10px;
- font-size: var(--font-size-lg);
- font-weight: 700;
- color: var(--font-color100) !important;
-}
-
-.buttons {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: flex-end;
-}
-
-@media only screen and (max-width: 992px) {
- .header .buttons {
- flex: 1;
- }
-}
diff --git a/src/app/share/[...shareId]/Header.tsx b/src/app/share/[...shareId]/Header.tsx
index a71a5b56..d7b7dcb4 100644
--- a/src/app/share/[...shareId]/Header.tsx
+++ b/src/app/share/[...shareId]/Header.tsx
@@ -1,29 +1,24 @@
-import { Icon, Text } from 'react-basics';
-import Link from 'next/link';
-import LanguageButton from '@/components/input/LanguageButton';
-import ThemeButton from '@/components/input/ThemeButton';
-import SettingsButton from '@/components/input/SettingsButton';
-import Icons from '@/components/icons';
-import styles from './Header.module.css';
+import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
+import { LanguageButton } from '@/components/input/LanguageButton';
+import { PreferencesButton } from '@/components/input/PreferencesButton';
+import { Logo } from '@/components/svg';
export function Header() {
return (
-
+
+
+
);
}
-
-export default Header;
diff --git a/src/app/share/[...shareId]/SharePage.module.css b/src/app/share/[...shareId]/SharePage.module.css
deleted file mode 100644
index f6c68cf6..00000000
--- a/src/app/share/[...shareId]/SharePage.module.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.container {
- flex: 1;
- min-height: calc(100vh - 200px);
- min-height: calc(100dvh - 200px);
-}
diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[...shareId]/SharePage.tsx
index 00c7ec3f..7ed06673 100644
--- a/src/app/share/[...shareId]/SharePage.tsx
+++ b/src/app/share/[...shareId]/SharePage.tsx
@@ -1,28 +1,41 @@
'use client';
-import WebsiteDetailsPage from '../../(main)/websites/[websiteId]/WebsiteDetailsPage';
-import { useShareToken } from '@/components/hooks';
-import Page from '@/components/layout/Page';
-import Header from './Header';
-import Footer from './Footer';
-import styles from './SharePage.module.css';
-import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
+import { Column, useTheme } from '@umami/react-zen';
+import { useEffect } from 'react';
+import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
+import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
+import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
+import { PageBody } from '@/components/common/PageBody';
+import { useShareTokenQuery } from '@/components/hooks';
+import { Footer } from './Footer';
+import { Header } from './Header';
-export default function SharePage({ shareId }) {
- const { shareToken, isLoading } = useShareToken(shareId);
+export function SharePage({ shareId }) {
+ const { shareToken, isLoading } = useShareTokenQuery(shareId);
+ const { setTheme } = useTheme();
+
+ useEffect(() => {
+ const url = new URL(window?.location?.href);
+ const theme = url.searchParams.get('theme');
+
+ if (theme === 'light' || theme === 'dark') {
+ setTheme(theme);
+ }
+ }, []);
if (isLoading || !shareToken) {
return null;
}
return (
-
+
+
);
}
diff --git a/src/app/share/[...shareId]/page.tsx b/src/app/share/[...shareId]/page.tsx
index 548082fb..b9900eb7 100644
--- a/src/app/share/[...shareId]/page.tsx
+++ b/src/app/share/[...shareId]/page.tsx
@@ -1,6 +1,6 @@
-import SharePage from './SharePage';
+import { SharePage } from './SharePage';
-export default async function ({ params }: { params: Promise<{ shareId: string }> }) {
+export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
const { shareId } = await params;
return ;
diff --git a/src/app/sso/SSOPage.tsx b/src/app/sso/SSOPage.tsx
index eb7c0f0a..3cc95093 100644
--- a/src/app/sso/SSOPage.tsx
+++ b/src/app/sso/SSOPage.tsx
@@ -1,10 +1,10 @@
'use client';
-import { useEffect } from 'react';
-import { Loading } from 'react-basics';
+import { Loading } from '@umami/react-zen';
import { useRouter, useSearchParams } from 'next/navigation';
+import { useEffect } from 'react';
import { setClientAuthToken } from '@/lib/client';
-export default function SSOPage() {
+export function SSOPage() {
const router = useRouter();
const search = useSearchParams();
const url = search.get('url');
@@ -18,5 +18,5 @@ export default function SSOPage() {
}
}, [router, url, token]);
- return ;
+ return ;
}
diff --git a/src/app/sso/page.tsx b/src/app/sso/page.tsx
index 0cfef1a9..f6290d41 100644
--- a/src/app/sso/page.tsx
+++ b/src/app/sso/page.tsx
@@ -1,5 +1,5 @@
import { Suspense } from 'react';
-import SSOPage from './SSOPage';
+import { SSOPage } from './SSOPage';
export default function () {
return (
diff --git a/src/assets/calendar.svg b/src/assets/calendar.svg
deleted file mode 100644
index 230c4e66..00000000
--- a/src/assets/calendar.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/assets/clock.svg b/src/assets/clock.svg
deleted file mode 100644
index ab4c1dec..00000000
--- a/src/assets/clock.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/assets/dashboard.svg b/src/assets/dashboard.svg
index 2090e5dc..398f2f22 100644
--- a/src/assets/dashboard.svg
+++ b/src/assets/dashboard.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/assets/download.svg b/src/assets/download.svg
new file mode 100644
index 00000000..b2482c9b
--- /dev/null
+++ b/src/assets/download.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/export.svg b/src/assets/export.svg
new file mode 100644
index 00000000..d7585b15
--- /dev/null
+++ b/src/assets/export.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/eye.svg b/src/assets/eye.svg
deleted file mode 100644
index 09c93453..00000000
--- a/src/assets/eye.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/assets/funnel.svg b/src/assets/funnel.svg
index 63fb7158..c97b2fd9 100644
--- a/src/assets/funnel.svg
+++ b/src/assets/funnel.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/assets/globe.svg b/src/assets/globe.svg
deleted file mode 100644
index 509eaba6..00000000
--- a/src/assets/globe.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/assets/lightbulb.svg b/src/assets/lightbulb.svg
index 4ff96dcc..46572b03 100644
--- a/src/assets/lightbulb.svg
+++ b/src/assets/lightbulb.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/assets/link.svg b/src/assets/link.svg
deleted file mode 100644
index c53d1ada..00000000
--- a/src/assets/link.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/assets/logo.svg b/src/assets/logo.svg
index b1395313..c7f45174 100644
--- a/src/assets/logo.svg
+++ b/src/assets/logo.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/assets/magnet.svg b/src/assets/magnet.svg
index 67007a01..79e1627a 100644
--- a/src/assets/magnet.svg
+++ b/src/assets/magnet.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/assets/money.svg b/src/assets/money.svg
index 954f954d..2f364d83 100644
--- a/src/assets/money.svg
+++ b/src/assets/money.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/assets/moon.svg b/src/assets/moon.svg
deleted file mode 100644
index 638286fd..00000000
--- a/src/assets/moon.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/assets/network.svg b/src/assets/network.svg
index 72855047..93c941ce 100644
--- a/src/assets/network.svg
+++ b/src/assets/network.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/assets/path.svg b/src/assets/path.svg
index eb9e24ba..e99207d5 100644
--- a/src/assets/path.svg
+++ b/src/assets/path.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/assets/pushpin.svg b/src/assets/pushpin.svg
index 653a552d..69262210 100644
--- a/src/assets/pushpin.svg
+++ b/src/assets/pushpin.svg
@@ -1,8 +1 @@
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/assets/security.svg b/src/assets/security.svg
new file mode 100644
index 00000000..dd20891c
--- /dev/null
+++ b/src/assets/security.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/sun.svg b/src/assets/sun.svg
deleted file mode 100644
index c9654776..00000000
--- a/src/assets/sun.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/assets/switch.svg b/src/assets/switch.svg
new file mode 100644
index 00000000..86166cc5
--- /dev/null
+++ b/src/assets/switch.svg
@@ -0,0 +1 @@
+
diff --git a/src/assets/tag.svg b/src/assets/tag.svg
index 0e0f3668..d1238692 100644
--- a/src/assets/tag.svg
+++ b/src/assets/tag.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/assets/target.svg b/src/assets/target.svg
index c2e47e32..ae9fef25 100644
--- a/src/assets/target.svg
+++ b/src/assets/target.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/src/assets/user.svg b/src/assets/user.svg
deleted file mode 100644
index 245a67f6..00000000
--- a/src/assets/user.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/assets/users.svg b/src/assets/users.svg
deleted file mode 100644
index 7036a22c..00000000
--- a/src/assets/users.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/components/boards/Board.tsx b/src/components/boards/Board.tsx
new file mode 100644
index 00000000..70f0fa01
--- /dev/null
+++ b/src/components/boards/Board.tsx
@@ -0,0 +1,9 @@
+import { Column } from '@umami/react-zen';
+
+export interface BoardProps {
+ children?: React.ReactNode;
+}
+
+export function Board({ children }: BoardProps) {
+ return {children} ;
+}
diff --git a/src/components/charts/BarChart.tsx b/src/components/charts/BarChart.tsx
index f6a6e5e0..7bfc72d2 100644
--- a/src/components/charts/BarChart.tsx
+++ b/src/components/charts/BarChart.tsx
@@ -1,46 +1,65 @@
-import BarChartTooltip from '@/components/charts/BarChartTooltip';
-import Chart, { ChartProps } from '@/components/charts/Chart';
-import { useTheme } from '@/components/hooks';
-import { renderNumberLabels } from '@/lib/charts';
+import { useTheme } from '@umami/react-zen';
import { useMemo, useState } from 'react';
+import { Chart, type ChartProps } from '@/components/charts/Chart';
+import { ChartTooltip } from '@/components/charts/ChartTooltip';
+import { useLocale } from '@/components/hooks';
+import { renderNumberLabels } from '@/lib/charts';
+import { getThemeColors } from '@/lib/colors';
+import { DATE_FORMATS, formatDate } from '@/lib/date';
+import { formatLongCurrency, formatLongNumber } from '@/lib/format';
+
+const dateFormats = {
+ millisecond: 'T',
+ second: 'pp',
+ minute: 'p',
+ hour: 'p - PP',
+ day: 'PPPP',
+ week: 'PPPP',
+ month: 'LLLL yyyy',
+ quarter: 'qqq',
+ year: 'yyyy',
+};
export interface BarChartProps extends ChartProps {
- unit: string;
+ unit?: string;
stacked?: boolean;
currency?: string;
renderXLabel?: (label: string, index: number, values: any[]) => string;
renderYLabel?: (label: string, index: number, values: any[]) => string;
XAxisType?: string;
YAxisType?: string;
- minDate?: number | string;
- maxDate?: number | string;
- isAllTime?: boolean;
+ minDate?: Date;
+ maxDate?: Date;
}
-export function BarChart(props: BarChartProps) {
+export function BarChart({
+ chartData,
+ renderXLabel,
+ renderYLabel,
+ unit,
+ XAxisType = 'timeseries',
+ YAxisType = 'linear',
+ stacked = false,
+ minDate,
+ maxDate,
+ currency,
+ ...props
+}: BarChartProps) {
const [tooltip, setTooltip] = useState(null);
- const { colors } = useTheme();
- const {
- renderXLabel,
- renderYLabel,
- unit,
- XAxisType = 'time',
- YAxisType = 'linear',
- stacked = false,
- minDate,
- maxDate,
- currency,
- isAllTime,
- } = props;
+ const { theme } = useTheme();
+ const { locale } = useLocale();
+ const { colors } = useMemo(() => getThemeColors(theme), [theme]);
- const options: any = useMemo(() => {
+ const chartOptions: any = useMemo(() => {
return {
+ __id: Date.now(),
scales: {
x: {
type: XAxisType,
stacked: true,
- min: isAllTime ? '' : minDate,
- max: maxDate,
+ min: formatDate(minDate, DATE_FORMATS[unit], locale),
+ max: formatDate(maxDate, DATE_FORMATS[unit], locale),
+ offset: true,
time: {
unit,
},
@@ -61,7 +80,7 @@ export function BarChart(props: BarChartProps) {
type: YAxisType,
min: 0,
beginAtZero: true,
- stacked,
+ stacked: !!stacked,
grid: {
color: colors.chart.line,
},
@@ -75,25 +94,38 @@ export function BarChart(props: BarChartProps) {
},
},
};
- }, [colors, unit, stacked, renderXLabel, renderYLabel]);
+ }, [chartData, colors, unit, stacked, renderXLabel, renderYLabel]);
const handleTooltip = ({ tooltip }: { tooltip: any }) => {
- const { opacity } = tooltip;
+ const { opacity, labelColors, dataPoints } = tooltip;
setTooltip(
- opacity ? : null,
+ opacity
+ ? {
+ title: formatDate(
+ new Date(dataPoints[0].raw?.d || dataPoints[0].raw?.x || dataPoints[0].raw),
+ dateFormats[unit],
+ locale,
+ ),
+ color: labelColors?.[0]?.backgroundColor,
+ value: currency
+ ? formatLongCurrency(dataPoints[0].raw.y, currency)
+ : `${formatLongNumber(dataPoints[0].raw.y)} ${dataPoints[0].dataset.label}`,
+ }
+ : null,
);
};
return (
-
+ <>
+
+ {tooltip && }
+ >
);
}
-
-export default BarChart;
diff --git a/src/components/charts/BarChartTooltip.tsx b/src/components/charts/BarChartTooltip.tsx
deleted file mode 100644
index 8bcbad8f..00000000
--- a/src/components/charts/BarChartTooltip.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useLocale } from '@/components/hooks';
-import { formatDate } from '@/lib/date';
-import { formatLongCurrency, formatLongNumber } from '@/lib/format';
-import { Flexbox, StatusLight } from 'react-basics';
-
-const formats = {
- millisecond: 'T',
- second: 'pp',
- minute: 'p',
- hour: 'p - PP',
- day: 'PPPP',
- week: 'PPPP',
- month: 'LLLL yyyy',
- quarter: 'qqq',
- year: 'yyyy',
-};
-
-export default function BarChartTooltip({ tooltip, unit, currency }) {
- const { locale } = useLocale();
- const { labelColors, dataPoints } = tooltip;
-
- return (
-
-
- {formatDate(new Date(dataPoints[0].raw.d || dataPoints[0].raw.x), formats[unit], locale)}
-
-
-
- {currency
- ? formatLongCurrency(dataPoints[0].raw.y, currency)
- : formatLongNumber(dataPoints[0].raw.y)}{' '}
- {dataPoints[0].dataset.label}
-
-
-
- );
-}
diff --git a/src/components/charts/BubbleChart.tsx b/src/components/charts/BubbleChart.tsx
index dfe67f3a..bf487ac0 100644
--- a/src/components/charts/BubbleChart.tsx
+++ b/src/components/charts/BubbleChart.tsx
@@ -1,27 +1,31 @@
-import { Chart, ChartProps } from '@/components/charts/Chart';
import { useState } from 'react';
-import { StatusLight } from 'react-basics';
-import { formatLongNumber } from '@/lib/format';
+import { Chart, type ChartProps } from '@/components/charts/Chart';
+import { ChartTooltip } from '@/components/charts/ChartTooltip';
export interface BubbleChartProps extends ChartProps {
type?: 'bubble';
}
-export default function BubbleChart(props: BubbleChartProps) {
+export function BubbleChart({ type = 'bubble', ...props }: BubbleChartProps) {
const [tooltip, setTooltip] = useState(null);
- const { type = 'bubble' } = props;
const handleTooltip = ({ tooltip }) => {
- const { labelColors, dataPoints } = tooltip;
+ const { opacity, labelColors, title, dataPoints } = tooltip;
setTooltip(
- tooltip.opacity ? (
-
- {formatLongNumber(dataPoints?.[0]?.raw)} {dataPoints?.[0]?.label}
-
- ) : null,
+ opacity
+ ? {
+ color: labelColors?.[0]?.backgroundColor,
+ value: `${title}: ${dataPoints[0].raw}`,
+ }
+ : null,
);
};
- return ;
+ return (
+ <>
+
+ {tooltip && }
+ >
+ );
}
diff --git a/src/components/charts/Chart.module.css b/src/components/charts/Chart.module.css
deleted file mode 100644
index ee61f29b..00000000
--- a/src/components/charts/Chart.module.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.chart {
- position: relative;
- height: 400px;
- overflow: hidden;
-}
-
-.tooltip {
- display: flex;
- flex-direction: column;
- gap: 10px;
-}
diff --git a/src/components/charts/Chart.tsx b/src/components/charts/Chart.tsx
index a21086bf..b6ae9d79 100644
--- a/src/components/charts/Chart.tsx
+++ b/src/components/charts/Chart.tsx
@@ -1,38 +1,33 @@
-import { useState, useRef, useEffect, useMemo, ReactNode } from 'react';
-import { Loading } from 'react-basics';
-import classNames from 'classnames';
-import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto';
-import HoverTooltip from '@/components/common/HoverTooltip';
-import Legend from '@/components/metrics/Legend';
+import { Box, type BoxProps, Column } from '@umami/react-zen';
+import ChartJS, {
+ type ChartData,
+ type ChartOptions,
+ type LegendItem,
+ type UpdateMode,
+} from 'chart.js/auto';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import { Legend } from '@/components/metrics/Legend';
import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants';
-import styles from './Chart.module.css';
-export interface ChartProps {
+ChartJS.defaults.font.family = 'Inter';
+
+export interface ChartProps extends BoxProps {
type?: 'bar' | 'bubble' | 'doughnut' | 'pie' | 'line' | 'polarArea' | 'radar' | 'scatter';
- data?: object;
- isLoading?: boolean;
- animationDuration?: number;
- updateMode?: string;
- onCreate?: (chart: any) => void;
- onUpdate?: (chart: any) => void;
- onTooltip?: (model: any) => void;
- className?: string;
+ chartData?: ChartData & { focusLabel?: string };
chartOptions?: ChartOptions;
- tooltip?: ReactNode;
+ updateMode?: UpdateMode;
+ animationDuration?: number;
+ onTooltip?: (model: any) => void;
}
export function Chart({
type,
- data,
- isLoading = false,
+ chartData,
animationDuration = DEFAULT_ANIMATION_DURATION,
- tooltip,
updateMode,
- onCreate,
- onUpdate,
onTooltip,
- className,
chartOptions,
+ ...props
}: ChartProps) {
const canvas = useRef(null);
const chart = useRef(null);
@@ -57,6 +52,7 @@ export function Chart({
},
tooltip: {
enabled: false,
+ intersect: true,
external: onTooltip,
},
},
@@ -64,53 +60,6 @@ export function Chart({
};
}, [chartOptions]);
- const createChart = (data: any) => {
- ChartJS.defaults.font.family = 'Inter';
-
- chart.current = new ChartJS(canvas.current, {
- type,
- data,
- options,
- });
-
- onCreate?.(chart.current);
-
- setLegendItems(chart.current.legend.legendItems);
- };
-
- const updateChart = (data: any) => {
- if (data.datasets) {
- if (data.datasets.length === chart.current.data.datasets.length) {
- chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
- if (data?.datasets[index]) {
- dataset.data = data?.datasets[index]?.data;
-
- if (chart.current.legend.legendItems[index]) {
- chart.current.legend.legendItems[index].text = data.datasets[index]?.label;
- }
- }
- });
- } else {
- chart.current.data.datasets = data.datasets;
- }
- }
-
- if (data.focusLabel !== null) {
- chart.current.data.datasets.forEach(ds => {
- ds.hidden = data.focusLabel ? ds.label !== data.focusLabel : false;
- });
- }
-
- chart.current.options = options;
-
- // Allow config changes before update
- onUpdate?.(chart.current);
-
- chart.current.update(updateMode);
-
- setLegendItems(chart.current.legend.legendItems);
- };
-
const handleLegendClick = (item: LegendItem) => {
if (type === 'bar') {
const { datasetIndex } = item;
@@ -132,30 +81,50 @@ export function Chart({
setLegendItems(chart.current.legend.legendItems);
};
+ // Create chart
useEffect(() => {
- if (data) {
- if (!chart.current) {
- createChart(data);
- } else {
- updateChart(data);
- }
+ if (canvas.current) {
+ chart.current = new ChartJS(canvas.current, {
+ type,
+ data: chartData,
+ options,
+ });
+
+ setLegendItems(chart.current.legend.legendItems);
}
- }, [data, options]);
+
+ return () => {
+ chart.current?.destroy();
+ };
+ }, []);
+
+ // Update chart
+ useEffect(() => {
+ if (chart.current && chartData) {
+ // Replace labels and datasets *in-place*
+ chart.current.data.labels = chartData.labels;
+ chart.current.data.datasets = chartData.datasets;
+
+ if (chartData.focusLabel !== null) {
+ chart.current.data.datasets.forEach((ds: { hidden: boolean; label: any }) => {
+ ds.hidden = chartData.focusLabel ? ds.label !== chartData.focusLabel : false;
+ });
+ }
+
+ chart.current.options = options;
+
+ chart.current.update(updateMode);
+
+ setLegendItems(chart.current.legend.legendItems);
+ }
+ }, [chartData, options, updateMode]);
return (
- <>
-
- {isLoading && }
+
+
-
+
- {tooltip && (
-
- {tooltip}
-
- )}
- >
+
);
}
-
-export default Chart;
diff --git a/src/components/charts/ChartTooltip.tsx b/src/components/charts/ChartTooltip.tsx
new file mode 100644
index 00000000..95ba2a2b
--- /dev/null
+++ b/src/components/charts/ChartTooltip.tsx
@@ -0,0 +1,23 @@
+import { Column, FloatingTooltip, Row, StatusLight } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+
+export function ChartTooltip({
+ title,
+ color,
+ value,
+}: {
+ title?: string;
+ color?: string;
+ value?: ReactNode;
+}) {
+ return (
+
+
+ {title && {title}
}
+
+ {value}
+
+
+
+ );
+}
diff --git a/src/components/charts/PieChart.tsx b/src/components/charts/PieChart.tsx
index a98b9730..2470fe77 100644
--- a/src/components/charts/PieChart.tsx
+++ b/src/components/charts/PieChart.tsx
@@ -1,27 +1,31 @@
-import { Chart, ChartProps } from '@/components/charts/Chart';
import { useState } from 'react';
-import { StatusLight } from 'react-basics';
-import { formatLongNumber } from '@/lib/format';
+import { Chart, type ChartProps } from '@/components/charts/Chart';
+import { ChartTooltip } from '@/components/charts/ChartTooltip';
export interface PieChartProps extends ChartProps {
type?: 'doughnut' | 'pie';
}
-export default function PieChart(props: PieChartProps) {
+export function PieChart({ type = 'pie', ...props }: PieChartProps) {
const [tooltip, setTooltip] = useState(null);
- const { type = 'pie' } = props;
const handleTooltip = ({ tooltip }) => {
- const { labelColors, dataPoints } = tooltip;
+ const { opacity, labelColors, title, dataPoints } = tooltip;
setTooltip(
- tooltip.opacity ? (
-
- {formatLongNumber(dataPoints?.[0]?.raw)} {dataPoints?.[0]?.label}
-
- ) : null,
+ opacity
+ ? {
+ color: labelColors?.[0]?.backgroundColor,
+ value: `${title}: ${dataPoints[0].raw}`,
+ }
+ : null,
);
};
- return ;
+ return (
+ <>
+
+ {tooltip && }
+ >
+ );
}
diff --git a/src/components/common/ActionForm.tsx b/src/components/common/ActionForm.tsx
new file mode 100644
index 00000000..c6f44e80
--- /dev/null
+++ b/src/components/common/ActionForm.tsx
@@ -0,0 +1,15 @@
+import { Column, Row, Text } from '@umami/react-zen';
+
+export function ActionForm({ label, description, children }) {
+ return (
+
+
+ {label}
+ {description}
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/components/common/AnimatedDiv.tsx b/src/components/common/AnimatedDiv.tsx
new file mode 100644
index 00000000..f9948971
--- /dev/null
+++ b/src/components/common/AnimatedDiv.tsx
@@ -0,0 +1,3 @@
+import { type AnimatedComponent, animated } from '@react-spring/web';
+
+export const AnimatedDiv: AnimatedComponent = animated.div;
diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx
index d0cae247..9b198b30 100644
--- a/src/components/common/Avatar.tsx
+++ b/src/components/common/Avatar.tsx
@@ -1,11 +1,11 @@
-import { useMemo } from 'react';
-import { createAvatar } from '@dicebear/core';
import { lorelei } from '@dicebear/collection';
+import { createAvatar } from '@dicebear/core';
+import { useMemo } from 'react';
import { getColor, getPastel } from '@/lib/colors';
const lib = lorelei;
-function Avatar({ seed, size = 128, ...props }: { seed: string; size?: number }) {
+export function Avatar({ seed, size = 128, ...props }: { seed: string; size?: number }) {
const backgroundColor = getPastel(getColor(seed), 4);
const avatar = useMemo(() => {
@@ -17,7 +17,5 @@ function Avatar({ seed, size = 128, ...props }: { seed: string; size?: number })
}).toDataUri();
}, []);
- return ;
+ return ;
}
-
-export default Avatar;
diff --git a/src/components/common/Breadcrumb.module.css b/src/components/common/Breadcrumb.module.css
deleted file mode 100644
index 81e7524f..00000000
--- a/src/components/common/Breadcrumb.module.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.bar {
- font-size: 12px;
- font-weight: 700;
- text-transform: uppercase;
- color: var(--base600);
-}
-
-.link span {
- color: var(--base700) !important;
-}
diff --git a/src/components/common/Breadcrumb.tsx b/src/components/common/Breadcrumb.tsx
deleted file mode 100644
index ebdce497..00000000
--- a/src/components/common/Breadcrumb.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import Link from 'next/link';
-import { Flexbox, Icon, Icons, Text } from 'react-basics';
-import styles from './Breadcrumb.module.css';
-import { Fragment } from 'react';
-
-export interface BreadcrumbProps {
- data: {
- url?: string;
- label: string;
- }[];
-}
-
-export function Breadcrumb({ data }: BreadcrumbProps) {
- return (
-
- {data.map((a, i) => {
- return (
-
- {a.url ? (
-
- {a.label}
-
- ) : (
- {a.label}
- )}
- {i !== data.length - 1 ? (
-
-
-
- ) : null}
-
- );
- })}
-
- );
-}
-
-export default Breadcrumb;
diff --git a/src/components/common/ConfirmationForm.tsx b/src/components/common/ConfirmationForm.tsx
index 3997bc4a..b909ef58 100644
--- a/src/components/common/ConfirmationForm.tsx
+++ b/src/components/common/ConfirmationForm.tsx
@@ -1,11 +1,11 @@
-import { ReactNode } from 'react';
-import { Button, LoadingButton, Form, FormButtons } from 'react-basics';
+import { Box, Button, Form, FormButtons, FormSubmitButton } from '@umami/react-zen';
+import type { ReactNode } from 'react';
import { useMessages } from '@/components/hooks';
export interface ConfirmationFormProps {
message: ReactNode;
buttonLabel?: ReactNode;
- buttonVariant?: 'none' | 'primary' | 'secondary' | 'quiet' | 'danger';
+ buttonVariant?: 'primary' | 'quiet' | 'danger';
isLoading?: boolean;
error?: string | Error;
onConfirm?: () => void;
@@ -21,24 +21,22 @@ export function ConfirmationForm({
onConfirm,
onClose,
}: ConfirmationFormProps) {
- const { formatMessage, labels } = useMessages();
+ const { formatMessage, labels, getErrorMessage } = useMessages();
return (
-
);
}
-
-export default ConfirmationForm;
diff --git a/src/components/common/DataGrid.tsx b/src/components/common/DataGrid.tsx
new file mode 100644
index 00000000..7e07b8dc
--- /dev/null
+++ b/src/components/common/DataGrid.tsx
@@ -0,0 +1,107 @@
+import type { UseQueryResult } from '@tanstack/react-query';
+import { Column, Row, SearchField } from '@umami/react-zen';
+import {
+ cloneElement,
+ isValidElement,
+ type ReactElement,
+ type ReactNode,
+ useCallback,
+ useState,
+} from 'react';
+import { Empty } from '@/components/common/Empty';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { Pager } from '@/components/common/Pager';
+import { useMessages, useMobile, useNavigation } from '@/components/hooks';
+import type { PageResult } from '@/lib/types';
+
+const DEFAULT_SEARCH_DELAY = 600;
+
+export interface DataGridProps {
+ query: UseQueryResult, any>;
+ searchDelay?: number;
+ allowSearch?: boolean;
+ allowPaging?: boolean;
+ autoFocus?: boolean;
+ renderActions?: () => ReactNode;
+ renderEmpty?: () => ReactNode;
+ children: ReactNode | ((data: any) => ReactNode);
+}
+
+export function DataGrid({
+ query,
+ searchDelay = 600,
+ allowSearch,
+ allowPaging = true,
+ autoFocus,
+ renderActions,
+ renderEmpty = () => ,
+ children,
+}: DataGridProps) {
+ const { formatMessage, labels } = useMessages();
+ const { data, error, isLoading, isFetching } = query;
+ const { router, updateParams, query: queryParams } = useNavigation();
+ const [search, setSearch] = useState(queryParams?.search || data?.search || '');
+ const showPager = allowPaging && data && data.count > data.pageSize;
+ const { isMobile } = useMobile();
+ const displayMode = isMobile ? 'cards' : undefined;
+
+ const handleSearch = (value: string) => {
+ if (value !== search) {
+ setSearch(value);
+ router.push(updateParams({ search: value, page: 1 }));
+ }
+ };
+
+ const handlePageChange = useCallback(
+ (page: number) => {
+ router.push(updateParams({ search, page }));
+ },
+ [search],
+ );
+
+ const child = data ? (typeof children === 'function' ? children(data) : children) : null;
+
+ return (
+
+ {allowSearch && (
+
+
+ {renderActions?.()}
+
+ )}
+
+ {data && (
+ <>
+
+ {isValidElement(child)
+ ? cloneElement(child as ReactElement, { displayMode })
+ : child}
+
+ {showPager && (
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+}
diff --git a/src/components/common/DataTable.module.css b/src/components/common/DataTable.module.css
deleted file mode 100644
index 9a7cffb7..00000000
--- a/src/components/common/DataTable.module.css
+++ /dev/null
@@ -1,34 +0,0 @@
-.search {
- max-width: 300px;
- margin: 20px 0;
-}
-
-.body {
- display: flex;
- flex-direction: column;
- position: relative;
- overflow-x: auto;
-}
-
-.body td {
- display: flex;
- gap: 10px;
- min-height: 70px;
- align-items: center;
-}
-
-.body > div > div > div {
- display: flex;
- gap: 10px;
-}
-
-.pager {
- margin: 20px 0;
-}
-
-.status {
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 200px;
-}
diff --git a/src/components/common/DataTable.tsx b/src/components/common/DataTable.tsx
deleted file mode 100644
index b19ddf91..00000000
--- a/src/components/common/DataTable.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { ReactNode } from 'react';
-import classNames from 'classnames';
-import { Loading, SearchField } from 'react-basics';
-import { useMessages, useNavigation } from '@/components/hooks';
-import Empty from '@/components/common/Empty';
-import Pager from '@/components/common/Pager';
-import { PagedQueryResult } from '@/lib/types';
-import styles from './DataTable.module.css';
-import { LoadingPanel } from '@/components/common/LoadingPanel';
-
-const DEFAULT_SEARCH_DELAY = 600;
-
-export interface DataTableProps {
- queryResult: PagedQueryResult;
- searchDelay?: number;
- allowSearch?: boolean;
- allowPaging?: boolean;
- autoFocus?: boolean;
- renderEmpty?: () => ReactNode;
- children: ReactNode | ((data: any) => ReactNode);
-}
-
-export function DataTable({
- queryResult,
- searchDelay = 600,
- allowSearch = true,
- allowPaging = true,
- autoFocus = true,
- renderEmpty,
- children,
-}: DataTableProps) {
- const { formatMessage, labels, messages } = useMessages();
- const {
- result,
- params,
- setParams,
- query: { error, isLoading, isFetched },
- } = queryResult || {};
- const { page, pageSize, count, data } = result || {};
- const { search } = params || {};
- const hasData = Boolean(!isLoading && data?.length);
- const noResults = Boolean(search && !hasData);
- const { router, renderUrl } = useNavigation();
-
- const handleSearch = (search: string) => {
- setParams({ ...params, search, page: params.page ? page : 1 });
- };
-
- const handlePageChange = (page: number) => {
- setParams({ ...params, search, page });
- router.push(renderUrl({ page }));
- };
-
- return (
- <>
- {allowSearch && (hasData || search) && (
-
- )}
-
-
- {hasData ? (typeof children === 'function' ? children(result) : children) : null}
- {isLoading && }
- {!isLoading && !hasData && !search && (renderEmpty ? renderEmpty() : )}
- {!isLoading && noResults && }
-
- {allowPaging && hasData && (
-
- )}
-
- >
- );
-}
-
-export default DataTable;
diff --git a/src/components/common/DateDisplay.tsx b/src/components/common/DateDisplay.tsx
new file mode 100644
index 00000000..0bece8ae
--- /dev/null
+++ b/src/components/common/DateDisplay.tsx
@@ -0,0 +1,28 @@
+import { Icon, Row, Text } from '@umami/react-zen';
+import { differenceInDays, isSameDay } from 'date-fns';
+import { useLocale } from '@/components/hooks';
+import { Calendar } from '@/components/icons';
+import { formatDate } from '@/lib/date';
+
+export function DateDisplay({ startDate, endDate }) {
+ const { locale } = useLocale();
+ const isSingleDate = differenceInDays(endDate, startDate) === 0;
+
+ return (
+
+
+
+
+
+ {isSingleDate ? (
+ formatDate(startDate, 'PP', locale)
+ ) : (
+ <>
+ {formatDate(startDate, 'PP', locale)}
+ {!isSameDay(startDate, endDate) && ` — ${formatDate(endDate, 'PP', locale)}`}
+ >
+ )}
+
+
+ );
+}
diff --git a/src/components/common/DateDistance.tsx b/src/components/common/DateDistance.tsx
new file mode 100644
index 00000000..e8bd2784
--- /dev/null
+++ b/src/components/common/DateDistance.tsx
@@ -0,0 +1,19 @@
+import { Text } from '@umami/react-zen';
+import { formatDistanceToNow } from 'date-fns';
+import { useLocale, useTimezone } from '@/components/hooks';
+import { isInvalidDate } from '@/lib/date';
+
+export function DateDistance({ date }: { date: Date }) {
+ const { formatTimezoneDate } = useTimezone();
+ const { dateLocale } = useLocale();
+
+ if (isInvalidDate(date)) {
+ return null;
+ }
+
+ return (
+
+ {formatDistanceToNow(date, { addSuffix: true, locale: dateLocale })}
+
+ );
+}
diff --git a/src/components/common/Empty.module.css b/src/components/common/Empty.module.css
deleted file mode 100644
index 3dccb68e..00000000
--- a/src/components/common/Empty.module.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.container {
- color: var(--base500);
- font-size: var(--font-size-md);
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- text-align: center;
- width: 100%;
- height: 100%;
- min-height: 70px;
-}
diff --git a/src/components/common/Empty.tsx b/src/components/common/Empty.tsx
index cf6d11cc..8bd8d82d 100644
--- a/src/components/common/Empty.tsx
+++ b/src/components/common/Empty.tsx
@@ -1,20 +1,24 @@
-import classNames from 'classnames';
+import { Row } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
-import styles from './Empty.module.css';
export interface EmptyProps {
message?: string;
- className?: string;
}
-export function Empty({ message, className }: EmptyProps) {
+export function Empty({ message }: EmptyProps) {
const { formatMessage, messages } = useMessages();
return (
-
+
{message || formatMessage(messages.noDataAvailable)}
-
+
);
}
-
-export default Empty;
diff --git a/src/components/common/EmptyPlaceholder.tsx b/src/components/common/EmptyPlaceholder.tsx
index 2fd606cd..64492e04 100644
--- a/src/components/common/EmptyPlaceholder.tsx
+++ b/src/components/common/EmptyPlaceholder.tsx
@@ -1,22 +1,28 @@
-import { ReactNode } from 'react';
-import { Icon, Text, Flexbox } from 'react-basics';
-import Logo from '@/assets/logo.svg';
+import { Column, Icon, Text } from '@umami/react-zen';
+import type { ReactNode } from 'react';
export interface EmptyPlaceholderProps {
- message?: string;
+ title?: string;
+ description?: string;
+ icon?: ReactNode;
children?: ReactNode;
}
-export function EmptyPlaceholder({ message, children }: EmptyPlaceholderProps) {
+export function EmptyPlaceholder({ title, description, icon, children }: EmptyPlaceholderProps) {
return (
-
-
-
-
- {message}
- {children}
-
+
+ {icon && (
+
+ {icon}
+
+ )}
+ {title && (
+
+ {title}
+
+ )}
+ {description && {description} }
+ {children}
+
);
}
-
-export default EmptyPlaceholder;
diff --git a/src/components/common/ErrorBoundary.module.css b/src/components/common/ErrorBoundary.module.css
deleted file mode 100644
index 915022c4..00000000
--- a/src/components/common/ErrorBoundary.module.css
+++ /dev/null
@@ -1,19 +0,0 @@
-.error {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- margin: auto;
- z-index: var(--z-index-overlay);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- min-height: 600px;
- gap: 20px;
-}
-
-.error button {
- align-self: center;
-}
diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx
index b9521bb4..4c0c82ed 100644
--- a/src/components/common/ErrorBoundary.tsx
+++ b/src/components/common/ErrorBoundary.tsx
@@ -1,8 +1,7 @@
-import { ErrorInfo, ReactNode } from 'react';
+import { Button, Column } from '@umami/react-zen';
+import type { ErrorInfo, ReactNode } from 'react';
import { ErrorBoundary as Boundary } from 'react-error-boundary';
-import { Button } from 'react-basics';
import { useMessages } from '@/components/hooks';
-import styles from './ErrorBoundary.module.css';
const logError = (error: Error, info: ErrorInfo) => {
// eslint-disable-next-line no-console
@@ -14,12 +13,20 @@ export function ErrorBoundary({ children }: { children: ReactNode }) {
const fallbackRender = ({ error, resetErrorBoundary }) => {
return (
-
+
{formatMessage(messages.error)}
{error.message}
{error.stack}
OK
-
+
);
};
@@ -29,5 +36,3 @@ export function ErrorBoundary({ children }: { children: ReactNode }) {
);
}
-
-export default ErrorBoundary;
diff --git a/src/components/common/ErrorMessage.module.css b/src/components/common/ErrorMessage.module.css
deleted file mode 100644
index fe976613..00000000
--- a/src/components/common/ErrorMessage.module.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.error {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- margin: auto;
- display: flex;
- background-color: var(--base50);
- padding: 10px;
- z-index: 1;
-}
-
-.icon {
- margin-inline-end: 10px;
-}
diff --git a/src/components/common/ErrorMessage.tsx b/src/components/common/ErrorMessage.tsx
index bf3eefb1..3c301513 100644
--- a/src/components/common/ErrorMessage.tsx
+++ b/src/components/common/ErrorMessage.tsx
@@ -1,18 +1,16 @@
-import { Icon, Icons, Text } from 'react-basics';
-import styles from './ErrorMessage.module.css';
+import { Icon, Row, Text } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
+import { AlertTriangle } from '@/components/icons';
export function ErrorMessage() {
const { formatMessage, messages } = useMessages();
return (
-
-
-
+
+
+
{formatMessage(messages.error)}
-
+
);
}
-
-export default ErrorMessage;
diff --git a/src/components/common/ExternalLink.tsx b/src/components/common/ExternalLink.tsx
new file mode 100644
index 00000000..dec0d16f
--- /dev/null
+++ b/src/components/common/ExternalLink.tsx
@@ -0,0 +1,23 @@
+import { Icon, Row, Text } from '@umami/react-zen';
+import Link, { type LinkProps } from 'next/link';
+import type { ReactNode } from 'react';
+import { ExternalLink as LinkIcon } from '@/components/icons';
+
+export function ExternalLink({
+ href,
+ children,
+ ...props
+}: LinkProps & { href: string; children: ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/common/Favicon.tsx b/src/components/common/Favicon.tsx
index c02fe74f..a6b5e522 100644
--- a/src/components/common/Favicon.tsx
+++ b/src/components/common/Favicon.tsx
@@ -1,3 +1,4 @@
+import { useConfig } from '@/components/hooks';
import { FAVICON_URL, GROUPED_DOMAINS } from '@/lib/constants';
function getHostName(url: string) {
@@ -6,16 +7,16 @@ function getHostName(url: string) {
}
export function Favicon({ domain, ...props }) {
- if (process.env.privateMode) {
+ const config = useConfig();
+
+ if (config?.privateMode) {
return null;
}
- const url = process.env.faviconURL || FAVICON_URL;
+ const url = config?.faviconUrl || FAVICON_URL;
const hostName = domain ? getHostName(domain) : null;
const domainName = GROUPED_DOMAINS[hostName]?.domain || hostName;
const src = hostName ? url.replace(/\{\{\s*domain\s*}}/, domainName) : null;
return hostName ? : null;
}
-
-export default Favicon;
diff --git a/src/components/common/FilterButtons.tsx b/src/components/common/FilterButtons.tsx
deleted file mode 100644
index a64a6482..00000000
--- a/src/components/common/FilterButtons.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Key } from 'react';
-import { ButtonGroup, Button, Flexbox } from 'react-basics';
-
-export interface FilterButtonsProps {
- items: any[];
- selectedKey?: Key;
- onSelect: (key: any) => void;
-}
-
-export function FilterButtons({ items, selectedKey, onSelect }: FilterButtonsProps) {
- return (
-
-
- {({ key, label }) => {label} }
-
-
- );
-}
-
-export default FilterButtons;
diff --git a/src/components/common/FilterLink.module.css b/src/components/common/FilterLink.module.css
deleted file mode 100644
index 894d6e00..00000000
--- a/src/components/common/FilterLink.module.css
+++ /dev/null
@@ -1,37 +0,0 @@
-.row {
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.row.inactive {
- color: var(--base500);
-}
-
-.row.inactive img {
- opacity: 0.35;
-}
-
-.row.active {
- color: var(--base900);
- font-weight: 600;
-}
-
-.row .link {
- display: none;
- margin-inline-start: 20px;
-}
-
-.row .label {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.row:hover .link {
- display: block;
-}
-
-.icon {
- cursor: pointer;
-}
diff --git a/src/components/common/FilterLink.tsx b/src/components/common/FilterLink.tsx
index 9d726b58..d719a37e 100644
--- a/src/components/common/FilterLink.tsx
+++ b/src/components/common/FilterLink.tsx
@@ -1,55 +1,49 @@
-import classNames from 'classnames';
-import { useMessages, useNavigation } from '@/components/hooks';
+import { Icon, Row, Text } from '@umami/react-zen';
import Link from 'next/link';
-import { ReactNode } from 'react';
-import { Icon, Icons } from 'react-basics';
-import styles from './FilterLink.module.css';
+import { type HTMLAttributes, type ReactNode, useState } from 'react';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { ExternalLink } from '@/components/icons';
-export interface FilterLinkProps {
- id: string;
+export interface FilterLinkProps extends HTMLAttributes {
+ type: string;
value: string;
label?: string;
+ icon?: ReactNode;
externalUrl?: string;
- className?: string;
- children?: ReactNode;
}
-export function FilterLink({
- id,
- value,
- label,
- externalUrl,
- children,
- className,
-}: FilterLinkProps) {
+export function FilterLink({ type, value, label, externalUrl, icon }: FilterLinkProps) {
+ const [showLink, setShowLink] = useState(false);
const { formatMessage, labels } = useMessages();
- const { renderUrl, query } = useNavigation();
- const active = query[id] !== undefined;
- const selected = query[id] === value;
+ const { updateParams, query } = useNavigation();
+ const active = query[type] !== undefined;
+ const selected = query[type] === value;
return (
- setShowLink(true)}
+ onMouseOut={() => setShowLink(false)}
>
- {children}
+ {icon}
{!value && `(${label || formatMessage(labels.unknown)})`}
{value && (
-
- {label || value}
-
+
+
+ {label || value}
+
+
)}
- {externalUrl && (
-
-
-
+ {externalUrl && showLink && (
+
+
+
)}
-
+
);
}
-
-export default FilterLink;
diff --git a/src/components/common/FilterRecord.tsx b/src/components/common/FilterRecord.tsx
new file mode 100644
index 00000000..04002648
--- /dev/null
+++ b/src/components/common/FilterRecord.tsx
@@ -0,0 +1,117 @@
+import { Button, Column, Grid, Icon, Label, ListItem, Select, TextField } from '@umami/react-zen';
+import { useState } from 'react';
+import { Empty } from '@/components/common/Empty';
+import { useFilters, useFormat, useWebsiteValuesQuery } from '@/components/hooks';
+import { X } from '@/components/icons';
+import { isSearchOperator } from '@/lib/params';
+
+export interface FilterRecordProps {
+ websiteId: string;
+ type: string;
+ startDate: Date;
+ endDate: Date;
+ name: string;
+ operator: string;
+ value: string;
+ onSelect?: (name: string, value: any) => void;
+ onRemove?: (name: string) => void;
+ onChange?: (name: string, value: string) => void;
+}
+
+export function FilterRecord({
+ websiteId,
+ type,
+ startDate,
+ endDate,
+ name,
+ operator,
+ value,
+ onSelect,
+ onRemove,
+ onChange,
+}: FilterRecordProps) {
+ const { fields, operators } = useFilters();
+ const [selected, setSelected] = useState(value);
+ const [search, setSearch] = useState('');
+ const { formatValue } = useFormat();
+ const { data, isLoading } = useWebsiteValuesQuery({
+ websiteId,
+ type,
+ search,
+ startDate,
+ endDate,
+ });
+ const isSearch = isSearchOperator(operator);
+ const items = data?.filter(({ value }) => value) || [];
+
+ const handleSearch = (value: string) => {
+ setSearch(value);
+ };
+
+ const handleSelectOperator = (value: any) => {
+ onSelect?.(name, value);
+ };
+
+ const handleSelectValue = (value: string) => {
+ setSelected(value);
+ onChange?.(name, value);
+ };
+
+ const renderValue = () => {
+ return formatValue(selected, type);
+ };
+
+ return (
+
+ {fields.find(f => f.name === name)?.label}
+
+
+ type === 'string')}
+ value={operator}
+ onChange={handleSelectOperator}
+ >
+ {({ name, label }: any) => {
+ return (
+
+ {label}
+
+ );
+ }}
+
+ {isSearch && (
+
+ )}
+ {!isSearch && (
+ }}
+ allowSearch
+ >
+ {items?.map(({ value }) => {
+ return (
+
+ {formatValue(value, type)}
+
+ );
+ })}
+
+ )}
+
+
+ onRemove?.(name)}>
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/common/GridRow.tsx b/src/components/common/GridRow.tsx
new file mode 100644
index 00000000..72f1db6e
--- /dev/null
+++ b/src/components/common/GridRow.tsx
@@ -0,0 +1,32 @@
+import { Grid } from '@umami/react-zen';
+
+const LAYOUTS = {
+ one: { columns: '1fr' },
+ two: {
+ columns: {
+ xs: '1fr',
+ md: 'repeat(auto-fill, minmax(560px, 1fr))',
+ },
+ },
+ three: {
+ columns: {
+ xs: '1fr',
+ md: 'repeat(auto-fill, minmax(360px, 1fr))',
+ },
+ },
+ 'one-two': { columns: { xs: '1fr', md: 'repeat(3, 1fr)' } },
+ 'two-one': { columns: { xs: '1fr', md: 'repeat(3, 1fr)' } },
+};
+
+export function GridRow(props: {
+ layout?: 'one' | 'two' | 'three' | 'one-two' | 'two-one' | 'compare';
+ className?: string;
+ children?: any;
+}) {
+ const { layout = 'two', children, ...otherProps } = props;
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/common/HamburgerButton.module.css b/src/components/common/HamburgerButton.module.css
deleted file mode 100644
index 60398585..00000000
--- a/src/components/common/HamburgerButton.module.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.button {
- display: none;
-}
-
-@media only screen and (max-width: 768px) {
- .button {
- display: flex;
- }
-}
diff --git a/src/components/common/HamburgerButton.tsx b/src/components/common/HamburgerButton.tsx
deleted file mode 100644
index 5a81f3a3..00000000
--- a/src/components/common/HamburgerButton.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Button, Icon, Icons } from 'react-basics';
-import { useState } from 'react';
-import MobileMenu from './MobileMenu';
-
-export function HamburgerButton({ menuItems }: { menuItems: any[] }) {
- const [active, setActive] = useState(false);
-
- const handleClick = () => setActive(state => !state);
- const handleClose = () => setActive(false);
-
- return (
- <>
-
- {active ? : }
-
- {active && }
- >
- );
-}
-
-export default HamburgerButton;
diff --git a/src/components/common/HoverTooltip.module.css b/src/components/common/HoverTooltip.module.css
deleted file mode 100644
index c4bb76ea..00000000
--- a/src/components/common/HoverTooltip.module.css
+++ /dev/null
@@ -1,6 +0,0 @@
-.tooltip {
- position: fixed;
- pointer-events: none;
- z-index: var(--z-index-popup);
- transform: translate(-50%, calc(-100% - 5px));
-}
diff --git a/src/components/common/HoverTooltip.tsx b/src/components/common/HoverTooltip.tsx
deleted file mode 100644
index e5e31219..00000000
--- a/src/components/common/HoverTooltip.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { ReactNode, useEffect, useState } from 'react';
-import { Tooltip } from 'react-basics';
-import styles from './HoverTooltip.module.css';
-
-export function HoverTooltip({ children }: { children: ReactNode }) {
- const [position, setPosition] = useState({ x: -1000, y: -1000 });
-
- useEffect(() => {
- const handler = e => {
- setPosition({ x: e.clientX, y: e.clientY });
- };
-
- document.addEventListener('mousemove', handler);
-
- return () => {
- document.removeEventListener('mousemove', handler);
- };
- }, []);
-
- return (
-
- {children}
-
- );
-}
-
-export default HoverTooltip;
diff --git a/src/components/common/LinkButton.module.css b/src/components/common/LinkButton.module.css
deleted file mode 100644
index 5561f536..00000000
--- a/src/components/common/LinkButton.module.css
+++ /dev/null
@@ -1,107 +0,0 @@
-.button {
- display: flex;
- align-items: center;
- align-self: flex-start;
- white-space: nowrap;
- gap: var(--size200);
- font-family: inherit;
- color: var(--base900);
- background: var(--base100);
- border: 1px solid transparent;
- border-radius: var(--border-radius);
- min-height: var(--base-height);
- padding: 0 var(--size600);
- position: relative;
- cursor: pointer;
-}
-
-.button:hover {
- background: var(--base200);
-}
-
-.button:active {
- background: var(--base300);
-}
-
-.button:visited {
- color: var(--base900);
-}
-
-.button.disabled {
- color: var(--disabled-color) !important;
- background-color: var(--disabled-background) !important;
- border-color: transparent !important;
- pointer-events: none;
-}
-
-.button.primary {
- color: var(--light50);
- background: var(--primary400);
-}
-
-.button.primary:hover {
- color: var(--light50);
- background: var(--primary500);
-}
-
-.button.primary:active {
- color: var(--light50);
- background: var(--primary600);
-}
-
-.button.secondary {
- border: 1px solid var(--border-color);
- background: var(--base50);
-}
-
-.button.secondary:hover {
- background: var(--base75);
-}
-
-.button.secondary:active {
- background: var(--base100);
-}
-
-.button.quiet {
- color: var(--base900);
- background: transparent;
-}
-
-.button.quiet:hover {
- background: var(--base100);
-}
-
-.button.quiet:active {
- background: var(--base200);
-}
-
-.button.danger {
- color: var(--light50);
- background: var(--red800);
-}
-
-.button.danger:hover {
- color: var(--light50);
- background: var(--red900);
-}
-
-.button.danger:active {
- color: var(--light50);
- background: var(--red1000);
-}
-
-.button.size-sm {
- font-size: var(--font-size-sm);
- height: calc(var(--base-height) * 0.75);
- padding: 0 calc(var(--size600) * 0.75);
-}
-
-.button.size-md {
- font-size: var(--font-size-md);
-}
-
-.button.size-lg {
- font-size: var(--font-size-lg);
- height: calc(var(--base-height) * 1.25);
- padding: 0 calc(var(--size600) * 1.25);
-}
diff --git a/src/components/common/LinkButton.tsx b/src/components/common/LinkButton.tsx
index 3aa2a76a..35292ba4 100644
--- a/src/components/common/LinkButton.tsx
+++ b/src/components/common/LinkButton.tsx
@@ -1,30 +1,41 @@
-import { ReactNode } from 'react';
-import classNames from 'classnames';
+import { Button, type ButtonProps } from '@umami/react-zen';
import Link from 'next/link';
+import type { ReactNode } from 'react';
import { useLocale } from '@/components/hooks';
-import styles from './LinkButton.module.css';
-export interface LinkButtonProps {
+export interface LinkButtonProps extends ButtonProps {
href: string;
- className?: string;
- variant?: string;
+ target?: string;
scroll?: boolean;
+ variant?: any;
+ prefetch?: boolean;
+ asAnchor?: boolean;
children?: ReactNode;
}
-export function LinkButton({ href, className, variant, scroll = true, children }: LinkButtonProps) {
+export function LinkButton({
+ href,
+ variant,
+ scroll = true,
+ target,
+ prefetch,
+ children,
+ asAnchor,
+ ...props
+}: LinkButtonProps) {
const { dir } = useLocale();
return (
-
- {children}
-
+
+ {asAnchor ? (
+
+ {children}
+
+ ) : (
+
+ {children}
+
+ )}
+
);
}
-
-export default LinkButton;
diff --git a/src/components/common/LoadingPanel.module.css b/src/components/common/LoadingPanel.module.css
deleted file mode 100644
index 00d6cbb4..00000000
--- a/src/components/common/LoadingPanel.module.css
+++ /dev/null
@@ -1,16 +0,0 @@
-.panel {
- display: flex;
- flex-direction: column;
- position: relative;
- flex: 1;
- height: 100%;
-}
-
-.loading {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- margin: auto;
-}
diff --git a/src/components/common/LoadingPanel.tsx b/src/components/common/LoadingPanel.tsx
index 4d27618a..fb37e140 100644
--- a/src/components/common/LoadingPanel.tsx
+++ b/src/components/common/LoadingPanel.tsx
@@ -1,36 +1,71 @@
-import { ReactNode } from 'react';
-import classNames from 'classnames';
-import { Loading } from 'react-basics';
-import ErrorMessage from '@/components/common/ErrorMessage';
-import Empty from '@/components/common/Empty';
-import styles from './LoadingPanel.module.css';
+import { Column, type ColumnProps, Loading } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { Empty } from '@/components/common/Empty';
+import { ErrorMessage } from '@/components/common/ErrorMessage';
+
+export interface LoadingPanelProps extends ColumnProps {
+ data?: any;
+ error?: unknown;
+ isEmpty?: boolean;
+ isLoading?: boolean;
+ isFetching?: boolean;
+ loadingIcon?: 'dots' | 'spinner';
+ loadingPlacement?: 'center' | 'absolute' | 'inline';
+ renderEmpty?: () => ReactNode;
+ children: ReactNode;
+}
export function LoadingPanel({
data,
error,
- isFetched,
+ isEmpty,
isLoading,
+ isFetching,
loadingIcon = 'dots',
- className,
+ loadingPlacement = 'absolute',
+ renderEmpty = () => ,
children,
-}: {
- data?: any;
- error?: Error;
- isFetched?: boolean;
- isLoading?: boolean;
- loadingIcon?: 'dots' | 'spinner';
- isEmpty?: boolean;
- className?: string;
- children: ReactNode;
-}) {
- const isEmpty = !isLoading && isFetched && data && Array.isArray(data) && data.length === 0;
+ ...props
+}: LoadingPanelProps): ReactNode {
+ const empty = isEmpty ?? checkEmpty(data);
- return (
-
- {isLoading && !isFetched && }
- {error && }
- {!error && isEmpty && }
- {!error && !isEmpty && data && children}
-
- );
+ // Show loading spinner only if no data exists
+ if (isLoading || isFetching) {
+ return (
+
+
+
+ );
+ }
+
+ // Show error
+ if (error) {
+ return ;
+ }
+
+ // Show empty state (once loaded)
+ if (!error && !isLoading && !isFetching && empty) {
+ return renderEmpty();
+ }
+
+ // Show main content when data exists
+ if (!isLoading && !isFetching && !error && !empty) {
+ return children;
+ }
+
+ return null;
+}
+
+function checkEmpty(data: any) {
+ if (!data) return false;
+
+ if (Array.isArray(data)) {
+ return data.length <= 0;
+ }
+
+ if (typeof data === 'object') {
+ return Object.keys(data).length <= 0;
+ }
+
+ return !!data;
}
diff --git a/src/components/common/MobileMenu.module.css b/src/components/common/MobileMenu.module.css
deleted file mode 100644
index 63592bec..00000000
--- a/src/components/common/MobileMenu.module.css
+++ /dev/null
@@ -1,39 +0,0 @@
-.menu {
- position: fixed;
- top: 60px;
- left: 0;
- right: 0;
- bottom: 0;
- margin: auto;
- display: flex;
- flex-direction: column;
- background-color: var(--base50);
- z-index: var(--z-index-popup);
- overflow: auto;
-}
-
-.items {
- display: flex;
- flex-direction: column;
-}
-
-.item {
- font-size: var(--font-size-lg);
- font-weight: 700;
- line-height: 80px;
- padding: 0 40px;
-}
-
-a.item {
- color: var(--base600);
-}
-
-a.item.selected,
-.submenu a.item.selected {
- color: var(--base900);
-}
-
-.submenu a.item {
- color: var(--base600);
- margin-inline-start: 40px;
-}
diff --git a/src/components/common/MobileMenu.tsx b/src/components/common/MobileMenu.tsx
deleted file mode 100644
index e14f0b83..00000000
--- a/src/components/common/MobileMenu.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { createPortal } from 'react-dom';
-import classNames from 'classnames';
-import { usePathname } from 'next/navigation';
-import Link from 'next/link';
-import styles from './MobileMenu.module.css';
-
-export function MobileMenu({
- items = [],
- onClose,
-}: {
- items: any[];
- className?: string;
- onClose: () => void;
-}): any {
- const pathname = usePathname();
-
- const Items = ({ items, className }: { items: any[]; className?: string }): any => (
-
- {items.map(({ label, url, children }: { label: string; url: string; children: any[] }) => {
- const selected = pathname.startsWith(url);
-
- return (
- <>
-
- {label}
-
- {children && }
- >
- );
- })}
-
- );
-
- return createPortal(
-
-
-
,
- document.body,
- );
-}
-
-export default MobileMenu;
diff --git a/src/components/common/PageBody.tsx b/src/components/common/PageBody.tsx
new file mode 100644
index 00000000..f07e589b
--- /dev/null
+++ b/src/components/common/PageBody.tsx
@@ -0,0 +1,42 @@
+'use client';
+import { AlertBanner, Column, type ColumnProps, Loading } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { useMessages } from '@/components/hooks';
+
+const DEFAULT_WIDTH = '1320px';
+
+export function PageBody({
+ maxWidth = DEFAULT_WIDTH,
+ error,
+ isLoading,
+ children,
+ ...props
+}: {
+ maxWidth?: string;
+ error?: unknown;
+ isLoading?: boolean;
+ children?: ReactNode;
+} & ColumnProps) {
+ const { formatMessage, messages } = useMessages();
+
+ if (error) {
+ return ;
+ }
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/common/PageHeader.tsx b/src/components/common/PageHeader.tsx
new file mode 100644
index 00000000..92167888
--- /dev/null
+++ b/src/components/common/PageHeader.tsx
@@ -0,0 +1,58 @@
+import { Column, Grid, Heading, Icon, Row, Text } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { LinkButton } from './LinkButton';
+
+export function PageHeader({
+ title,
+ description,
+ label,
+ icon,
+ showBorder = true,
+ titleHref,
+ children,
+}: {
+ title: string;
+ description?: string;
+ label?: ReactNode;
+ icon?: ReactNode;
+ showBorder?: boolean;
+ titleHref?: string;
+ allowEdit?: boolean;
+ className?: string;
+ children?: ReactNode;
+}) {
+ return (
+
+
+ {label}
+
+ {icon && (
+
+ {icon}
+
+ )}
+ {title && titleHref ? (
+
+ {title}
+
+ ) : (
+ title && {title}
+ )}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/components/common/Pager.module.css b/src/components/common/Pager.module.css
deleted file mode 100644
index c9330c32..00000000
--- a/src/components/common/Pager.module.css
+++ /dev/null
@@ -1,32 +0,0 @@
-.pager {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- align-items: center;
-}
-
-.nav {
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.text {
- font-size: var(--font-size-md);
- margin: 0 16px;
- justify-content: center;
-}
-
-.count {
- color: var(--base600);
- font-weight: 700;
-}
-
-@media only screen and (max-width: 992px) {
- .pager {
- grid-template-columns: repeat(2, 1fr);
- }
-
- .nav {
- justify-content: flex-end;
- }
-}
diff --git a/src/components/common/Pager.tsx b/src/components/common/Pager.tsx
index b33d2236..c65e2f6a 100644
--- a/src/components/common/Pager.tsx
+++ b/src/components/common/Pager.tsx
@@ -1,19 +1,18 @@
-import classNames from 'classnames';
-import { Button, Icon, Icons } from 'react-basics';
+import { Button, Icon, Row, Text } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
-import styles from './Pager.module.css';
+import { ChevronRight } from '@/components/icons';
export interface PagerProps {
- page: number;
- pageSize: number;
- count: number;
+ page: string | number;
+ pageSize: string | number;
+ count: string | number;
onPageChange: (nextPage: number) => void;
className?: string;
}
-export function Pager({ page, pageSize, count, onPageChange, className }: PagerProps) {
+export function Pager({ page, pageSize, count, onPageChange }: PagerProps) {
const { formatMessage, labels } = useMessages();
- const maxPage = pageSize && count ? Math.ceil(count / pageSize) : 0;
+ const maxPage = pageSize && count ? Math.ceil(+count / +pageSize) : 0;
const lastPage = page === maxPage;
const firstPage = page === 1;
@@ -22,7 +21,7 @@ export function Pager({ page, pageSize, count, onPageChange, className }: PagerP
}
const handlePageChange = (value: number) => {
- const nextPage = page + value;
+ const nextPage = +page + +value;
if (nextPage > 0 && nextPage <= maxPage) {
onPageChange(nextPage);
@@ -34,26 +33,28 @@ export function Pager({ page, pageSize, count, onPageChange, className }: PagerP
}
return (
-
-
{formatMessage(labels.numberOfRecords, { x: count })}
-
-
handlePageChange(-1)} disabled={firstPage}>
-
-
-
-
-
- {formatMessage(labels.pageOf, { current: page, total: maxPage })}
-
-
handlePageChange(1)} disabled={lastPage}>
-
-
-
-
-
-
-
+
+ {formatMessage(labels.numberOfRecords, { x: count.toLocaleString() })}
+
+
+ {formatMessage(labels.pageOf, {
+ current: page.toLocaleString(),
+ total: maxPage.toLocaleString(),
+ })}
+
+
+ handlePageChange(-1)} isDisabled={firstPage}>
+
+
+
+
+ handlePageChange(1)} isDisabled={lastPage}>
+
+
+
+
+
+
+
);
}
-
-export default Pager;
diff --git a/src/components/common/Panel.tsx b/src/components/common/Panel.tsx
new file mode 100644
index 00000000..bb667465
--- /dev/null
+++ b/src/components/common/Panel.tsx
@@ -0,0 +1,64 @@
+import {
+ Button,
+ Column,
+ type ColumnProps,
+ Heading,
+ Icon,
+ Row,
+ Tooltip,
+ TooltipTrigger,
+} from '@umami/react-zen';
+import { useState } from 'react';
+import { useMessages } from '@/components/hooks';
+import { Maximize, X } from '@/components/icons';
+
+export interface PanelProps extends ColumnProps {
+ title?: string;
+ allowFullscreen?: boolean;
+}
+
+const fullscreenStyles = {
+ position: 'fixed',
+ width: '100%',
+ height: '100%',
+ top: 0,
+ left: 0,
+ border: 'none',
+ zIndex: 9999,
+} as any;
+
+export function Panel({ title, allowFullscreen, style, children, ...props }: PanelProps) {
+ const { formatMessage, labels } = useMessages();
+ const [isFullscreen, setIsFullscreen] = useState(false);
+
+ const handleFullscreen = () => {
+ setIsFullscreen(!isFullscreen);
+ };
+
+ return (
+
+ {title && {title} }
+ {allowFullscreen && (
+
+
+
+ {isFullscreen ? : }
+
+ {formatMessage(labels.maximize)}
+
+
+ )}
+ {children}
+
+ );
+}
diff --git a/src/components/common/SectionHeader.tsx b/src/components/common/SectionHeader.tsx
new file mode 100644
index 00000000..5b911efb
--- /dev/null
+++ b/src/components/common/SectionHeader.tsx
@@ -0,0 +1,28 @@
+import { Heading, Icon, Row, type RowProps, Text } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+
+export function SectionHeader({
+ title,
+ description,
+ icon,
+ children,
+ ...props
+}: {
+ title?: string;
+ description?: string;
+ icon?: ReactNode;
+ allowEdit?: boolean;
+ className?: string;
+ children?: ReactNode;
+} & RowProps) {
+ return (
+
+
+ {icon && {icon} }
+ {title && {title} }
+ {description && {description} }
+
+ {children}
+
+ );
+}
diff --git a/src/components/common/SideMenu.tsx b/src/components/common/SideMenu.tsx
new file mode 100644
index 00000000..92ff798a
--- /dev/null
+++ b/src/components/common/SideMenu.tsx
@@ -0,0 +1,80 @@
+import {
+ Column,
+ Heading,
+ IconLabel,
+ NavMenu,
+ NavMenuGroup,
+ NavMenuItem,
+ type NavMenuProps,
+ Row,
+} from '@umami/react-zen';
+import Link from 'next/link';
+
+interface SideMenuData {
+ id: string;
+ label: string;
+ icon?: any;
+ path: string;
+}
+
+interface SideMenuItems {
+ label?: string;
+ items: SideMenuData[];
+}
+
+export interface SideMenuProps extends NavMenuProps {
+ items: SideMenuItems[];
+ title?: string;
+ selectedKey?: string;
+ allowMinimize?: boolean;
+}
+
+export function SideMenu({
+ items = [],
+ title,
+ selectedKey,
+ allowMinimize,
+ ...props
+}: SideMenuProps) {
+ const renderItems = (items: SideMenuData[]) => {
+ return items?.map(({ id, label, icon, path }) => {
+ const isSelected = selectedKey === id;
+
+ return (
+
+
+ {label}
+
+
+ );
+ });
+ };
+
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {items?.map(({ label, items }, index) => {
+ if (label) {
+ return (
+
+ {renderItems(items)}
+
+ );
+ }
+ return null;
+ })}
+
+
+ );
+}
diff --git a/src/components/common/TypeConfirmationForm.tsx b/src/components/common/TypeConfirmationForm.tsx
index 9ef5b30a..1121fa7d 100644
--- a/src/components/common/TypeConfirmationForm.tsx
+++ b/src/components/common/TypeConfirmationForm.tsx
@@ -2,11 +2,10 @@ import {
Button,
Form,
FormButtons,
- FormRow,
- FormInput,
+ FormField,
+ FormSubmitButton,
TextField,
- SubmitButton,
-} from 'react-basics';
+} from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
export function TypeConfirmationForm({
@@ -20,38 +19,37 @@ export function TypeConfirmationForm({
}: {
confirmationValue: string;
buttonLabel?: string;
- buttonVariant?: 'none' | 'primary' | 'secondary' | 'quiet' | 'danger';
+ buttonVariant?: 'primary' | 'outline' | 'quiet' | 'danger' | 'zero';
isLoading?: boolean;
error?: string | Error;
onConfirm?: () => void;
onClose?: () => void;
}) {
- const { formatMessage, labels, messages } = useMessages();
-
+ const { formatMessage, labels, messages, getErrorMessage } = useMessages();
if (!confirmationValue) {
return null;
}
return (
-
);
}
-
-export default TypeConfirmationForm;
diff --git a/src/components/common/TypeIcon.tsx b/src/components/common/TypeIcon.tsx
index 2a180445..8894b3a9 100644
--- a/src/components/common/TypeIcon.tsx
+++ b/src/components/common/TypeIcon.tsx
@@ -1,4 +1,5 @@
-import { ReactNode } from 'react';
+import { Row } from '@umami/react-zen';
+import type { ReactNode } from 'react';
export function TypeIcon({
type,
@@ -10,11 +11,11 @@ export function TypeIcon({
children?: ReactNode;
}) {
return (
- <>
+
{
e.currentTarget.src = `${process.env.basePath || ''}/images/${type}/unknown.png`;
}}
@@ -23,8 +24,6 @@ export function TypeIcon({
height={type === 'country' ? undefined : 16}
/>
{children}
- >
+
);
}
-
-export default TypeIcon;
diff --git a/src/components/declarations.d.ts b/src/components/declarations.d.ts
deleted file mode 100644
index ca55157b..00000000
--- a/src/components/declarations.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare module '*.css';
-declare module '*.svg';
-declare module '*.json';
-declare module 'uuid';
diff --git a/src/components/hooks/context/useLink.ts b/src/components/hooks/context/useLink.ts
new file mode 100644
index 00000000..8766bbb6
--- /dev/null
+++ b/src/components/hooks/context/useLink.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { LinkContext } from '@/app/(main)/links/LinkProvider';
+
+export function useLink() {
+ return useContext(LinkContext);
+}
diff --git a/src/components/hooks/context/usePixel.ts b/src/components/hooks/context/usePixel.ts
new file mode 100644
index 00000000..69cad6f5
--- /dev/null
+++ b/src/components/hooks/context/usePixel.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { PixelContext } from '@/app/(main)/pixels/PixelProvider';
+
+export function usePixel() {
+ return useContext(PixelContext);
+}
diff --git a/src/components/hooks/context/useTeam.ts b/src/components/hooks/context/useTeam.ts
new file mode 100644
index 00000000..95ff4bee
--- /dev/null
+++ b/src/components/hooks/context/useTeam.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { TeamContext } from '@/app/(main)/teams/TeamProvider';
+
+export function useTeam() {
+ return useContext(TeamContext);
+}
diff --git a/src/components/hooks/context/useUser.ts b/src/components/hooks/context/useUser.ts
new file mode 100644
index 00000000..fa97ea9d
--- /dev/null
+++ b/src/components/hooks/context/useUser.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { UserContext } from '@/app/(main)/admin/users/[userId]/UserProvider';
+
+export function useUser() {
+ return useContext(UserContext);
+}
diff --git a/src/components/hooks/context/useWebsite.ts b/src/components/hooks/context/useWebsite.ts
new file mode 100644
index 00000000..3d4be27f
--- /dev/null
+++ b/src/components/hooks/context/useWebsite.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { WebsiteContext } from '@/app/(main)/websites/WebsiteProvider';
+
+export function useWebsite() {
+ return useContext(WebsiteContext);
+}
diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts
index 7c16eeee..e8e5c135 100644
--- a/src/components/hooks/index.ts
+++ b/src/components/hooks/index.ts
@@ -1,48 +1,84 @@
-export * from './queries/useConfig';
-export * from './queries/useEventDataEvents';
-export * from './queries/useEventDataProperties';
-export * from './queries/useEventDataValues';
-export * from './queries/useLogin';
-export * from './queries/useRealtime';
-export * from './queries/useReport';
-export * from './queries/useReports';
-export * from './queries/useSessionActivity';
-export * from './queries/useSessionData';
-export * from './queries/useSessionDataProperties';
-export * from './queries/useSessionDataValues';
-export * from './queries/useWebsiteSession';
-export * from './queries/useWebsiteSessions';
-export * from './queries/useWebsiteSessionsWeekly';
-export * from './queries/useShareToken';
-export * from './queries/useTeam';
-export * from './queries/useTeams';
-export * from './queries/useTeamWebsites';
-export * from './queries/useTeamMembers';
-export * from './queries/useUser';
-export * from './queries/useUsers';
-export * from './queries/useWebsite';
-export * from './queries/useWebsites';
-export * from './queries/useWebsiteEvents';
-export * from './queries/useWebsiteEventsSeries';
-export * from './queries/useWebsiteMetrics';
-export * from './queries/useWebsiteValues';
+'use client';
+
+// Context hooks
+export * from './context/useLink';
+export * from './context/usePixel';
+export * from './context/useTeam';
+export * from './context/useUser';
+export * from './context/useWebsite';
+
+// Query hooks
+export * from './queries/useActiveUsersQuery';
+export * from './queries/useDateRangeQuery';
+export * from './queries/useDeleteQuery';
+export * from './queries/useEventDataEventsQuery';
+export * from './queries/useEventDataPropertiesQuery';
+export * from './queries/useEventDataQuery';
+export * from './queries/useEventDataValuesQuery';
+export * from './queries/useLinkQuery';
+export * from './queries/useLinksQuery';
+export * from './queries/useLoginQuery';
+export * from './queries/usePixelQuery';
+export * from './queries/usePixelsQuery';
+export * from './queries/useRealtimeQuery';
+export * from './queries/useReportQuery';
+export * from './queries/useReportsQuery';
+export * from './queries/useResultQuery';
+export * from './queries/useSessionActivityQuery';
+export * from './queries/useSessionDataPropertiesQuery';
+export * from './queries/useSessionDataQuery';
+export * from './queries/useSessionDataValuesQuery';
+export * from './queries/useShareTokenQuery';
+export * from './queries/useTeamMembersQuery';
+export * from './queries/useTeamQuery';
+export * from './queries/useTeamsQuery';
+export * from './queries/useTeamWebsitesQuery';
+export * from './queries/useUpdateQuery';
+export * from './queries/useUserQuery';
+export * from './queries/useUsersQuery';
+export * from './queries/useUserTeamsQuery';
+export * from './queries/useUserWebsitesQuery';
+export * from './queries/useWebsiteCohortQuery';
+export * from './queries/useWebsiteCohortsQuery';
+export * from './queries/useWebsiteEventsQuery';
+export * from './queries/useWebsiteEventsSeriesQuery';
+export * from './queries/useWebsiteExpandedMetricsQuery';
+export * from './queries/useWebsiteMetricsQuery';
+export * from './queries/useWebsitePageviewsQuery';
+export * from './queries/useWebsiteQuery';
+export * from './queries/useWebsiteSegmentQuery';
+export * from './queries/useWebsiteSegmentsQuery';
+export * from './queries/useWebsiteSessionQuery';
+export * from './queries/useWebsiteSessionStatsQuery';
+export * from './queries/useWebsiteSessionsQuery';
+export * from './queries/useWebsiteStatsQuery';
+export * from './queries/useWebsitesQuery';
+export * from './queries/useWebsiteValuesQuery';
+export * from './queries/useWeeklyTrafficQuery';
+
+// Regular hooks
export * from './useApi';
+export * from './useConfig';
export * from './useCountryNames';
+export * from './useDateParameters';
export * from './useDateRange';
export * from './useDocumentClick';
export * from './useEscapeKey';
export * from './useFields';
+export * from './useFilterParameters';
export * from './useFilters';
export * from './useForceUpdate';
export * from './useFormat';
+export * from './useGlobalState';
export * from './useLanguageNames';
export * from './useLocale';
export * from './useMessages';
+export * from './useMobile';
export * from './useModified';
export * from './useNavigation';
export * from './usePagedQuery';
+export * from './usePageParameters';
export * from './useRegionNames';
+export * from './useSlug';
export * from './useSticky';
-export * from './useTeamUrl';
-export * from './useTheme';
export * from './useTimezone';
diff --git a/src/components/hooks/queries/useActiveUsersQuery.ts b/src/components/hooks/queries/useActiveUsersQuery.ts
new file mode 100644
index 00000000..42867c19
--- /dev/null
+++ b/src/components/hooks/queries/useActiveUsersQuery.ts
@@ -0,0 +1,12 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+
+export function useActyiveUsersQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ return useQuery({
+ queryKey: ['websites:active', websiteId],
+ queryFn: () => get(`/websites/${websiteId}/active`),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useConfig.ts b/src/components/hooks/queries/useConfig.ts
deleted file mode 100644
index 223f4550..00000000
--- a/src/components/hooks/queries/useConfig.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { useEffect } from 'react';
-import useStore, { setConfig } from '@/store/app';
-import { getConfig } from '@/app/actions/getConfig';
-
-export function useConfig() {
- const { config } = useStore();
-
- async function loadConfig() {
- setConfig(await getConfig());
- }
-
- useEffect(() => {
- if (!config) {
- loadConfig();
- }
- }, []);
-
- return config;
-}
-
-export default useConfig;
diff --git a/src/components/hooks/queries/useDateRangeQuery.ts b/src/components/hooks/queries/useDateRangeQuery.ts
new file mode 100644
index 00000000..84b7eec7
--- /dev/null
+++ b/src/components/hooks/queries/useDateRangeQuery.ts
@@ -0,0 +1,23 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+
+type DateRange = {
+ startDate?: string;
+ endDate?: string;
+};
+
+export function useDateRangeQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+
+ const { data } = useQuery({
+ queryKey: ['date-range', websiteId],
+ queryFn: () => get(`/websites/${websiteId}/daterange`),
+ enabled: !!websiteId,
+ ...options,
+ });
+
+ return {
+ startDate: data?.startDate ? new Date(data.startDate) : null,
+ endDate: data?.endDate ? new Date(data.endDate) : null,
+ };
+}
diff --git a/src/components/hooks/queries/useDeleteQuery.ts b/src/components/hooks/queries/useDeleteQuery.ts
new file mode 100644
index 00000000..556231a9
--- /dev/null
+++ b/src/components/hooks/queries/useDeleteQuery.ts
@@ -0,0 +1,12 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useDeleteQuery(path: string, params?: Record) {
+ const { del, useMutation } = useApi();
+ const query = useMutation({
+ mutationFn: () => del(path, params),
+ });
+ const { touch } = useModified();
+
+ return { ...query, touch };
+}
diff --git a/src/components/hooks/queries/useEventDataEvents.ts b/src/components/hooks/queries/useEventDataEvents.ts
deleted file mode 100644
index 5cad9916..00000000
--- a/src/components/hooks/queries/useEventDataEvents.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useApi } from '../useApi';
-import { UseQueryOptions } from '@tanstack/react-query';
-import { useFilterParams } from '../useFilterParams';
-
-export function useEventDataEvents(
- websiteId: string,
- options?: Omit,
-) {
- const { get, useQuery } = useApi();
- const params = useFilterParams(websiteId);
-
- return useQuery({
- queryKey: ['websites:event-data:events', { websiteId, ...params }],
- queryFn: () => get(`/websites/${websiteId}/event-data/events`, { ...params }),
- enabled: !!websiteId,
- ...options,
- });
-}
-
-export default useEventDataEvents;
diff --git a/src/components/hooks/queries/useEventDataEventsQuery.ts b/src/components/hooks/queries/useEventDataEventsQuery.ts
new file mode 100644
index 00000000..1401989f
--- /dev/null
+++ b/src/components/hooks/queries/useEventDataEventsQuery.ts
@@ -0,0 +1,27 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useEventDataEventsQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'websites:event-data:events',
+ { websiteId, startAt, endAt, unit, timezone, ...filters },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/event-data/events`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useEventDataProperties.ts b/src/components/hooks/queries/useEventDataProperties.ts
deleted file mode 100644
index b841a8f4..00000000
--- a/src/components/hooks/queries/useEventDataProperties.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { UseQueryOptions } from '@tanstack/react-query';
-import { useApi } from '../useApi';
-import { useFilterParams } from '../useFilterParams';
-
-export function useEventDataProperties(
- websiteId: string,
- options?: Omit,
-) {
- const { get, useQuery } = useApi();
- const params = useFilterParams(websiteId);
-
- return useQuery({
- queryKey: ['websites:event-data:properties', { websiteId, ...params }],
- queryFn: () => get(`/websites/${websiteId}/event-data/properties`, { ...params }),
- enabled: !!websiteId,
- ...options,
- });
-}
-
-export default useEventDataProperties;
diff --git a/src/components/hooks/queries/useEventDataPropertiesQuery.ts b/src/components/hooks/queries/useEventDataPropertiesQuery.ts
new file mode 100644
index 00000000..dfa6e929
--- /dev/null
+++ b/src/components/hooks/queries/useEventDataPropertiesQuery.ts
@@ -0,0 +1,27 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useEventDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'websites:event-data:properties',
+ { websiteId, startAt, endAt, unit, timezone, ...filters },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/event-data/properties`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useEventDataQuery.ts b/src/components/hooks/queries/useEventDataQuery.ts
new file mode 100644
index 00000000..2ccbd634
--- /dev/null
+++ b/src/components/hooks/queries/useEventDataQuery.ts
@@ -0,0 +1,27 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useEventDataQuery(websiteId: string, eventId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const params = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'websites:event-data',
+ { websiteId, eventId, startAt, endAt, unit, timezone, ...params },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/event-data/${eventId}`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...params,
+ }),
+ enabled: !!(websiteId && eventId),
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useEventDataValues.ts b/src/components/hooks/queries/useEventDataValues.ts
deleted file mode 100644
index de6783a0..00000000
--- a/src/components/hooks/queries/useEventDataValues.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { UseQueryOptions } from '@tanstack/react-query';
-import { useApi } from '../useApi';
-import { useFilterParams } from '../useFilterParams';
-
-export function useEventDataValues(
- websiteId: string,
- eventName: string,
- propertyName: string,
- options?: Omit,
-) {
- const { get, useQuery } = useApi();
- const params = useFilterParams(websiteId);
-
- return useQuery({
- queryKey: ['websites:event-data:values', { websiteId, eventName, propertyName, ...params }],
- queryFn: () =>
- get(`/websites/${websiteId}/event-data/values`, { ...params, eventName, propertyName }),
- enabled: !!(websiteId && propertyName),
- ...options,
- });
-}
-
-export default useEventDataValues;
diff --git a/src/components/hooks/queries/useEventDataValuesQuery.ts b/src/components/hooks/queries/useEventDataValuesQuery.ts
new file mode 100644
index 00000000..6529e142
--- /dev/null
+++ b/src/components/hooks/queries/useEventDataValuesQuery.ts
@@ -0,0 +1,34 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useEventDataValuesQuery(
+ websiteId: string,
+ event: string,
+ propertyName: string,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'websites:event-data:values',
+ { websiteId, event, propertyName, startAt, endAt, unit, timezone, ...filters },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/event-data/values`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ event,
+ propertyName,
+ }),
+ enabled: !!(websiteId && propertyName),
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useLinkQuery.ts b/src/components/hooks/queries/useLinkQuery.ts
new file mode 100644
index 00000000..2a5d4a9b
--- /dev/null
+++ b/src/components/hooks/queries/useLinkQuery.ts
@@ -0,0 +1,15 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useLinkQuery(linkId: string) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`link:${linkId}`);
+
+ return useQuery({
+ queryKey: ['link', { linkId, modified }],
+ queryFn: () => {
+ return get(`/links/${linkId}`);
+ },
+ enabled: !!linkId,
+ });
+}
diff --git a/src/components/hooks/queries/useLinksQuery.ts b/src/components/hooks/queries/useLinksQuery.ts
new file mode 100644
index 00000000..ebf945fb
--- /dev/null
+++ b/src/components/hooks/queries/useLinksQuery.ts
@@ -0,0 +1,17 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useLinksQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) {
+ const { modified } = useModified('links');
+ const { get } = useApi();
+
+ return usePagedQuery({
+ queryKey: ['links', { teamId, modified }],
+ queryFn: pageParams => {
+ return get(teamId ? `/teams/${teamId}/links` : '/links', pageParams);
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useLogin.ts b/src/components/hooks/queries/useLoginQuery.ts
similarity index 58%
rename from src/components/hooks/queries/useLogin.ts
rename to src/components/hooks/queries/useLoginQuery.ts
index 635c5000..a64b7844 100644
--- a/src/components/hooks/queries/useLogin.ts
+++ b/src/components/hooks/queries/useLoginQuery.ts
@@ -1,15 +1,11 @@
-import { UseQueryResult } from '@tanstack/react-query';
-import useStore, { setUser } from '@/store/app';
+import { setUser, useApp } from '@/store/app';
import { useApi } from '../useApi';
const selector = (state: { user: any }) => state.user;
-export function useLogin(): {
- user: any;
- setUser: (data: any) => void;
-} & UseQueryResult {
+export function useLoginQuery() {
const { post, useQuery } = useApi();
- const user = useStore(selector);
+ const user = useApp(selector);
const query = useQuery({
queryKey: ['login'],
@@ -25,5 +21,3 @@ export function useLogin(): {
return { user, setUser, ...query };
}
-
-export default useLogin;
diff --git a/src/components/hooks/queries/usePixelQuery.ts b/src/components/hooks/queries/usePixelQuery.ts
new file mode 100644
index 00000000..7fd83c27
--- /dev/null
+++ b/src/components/hooks/queries/usePixelQuery.ts
@@ -0,0 +1,15 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function usePixelQuery(pixelId: string) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`pixel:${pixelId}`);
+
+ return useQuery({
+ queryKey: ['pixel', { pixelId, modified }],
+ queryFn: () => {
+ return get(`/pixels/${pixelId}`);
+ },
+ enabled: !!pixelId,
+ });
+}
diff --git a/src/components/hooks/queries/usePixelsQuery.ts b/src/components/hooks/queries/usePixelsQuery.ts
new file mode 100644
index 00000000..c431179b
--- /dev/null
+++ b/src/components/hooks/queries/usePixelsQuery.ts
@@ -0,0 +1,17 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function usePixelsQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) {
+ const { modified } = useModified('pixels');
+ const { get } = useApi();
+
+ return usePagedQuery({
+ queryKey: ['pixels', { teamId, modified }],
+ queryFn: pageParams => {
+ return get(teamId ? `/teams/${teamId}/pixels` : '/pixels', pageParams);
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useRealtime.ts b/src/components/hooks/queries/useRealtimeQuery.ts
similarity index 50%
rename from src/components/hooks/queries/useRealtime.ts
rename to src/components/hooks/queries/useRealtimeQuery.ts
index 670b23be..1a5bd1c5 100644
--- a/src/components/hooks/queries/useRealtime.ts
+++ b/src/components/hooks/queries/useRealtimeQuery.ts
@@ -1,15 +1,13 @@
-import { useTimezone } from '@/components/hooks/useTimezone';
import { REALTIME_INTERVAL } from '@/lib/constants';
-import { RealtimeData } from '@/lib/types';
+import type { RealtimeData } from '@/lib/types';
import { useApi } from '../useApi';
-export function useRealtime(websiteId: string) {
+export function useRealtimeQuery(websiteId: string) {
const { get, useQuery } = useApi();
- const { timezone } = useTimezone();
const { data, isLoading, error } = useQuery({
- queryKey: ['realtime', { websiteId, timezone }],
+ queryKey: ['realtime', { websiteId }],
queryFn: async () => {
- return get(`/realtime/${websiteId}`, { timezone });
+ return get(`/realtime/${websiteId}`);
},
enabled: !!websiteId,
refetchInterval: REALTIME_INTERVAL,
@@ -17,5 +15,3 @@ export function useRealtime(websiteId: string) {
return { data, isLoading, error };
}
-
-export default useRealtime;
diff --git a/src/components/hooks/queries/useReport.ts b/src/components/hooks/queries/useReport.ts
deleted file mode 100644
index 45aea19c..00000000
--- a/src/components/hooks/queries/useReport.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { produce } from 'immer';
-import { useCallback, useEffect, useState } from 'react';
-import { useApi } from '../useApi';
-import { useTimezone } from '../useTimezone';
-import { useMessages } from '../useMessages';
-import { parseDateRange } from '@/lib/date';
-
-export function useReport(
- reportId: string,
- defaultParameters?: { type: string; parameters: { [key: string]: any } },
-) {
- const [report, setReport] = useState(null);
- const [isRunning, setIsRunning] = useState(false);
- const { get, post } = useApi();
- const { timezone } = useTimezone();
- const { formatMessage, labels } = useMessages();
-
- const baseParameters = {
- name: formatMessage(labels.untitled),
- description: '',
- parameters: {},
- };
-
- const loadReport = async (id: string) => {
- const data: any = await get(`/reports/${id}`);
-
- const { dateRange } = data?.parameters || {};
-
- data.parameters = {
- ...defaultParameters?.parameters,
- ...data.parameters,
- dateRange: parseDateRange(dateRange.value),
- };
-
- setReport(data);
- };
-
- const runReport = useCallback(
- async (parameters: { [key: string]: any }) => {
- setIsRunning(true);
-
- const { type } = report;
-
- const data = await post(`/reports/${type}`, { ...parameters, timezone });
-
- setReport(
- produce((state: any) => {
- state.parameters = { ...defaultParameters?.parameters, ...parameters };
- state.data = data;
-
- return state;
- }),
- );
-
- setIsRunning(false);
- },
- [report, timezone],
- );
-
- const updateReport = useCallback(
- async (data: { [x: string]: any; parameters: any }) => {
- setReport(
- produce((state: any) => {
- const { parameters, ...rest } = data;
-
- if (parameters) {
- state.parameters = {
- ...defaultParameters?.parameters,
- ...state.parameters,
- ...parameters,
- };
- }
-
- for (const key in rest) {
- state[key] = rest[key];
- }
-
- return state;
- }),
- );
- },
- [report],
- );
-
- useEffect(() => {
- if (!reportId) {
- setReport({ ...baseParameters, ...defaultParameters });
- } else {
- loadReport(reportId);
- }
- }, [reportId]);
-
- return { report, runReport, updateReport, isRunning };
-}
-
-export default useReport;
diff --git a/src/components/hooks/queries/useReportQuery.ts b/src/components/hooks/queries/useReportQuery.ts
new file mode 100644
index 00000000..6973e2d3
--- /dev/null
+++ b/src/components/hooks/queries/useReportQuery.ts
@@ -0,0 +1,15 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useReportQuery(reportId: string) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`report:${reportId}`);
+
+ return useQuery({
+ queryKey: ['report', { reportId, modified }],
+ queryFn: () => {
+ return get(`/reports/${reportId}`);
+ },
+ enabled: !!reportId,
+ });
+}
diff --git a/src/components/hooks/queries/useReports.ts b/src/components/hooks/queries/useReports.ts
deleted file mode 100644
index 21db1536..00000000
--- a/src/components/hooks/queries/useReports.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import useApi from '../useApi';
-import usePagedQuery from '../usePagedQuery';
-import useModified from '../useModified';
-
-export function useReports({ websiteId, teamId }: { websiteId?: string; teamId?: string }) {
- const { modified } = useModified(`reports`);
- const { get, del, useMutation } = useApi();
- const queryResult = usePagedQuery({
- queryKey: ['reports', { websiteId, teamId, modified }],
- queryFn: (params: any) => {
- return get('/reports', { websiteId, teamId, ...params });
- },
- });
- const { mutate } = useMutation({ mutationFn: (reportId: string) => del(`/reports/${reportId}`) });
-
- const deleteReport = (reportId: any) => {
- mutate(reportId, {
- onSuccess: () => {},
- });
- };
-
- return {
- ...queryResult,
- deleteReport,
- };
-}
-
-export default useReports;
diff --git a/src/components/hooks/queries/useReportsQuery.ts b/src/components/hooks/queries/useReportsQuery.ts
new file mode 100644
index 00000000..ba1bdd4d
--- /dev/null
+++ b/src/components/hooks/queries/useReportsQuery.ts
@@ -0,0 +1,19 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useReportsQuery(
+ { websiteId, type }: { websiteId: string; type?: string },
+ options?: ReactQueryOptions,
+) {
+ const { modified } = useModified(`reports:${type}`);
+ const { get } = useApi();
+
+ return usePagedQuery({
+ queryKey: ['reports', { websiteId, type, modified }],
+ queryFn: async () => get('/reports', { websiteId, type }),
+ enabled: !!websiteId && !!type,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useResultQuery.ts b/src/components/hooks/queries/useResultQuery.ts
new file mode 100644
index 00000000..c6fce128
--- /dev/null
+++ b/src/components/hooks/queries/useResultQuery.ts
@@ -0,0 +1,44 @@
+import { useDateParameters } from '@/components/hooks/useDateParameters';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useResultQuery(
+ type: string,
+ params?: Record,
+ options?: ReactQueryOptions,
+) {
+ const { websiteId, ...parameters } = params;
+ const { post, useQuery } = useApi();
+ const { startDate, endDate, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'reports',
+ {
+ type,
+ websiteId,
+ startDate,
+ endDate,
+ timezone,
+ ...params,
+ ...filters,
+ },
+ ],
+ queryFn: () =>
+ post(`/reports/${type}`, {
+ websiteId,
+ type,
+ filters,
+ parameters: {
+ startDate,
+ endDate,
+ timezone,
+ ...parameters,
+ },
+ }),
+ enabled: !!type,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useRevenueValues.ts b/src/components/hooks/queries/useRevenueValues.ts
deleted file mode 100644
index 007ca3c5..00000000
--- a/src/components/hooks/queries/useRevenueValues.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useApi } from '../useApi';
-
-export function useRevenueValues(websiteId: string, startDate: Date, endDate: Date) {
- const { get, useQuery } = useApi();
-
- return useQuery({
- queryKey: ['revenue:values', { websiteId, startDate, endDate }],
- queryFn: () =>
- get(`/reports/revenue`, {
- websiteId,
- startDate,
- endDate,
- }),
- enabled: !!(websiteId && startDate && endDate),
- });
-}
-
-export default useRevenueValues;
diff --git a/src/components/hooks/queries/useSessionActivity.ts b/src/components/hooks/queries/useSessionActivityQuery.ts
similarity index 92%
rename from src/components/hooks/queries/useSessionActivity.ts
rename to src/components/hooks/queries/useSessionActivityQuery.ts
index 1c9c8f57..d8d34aca 100644
--- a/src/components/hooks/queries/useSessionActivity.ts
+++ b/src/components/hooks/queries/useSessionActivityQuery.ts
@@ -1,6 +1,6 @@
import { useApi } from '../useApi';
-export function useSessionActivity(
+export function useSessionActivityQuery(
websiteId: string,
sessionId: string,
startDate: Date,
diff --git a/src/components/hooks/queries/useSessionDataProperties.ts b/src/components/hooks/queries/useSessionDataProperties.ts
deleted file mode 100644
index ca3798f0..00000000
--- a/src/components/hooks/queries/useSessionDataProperties.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useApi } from '../useApi';
-import { UseQueryOptions } from '@tanstack/react-query';
-import { useFilterParams } from '../useFilterParams';
-
-export function useSessionDataProperties(
- websiteId: string,
- options?: Omit,
-) {
- const { get, useQuery } = useApi();
- const params = useFilterParams(websiteId);
-
- return useQuery({
- queryKey: ['websites:session-data:properties', { websiteId, ...params }],
- queryFn: () => get(`/websites/${websiteId}/session-data/properties`, { ...params }),
- enabled: !!websiteId,
- ...options,
- });
-}
-
-export default useSessionDataProperties;
diff --git a/src/components/hooks/queries/useSessionDataPropertiesQuery.ts b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts
new file mode 100644
index 00000000..ac651bb9
--- /dev/null
+++ b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts
@@ -0,0 +1,27 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useSessionDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'websites:session-data:properties',
+ { websiteId, startAt, endAt, unit, timezone, ...filters },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/session-data/properties`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useSessionData.ts b/src/components/hooks/queries/useSessionDataQuery.ts
similarity index 78%
rename from src/components/hooks/queries/useSessionData.ts
rename to src/components/hooks/queries/useSessionDataQuery.ts
index 521ba7d5..62b53983 100644
--- a/src/components/hooks/queries/useSessionData.ts
+++ b/src/components/hooks/queries/useSessionDataQuery.ts
@@ -1,6 +1,6 @@
import { useApi } from '../useApi';
-export function useSessionData(websiteId: string, sessionId: string) {
+export function useSessionDataQuery(websiteId: string, sessionId: string) {
const { get, useQuery } = useApi();
return useQuery({
diff --git a/src/components/hooks/queries/useSessionDataValues.ts b/src/components/hooks/queries/useSessionDataValues.ts
deleted file mode 100644
index 85529fc0..00000000
--- a/src/components/hooks/queries/useSessionDataValues.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { useApi } from '../useApi';
-import { UseQueryOptions } from '@tanstack/react-query';
-import { useFilterParams } from '../useFilterParams';
-
-export function useSessionDataValues(
- websiteId: string,
- propertyName: string,
- options?: Omit,
-) {
- const { get, useQuery } = useApi();
- const params = useFilterParams(websiteId);
-
- return useQuery({
- queryKey: ['websites:session-data:values', { websiteId, propertyName, ...params }],
- queryFn: () => get(`/websites/${websiteId}/session-data/values`, { ...params, propertyName }),
- enabled: !!(websiteId && propertyName),
- ...options,
- });
-}
-
-export default useSessionDataValues;
diff --git a/src/components/hooks/queries/useSessionDataValuesQuery.ts b/src/components/hooks/queries/useSessionDataValuesQuery.ts
new file mode 100644
index 00000000..d5e180bb
--- /dev/null
+++ b/src/components/hooks/queries/useSessionDataValuesQuery.ts
@@ -0,0 +1,32 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useSessionDataValuesQuery(
+ websiteId: string,
+ propertyName: string,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'websites:session-data:values',
+ { websiteId, propertyName, startAt, endAt, unit, timezone, ...filters },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/session-data/values`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ propertyName,
+ }),
+ enabled: !!(websiteId && propertyName),
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useShareToken.ts b/src/components/hooks/queries/useShareTokenQuery.ts
similarity index 72%
rename from src/components/hooks/queries/useShareToken.ts
rename to src/components/hooks/queries/useShareTokenQuery.ts
index cf17c756..dbad3dcd 100644
--- a/src/components/hooks/queries/useShareToken.ts
+++ b/src/components/hooks/queries/useShareTokenQuery.ts
@@ -1,14 +1,14 @@
-import useStore, { setShareToken } from '@/store/app';
+import { setShareToken, useApp } from '@/store/app';
import { useApi } from '../useApi';
const selector = (state: { shareToken: string }) => state.shareToken;
-export function useShareToken(shareId: string): {
+export function useShareTokenQuery(shareId: string): {
shareToken: any;
isLoading?: boolean;
error?: Error;
} {
- const shareToken = useStore(selector);
+ const shareToken = useApp(selector);
const { get, useQuery } = useApi();
const { isLoading, error } = useQuery({
queryKey: ['share', shareId],
@@ -23,5 +23,3 @@ export function useShareToken(shareId: string): {
return { shareToken, isLoading, error };
}
-
-export default useShareToken;
diff --git a/src/components/hooks/queries/useTeam.ts b/src/components/hooks/queries/useTeam.ts
deleted file mode 100644
index d0ce7499..00000000
--- a/src/components/hooks/queries/useTeam.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { useApi } from '../useApi';
-
-export function useTeam(teamId: string) {
- const { get, useQuery } = useApi();
- return useQuery({
- queryKey: ['teams', teamId],
- queryFn: () => get(`/teams/${teamId}`),
- enabled: !!teamId,
- });
-}
-
-export default useTeam;
diff --git a/src/components/hooks/queries/useTeamMembers.ts b/src/components/hooks/queries/useTeamMembersQuery.ts
similarity index 65%
rename from src/components/hooks/queries/useTeamMembers.ts
rename to src/components/hooks/queries/useTeamMembersQuery.ts
index b6353afc..6f6f815c 100644
--- a/src/components/hooks/queries/useTeamMembers.ts
+++ b/src/components/hooks/queries/useTeamMembersQuery.ts
@@ -1,8 +1,8 @@
import { useApi } from '../useApi';
-import usePagedQuery from '../usePagedQuery';
-import useModified from '../useModified';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
-export function useTeamMembers(teamId: string) {
+export function useTeamMembersQuery(teamId: string) {
const { get } = useApi();
const { modified } = useModified(`teams:members`);
@@ -14,5 +14,3 @@ export function useTeamMembers(teamId: string) {
enabled: !!teamId,
});
}
-
-export default useTeamMembers;
diff --git a/src/components/hooks/queries/useTeamQuery.ts b/src/components/hooks/queries/useTeamQuery.ts
new file mode 100644
index 00000000..c076a6aa
--- /dev/null
+++ b/src/components/hooks/queries/useTeamQuery.ts
@@ -0,0 +1,17 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useTeamQuery(teamId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`teams:${teamId}`);
+
+ return useQuery({
+ queryKey: ['teams', { teamId, modified }],
+ queryFn: () => get(`/teams/${teamId}`),
+ enabled: !!teamId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useTeamWebsites.ts b/src/components/hooks/queries/useTeamWebsitesQuery.ts
similarity index 73%
rename from src/components/hooks/queries/useTeamWebsites.ts
rename to src/components/hooks/queries/useTeamWebsitesQuery.ts
index 5606407e..ffe601bf 100644
--- a/src/components/hooks/queries/useTeamWebsites.ts
+++ b/src/components/hooks/queries/useTeamWebsitesQuery.ts
@@ -1,8 +1,8 @@
import { useApi } from '../useApi';
+import { useModified } from '../useModified';
import { usePagedQuery } from '../usePagedQuery';
-import useModified from '../useModified';
-export function useTeamWebsites(teamId: string) {
+export function useTeamWebsitesQuery(teamId: string) {
const { get } = useApi();
const { modified } = useModified(`websites`);
@@ -13,5 +13,3 @@ export function useTeamWebsites(teamId: string) {
},
});
}
-
-export default useTeamWebsites;
diff --git a/src/components/hooks/queries/useTeams.ts b/src/components/hooks/queries/useTeams.ts
deleted file mode 100644
index d09e2f7d..00000000
--- a/src/components/hooks/queries/useTeams.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useApi } from '../useApi';
-import { usePagedQuery } from '../usePagedQuery';
-import useModified from '../useModified';
-
-export function useTeams(userId: string) {
- const { get } = useApi();
- const { modified } = useModified(`teams`);
-
- return usePagedQuery({
- queryKey: ['teams', { userId, modified }],
- queryFn: (params: any) => {
- return get(`/users/${userId}/teams`, params);
- },
- enabled: !!userId,
- });
-}
-
-export default useTeams;
diff --git a/src/components/hooks/queries/useTeamsQuery.ts b/src/components/hooks/queries/useTeamsQuery.ts
new file mode 100644
index 00000000..f1a09f4d
--- /dev/null
+++ b/src/components/hooks/queries/useTeamsQuery.ts
@@ -0,0 +1,20 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useTeamsQuery(params?: Record, options?: ReactQueryOptions) {
+ const { get } = useApi();
+ const { modified } = useModified(`teams`);
+
+ return usePagedQuery({
+ queryKey: ['teams:admin', { modified, ...params }],
+ queryFn: pageParams => {
+ return get(`/admin/teams`, {
+ ...pageParams,
+ ...params,
+ });
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useUpdateQuery.ts b/src/components/hooks/queries/useUpdateQuery.ts
new file mode 100644
index 00000000..85a94425
--- /dev/null
+++ b/src/components/hooks/queries/useUpdateQuery.ts
@@ -0,0 +1,15 @@
+import { useToast } from '@umami/react-zen';
+import type { ApiError } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useUpdateQuery(path: string, params?: Record) {
+ const { post, useMutation } = useApi();
+ const query = useMutation>({
+ mutationFn: (data: Record) => post(path, { ...data, ...params }),
+ });
+ const { touch } = useModified();
+ const { toast } = useToast();
+
+ return { ...query, touch, toast };
+}
diff --git a/src/components/hooks/queries/useUser.ts b/src/components/hooks/queries/useUser.ts
deleted file mode 100644
index 8541a220..00000000
--- a/src/components/hooks/queries/useUser.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { useApi } from '../useApi';
-
-export function useUser(userId: string, options?: { [key: string]: any }) {
- const { get, useQuery } = useApi();
- return useQuery({
- queryKey: ['users', userId],
- queryFn: () => get(`/users/${userId}`),
- enabled: !!userId,
- ...options,
- });
-}
-
-export default useUser;
diff --git a/src/components/hooks/queries/useUserQuery.ts b/src/components/hooks/queries/useUserQuery.ts
new file mode 100644
index 00000000..07e23f08
--- /dev/null
+++ b/src/components/hooks/queries/useUserQuery.ts
@@ -0,0 +1,17 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useUserQuery(userId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`user:${userId}`);
+
+ return useQuery({
+ queryKey: ['users', { userId, modified }],
+ queryFn: () => get(`/users/${userId}`),
+ enabled: !!userId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useUserTeamsQuery.ts b/src/components/hooks/queries/useUserTeamsQuery.ts
new file mode 100644
index 00000000..82f65496
--- /dev/null
+++ b/src/components/hooks/queries/useUserTeamsQuery.ts
@@ -0,0 +1,15 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useUserTeamsQuery(userId: string) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`teams`);
+
+ return useQuery({
+ queryKey: ['teams', { userId, modified }],
+ queryFn: () => {
+ return get(`/users/${userId}/teams`);
+ },
+ enabled: !!userId,
+ });
+}
diff --git a/src/components/hooks/queries/useUserWebsitesQuery.ts b/src/components/hooks/queries/useUserWebsitesQuery.ts
new file mode 100644
index 00000000..f98eaffb
--- /dev/null
+++ b/src/components/hooks/queries/useUserWebsitesQuery.ts
@@ -0,0 +1,31 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useUserWebsitesQuery(
+ { userId, teamId }: { userId?: string; teamId?: string },
+ params?: Record,
+ options?: ReactQueryOptions,
+) {
+ const { get } = useApi();
+ const { modified } = useModified(`websites`);
+
+ return usePagedQuery({
+ queryKey: ['websites', { userId, teamId, modified, ...params }],
+ queryFn: pageParams => {
+ return get(
+ teamId
+ ? `/teams/${teamId}/websites`
+ : userId
+ ? `/users/${userId}/websites`
+ : '/me/websites',
+ {
+ ...pageParams,
+ ...params,
+ },
+ );
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useUsers.ts b/src/components/hooks/queries/useUsersQuery.ts
similarity index 56%
rename from src/components/hooks/queries/useUsers.ts
rename to src/components/hooks/queries/useUsersQuery.ts
index 3d70d262..d87900ba 100644
--- a/src/components/hooks/queries/useUsers.ts
+++ b/src/components/hooks/queries/useUsersQuery.ts
@@ -1,19 +1,17 @@
import { useApi } from '../useApi';
+import { useModified } from '../useModified';
import { usePagedQuery } from '../usePagedQuery';
-import useModified from '../useModified';
-export function useUsers() {
+export function useUsersQuery() {
const { get } = useApi();
const { modified } = useModified(`users`);
return usePagedQuery({
- queryKey: ['users', { modified }],
- queryFn: (params: any) => {
+ queryKey: ['users:admin', { modified }],
+ queryFn: (pageParams: any) => {
return get('/admin/users', {
- ...params,
+ ...pageParams,
});
},
});
}
-
-export default useUsers;
diff --git a/src/components/hooks/queries/useWebsite.ts b/src/components/hooks/queries/useWebsite.ts
deleted file mode 100644
index 9151b55d..00000000
--- a/src/components/hooks/queries/useWebsite.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { useApi } from '../useApi';
-
-export function useWebsite(websiteId: string, options?: { [key: string]: any }) {
- const { get, useQuery } = useApi();
-
- return useQuery({
- queryKey: ['website', { websiteId }],
- queryFn: () => get(`/websites/${websiteId}`),
- enabled: !!websiteId,
- ...options,
- });
-}
-
-export default useWebsite;
diff --git a/src/components/hooks/queries/useWebsiteCohortQuery.ts b/src/components/hooks/queries/useWebsiteCohortQuery.ts
new file mode 100644
index 00000000..975766e9
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteCohortQuery.ts
@@ -0,0 +1,21 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useWebsiteCohortQuery(
+ websiteId: string,
+ cohortId: string,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`cohorts`);
+
+ return useQuery({
+ queryKey: ['website:cohorts', { websiteId, cohortId, modified }],
+ queryFn: () => get(`/websites/${websiteId}/segments/${cohortId}`),
+ enabled: !!(websiteId && cohortId),
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteCohortsQuery.ts b/src/components/hooks/queries/useWebsiteCohortsQuery.ts
new file mode 100644
index 00000000..e0cbf4ce
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteCohortsQuery.ts
@@ -0,0 +1,25 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import { useFilterParameters } from '@/components/hooks/useFilterParameters';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useWebsiteCohortsQuery(
+ websiteId: string,
+ params?: Record,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`cohorts`);
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: ['website:cohorts', { websiteId, modified, ...filters, ...params }],
+ queryFn: pageParams => {
+ return get(`/websites/${websiteId}/segments`, { ...pageParams, ...filters, ...params });
+ },
+ enabled: !!websiteId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteEvents.ts b/src/components/hooks/queries/useWebsiteEvents.ts
deleted file mode 100644
index 2a47c3eb..00000000
--- a/src/components/hooks/queries/useWebsiteEvents.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useApi } from '../useApi';
-import { UseQueryOptions } from '@tanstack/react-query';
-import { useFilterParams } from '../useFilterParams';
-import { usePagedQuery } from '../usePagedQuery';
-
-export function useWebsiteEvents(
- websiteId: string,
- options?: Omit,
-) {
- const { get } = useApi();
- const params = useFilterParams(websiteId);
-
- return usePagedQuery({
- queryKey: ['websites:events', { websiteId, ...params }],
- queryFn: pageParams =>
- get(`/websites/${websiteId}/events`, { ...params, ...pageParams, pageSize: 20 }),
- enabled: !!websiteId,
- ...options,
- });
-}
-
-export default useWebsiteEvents;
diff --git a/src/components/hooks/queries/useWebsiteEventsQuery.ts b/src/components/hooks/queries/useWebsiteEventsQuery.ts
new file mode 100644
index 00000000..fc4dad5b
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteEventsQuery.ts
@@ -0,0 +1,39 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+import { usePagedQuery } from '../usePagedQuery';
+
+const EVENT_TYPES = {
+ views: 1,
+ events: 2,
+};
+
+export function useWebsiteEventsQuery(
+ websiteId: string,
+ params?: Record,
+ options?: ReactQueryOptions,
+) {
+ const { get } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return usePagedQuery({
+ queryKey: [
+ 'websites:events',
+ { websiteId, startAt, endAt, unit, timezone, ...filters, ...params },
+ ],
+ queryFn: pageParams =>
+ get(`/websites/${websiteId}/events`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...pageParams,
+ eventType: EVENT_TYPES[params.view],
+ }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteEventsSeries.ts b/src/components/hooks/queries/useWebsiteEventsSeries.ts
deleted file mode 100644
index 91c50fff..00000000
--- a/src/components/hooks/queries/useWebsiteEventsSeries.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useApi } from '../useApi';
-import { UseQueryOptions } from '@tanstack/react-query';
-import { useFilterParams } from '../useFilterParams';
-
-export function useWebsiteEventsSeries(
- websiteId: string,
- options?: Omit,
-) {
- const { get, useQuery } = useApi();
- const params = useFilterParams(websiteId);
-
- return useQuery({
- queryKey: ['websites:events:series', { websiteId, ...params }],
- queryFn: () => get(`/websites/${websiteId}/events/series`, { ...params }),
- enabled: !!websiteId,
- ...options,
- });
-}
-
-export default useWebsiteEventsSeries;
diff --git a/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts b/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts
new file mode 100644
index 00000000..6c1d112d
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts
@@ -0,0 +1,18 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useWebsiteEventsSeriesQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: ['websites:events:series', { websiteId, startAt, endAt, unit, timezone, ...filters }],
+ queryFn: () =>
+ get(`/websites/${websiteId}/events/series`, { startAt, endAt, unit, timezone, ...filters }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts b/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts
new file mode 100644
index 00000000..b2e90199
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts
@@ -0,0 +1,51 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export type WebsiteExpandedMetricsData = {
+ name: string;
+ pageviews: number;
+ visitors: number;
+ visits: number;
+ bounces: number;
+ totaltime: number;
+}[];
+
+export function useWebsiteExpandedMetricsQuery(
+ websiteId: string,
+ params: { type: string; limit?: number; search?: string },
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'websites:metrics:expanded',
+ {
+ websiteId,
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...params,
+ },
+ ],
+ queryFn: async () =>
+ get(`/websites/${websiteId}/metrics/expanded`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...params,
+ }),
+ enabled: !!websiteId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteMetrics.ts b/src/components/hooks/queries/useWebsiteMetrics.ts
deleted file mode 100644
index 1a4202e8..00000000
--- a/src/components/hooks/queries/useWebsiteMetrics.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { UseQueryOptions } from '@tanstack/react-query';
-import { useApi } from '../useApi';
-import { useFilterParams } from '../useFilterParams';
-import { useSearchParams } from 'next/navigation';
-
-export function useWebsiteMetrics(
- websiteId: string,
- queryParams: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number },
- options?: Omit void }, 'queryKey' | 'queryFn'>,
-) {
- const { get, useQuery } = useApi();
- const params = useFilterParams(websiteId);
- const searchParams = useSearchParams();
-
- return useQuery({
- queryKey: [
- 'websites:metrics',
- {
- websiteId,
- ...params,
- ...queryParams,
- },
- ],
- queryFn: async () => {
- const data = await get(`/websites/${websiteId}/metrics`, {
- ...params,
- [searchParams.get('view')]: undefined,
- ...queryParams,
- });
-
- options?.onDataLoad?.(data);
-
- return data;
- },
- enabled: !!websiteId,
- ...options,
- });
-}
-
-export default useWebsiteMetrics;
diff --git a/src/components/hooks/queries/useWebsiteMetricsQuery.ts b/src/components/hooks/queries/useWebsiteMetricsQuery.ts
new file mode 100644
index 00000000..67c5e4d4
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteMetricsQuery.ts
@@ -0,0 +1,47 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export type WebsiteMetricsData = {
+ x: string;
+ y: number;
+}[];
+
+export function useWebsiteMetricsQuery(
+ websiteId: string,
+ params: { type: string; limit?: number; search?: string },
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'websites:metrics',
+ {
+ websiteId,
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...params,
+ },
+ ],
+ queryFn: async () =>
+ get(`/websites/${websiteId}/metrics`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...params,
+ }),
+ enabled: !!websiteId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsitePageviews.ts b/src/components/hooks/queries/useWebsitePageviews.ts
deleted file mode 100644
index 43c51745..00000000
--- a/src/components/hooks/queries/useWebsitePageviews.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { UseQueryOptions } from '@tanstack/react-query';
-import { useApi } from '../useApi';
-import { useFilterParams } from '../useFilterParams';
-
-export function useWebsitePageviews(
- websiteId: string,
- compare?: string,
- options?: Omit,
-) {
- const { get, useQuery } = useApi();
- const params = useFilterParams(websiteId);
-
- return useQuery({
- queryKey: ['websites:pageviews', { websiteId, ...params, compare }],
- queryFn: () => get(`/websites/${websiteId}/pageviews`, { ...params, compare }),
- enabled: !!websiteId,
- ...options,
- });
-}
-
-export default useWebsitePageviews;
diff --git a/src/components/hooks/queries/useWebsitePageviewsQuery.ts b/src/components/hooks/queries/useWebsitePageviewsQuery.ts
new file mode 100644
index 00000000..b35c8200
--- /dev/null
+++ b/src/components/hooks/queries/useWebsitePageviewsQuery.ts
@@ -0,0 +1,36 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export interface WebsitePageviewsData {
+ pageviews: { x: string; y: number }[];
+ sessions: { x: string; y: number }[];
+}
+
+export function useWebsitePageviewsQuery(
+ { websiteId, compare }: { websiteId: string; compare?: string },
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const queryParams = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'websites:pageviews',
+ { websiteId, compare, startAt, endAt, unit, timezone, ...queryParams },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/pageviews`, {
+ compare,
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...queryParams,
+ }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteQuery.ts b/src/components/hooks/queries/useWebsiteQuery.ts
new file mode 100644
index 00000000..b9a5533d
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteQuery.ts
@@ -0,0 +1,17 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useWebsiteQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`website:${websiteId}`);
+
+ return useQuery({
+ queryKey: ['website', { websiteId, modified }],
+ queryFn: () => get(`/websites/${websiteId}`),
+ enabled: !!websiteId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteSegmentQuery.ts b/src/components/hooks/queries/useWebsiteSegmentQuery.ts
new file mode 100644
index 00000000..1923fbd8
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSegmentQuery.ts
@@ -0,0 +1,21 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useWebsiteSegmentQuery(
+ websiteId: string,
+ segmentId: string,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`segments`);
+
+ return useQuery({
+ queryKey: ['website:segments', { websiteId, segmentId, modified }],
+ queryFn: () => get(`/websites/${websiteId}/segments/${segmentId}`),
+ enabled: !!(websiteId && segmentId),
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteSegmentsQuery.ts b/src/components/hooks/queries/useWebsiteSegmentsQuery.ts
new file mode 100644
index 00000000..8d3af963
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSegmentsQuery.ts
@@ -0,0 +1,24 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import { useFilterParameters } from '@/components/hooks/useFilterParameters';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useWebsiteSegmentsQuery(
+ websiteId: string,
+ params?: Record,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`segments`);
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: ['website:segments', { websiteId, modified, ...filters, ...params }],
+ queryFn: pageParams =>
+ get(`/websites/${websiteId}/segments`, { ...pageParams, ...filters, ...params }),
+ enabled: !!websiteId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteSession.ts b/src/components/hooks/queries/useWebsiteSessionQuery.ts
similarity index 66%
rename from src/components/hooks/queries/useWebsiteSession.ts
rename to src/components/hooks/queries/useWebsiteSessionQuery.ts
index 93e9057c..21e94911 100644
--- a/src/components/hooks/queries/useWebsiteSession.ts
+++ b/src/components/hooks/queries/useWebsiteSessionQuery.ts
@@ -1,6 +1,6 @@
import { useApi } from '../useApi';
-export function useWebsiteSession(websiteId: string, sessionId: string) {
+export function useWebsiteSessionQuery(websiteId: string, sessionId: string) {
const { get, useQuery } = useApi();
return useQuery({
@@ -8,7 +8,6 @@ export function useWebsiteSession(websiteId: string, sessionId: string) {
queryFn: () => {
return get(`/websites/${websiteId}/sessions/${sessionId}`);
},
+ enabled: Boolean(websiteId && sessionId),
});
}
-
-export default useWebsiteSession;
diff --git a/src/components/hooks/queries/useWebsiteSessionStats.ts b/src/components/hooks/queries/useWebsiteSessionStats.ts
deleted file mode 100644
index 5c02cfdc..00000000
--- a/src/components/hooks/queries/useWebsiteSessionStats.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { useApi } from '../useApi';
-import { useFilterParams } from '../useFilterParams';
-
-export function useWebsiteSessionStats(websiteId: string, options?: { [key: string]: string }) {
- const { get, useQuery } = useApi();
- const params = useFilterParams(websiteId);
-
- return useQuery({
- queryKey: ['sessions:stats', { websiteId, ...params }],
- queryFn: () => get(`/websites/${websiteId}/sessions/stats`, { ...params }),
- enabled: !!websiteId,
- ...options,
- });
-}
-
-export default useWebsiteSessionStats;
diff --git a/src/components/hooks/queries/useWebsiteSessionStatsQuery.ts b/src/components/hooks/queries/useWebsiteSessionStatsQuery.ts
new file mode 100644
index 00000000..bac9fc90
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSessionStatsQuery.ts
@@ -0,0 +1,17 @@
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useWebsiteSessionStatsQuery(websiteId: string, options?: Record) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: ['sessions:stats', { websiteId, startAt, endAt, unit, timezone, ...filters }],
+ queryFn: () =>
+ get(`/websites/${websiteId}/sessions/stats`, { startAt, endAt, unit, timezone, ...filters }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteSessions.ts b/src/components/hooks/queries/useWebsiteSessions.ts
deleted file mode 100644
index 09e34a80..00000000
--- a/src/components/hooks/queries/useWebsiteSessions.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useApi } from '../useApi';
-import { usePagedQuery } from '../usePagedQuery';
-import useModified from '../useModified';
-import { useFilterParams } from '@/components/hooks/useFilterParams';
-
-export function useWebsiteSessions(websiteId: string, params?: { [key: string]: string | number }) {
- const { get } = useApi();
- const { modified } = useModified(`sessions`);
- const filters = useFilterParams(websiteId);
-
- return usePagedQuery({
- queryKey: ['sessions', { websiteId, modified, ...params, ...filters }],
- queryFn: (data: any) => {
- return get(`/websites/${websiteId}/sessions`, {
- ...data,
- ...params,
- ...filters,
- pageSize: 20,
- });
- },
- });
-}
-
-export default useWebsiteSessions;
diff --git a/src/components/hooks/queries/useWebsiteSessionsQuery.ts b/src/components/hooks/queries/useWebsiteSessionsQuery.ts
new file mode 100644
index 00000000..31906be9
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSessionsQuery.ts
@@ -0,0 +1,34 @@
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useWebsiteSessionsQuery(
+ websiteId: string,
+ params?: Record,
+) {
+ const { get } = useApi();
+ const { modified } = useModified(`sessions`);
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return usePagedQuery({
+ queryKey: [
+ 'sessions',
+ { websiteId, modified, startAt, endAt, unit, timezone, ...params, ...filters },
+ ],
+ queryFn: pageParams => {
+ return get(`/websites/${websiteId}/sessions`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...pageParams,
+ ...params,
+ pageSize: 20,
+ });
+ },
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteSessionsWeekly.ts b/src/components/hooks/queries/useWebsiteSessionsWeekly.ts
deleted file mode 100644
index f3aa3b00..00000000
--- a/src/components/hooks/queries/useWebsiteSessionsWeekly.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useApi } from '../useApi';
-import useModified from '../useModified';
-import { useFilterParams } from '@/components/hooks/useFilterParams';
-
-export function useWebsiteSessionsWeekly(
- websiteId: string,
- params?: { [key: string]: string | number },
-) {
- const { get, useQuery } = useApi();
- const { modified } = useModified(`sessions`);
- const filters = useFilterParams(websiteId);
-
- return useQuery({
- queryKey: ['sessions', { websiteId, modified, ...params, ...filters }],
- queryFn: () => {
- return get(`/websites/${websiteId}/sessions/weekly`, {
- ...params,
- ...filters,
- });
- },
- });
-}
-
-export default useWebsiteSessionsWeekly;
diff --git a/src/components/hooks/queries/useWebsiteStats.ts b/src/components/hooks/queries/useWebsiteStats.ts
deleted file mode 100644
index 6d42009e..00000000
--- a/src/components/hooks/queries/useWebsiteStats.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useApi } from '../useApi';
-import { useFilterParams } from '../useFilterParams';
-
-export function useWebsiteStats(
- websiteId: string,
- compare?: string,
- options?: { [key: string]: string },
-) {
- const { get, useQuery } = useApi();
- const params = useFilterParams(websiteId);
-
- return useQuery({
- queryKey: ['websites:stats', { websiteId, ...params, compare }],
- queryFn: () => get(`/websites/${websiteId}/stats`, { ...params, compare }),
- enabled: !!websiteId,
- ...options,
- });
-}
-
-export default useWebsiteStats;
diff --git a/src/components/hooks/queries/useWebsiteStatsQuery.ts b/src/components/hooks/queries/useWebsiteStatsQuery.ts
new file mode 100644
index 00000000..e9a0c48c
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteStatsQuery.ts
@@ -0,0 +1,36 @@
+import type { UseQueryOptions } from '@tanstack/react-query';
+import { useDateParameters } from '@/components/hooks/useDateParameters';
+import { useApi } from '../useApi';
+import { useFilterParameters } from '../useFilterParameters';
+
+export interface WebsiteStatsData {
+ pageviews: number;
+ visitors: number;
+ visits: number;
+ bounces: number;
+ totaltime: number;
+ comparison: {
+ pageviews: number;
+ visitors: number;
+ visits: number;
+ bounces: number;
+ totaltime: number;
+ };
+}
+
+export function useWebsiteStatsQuery(
+ websiteId: string,
+ options?: UseQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: ['websites:stats', { websiteId, startAt, endAt, unit, timezone, ...filters }],
+ queryFn: () =>
+ get(`/websites/${websiteId}/stats`, { startAt, endAt, unit, timezone, ...filters }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteValues.ts b/src/components/hooks/queries/useWebsiteValuesQuery.ts
similarity index 93%
rename from src/components/hooks/queries/useWebsiteValues.ts
rename to src/components/hooks/queries/useWebsiteValuesQuery.ts
index 77f65fe5..1e097369 100644
--- a/src/components/hooks/queries/useWebsiteValues.ts
+++ b/src/components/hooks/queries/useWebsiteValuesQuery.ts
@@ -1,9 +1,9 @@
-import { useApi } from '../useApi';
import { useCountryNames } from '@/components/hooks/useCountryNames';
import { useRegionNames } from '@/components/hooks/useRegionNames';
-import useLocale from '../useLocale';
+import { useApi } from '../useApi';
+import { useLocale } from '../useLocale';
-export function useWebsiteValues({
+export function useWebsiteValuesQuery({
websiteId,
type,
startDate,
@@ -60,5 +60,3 @@ export function useWebsiteValues({
enabled: !!(websiteId && type && startDate && endDate),
});
}
-
-export default useWebsiteValues;
diff --git a/src/components/hooks/queries/useWebsites.ts b/src/components/hooks/queries/useWebsites.ts
deleted file mode 100644
index 7a5004d7..00000000
--- a/src/components/hooks/queries/useWebsites.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { useApi } from '../useApi';
-import { usePagedQuery } from '../usePagedQuery';
-import { useLogin } from './useLogin';
-import useModified from '../useModified';
-
-export function useWebsites(
- { userId, teamId }: { userId?: string; teamId?: string },
- params?: { [key: string]: string | number },
-) {
- const { get } = useApi();
- const { user } = useLogin();
- const { modified } = useModified(`websites`);
-
- return usePagedQuery({
- queryKey: ['websites', { userId, teamId, modified, ...params }],
- queryFn: (data: any) => {
- return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId || user.id}/websites`, {
- ...data,
- ...params,
- });
- },
- });
-}
-
-export default useWebsites;
diff --git a/src/components/hooks/queries/useWebsitesQuery.ts b/src/components/hooks/queries/useWebsitesQuery.ts
new file mode 100644
index 00000000..a7b66186
--- /dev/null
+++ b/src/components/hooks/queries/useWebsitesQuery.ts
@@ -0,0 +1,20 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useWebsitesQuery(params?: Record, options?: ReactQueryOptions) {
+ const { get } = useApi();
+ const { modified } = useModified(`websites`);
+
+ return usePagedQuery({
+ queryKey: ['websites:admin', { modified, ...params }],
+ queryFn: pageParams => {
+ return get(`/admin/websites`, {
+ ...pageParams,
+ ...params,
+ });
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWeeklyTrafficQuery.ts b/src/components/hooks/queries/useWeeklyTrafficQuery.ts
new file mode 100644
index 00000000..a76ebb3d
--- /dev/null
+++ b/src/components/hooks/queries/useWeeklyTrafficQuery.ts
@@ -0,0 +1,28 @@
+import { useFilterParameters } from '@/components/hooks/useFilterParameters';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useModified } from '../useModified';
+
+export function useWeeklyTrafficQuery(websiteId: string, params?: Record) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`sessions`);
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'sessions',
+ { websiteId, modified, startAt, endAt, unit, timezone, ...params, ...filters },
+ ],
+ queryFn: () => {
+ return get(`/websites/${websiteId}/sessions/weekly`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...params,
+ ...filters,
+ });
+ },
+ });
+}
diff --git a/src/components/hooks/useApi.ts b/src/components/hooks/useApi.ts
index dfa48e2f..35cabd52 100644
--- a/src/components/hooks/useApi.ts
+++ b/src/components/hooks/useApi.ts
@@ -1,25 +1,23 @@
+import { useMutation, useQuery } from '@tanstack/react-query';
import { useCallback } from 'react';
-import * as reactQuery from '@tanstack/react-query';
import { getClientAuthToken } from '@/lib/client';
import { SHARE_TOKEN_HEADER } from '@/lib/constants';
-import { httpGet, httpPost, httpPut, httpDelete, FetchResponse } from '@/lib/fetch';
-import useStore from '@/store/app';
+import { type FetchResponse, httpDelete, httpGet, httpPost, httpPut } from '@/lib/fetch';
+import { useApp } from '@/store/app';
const selector = (state: { shareToken: { token?: string } }) => state.shareToken;
async function handleResponse(res: FetchResponse): Promise {
if (!res.ok) {
- return Promise.reject(new Error(res.error?.error || res.error || 'Unexpectd error.'));
+ const { message, code, status } = res?.data?.error || {};
+
+ return Promise.reject(Object.assign(new Error(message), { code, status }));
}
return Promise.resolve(res.data);
}
-function handleError(err: Error | string) {
- return Promise.reject((err as Error)?.message || err || null);
-}
-
export function useApi() {
- const shareToken = useStore(selector);
+ const shareToken = useApp(selector);
const defaultHeaders = {
authorization: `Bearer ${getClientAuthToken()}`,
@@ -38,41 +36,32 @@ export function useApi() {
return {
get: useCallback(
async (url: string, params: object = {}, headers: object = {}) => {
- return httpGet(getUrl(url), params, getHeaders(headers))
- .then(handleResponse)
- .catch(handleError);
+ return httpGet(getUrl(url), params, getHeaders(headers)).then(handleResponse);
},
[httpGet],
),
post: useCallback(
async (url: string, params: object = {}, headers: object = {}) => {
- return httpPost(getUrl(url), params, getHeaders(headers))
- .then(handleResponse)
- .catch(handleError);
+ return httpPost(getUrl(url), params, getHeaders(headers)).then(handleResponse);
},
[httpPost],
),
put: useCallback(
async (url: string, params: object = {}, headers: object = {}) => {
- return httpPut(getUrl(url), params, getHeaders(headers))
- .then(handleResponse)
- .catch(handleError);
+ return httpPut(getUrl(url), params, getHeaders(headers)).then(handleResponse);
},
[httpPut],
),
del: useCallback(
async (url: string, params: object = {}, headers: object = {}) => {
- return httpDelete(getUrl(url), params, getHeaders(headers))
- .then(handleResponse)
- .catch(handleError);
+ return httpDelete(getUrl(url), params, getHeaders(headers)).then(handleResponse);
},
[httpDelete],
),
- ...reactQuery,
+ useQuery,
+ useMutation,
};
}
-
-export default useApi;
diff --git a/src/components/hooks/useConfig.ts b/src/components/hooks/useConfig.ts
new file mode 100644
index 00000000..c1cdcaf6
--- /dev/null
+++ b/src/components/hooks/useConfig.ts
@@ -0,0 +1,33 @@
+import { useEffect } from 'react';
+import { useApi } from '@/components/hooks/useApi';
+import { setConfig, useApp } from '@/store/app';
+
+export type Config = {
+ cloudMode: boolean;
+ faviconUrl?: string;
+ linksUrl?: string;
+ pixelsUrl?: string;
+ privateMode: boolean;
+ telemetryDisabled: boolean;
+ trackerScriptName?: string;
+ updatesDisabled: boolean;
+};
+
+export function useConfig(): Config {
+ const { config } = useApp();
+ const { get } = useApi();
+
+ async function loadConfig() {
+ const data = await get(`/config`);
+
+ setConfig(data);
+ }
+
+ useEffect(() => {
+ if (!config) {
+ loadConfig();
+ }
+ }, []);
+
+ return config;
+}
diff --git a/src/components/hooks/useCountryNames.ts b/src/components/hooks/useCountryNames.ts
index 12f2f0dd..1ec9fc13 100644
--- a/src/components/hooks/useCountryNames.ts
+++ b/src/components/hooks/useCountryNames.ts
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { httpGet } from '@/lib/fetch';
import enUS from '../../../public/intl/country/en-US.json';
@@ -30,5 +30,3 @@ export function useCountryNames(locale: string) {
return { countryNames: list };
}
-
-export default useCountryNames;
diff --git a/src/components/hooks/useDateParameters.ts b/src/components/hooks/useDateParameters.ts
new file mode 100644
index 00000000..d84b4236
--- /dev/null
+++ b/src/components/hooks/useDateParameters.ts
@@ -0,0 +1,18 @@
+import { useDateRange } from './useDateRange';
+import { useTimezone } from './useTimezone';
+
+export function useDateParameters() {
+ const {
+ dateRange: { startDate, endDate, unit },
+ } = useDateRange();
+ const { timezone, localToUtc, canonicalizeTimezone } = useTimezone();
+
+ return {
+ startAt: +localToUtc(startDate),
+ endAt: +localToUtc(endDate),
+ startDate: localToUtc(startDate).toISOString(),
+ endDate: localToUtc(endDate).toISOString(),
+ unit,
+ timezone: canonicalizeTimezone(timezone),
+ };
+}
diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts
index 61838980..755f36ee 100644
--- a/src/components/hooks/useDateRange.ts
+++ b/src/components/hooks/useDateRange.ts
@@ -1,61 +1,37 @@
-import { getMinimumUnit, parseDateRange } from '@/lib/date';
-import { setItem } from '@/lib/storage';
-import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from '@/lib/constants';
-import websiteStore, { setWebsiteDateRange, setWebsiteDateCompare } from '@/store/websites';
-import appStore, { setDateRange } from '@/store/app';
-import { DateRange } from '@/lib/types';
-import { useLocale } from './useLocale';
-import { useApi } from './useApi';
+import { useMemo } from 'react';
+import { useLocale } from '@/components/hooks/useLocale';
+import { useNavigation } from '@/components/hooks/useNavigation';
+import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
+import { getCompareDate, getOffsetDateRange, parseDateRange } from '@/lib/date';
+import { getItem } from '@/lib/storage';
-export function useDateRange(websiteId?: string): {
- dateRange: DateRange;
- saveDateRange: (value: string | DateRange) => void;
- dateCompare: string;
- saveDateCompare: (value: string) => void;
-} {
- const { get } = useApi();
+export function useDateRange(options: { ignoreOffset?: boolean; timezone?: string } = {}) {
+ const {
+ query: { date = '', offset = 0, compare = 'prev' },
+ } = useNavigation();
const { locale } = useLocale();
- const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
- const defaultConfig = DEFAULT_DATE_RANGE;
- const globalConfig = appStore(state => state.dateRange);
- const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale);
- const dateCompare = websiteStore(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE);
- const saveDateRange = async (value: DateRange | string) => {
- if (websiteId) {
- let dateRange: DateRange | string = value;
+ const dateRange = useMemo(() => {
+ const dateRangeObject = parseDateRange(
+ date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
+ locale,
+ options.timezone,
+ );
- if (typeof value === 'string') {
- if (value === 'all') {
- const result: any = await get(`/websites/${websiteId}/daterange`);
- const { mindate, maxdate } = result;
+ return !options.ignoreOffset && offset
+ ? getOffsetDateRange(dateRangeObject, +offset)
+ : dateRangeObject;
+ }, [date, offset, options]);
- const startDate = new Date(mindate);
- const endDate = new Date(maxdate);
+ const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
- dateRange = {
- startDate,
- endDate,
- unit: getMinimumUnit(startDate, endDate),
- value,
- };
- } else {
- dateRange = parseDateRange(value, locale);
- }
- }
-
- setWebsiteDateRange(websiteId, dateRange as DateRange);
- } else {
- setItem(DATE_RANGE_CONFIG, value);
- setDateRange(value);
- }
+ return {
+ date,
+ offset,
+ compare,
+ isAllTime: date.endsWith(`:all`),
+ isCustomRange: date.startsWith('range:'),
+ dateRange,
+ dateCompare,
};
-
- const saveDateCompare = (value: string) => {
- setWebsiteDateCompare(websiteId, value);
- };
-
- return { dateRange, saveDateRange, dateCompare, saveDateCompare };
}
-
-export default useDateRange;
diff --git a/src/components/hooks/useDocumentClick.ts b/src/components/hooks/useDocumentClick.ts
index eefd9366..611f6285 100644
--- a/src/components/hooks/useDocumentClick.ts
+++ b/src/components/hooks/useDocumentClick.ts
@@ -11,5 +11,3 @@ export function useDocumentClick(handler: (event: MouseEvent) => any) {
return null;
}
-
-export default useDocumentClick;
diff --git a/src/components/hooks/useEscapeKey.ts b/src/components/hooks/useEscapeKey.ts
index 5c3350e7..cc1d3089 100644
--- a/src/components/hooks/useEscapeKey.ts
+++ b/src/components/hooks/useEscapeKey.ts
@@ -1,4 +1,4 @@
-import { useEffect, useCallback, KeyboardEvent } from 'react';
+import { type KeyboardEvent, useCallback, useEffect } from 'react';
export function useEscapeKey(handler: (event: KeyboardEvent) => void) {
const escFunction = useCallback((event: KeyboardEvent) => {
@@ -17,5 +17,3 @@ export function useEscapeKey(handler: (event: KeyboardEvent) => void) {
return null;
}
-
-export default useEscapeKey;
diff --git a/src/components/hooks/useFields.ts b/src/components/hooks/useFields.ts
index 859ca1ce..22a1dcf3 100644
--- a/src/components/hooks/useFields.ts
+++ b/src/components/hooks/useFields.ts
@@ -4,21 +4,20 @@ export function useFields() {
const { formatMessage, labels } = useMessages();
const fields = [
- { name: 'url', type: 'string', label: formatMessage(labels.url) },
+ { name: 'path', type: 'string', label: formatMessage(labels.path) },
+ { name: 'query', type: 'string', label: formatMessage(labels.query) },
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
- { name: 'query', type: 'string', label: formatMessage(labels.query) },
{ name: 'browser', type: 'string', label: formatMessage(labels.browser) },
{ name: 'os', type: 'string', label: formatMessage(labels.os) },
{ name: 'device', type: 'string', label: formatMessage(labels.device) },
{ name: 'country', type: 'string', label: formatMessage(labels.country) },
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
- { name: 'host', type: 'string', label: formatMessage(labels.host) },
+ { name: 'hostname', type: 'string', label: formatMessage(labels.hostname) },
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
+ { name: 'event', type: 'string', label: formatMessage(labels.event) },
];
return { fields };
}
-
-export default useFields;
diff --git a/src/components/hooks/useFilterParameters.ts b/src/components/hooks/useFilterParameters.ts
new file mode 100644
index 00000000..54032120
--- /dev/null
+++ b/src/components/hooks/useFilterParameters.ts
@@ -0,0 +1,70 @@
+import { useMemo } from 'react';
+import { useNavigation } from './useNavigation';
+
+export function useFilterParameters() {
+ const {
+ query: {
+ path,
+ referrer,
+ title,
+ query,
+ host,
+ os,
+ browser,
+ device,
+ country,
+ region,
+ city,
+ event,
+ tag,
+ hostname,
+ page,
+ pageSize,
+ search,
+ segment,
+ cohort,
+ },
+ } = useNavigation();
+
+ return useMemo(() => {
+ return {
+ path,
+ referrer,
+ title,
+ query,
+ host,
+ os,
+ browser,
+ device,
+ country,
+ region,
+ city,
+ event,
+ tag,
+ hostname,
+ search,
+ segment,
+ cohort,
+ };
+ }, [
+ path,
+ referrer,
+ title,
+ query,
+ host,
+ os,
+ browser,
+ device,
+ country,
+ region,
+ city,
+ event,
+ tag,
+ hostname,
+ page,
+ pageSize,
+ search,
+ segment,
+ cohort,
+ ]);
+}
diff --git a/src/components/hooks/useFilterParams.ts b/src/components/hooks/useFilterParams.ts
deleted file mode 100644
index 55deed14..00000000
--- a/src/components/hooks/useFilterParams.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useNavigation } from './useNavigation';
-import { useDateRange } from './useDateRange';
-import { useTimezone } from './useTimezone';
-
-export function useFilterParams(websiteId: string) {
- const { dateRange } = useDateRange(websiteId);
- const { startDate, endDate, unit } = dateRange;
- const { timezone, toUtc } = useTimezone();
- const {
- query: {
- url,
- referrer,
- title,
- query,
- host,
- os,
- browser,
- device,
- country,
- region,
- city,
- event,
- tag,
- },
- } = useNavigation();
-
- return {
- startAt: +toUtc(startDate),
- endAt: +toUtc(endDate),
- unit,
- timezone,
- url,
- referrer,
- title,
- query,
- host,
- os,
- browser,
- device,
- country,
- region,
- city,
- event,
- tag,
- };
-}
diff --git a/src/components/hooks/useFilters.ts b/src/components/hooks/useFilters.ts
index 2b99785a..850e2afb 100644
--- a/src/components/hooks/useFilters.ts
+++ b/src/components/hooks/useFilters.ts
@@ -1,8 +1,33 @@
+import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants';
+import { safeDecodeURIComponent } from '@/lib/url';
+import { useFields } from './useFields';
import { useMessages } from './useMessages';
-import { OPERATORS } from '@/lib/constants';
+import { useNavigation } from './useNavigation';
export function useFilters() {
const { formatMessage, labels } = useMessages();
+ const { query } = useNavigation();
+ const { fields } = useFields();
+
+ const operators = [
+ { name: 'eq', type: 'string', label: formatMessage(labels.is) },
+ { name: 'neq', type: 'string', label: formatMessage(labels.isNot) },
+ { name: 'c', type: 'string', label: formatMessage(labels.contains) },
+ { name: 'dnc', type: 'string', label: formatMessage(labels.doesNotContain) },
+ { name: 'i', type: 'array', label: formatMessage(labels.includes) },
+ { name: 'dni', type: 'array', label: formatMessage(labels.doesNotInclude) },
+ { name: 't', type: 'boolean', label: formatMessage(labels.isTrue) },
+ { name: 'f', type: 'boolean', label: formatMessage(labels.isFalse) },
+ { name: 'eq', type: 'number', label: formatMessage(labels.is) },
+ { name: 'neq', type: 'number', label: formatMessage(labels.isNot) },
+ { name: 'gt', type: 'number', label: formatMessage(labels.greaterThan) },
+ { name: 'lt', type: 'number', label: formatMessage(labels.lessThan) },
+ { name: 'gte', type: 'number', label: formatMessage(labels.greaterThanEquals) },
+ { name: 'lte', type: 'number', label: formatMessage(labels.lessThanEquals) },
+ { name: 'bf', type: 'date', label: formatMessage(labels.before) },
+ { name: 'af', type: 'date', label: formatMessage(labels.after) },
+ { name: 'eq', type: 'uuid', label: formatMessage(labels.is) },
+ ];
const operatorLabels = {
[OPERATORS.equals]: formatMessage(labels.is),
@@ -37,17 +62,38 @@ export function useFilters() {
uuid: [OPERATORS.equals],
};
- const filters = Object.keys(typeFilters).flatMap(key => {
- return (
- typeFilters[key]?.map(value => ({ type: key, value, label: operatorLabels[value] })) ?? []
- );
- });
+ const filters = Object.keys(query).reduce((arr, key) => {
+ if (FILTER_COLUMNS[key]) {
+ let operator = 'eq';
+ let value = safeDecodeURIComponent(query[key]);
+ const label = fields.find(({ name }) => name === key)?.label;
- const getFilters = type => {
- return typeFilters[type]?.map(key => ({ type, value: key, label: operatorLabels[key] })) ?? [];
+ const match = value.match(/^([a-z]+)\.(.*)/);
+
+ if (match) {
+ operator = match[1];
+ value = match[2];
+ }
+
+ return arr.concat({
+ name: key,
+ operator,
+ value,
+ label,
+ });
+ }
+ return arr;
+ }, []);
+
+ const getFilters = (type: string) => {
+ return (
+ typeFilters[type]?.map((key: string | number) => ({
+ type,
+ value: key,
+ label: operatorLabels[key],
+ })) ?? []
+ );
};
- return { filters, operatorLabels, typeFilters, getFilters };
+ return { fields, operators, filters, operatorLabels, typeFilters, getFilters };
}
-
-export default useFilters;
diff --git a/src/components/hooks/useForceUpdate.ts b/src/components/hooks/useForceUpdate.ts
index 35f7fe16..550cc5cd 100644
--- a/src/components/hooks/useForceUpdate.ts
+++ b/src/components/hooks/useForceUpdate.ts
@@ -7,5 +7,3 @@ export function useForceUpdate() {
update(Object.create(null));
}, [update]);
}
-
-export default useForceUpdate;
diff --git a/src/components/hooks/useFormat.ts b/src/components/hooks/useFormat.ts
index 43cba374..896fa076 100644
--- a/src/components/hooks/useFormat.ts
+++ b/src/components/hooks/useFormat.ts
@@ -1,9 +1,9 @@
-import useMessages from './useMessages';
import { BROWSERS, OS_NAMES } from '@/lib/constants';
-import useLocale from './useLocale';
-import useCountryNames from './useCountryNames';
-import useLanguageNames from './useLanguageNames';
import regions from '../../../public/iso-3166-2.json';
+import { useCountryNames } from './useCountryNames';
+import { useLanguageNames } from './useLanguageNames';
+import { useLocale } from './useLocale';
+import { useMessages } from './useMessages';
export function useFormat() {
const { formatMessage, labels } = useMessages();
@@ -40,7 +40,7 @@ export function useFormat() {
return languageNames[value?.split('-')[0]] || value;
};
- const formatValue = (value: string, type: string, data?: { [key: string]: any }): string => {
+ const formatValue = (value: string, type: string, data?: Record): string => {
switch (type) {
case 'os':
return formatOS(value);
@@ -57,7 +57,7 @@ export function useFormat() {
case 'language':
return formatLanguage(value);
default:
- return value;
+ return typeof value === 'string' ? value : undefined;
}
};
@@ -72,5 +72,3 @@ export function useFormat() {
formatValue,
};
}
-
-export default useFormat;
diff --git a/src/components/hooks/useGlobalState.ts b/src/components/hooks/useGlobalState.ts
new file mode 100644
index 00000000..6f21226b
--- /dev/null
+++ b/src/components/hooks/useGlobalState.ts
@@ -0,0 +1,13 @@
+import { create } from 'zustand';
+
+const store = create(() => ({}));
+
+const useGlobalState = (key: string, value?: any) => {
+ if (value !== undefined && store.getState()[key] === undefined) {
+ store.setState({ [key]: value });
+ }
+
+ return [store(state => state[key]), (value: any) => store.setState({ [key]: value })];
+};
+
+export { useGlobalState };
diff --git a/src/components/hooks/useLanguageNames.ts b/src/components/hooks/useLanguageNames.ts
index 8c28d560..0cc03d7c 100644
--- a/src/components/hooks/useLanguageNames.ts
+++ b/src/components/hooks/useLanguageNames.ts
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { httpGet } from '@/lib/fetch';
import enUS from '../../../public/intl/language/en-US.json';
@@ -30,5 +30,3 @@ export function useLanguageNames(locale) {
return { languageNames: list };
}
-
-export default useLanguageNames;
diff --git a/src/components/hooks/useLocale.ts b/src/components/hooks/useLocale.ts
index 863b20a5..3eb669e1 100644
--- a/src/components/hooks/useLocale.ts
+++ b/src/components/hooks/useLocale.ts
@@ -1,20 +1,20 @@
import { useEffect } from 'react';
-import { httpGet } from '@/lib/fetch';
-import { setItem } from '@/lib/storage';
import { LOCALE_CONFIG } from '@/lib/constants';
+import { httpGet } from '@/lib/fetch';
import { getDateLocale, getTextDirection } from '@/lib/lang';
-import useStore, { setLocale } from '@/store/app';
-import { useForceUpdate } from './useForceUpdate';
+import { setItem } from '@/lib/storage';
+import { setLocale, useApp } from '@/store/app';
import enUS from '../../../public/intl/country/en-US.json';
+import { useForceUpdate } from './useForceUpdate';
const messages = {
'en-US': enUS,
};
-const selector = (state: { locale: any }) => state.locale;
+const selector = (state: { locale: string }) => state.locale;
export function useLocale() {
- const locale = useStore(selector);
+ const locale = useApp(selector);
const forceUpdate = useForceUpdate();
const dir = getTextDirection(locale);
const dateLocale = getDateLocale(locale);
@@ -58,5 +58,3 @@ export function useLocale() {
return { locale, saveLocale, messages, dir, dateLocale };
}
-
-export default useLocale;
diff --git a/src/components/hooks/useMessages.ts b/src/components/hooks/useMessages.ts
index fc73494f..d5bc2423 100644
--- a/src/components/hooks/useMessages.ts
+++ b/src/components/hooks/useMessages.ts
@@ -1,27 +1,48 @@
-import { useIntl } from 'react-intl';
-import { messages, labels } from '@/components/messages';
+import { FormattedMessage, type MessageDescriptor, useIntl } from 'react-intl';
+import { labels, messages } from '@/components/messages';
+import type { ApiError } from '@/lib/types';
-export function useMessages(): any {
+type FormatMessage = (
+ descriptor: MessageDescriptor,
+ values?: Record,
+ opts?: any,
+) => string | null;
+
+interface UseMessages {
+ formatMessage: FormatMessage;
+ messages: typeof messages;
+ labels: typeof labels;
+ getMessage: (id: string) => string;
+ getErrorMessage: (error: ApiError) => string | undefined;
+ FormattedMessage: typeof FormattedMessage;
+}
+
+export function useMessages(): UseMessages {
const intl = useIntl();
const getMessage = (id: string) => {
- const message = Object.values(messages).find(value => value.id === id);
+ const message = Object.values(messages).find(value => value.id === `message.${id}`);
return message ? formatMessage(message) : id;
};
+ const getErrorMessage = (error: ApiError) => {
+ if (!error) {
+ return undefined;
+ }
+
+ const code = error?.code;
+
+ return code ? getMessage(code) : error?.message || 'Unknown error';
+ };
+
const formatMessage = (
- descriptor: {
- id: string;
- defaultMessage: string;
- },
- values?: { [key: string]: string },
+ descriptor: MessageDescriptor,
+ values?: Record,
opts?: any,
) => {
return descriptor ? intl.formatMessage(descriptor, values, opts) : null;
};
- return { formatMessage, messages, labels, getMessage };
+ return { formatMessage, messages, labels, getMessage, getErrorMessage, FormattedMessage };
}
-
-export default useMessages;
diff --git a/src/components/hooks/useMobile.ts b/src/components/hooks/useMobile.ts
new file mode 100644
index 00000000..6b40f3da
--- /dev/null
+++ b/src/components/hooks/useMobile.ts
@@ -0,0 +1,9 @@
+import { useBreakpoint } from '@umami/react-zen';
+
+export function useMobile() {
+ const breakpoint = useBreakpoint();
+ const isMobile = ['xs', 'sm', 'md'].includes(breakpoint);
+ const isPhone = ['xs', 'sm'].includes(breakpoint);
+
+ return { breakpoint, isMobile, isPhone };
+}
diff --git a/src/components/hooks/useModified.ts b/src/components/hooks/useModified.ts
index fd8dc2e6..ea88888a 100644
--- a/src/components/hooks/useModified.ts
+++ b/src/components/hooks/useModified.ts
@@ -1,15 +1,13 @@
-import useStore from '@/store/modified';
+import { create } from 'zustand';
+
+const store = create(() => ({}));
+
+export function touch(key: string) {
+ store.setState({ [key]: Date.now() });
+}
export function useModified(key?: string) {
- const modified = useStore(state => state?.[key]);
-
- const touch = (id?: string) => {
- if (id || key) {
- useStore.setState({ [id || key]: Date.now() });
- }
- };
+ const modified = store(state => state?.[key]);
return { modified, touch };
}
-
-export default useModified;
diff --git a/src/components/hooks/useNavigation.ts b/src/components/hooks/useNavigation.ts
index b727ee90..0a18ac7b 100644
--- a/src/components/hooks/useNavigation.ts
+++ b/src/components/hooks/useNavigation.ts
@@ -1,32 +1,43 @@
-import { useMemo } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-import { buildUrl } from '@/lib/url';
+import { useEffect, useState } from 'react';
+import { buildPath } from '@/lib/url';
-export function useNavigation(): {
- pathname: string;
- query: { [key: string]: string };
- router: any;
- renderUrl: (params: any, reset?: boolean) => string;
-} {
+export function useNavigation() {
const router = useRouter();
const pathname = usePathname();
- const params = useSearchParams();
+ const searchParams = useSearchParams();
+ const [, teamId] = pathname.match(/\/teams\/([a-f0-9-]+)/) || [];
+ const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || [];
+ const [queryParams, setQueryParams] = useState(Object.fromEntries(searchParams));
- const query = useMemo(() => {
- const obj = {};
+ const updateParams = (params?: Record) => {
+ return buildPath(pathname, { ...queryParams, ...params });
+ };
- for (const [key, value] of params.entries()) {
- obj[key] = value;
- }
+ const replaceParams = (params?: Record) => {
+ return buildPath(pathname, params);
+ };
- return obj;
- }, [params]);
+ const renderUrl = (path: string, params?: Record | false) => {
+ return buildPath(
+ teamId ? `/teams/${teamId}${path}` : path,
+ params === false ? {} : { ...queryParams, ...params },
+ );
+ };
- function renderUrl(params: any, reset?: boolean) {
- return reset ? pathname : buildUrl(pathname, { ...query, ...params });
- }
+ useEffect(() => {
+ setQueryParams(Object.fromEntries(searchParams));
+ }, [searchParams.toString()]);
- return { pathname, query, router, renderUrl };
+ return {
+ router,
+ pathname,
+ searchParams,
+ query: queryParams,
+ teamId,
+ websiteId,
+ updateParams,
+ replaceParams,
+ renderUrl,
+ };
}
-
-export default useNavigation;
diff --git a/src/components/hooks/usePageParameters.ts b/src/components/hooks/usePageParameters.ts
new file mode 100644
index 00000000..42cf3911
--- /dev/null
+++ b/src/components/hooks/usePageParameters.ts
@@ -0,0 +1,16 @@
+import { useMemo } from 'react';
+import { useNavigation } from './useNavigation';
+
+export function usePageParameters() {
+ const {
+ query: { page, pageSize, search },
+ } = useNavigation();
+
+ return useMemo(() => {
+ return {
+ page,
+ pageSize,
+ search,
+ };
+ }, [page, pageSize, search]);
+}
diff --git a/src/components/hooks/usePagedQuery.ts b/src/components/hooks/usePagedQuery.ts
index b6b06e1c..c818de64 100644
--- a/src/components/hooks/usePagedQuery.ts
+++ b/src/components/hooks/usePagedQuery.ts
@@ -1,33 +1,27 @@
-import { UseQueryOptions } from '@tanstack/react-query';
-import { useState } from 'react';
-import { PageResult, PageParams, PagedQueryResult } from '@/lib/types';
+import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
+import type { PageResult } from '@/lib/types';
import { useApi } from './useApi';
import { useNavigation } from './useNavigation';
-export function usePagedQuery({
+export function usePagedQuery({
queryKey,
queryFn,
...options
-}: Omit & { queryFn: (params?: object) => any }): PagedQueryResult {
- const { query: queryParams } = useNavigation();
- const [params, setParams] = useState({
- search: '',
- page: +queryParams.page || 1,
- });
-
+}: Omit<
+ UseQueryOptions, TError, PageResult, readonly unknown[]>,
+ 'queryFn' | 'queryKey'
+> & {
+ queryKey: readonly unknown[];
+ queryFn: (params?: object) => Promise> | PageResult;
+}): UseQueryResult, TError> {
+ const {
+ query: { page, search },
+ } = useNavigation();
const { useQuery } = useApi();
- const { data, ...query } = useQuery({
- queryKey: [{ ...queryKey, ...params }],
- queryFn: () => queryFn(params as any),
+
+ return useQuery, TError>({
+ queryKey: [...queryKey, page, search] as const,
+ queryFn: () => queryFn({ page, search }),
...options,
});
-
- return {
- result: data as PageResult,
- query,
- params,
- setParams,
- };
}
-
-export default usePagedQuery;
diff --git a/src/components/hooks/useRegionNames.ts b/src/components/hooks/useRegionNames.ts
index 1ba7feaa..57dcc416 100644
--- a/src/components/hooks/useRegionNames.ts
+++ b/src/components/hooks/useRegionNames.ts
@@ -1,5 +1,5 @@
-import useCountryNames from './useCountryNames';
import regions from '../../../public/iso-3166-2.json';
+import { useCountryNames } from './useCountryNames';
export function useRegionNames(locale: string) {
const { countryNames } = useCountryNames(locale);
@@ -9,11 +9,14 @@ export function useRegionNames(locale: string) {
return regions[regionCode];
}
- const region = regionCode.includes('-') ? regionCode : `${countryCode}-${regionCode}`;
+ if (!regionCode) {
+ return null;
+ }
+
+ const region = regionCode?.includes('-') ? regionCode : `${countryCode}-${regionCode}`;
+
return regions[region] ? `${regions[region]}, ${countryNames[countryCode]}` : region;
};
return { regionNames: regions, getRegionName };
}
-
-export default useRegionNames;
diff --git a/src/components/hooks/useSlug.ts b/src/components/hooks/useSlug.ts
new file mode 100644
index 00000000..f795dfeb
--- /dev/null
+++ b/src/components/hooks/useSlug.ts
@@ -0,0 +1,14 @@
+import { useConfig } from '@/components/hooks/useConfig';
+import { LINKS_URL, PIXELS_URL } from '@/lib/constants';
+
+export function useSlug(type: 'link' | 'pixel') {
+ const { linksUrl, pixelsUrl } = useConfig();
+
+ const hostUrl = type === 'link' ? linksUrl || LINKS_URL : pixelsUrl || PIXELS_URL;
+
+ const getSlugUrl = (slug: string) => {
+ return `${hostUrl}/${slug}`;
+ };
+
+ return { getSlugUrl, hostUrl };
+}
diff --git a/src/components/hooks/useSticky.ts b/src/components/hooks/useSticky.ts
index 459c489a..ef9fb36f 100644
--- a/src/components/hooks/useSticky.ts
+++ b/src/components/hooks/useSticky.ts
@@ -1,4 +1,4 @@
-import { useState, useEffect, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
export function useSticky({ enabled = true, threshold = 1 }) {
const [isSticky, setIsSticky] = useState(false);
@@ -6,6 +6,7 @@ export function useSticky({ enabled = true, threshold = 1 }) {
useEffect(() => {
let observer: IntersectionObserver | undefined;
+ // eslint-disable-next-line no-undef
const handler: IntersectionObserverCallback = ([entry]) =>
setIsSticky(entry.intersectionRatio < threshold);
@@ -22,5 +23,3 @@ export function useSticky({ enabled = true, threshold = 1 }) {
return { ref, isSticky };
}
-
-export default useSticky;
diff --git a/src/components/hooks/useTeamUrl.ts b/src/components/hooks/useTeamUrl.ts
deleted file mode 100644
index b2aa8ea7..00000000
--- a/src/components/hooks/useTeamUrl.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { usePathname } from 'next/navigation';
-
-export function useTeamUrl(): {
- teamId?: string;
- renderTeamUrl: (url: string) => string;
-} {
- const pathname = usePathname();
- const [, teamId] = pathname.match(/^\/teams\/([a-f0-9-]+)/) || [];
-
- function renderTeamUrl(url: string) {
- return teamId ? `/teams/${teamId}${url}` : url;
- }
-
- return { teamId, renderTeamUrl };
-}
-
-export default useTeamUrl;
diff --git a/src/components/hooks/useTheme.ts b/src/components/hooks/useTheme.ts
deleted file mode 100644
index 9bbe063c..00000000
--- a/src/components/hooks/useTheme.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { useEffect, useMemo } from 'react';
-import useStore, { setTheme } from '@/store/app';
-import { getItem, setItem } from '@/lib/storage';
-import { DEFAULT_THEME, THEME_COLORS, THEME_CONFIG } from '@/lib/constants';
-import { colord } from 'colord';
-
-const selector = (state: { theme: string }) => state.theme;
-
-export function useTheme() {
- const theme = useStore(selector) || getItem(THEME_CONFIG) || DEFAULT_THEME;
- const primaryColor = colord(THEME_COLORS[theme].primary);
-
- const colors = useMemo(() => {
- return {
- theme: {
- ...THEME_COLORS[theme],
- },
- chart: {
- text: THEME_COLORS[theme].gray700,
- line: THEME_COLORS[theme].gray200,
- views: {
- hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(),
- backgroundColor: primaryColor.alpha(0.4).toRgbString(),
- borderColor: primaryColor.alpha(0.7).toRgbString(),
- hoverBorderColor: primaryColor.toRgbString(),
- },
- visitors: {
- hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(),
- backgroundColor: primaryColor.alpha(0.6).toRgbString(),
- borderColor: primaryColor.alpha(0.9).toRgbString(),
- hoverBorderColor: primaryColor.toRgbString(),
- },
- },
- map: {
- baseColor: THEME_COLORS[theme].primary,
- fillColor: THEME_COLORS[theme].gray100,
- strokeColor: THEME_COLORS[theme].primary,
- hoverColor: THEME_COLORS[theme].primary,
- },
- };
- }, [theme]);
-
- const saveTheme = (value: string) => {
- setItem(THEME_CONFIG, value);
- setTheme(value);
- };
-
- useEffect(() => {
- document.body.setAttribute('data-theme', theme);
- }, [theme]);
-
- useEffect(() => {
- const url = new URL(window?.location?.href);
- const theme = url.searchParams.get('theme');
-
- if (['light', 'dark'].includes(theme)) {
- saveTheme(theme);
- }
- }, []);
-
- return { theme, saveTheme, colors };
-}
-
-export default useTheme;
diff --git a/src/components/hooks/useTimezone.ts b/src/components/hooks/useTimezone.ts
index 99c1f115..ef255390 100644
--- a/src/components/hooks/useTimezone.ts
+++ b/src/components/hooks/useTimezone.ts
@@ -1,13 +1,15 @@
+import { formatInTimeZone, utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';
+import { TIMEZONE_CONFIG, TIMEZONE_LEGACY } from '@/lib/constants';
+import { getTimezone } from '@/lib/date';
import { setItem } from '@/lib/storage';
-import { TIMEZONE_CONFIG } from '@/lib/constants';
-import { formatInTimeZone, zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz';
-import useStore, { setTimezone } from '@/store/app';
-import useLocale from './useLocale';
+import { setTimezone, useApp } from '@/store/app';
+import { useLocale } from './useLocale';
const selector = (state: { timezone: string }) => state.timezone;
export function useTimezone() {
- const timezone = useStore(selector);
+ const timezone = useApp(selector);
+ const localTimeZone = getTimezone();
const { dateLocale } = useLocale();
const saveTimezone = (value: string) => {
@@ -19,13 +21,45 @@ export function useTimezone() {
return formatInTimeZone(
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3})?Z$/.test(date)
? date
- : date.split(' ').join('T') + 'Z',
+ : `${date.split(' ').join('T')}Z`,
timezone,
pattern,
{ locale: dateLocale },
);
};
+ const formatSeriesTimezone = (data: any, column: string, timezone: string) => {
+ return data.map(item => {
+ const date = new Date(item[column]);
+
+ const format = new Intl.DateTimeFormat('en-US', {
+ timeZone: timezone,
+ hour12: false,
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ });
+
+ const parts = format.formatToParts(date);
+ const get = type => parts.find(p => p.type === type)?.value;
+
+ const year = get('year');
+ const month = get('month');
+ const day = get('day');
+ const hour = get('hour');
+ const minute = get('minute');
+ const second = get('second');
+
+ return {
+ ...item,
+ [column]: `${year}-${month}-${day} ${hour}:${minute}:${second}`,
+ };
+ });
+ };
+
const toUtc = (date: Date | string | number) => {
return zonedTimeToUtc(date, timezone);
};
@@ -34,7 +68,28 @@ export function useTimezone() {
return utcToZonedTime(date, timezone);
};
- return { timezone, saveTimezone, formatTimezoneDate, toUtc, fromUtc };
-}
+ const localToUtc = (date: Date | string | number) => {
+ return zonedTimeToUtc(date, localTimeZone);
+ };
-export default useTimezone;
+ const localFromUtc = (date: Date | string | number) => {
+ return utcToZonedTime(date, localTimeZone);
+ };
+
+ const canonicalizeTimezone = (timezone: string): string => {
+ return TIMEZONE_LEGACY[timezone] ?? timezone;
+ };
+
+ return {
+ timezone,
+ localTimeZone,
+ toUtc,
+ fromUtc,
+ localToUtc,
+ localFromUtc,
+ saveTimezone,
+ formatTimezoneDate,
+ formatSeriesTimezone,
+ canonicalizeTimezone,
+ };
+}
diff --git a/src/components/icons.ts b/src/components/icons.ts
index e952e500..fe433d5d 100644
--- a/src/components/icons.ts
+++ b/src/components/icons.ts
@@ -1,59 +1 @@
-import { Icons } from 'react-basics';
-import AddUser from '@/assets/add-user.svg';
-import Bars from '@/assets/bars.svg';
-import BarChart from '@/assets/bar-chart.svg';
-import Bolt from '@/assets/bolt.svg';
-import Calendar from '@/assets/calendar.svg';
-import Change from '@/assets/change.svg';
-import Clock from '@/assets/clock.svg';
-import Compare from '@/assets/compare.svg';
-import Dashboard from '@/assets/dashboard.svg';
-import Eye from '@/assets/eye.svg';
-import Gear from '@/assets/gear.svg';
-import Globe from '@/assets/globe.svg';
-import Location from '@/assets/location.svg';
-import Lock from '@/assets/lock.svg';
-import Logo from '@/assets/logo.svg';
-import Magnet from '@/assets/magnet.svg';
-import Moon from '@/assets/moon.svg';
-import Nodes from '@/assets/nodes.svg';
-import Overview from '@/assets/overview.svg';
-import Profile from '@/assets/profile.svg';
-import PushPin from '@/assets/pushpin.svg';
-import Reports from '@/assets/reports.svg';
-import Sun from '@/assets/sun.svg';
-import User from '@/assets/user.svg';
-import Users from '@/assets/users.svg';
-import Visitor from '@/assets/visitor.svg';
-
-const icons = {
- ...Icons,
- AddUser,
- Bars,
- BarChart,
- Bolt,
- Calendar,
- Change,
- Clock,
- Compare,
- Dashboard,
- Eye,
- Gear,
- Globe,
- Location,
- Lock,
- Logo,
- Magnet,
- Moon,
- Nodes,
- Overview,
- Profile,
- PushPin,
- Reports,
- Sun,
- User,
- Users,
- Visitor,
-};
-
-export default icons;
+export * from 'lucide-react';
diff --git a/src/components/input/ActionSelect.tsx b/src/components/input/ActionSelect.tsx
new file mode 100644
index 00000000..616ee347
--- /dev/null
+++ b/src/components/input/ActionSelect.tsx
@@ -0,0 +1,18 @@
+import { ListItem, Select } from '@umami/react-zen';
+import { useMessages } from '@/components/hooks';
+
+export interface ActionSelectProps {
+ value?: string;
+ onChange?: (value: string) => void;
+}
+
+export function ActionSelect({ value = 'path', onChange }: ActionSelectProps) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+ {formatMessage(labels.viewedPage)}
+ {formatMessage(labels.triggeredEvent)}
+
+ );
+}
diff --git a/src/components/input/CurrencySelect.tsx b/src/components/input/CurrencySelect.tsx
new file mode 100644
index 00000000..2b6045b4
--- /dev/null
+++ b/src/components/input/CurrencySelect.tsx
@@ -0,0 +1,34 @@
+import { ListItem, Select } from '@umami/react-zen';
+import { useState } from 'react';
+import { useMessages } from '@/components/hooks';
+import { CURRENCIES } from '@/lib/constants';
+
+export function CurrencySelect({ value, onChange }) {
+ const { formatMessage, labels } = useMessages();
+ const [search, setSearch] = useState('');
+
+ return (
+
+ {CURRENCIES.map(({ id, name }) => {
+ if (search && !`${id}${name}`.toLowerCase().includes(search)) {
+ return null;
+ }
+
+ return (
+
+ {id} — {name}
+
+ );
+ }).filter(n => n)}
+
+ );
+}
diff --git a/src/components/input/DateFilter.module.css b/src/components/input/DateFilter.module.css
deleted file mode 100644
index bd9ec1a8..00000000
--- a/src/components/input/DateFilter.module.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.dropdown span {
- white-space: nowrap;
-}
diff --git a/src/components/input/DateFilter.tsx b/src/components/input/DateFilter.tsx
index 443827a0..2e175298 100644
--- a/src/components/input/DateFilter.tsx
+++ b/src/components/input/DateFilter.tsx
@@ -1,42 +1,36 @@
-import { useState } from 'react';
-import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics';
-import { endOfYear, isSameDay } from 'date-fns';
-import DatePickerForm from '@/components/metrics/DatePickerForm';
-import { useLocale, useMessages } from '@/components/hooks';
-import Icons from '@/components/icons';
-import { formatDate, parseDateValue } from '@/lib/date';
-import styles from './DateFilter.module.css';
-import classNames from 'classnames';
+import { Dialog, ListItem, ListSeparator, Modal, Select, type SelectProps } from '@umami/react-zen';
+import { endOfYear } from 'date-fns';
+import { Fragment, type Key, useState } from 'react';
+import { DateDisplay } from '@/components/common/DateDisplay';
+import { useMessages, useMobile } from '@/components/hooks';
+import { DatePickerForm } from '@/components/metrics/DatePickerForm';
+import { parseDateRange } from '@/lib/date';
-export interface DateFilterProps {
- value: string;
- startDate: Date;
- endDate: Date;
- offset?: number;
- className?: string;
+export interface DateFilterProps extends SelectProps {
+ value?: string;
onChange?: (value: string) => void;
showAllTime?: boolean;
- alignment?: 'start' | 'center' | 'end';
+ renderDate?: boolean;
+ placement?: any;
}
export function DateFilter({
- startDate,
- endDate,
value,
- offset = 0,
- className,
onChange,
- showAllTime = false,
- alignment = 'end',
+ showAllTime,
+ renderDate,
+ placement = 'bottom',
+ ...props
}: DateFilterProps) {
const { formatMessage, labels } = useMessages();
const [showPicker, setShowPicker] = useState(false);
- const { locale } = useLocale();
+ const { startDate, endDate } = parseDateRange(value) || {};
+ const { isMobile } = useMobile();
const options = [
{ label: formatMessage(labels.today), value: '0day' },
{
- label: formatMessage(labels.lastHours, { x: 24 }),
+ label: formatMessage(labels.lastHours, { x: '24' }),
value: '24hour',
},
{
@@ -45,7 +39,7 @@ export function DateFilter({
divider: true,
},
{
- label: formatMessage(labels.lastDays, { x: 7 }),
+ label: formatMessage(labels.lastDays, { x: '7' }),
value: '7day',
},
{
@@ -54,20 +48,21 @@ export function DateFilter({
divider: true,
},
{
- label: formatMessage(labels.lastDays, { x: 30 }),
+ label: formatMessage(labels.lastDays, { x: '30' }),
value: '30day',
},
{
- label: formatMessage(labels.lastDays, { x: 90 }),
+ label: formatMessage(labels.lastDays, { x: '90' }),
value: '90day',
},
- { label: formatMessage(labels.thisYear), value: '0year', divider: true },
+ { label: formatMessage(labels.thisYear), value: '0year' },
{
- label: formatMessage(labels.lastMonths, { x: 6 }),
+ label: formatMessage(labels.lastMonths, { x: '6' }),
value: '6month',
+ divider: true,
},
{
- label: formatMessage(labels.lastMonths, { x: 12 }),
+ label: formatMessage(labels.lastMonths, { x: '12' }),
value: '12month',
},
showAllTime && {
@@ -80,109 +75,67 @@ export function DateFilter({
value: 'custom',
divider: true,
},
- ].filter(n => n);
+ ]
+ .filter(n => n)
+ .map((a, id) => ({ ...a, id }));
- const handleChange = (value: string) => {
+ const handleChange = (value: Key) => {
if (value === 'custom') {
setShowPicker(true);
return;
}
- onChange(value);
+ onChange(value.toString());
};
const handlePickerChange = (value: string) => {
setShowPicker(false);
- onChange(value);
+ onChange(value.toString());
};
- const handleClose = () => setShowPicker(false);
-
- const renderValue = (value: string) => {
- const { unit } = parseDateValue(value) || {};
-
- if (offset && unit === 'year') {
- return formatDate(startDate, 'yyyy', locale);
- }
-
- if (offset && unit === 'month') {
- return formatDate(startDate, 'MMMM yyyy', locale);
- }
-
- if (value.startsWith('range') || offset) {
- return (
- handleChange('custom')}
- />
- );
- }
-
- return options.find(e => e.value === value)?.label;
+ const renderValue = ({ defaultChildren }) => {
+ return value?.startsWith('range') || renderDate ? (
+
+ ) : (
+ defaultChildren
+ );
};
+ const selectedValue = value.endsWith(':all') ? 'all' : value;
+
return (
<>
- handleChange(key as any)}
+ onChange={handleChange}
+ renderValue={renderValue}
+ popoverProps={{ placement }}
+ isFullscreen={isMobile}
>
- {({ label, value, divider }) => (
- -
- {label}
-
- )}
-
+ {options.map(({ label, value, divider }: any) => {
+ return (
+
+ {divider && }
+ {label}
+
+ );
+ })}
+
{showPicker && (
-
- setShowPicker(false)}
- />
+
+
+ setShowPicker(false)}
+ />
+
)}
>
);
}
-
-const CustomRange = ({ startDate, endDate, unit, onClick }) => {
- const { locale } = useLocale();
-
- const monthFormat = unit === 'month';
-
- function handleClick(e) {
- e.stopPropagation();
-
- onClick();
- }
-
- return (
-
-
-
-
-
- {monthFormat ? (
- <>{formatDate(startDate, 'MMMM yyyy', locale)}>
- ) : (
- <>
- {formatDate(startDate, 'd LLL y', locale)}
- {!isSameDay(startDate, endDate) && ` — ${formatDate(endDate, 'd LLL y', locale)}`}
- >
- )}
-
-
- );
-};
-
-export default DateFilter;
diff --git a/src/components/input/DialogButton.tsx b/src/components/input/DialogButton.tsx
new file mode 100644
index 00000000..7527226d
--- /dev/null
+++ b/src/components/input/DialogButton.tsx
@@ -0,0 +1,64 @@
+import {
+ Button,
+ type ButtonProps,
+ Dialog,
+ type DialogProps,
+ DialogTrigger,
+ IconLabel,
+ Modal,
+} from '@umami/react-zen';
+import type { CSSProperties, ReactNode } from 'react';
+import { useMobile } from '@/components/hooks';
+
+export interface DialogButtonProps extends Omit {
+ icon?: ReactNode;
+ label?: ReactNode;
+ title?: ReactNode;
+ width?: string;
+ height?: string;
+ minWidth?: string;
+ minHeight?: string;
+ children?: DialogProps['children'];
+}
+
+export function DialogButton({
+ icon,
+ label,
+ title,
+ width,
+ height,
+ minWidth,
+ minHeight,
+ children,
+ ...props
+}: DialogButtonProps) {
+ const { isMobile } = useMobile();
+ const style: CSSProperties = {
+ width,
+ height,
+ minWidth,
+ minHeight,
+ maxHeight: 'calc(100dvh - 40px)',
+ padding: '32px',
+ };
+
+ if (isMobile) {
+ style.width = '100%';
+ style.height = '100%';
+ style.maxHeight = '100%';
+ style.overflowY = 'auto';
+ }
+
+ return (
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/components/input/DownloadButton.tsx b/src/components/input/DownloadButton.tsx
new file mode 100644
index 00000000..5df3305d
--- /dev/null
+++ b/src/components/input/DownloadButton.tsx
@@ -0,0 +1,42 @@
+import { Button, Icon, Tooltip, TooltipTrigger } from '@umami/react-zen';
+import Papa from 'papaparse';
+import { useMessages } from '@/components/hooks';
+import { Download } from '@/components/icons';
+
+export function DownloadButton({
+ filename = 'data',
+ data,
+}: {
+ filename?: string;
+ data?: any;
+ onClick?: () => void;
+}) {
+ const { formatMessage, labels } = useMessages();
+
+ const handleClick = async () => {
+ downloadCsv(`${filename}.csv`, Papa.unparse(data));
+ };
+
+ return (
+
+
+
+
+
+
+ {formatMessage(labels.download)}
+
+ );
+}
+
+function downloadCsv(filename: string, data: any) {
+ const blob = new Blob([data], { type: 'text/csv' });
+ const url = URL.createObjectURL(blob);
+
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ a.click();
+
+ URL.revokeObjectURL(url);
+}
diff --git a/src/components/input/ExportButton.tsx b/src/components/input/ExportButton.tsx
new file mode 100644
index 00000000..7b65a57b
--- /dev/null
+++ b/src/components/input/ExportButton.tsx
@@ -0,0 +1,64 @@
+import { Icon, LoadingButton, Tooltip, TooltipTrigger } from '@umami/react-zen';
+import { useSearchParams } from 'next/navigation';
+import { useState } from 'react';
+import { useApi, useMessages } from '@/components/hooks';
+import { useDateParameters } from '@/components/hooks/useDateParameters';
+import { useFilterParameters } from '@/components/hooks/useFilterParameters';
+import { Download } from '@/components/icons';
+
+export function ExportButton({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+ const [isLoading, setIsLoading] = useState(false);
+ const date = useDateParameters();
+ const filters = useFilterParameters();
+ const searchParams = useSearchParams();
+ const { get } = useApi();
+
+ const handleClick = async () => {
+ setIsLoading(true);
+
+ const { zip } = await get(`/websites/${websiteId}/export`, {
+ ...date,
+ ...filters,
+ ...searchParams,
+ format: 'json',
+ });
+
+ await loadZip(zip);
+
+ setIsLoading(false);
+ };
+
+ return (
+
+
+
+
+
+
+ {formatMessage(labels.download)}
+
+ );
+}
+
+async function loadZip(zip: string) {
+ const binary = atob(zip);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+
+ const blob = new Blob([bytes], { type: 'application/zip' });
+ const url = URL.createObjectURL(blob);
+
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'download.zip';
+ a.click();
+ URL.revokeObjectURL(url);
+}
diff --git a/src/components/input/FieldFilters.tsx b/src/components/input/FieldFilters.tsx
new file mode 100644
index 00000000..2174068f
--- /dev/null
+++ b/src/components/input/FieldFilters.tsx
@@ -0,0 +1,117 @@
+import {
+ Button,
+ Column,
+ Grid,
+ Icon,
+ List,
+ ListItem,
+ Menu,
+ MenuItem,
+ MenuTrigger,
+ Popover,
+ Row,
+} from '@umami/react-zen';
+import { endOfDay, subMonths } from 'date-fns';
+import type { Key } from 'react';
+import { Empty } from '@/components/common/Empty';
+import { FilterRecord } from '@/components/common/FilterRecord';
+import { useFields, useMessages, useMobile } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+
+export interface FieldFiltersProps {
+ websiteId: string;
+ value?: { name: string; operator: string; value: string }[];
+ exclude?: string[];
+ onChange?: (data: any) => void;
+}
+
+export function FieldFilters({ websiteId, value, exclude = [], onChange }: FieldFiltersProps) {
+ const { formatMessage, messages } = useMessages();
+ const { fields } = useFields();
+ const startDate = subMonths(endOfDay(new Date()), 6);
+ const endDate = endOfDay(new Date());
+ const { isMobile } = useMobile();
+
+ const updateFilter = (name: string, props: Record) => {
+ onChange(value.map(filter => (filter.name === name ? { ...filter, ...props } : filter)));
+ };
+
+ const handleAdd = (name: Key) => {
+ onChange(value.concat({ name: name.toString(), operator: 'eq', value: '' }));
+ };
+
+ const handleChange = (name: string, value: Key) => {
+ updateFilter(name, { value });
+ };
+
+ const handleSelect = (name: string, operator: Key) => {
+ updateFilter(name, { operator });
+ };
+
+ const handleRemove = (name: string) => {
+ onChange(value.filter(filter => filter.name !== name));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {fields
+ .filter(({ name }) => !exclude.includes(name))
+ .map(field => {
+ const isDisabled = !!value.find(({ name }) => name === field.name);
+ return (
+
+ {field.label}
+
+ );
+ })}
+
+
+
+
+
+
+ {fields
+ .filter(({ name }) => !exclude.includes(name))
+ .map(field => {
+ const isDisabled = !!value.find(({ name }) => name === field.name);
+ return (
+
+ {field.label}
+
+ );
+ })}
+
+
+
+ {value.map(filter => {
+ return (
+
+ );
+ })}
+ {!value.length && }
+
+
+ );
+}
diff --git a/src/components/input/FilterBar.tsx b/src/components/input/FilterBar.tsx
new file mode 100644
index 00000000..5a52e566
--- /dev/null
+++ b/src/components/input/FilterBar.tsx
@@ -0,0 +1,155 @@
+import {
+ Button,
+ Dialog,
+ DialogTrigger,
+ Icon,
+ Modal,
+ Row,
+ Text,
+ Tooltip,
+ TooltipTrigger,
+} from '@umami/react-zen';
+import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm';
+import {
+ useFilters,
+ useFormat,
+ useMessages,
+ useNavigation,
+ useWebsiteSegmentQuery,
+} from '@/components/hooks';
+import { Bookmark, X } from '@/components/icons';
+import { isSearchOperator } from '@/lib/params';
+
+export function FilterBar({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+ const { formatValue } = useFormat();
+ const {
+ router,
+ pathname,
+ updateParams,
+ replaceParams,
+ query: { segment, cohort },
+ } = useNavigation();
+ const { filters, operatorLabels } = useFilters();
+ const { data, isLoading } = useWebsiteSegmentQuery(websiteId, segment || cohort);
+ const canSaveSegment = filters.length > 0 && !segment && !cohort && !pathname.includes('/share');
+
+ const handleCloseFilter = (param: string) => {
+ router.push(updateParams({ [param]: undefined }));
+ };
+
+ const handleResetFilter = () => {
+ router.push(replaceParams());
+ };
+
+ const handleSegmentRemove = (type: string) => {
+ router.push(updateParams({ [type]: undefined }));
+ };
+
+ if (!filters.length && !segment && !cohort) {
+ return null;
+ }
+
+ return (
+
+
+ {segment && !isLoading && (
+ handleSegmentRemove('segment')}
+ />
+ )}
+ {cohort && !isLoading && (
+ handleSegmentRemove('cohort')}
+ />
+ )}
+ {filters.map(filter => {
+ const { name, label, operator, value } = filter;
+ const paramValue = isSearchOperator(operator) ? value : formatValue(value, name);
+
+ return (
+ handleCloseFilter(name)}
+ />
+ );
+ })}
+
+
+
+ {canSaveSegment && (
+
+
+
+
+
+
+
+ {formatMessage(labels.saveSegment)}
+
+
+ )}
+
+
+ {({ close }) => {
+ return ;
+ }}
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.clearAll)}
+
+
+
+
+ );
+}
+
+const FilterItem = ({ name, label, operator, value, onRemove }) => {
+ return (
+
+
+
+
+ {label}
+
+ {operator}
+
+ {value}
+
+
+ onRemove(name)} size="xs" style={{ cursor: 'pointer' }}>
+
+
+
+
+ );
+};
diff --git a/src/components/input/FilterButtons.tsx b/src/components/input/FilterButtons.tsx
new file mode 100644
index 00000000..ff37fb19
--- /dev/null
+++ b/src/components/input/FilterButtons.tsx
@@ -0,0 +1,33 @@
+import { Box, ToggleGroup, ToggleGroupItem } from '@umami/react-zen';
+import { useState } from 'react';
+
+export interface FilterButtonsProps {
+ items: { id: string; label: string }[];
+ value: string;
+ onChange?: (value: string) => void;
+}
+
+export function FilterButtons({ items, value, onChange }: FilterButtonsProps) {
+ const [selected, setSelected] = useState(value);
+
+ const handleChange = (value: string) => {
+ setSelected(value);
+ onChange?.(value);
+ };
+
+ return (
+
+ handleChange(e[0])}
+ disallowEmptySelection={true}
+ >
+ {items.map(({ id, label }) => (
+
+ {label}
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/input/FilterEditForm.tsx b/src/components/input/FilterEditForm.tsx
new file mode 100644
index 00000000..44f43844
--- /dev/null
+++ b/src/components/input/FilterEditForm.tsx
@@ -0,0 +1,95 @@
+import { Button, Column, Row, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
+import { useState } from 'react';
+import { useFilters, useMessages, useMobile, useNavigation } from '@/components/hooks';
+import { FieldFilters } from '@/components/input/FieldFilters';
+import { SegmentFilters } from '@/components/input/SegmentFilters';
+
+export interface FilterEditFormProps {
+ websiteId?: string;
+ onChange?: (params: { filters: any[]; segment?: string; cohort?: string }) => void;
+ onClose?: () => void;
+}
+
+export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormProps) {
+ const {
+ query: { segment, cohort },
+ pathname,
+ } = useNavigation();
+ const { filters } = useFilters();
+ const { formatMessage, labels } = useMessages();
+ const [currentFilters, setCurrentFilters] = useState(filters);
+ const [currentSegment, setCurrentSegment] = useState(segment);
+ const [currentCohort, setCurrentCohort] = useState(cohort);
+ const { isMobile } = useMobile();
+ const excludeFilters = pathname.includes('/pixels') || pathname.includes('/links');
+
+ const handleReset = () => {
+ setCurrentFilters([]);
+ setCurrentSegment(undefined);
+ setCurrentCohort(undefined);
+ };
+
+ const handleSave = () => {
+ onChange?.({
+ filters: currentFilters.filter(f => f.value),
+ segment: currentSegment,
+ cohort: currentCohort,
+ });
+ onClose?.();
+ };
+
+ const handleSegmentChange = (id: string, type: string) => {
+ setCurrentSegment(type === 'segment' ? id : undefined);
+ setCurrentCohort(type === 'cohort' ? id : undefined);
+ };
+
+ return (
+
+
+
+
+ {formatMessage(labels.fields)}
+ {!excludeFilters && (
+ <>
+ {formatMessage(labels.segments)}
+ {formatMessage(labels.cohorts)}
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.reset)}
+
+ {formatMessage(labels.cancel)}
+
+ {formatMessage(labels.apply)}
+
+
+
+
+ );
+}
diff --git a/src/components/input/LanguageButton.module.css b/src/components/input/LanguageButton.module.css
deleted file mode 100644
index dccb098d..00000000
--- a/src/components/input/LanguageButton.module.css
+++ /dev/null
@@ -1,45 +0,0 @@
-.menu {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- padding: 10px;
- background: var(--base50);
- z-index: var(--z-index-popup);
- border-radius: 5px;
- border: 1px solid var(--border-color);
- margin-inline-start: 10px;
-}
-
-.item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- min-width: 200px;
- border-radius: 5px;
- padding: 5px 10px;
-}
-
-.item:hover {
- background: var(--base75);
- cursor: pointer;
-}
-
-.selected {
- font-weight: 700;
- background: var(--blue100);
-}
-
-.icon {
- color: var(--primary400);
-}
-
-@media screen and (max-width: 992px) {
- .menu {
- grid-template-columns: repeat(2, 1fr);
- }
-}
-
-@media screen and (max-width: 768px) {
- .menu {
- transform: translateX(40px);
- }
-}
diff --git a/src/components/input/LanguageButton.tsx b/src/components/input/LanguageButton.tsx
index 54ce55eb..ac43dcb6 100644
--- a/src/components/input/LanguageButton.tsx
+++ b/src/components/input/LanguageButton.tsx
@@ -1,53 +1,41 @@
-import { Icon, Button, PopupTrigger, Popup } from 'react-basics';
-import classNames from 'classnames';
-import { languages } from '@/lib/lang';
+import { Button, Dialog, Grid, Icon, MenuTrigger, Popover, Text } from '@umami/react-zen';
+import { Globe } from 'lucide-react';
import { useLocale } from '@/components/hooks';
-import Icons from '@/components/icons';
-import styles from './LanguageButton.module.css';
+import { languages } from '@/lib/lang';
export function LanguageButton() {
- const { locale, saveLocale, dir } = useLocale();
+ const { locale, saveLocale } = useLocale();
const items = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
- function handleSelect(value: string, close: () => void, e: MouseEvent) {
- e.stopPropagation();
+ function handleSelect(value: string) {
saveLocale(value);
- close();
}
return (
-
+
-
+
-
- {(close: () => void) => {
- return (
-
- {items.map(({ value, label }) => {
- return (
-
handleSelect(value, close, e)}
+
+
+
+ {items.map(({ value, label }) => {
+ return (
+ handleSelect(value)}>
+
- {label}
- {value === locale && (
-
-
-
- )}
-
- );
- })}
-
- );
- }}
-
-
+ {label}
+
+
+ );
+ })}
+
+
+
+
);
}
-
-export default LanguageButton;
diff --git a/src/components/input/LogoutButton.tsx b/src/components/input/LogoutButton.tsx
deleted file mode 100644
index a1a34a00..00000000
--- a/src/components/input/LogoutButton.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Button, Icon, Icons, TooltipPopup } from 'react-basics';
-import Link from 'next/link';
-import { useMessages } from '@/components/hooks';
-
-export function LogoutButton({
- tooltipPosition = 'top',
-}: {
- tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
-}) {
- const { formatMessage, labels } = useMessages();
- return (
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default LogoutButton;
diff --git a/src/components/input/LookupField.tsx b/src/components/input/LookupField.tsx
new file mode 100644
index 00000000..c1d419f7
--- /dev/null
+++ b/src/components/input/LookupField.tsx
@@ -0,0 +1,65 @@
+import { ComboBox, type ComboBoxProps, ListItem, Loading, useDebounce } from '@umami/react-zen';
+import { endOfDay, subMonths } from 'date-fns';
+import { type SetStateAction, useMemo, useState } from 'react';
+import { Empty } from '@/components/common/Empty';
+import { useMessages, useWebsiteValuesQuery } from '@/components/hooks';
+
+export interface LookupFieldProps extends ComboBoxProps {
+ websiteId: string;
+ type: string;
+ value: string;
+ onChange: (value: string) => void;
+}
+
+export function LookupField({ websiteId, type, value, onChange, ...props }: LookupFieldProps) {
+ const { formatMessage, messages } = useMessages();
+ const [search, setSearch] = useState(value);
+ const searchValue = useDebounce(search, 300);
+ const startDate = subMonths(endOfDay(new Date()), 6);
+ const endDate = endOfDay(new Date());
+
+ const { data, isLoading } = useWebsiteValuesQuery({
+ websiteId,
+ type,
+ search: searchValue,
+ startDate,
+ endDate,
+ });
+
+ const items: string[] = useMemo(() => {
+ return data?.map(({ value }) => value) || [];
+ }, [data]);
+
+ const handleSearch = (value: SetStateAction) => {
+ setSearch(value);
+ };
+
+ return (
+ {
+ handleSearch(value);
+ onChange?.(value);
+ }}
+ formValue="text"
+ allowsEmptyCollection
+ allowsCustomValue
+ renderEmptyState={() =>
+ isLoading ? (
+
+ ) : (
+
+ )
+ }
+ >
+ {items.map(item => (
+
+ {item}
+
+ ))}
+
+ );
+}
diff --git a/src/components/input/MenuButton.tsx b/src/components/input/MenuButton.tsx
new file mode 100644
index 00000000..bac307fe
--- /dev/null
+++ b/src/components/input/MenuButton.tsx
@@ -0,0 +1,32 @@
+import { Button, DialogTrigger, Icon, Menu, Popover } from '@umami/react-zen';
+import type { Key, ReactNode } from 'react';
+import { Ellipsis } from '@/components/icons';
+
+export function MenuButton({
+ children,
+ onAction,
+ isDisabled,
+}: {
+ children: ReactNode;
+ onAction?: (action: string) => void;
+ isDisabled?: boolean;
+}) {
+ const handleAction = (key: Key) => {
+ onAction?.(key.toString());
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/components/input/MobileMenuButton.tsx b/src/components/input/MobileMenuButton.tsx
new file mode 100644
index 00000000..5e59cbbb
--- /dev/null
+++ b/src/components/input/MobileMenuButton.tsx
@@ -0,0 +1,17 @@
+import { Button, Dialog, type DialogProps, DialogTrigger, Icon, Modal } from '@umami/react-zen';
+import { Menu } from '@/components/icons';
+
+export function MobileMenuButton(props: DialogProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/input/MonthFilter.tsx b/src/components/input/MonthFilter.tsx
new file mode 100644
index 00000000..dec64b0f
--- /dev/null
+++ b/src/components/input/MonthFilter.tsx
@@ -0,0 +1,18 @@
+import { useDateRange, useNavigation } from '@/components/hooks';
+import { getMonthDateRangeValue } from '@/lib/date';
+import { MonthSelect } from './MonthSelect';
+
+export function MonthFilter() {
+ const { router, updateParams } = useNavigation();
+ const {
+ dateRange: { startDate },
+ } = useDateRange();
+
+ const handleMonthSelect = (date: Date) => {
+ const range = getMonthDateRangeValue(date);
+
+ router.push(updateParams({ date: range, offset: undefined }));
+ };
+
+ return ;
+}
diff --git a/src/components/input/MonthSelect.module.css b/src/components/input/MonthSelect.module.css
deleted file mode 100644
index 3b13bcc1..00000000
--- a/src/components/input/MonthSelect.module.css
+++ /dev/null
@@ -1,22 +0,0 @@
-.container {
- display: flex;
- align-items: center;
- justify-content: center;
- border: 1px solid var(--base400);
- border-radius: var(--border-radius);
-}
-
-.input {
- display: flex;
- align-items: center;
- gap: 10px;
- cursor: pointer;
-}
-
-.popup {
- border: 1px solid var(--base400);
- background: var(--base50);
- border-radius: var(--border-radius);
- padding: 20px;
- margin-top: 5px;
-}
diff --git a/src/components/input/MonthSelect.tsx b/src/components/input/MonthSelect.tsx
index 144f5bd8..241634ed 100644
--- a/src/components/input/MonthSelect.tsx
+++ b/src/components/input/MonthSelect.tsx
@@ -1,66 +1,47 @@
-import { useRef } from 'react';
-import {
- Text,
- Icon,
- CalendarMonthSelect,
- CalendarYearSelect,
- Button,
- PopupTrigger,
- Popup,
-} from 'react-basics';
-import { startOfMonth, endOfMonth } from 'date-fns';
-import Icons from '@/components/icons';
+import { ListItem, Row, Select } from '@umami/react-zen';
import { useLocale } from '@/components/hooks';
import { formatDate } from '@/lib/date';
-import styles from './MonthSelect.module.css';
export function MonthSelect({ date = new Date(), onChange }) {
- const { locale, dateLocale } = useLocale();
- const month = formatDate(date, 'MMMM', locale);
+ const { locale } = useLocale();
+ const month = date.getMonth();
const year = date.getFullYear();
- const ref = useRef();
+ const currentYear = new Date().getFullYear();
- const handleChange = (close: () => void, date: Date) => {
- onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`);
- close();
+ const months = [...Array(12)].map((_, i) => i);
+ const years = [...Array(10)].map((_, i) => currentYear - i);
+
+ const handleMonthChange = (month: number) => {
+ const d = new Date(date);
+ d.setMonth(month);
+ onChange?.(d);
+ };
+ const handleYearChange = (year: number) => {
+ const d = new Date(date);
+ d.setFullYear(year);
+ onChange?.(d);
};
return (
- <>
-
-
-
- {month}
-
-
-
-
-
- {close => (
-
- )}
-
-
-
-
- {year}
-
-
-
-
-
- {(close: any) => (
-
- )}
-
-
-
- >
+
+
+ {months.map(m => {
+ return (
+
+ {formatDate(new Date(year, m, 1), 'MMMM', locale)}
+
+ );
+ })}
+
+
+ {years.map(y => {
+ return (
+
+ {y}
+
+ );
+ })}
+
+
);
}
-
-export default MonthSelect;
diff --git a/src/components/input/NavButton.tsx b/src/components/input/NavButton.tsx
new file mode 100644
index 00000000..ab77ef06
--- /dev/null
+++ b/src/components/input/NavButton.tsx
@@ -0,0 +1,188 @@
+import {
+ Column,
+ Icon,
+ IconLabel,
+ Menu,
+ MenuItem,
+ MenuSection,
+ MenuSeparator,
+ MenuTrigger,
+ Popover,
+ Pressable,
+ Row,
+ SubmenuTrigger,
+ Text,
+} from '@umami/react-zen';
+import { ArrowRight } from 'lucide-react';
+import type { Key } from 'react';
+import {
+ useConfig,
+ useLoginQuery,
+ useMessages,
+ useMobile,
+ useNavigation,
+} from '@/components/hooks';
+import {
+ BookText,
+ ChevronRight,
+ ExternalLink,
+ LifeBuoy,
+ LockKeyhole,
+ LogOut,
+ Settings,
+ User,
+ Users,
+} from '@/components/icons';
+import { Switch } from '@/components/svg';
+import { DOCS_URL, LAST_TEAM_CONFIG } from '@/lib/constants';
+import { removeItem } from '@/lib/storage';
+
+export interface TeamsButtonProps {
+ showText?: boolean;
+ onAction?: (id: any) => void;
+}
+
+export function NavButton({ showText = true }: TeamsButtonProps) {
+ const { user } = useLoginQuery();
+ const { cloudMode } = useConfig();
+ const { formatMessage, labels } = useMessages();
+ const { teamId, router } = useNavigation();
+ const { isMobile } = useMobile();
+ const team = user?.teams?.find(({ id }) => id === teamId);
+ const selectedKeys = new Set([teamId || 'user']);
+ const label = teamId ? team?.name : user.username;
+
+ const getUrl = (url: string) => {
+ return cloudMode ? `${process.env.cloudUrl}${url}` : url;
+ };
+
+ const handleAction = async (key: Key) => {
+ if (key === 'user') {
+ removeItem(LAST_TEAM_CONFIG);
+ if (cloudMode) {
+ window.location.href = '/';
+ } else {
+ router.push('/');
+ }
+ }
+ };
+
+ return (
+
+
+
+
+ {teamId ? : }
+ {showText && {label} }
+
+ {showText && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ } label={formatMessage(labels.switchAccount)} />
+
+
+
+
+
+
+ } label={user.username} />
+
+
+
+
+ {user?.teams?.map(({ id, name }) => (
+
+ }>
+ {name}
+
+
+ ))}
+ {user?.teams?.length === 0 && (
+
+
+
+ Manage teams
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ }
+ label={formatMessage(labels.settings)}
+ />
+ {cloudMode && (
+ <>
+ }
+ label={formatMessage(labels.documentation)}
+ >
+
+
+
+
+ }
+ label={formatMessage(labels.support)}
+ />
+ >
+ )}
+ {!cloudMode && user.isAdmin && (
+ <>
+
+ }
+ label={formatMessage(labels.admin)}
+ />
+ >
+ )}
+
+ }
+ label={formatMessage(labels.logout)}
+ />
+
+
+
+
+ );
+}
diff --git a/src/components/input/PanelButton.tsx b/src/components/input/PanelButton.tsx
new file mode 100644
index 00000000..500c40c4
--- /dev/null
+++ b/src/components/input/PanelButton.tsx
@@ -0,0 +1,19 @@
+import { Button, type ButtonProps, Icon } from '@umami/react-zen';
+import { useGlobalState } from '@/components/hooks';
+import { PanelLeft } from '@/components/icons';
+
+export function PanelButton(props: ButtonProps) {
+ const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed');
+ return (
+ setIsCollapsed(!isCollapsed)}
+ variant="zero"
+ {...props}
+ style={{ padding: 0 }}
+ >
+
+
+
+
+ );
+}
diff --git a/src/components/input/PreferencesButton.tsx b/src/components/input/PreferencesButton.tsx
new file mode 100644
index 00000000..710a7fae
--- /dev/null
+++ b/src/components/input/PreferencesButton.tsx
@@ -0,0 +1,32 @@
+import { Button, Column, DialogTrigger, Icon, Label, Popover } from '@umami/react-zen';
+import { DateRangeSetting } from '@/app/(main)/settings/preferences/DateRangeSetting';
+import { TimezoneSetting } from '@/app/(main)/settings/preferences/TimezoneSetting';
+import { Panel } from '@/components/common/Panel';
+import { useMessages } from '@/components/hooks';
+import { Settings } from '@/components/icons';
+
+export function PreferencesButton() {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.timezone)}
+
+
+
+ {formatMessage(labels.defaultDateRange)}
+
+
+
+
+
+ );
+}
diff --git a/src/components/input/ProfileButton.module.css b/src/components/input/ProfileButton.module.css
deleted file mode 100644
index 37f51f78..00000000
--- a/src/components/input/ProfileButton.module.css
+++ /dev/null
@@ -1,28 +0,0 @@
-.menu {
- width: 200px;
- z-index: var(--z-index-popup);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- overflow: hidden;
- background: var(--base50);
-}
-
-.item {
- display: flex;
- gap: 12px;
- background: var(--base50);
-}
-
-.version {
- font-family: monospace;
- font-size: 11px;
- color: var(--base600);
- text-align: right;
- margin-inline-end: 10px;
-}
-
-.name {
- color: var(--font-color200);
- background: var(--base75);
- padding: var(--size300) var(--size600);
-}
diff --git a/src/components/input/ProfileButton.tsx b/src/components/input/ProfileButton.tsx
index 86a9d333..505cd888 100644
--- a/src/components/input/ProfileButton.tsx
+++ b/src/components/input/ProfileButton.tsx
@@ -1,59 +1,74 @@
-import { Key } from 'react';
-import { Icon, Button, PopupTrigger, Popup, Menu, Item, Text } from 'react-basics';
-import { useRouter } from 'next/navigation';
-import Icons from '@/components/icons';
-import { useMessages, useLogin, useLocale } from '@/components/hooks';
-import { CURRENT_VERSION } from '@/lib/constants';
-import styles from './ProfileButton.module.css';
+import {
+ Button,
+ Icon,
+ Menu,
+ MenuItem,
+ MenuSection,
+ MenuSeparator,
+ MenuTrigger,
+ Popover,
+ Row,
+ Text,
+} from '@umami/react-zen';
+import { Fragment } from 'react';
+import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
+import { LockKeyhole, LogOut, UserCircle } from '@/components/icons';
export function ProfileButton() {
const { formatMessage, labels } = useMessages();
- const { user } = useLogin();
- const router = useRouter();
- const { dir } = useLocale();
- const cloudMode = !!process.env.cloudMode;
+ const { user } = useLoginQuery();
+ const { renderUrl } = useNavigation();
- const handleSelect = (key: Key, close: () => void) => {
- if (key === 'profile') {
- router.push('/profile');
- }
- if (key === 'logout') {
- router.push('/logout');
- }
- close();
- };
+ const items = [
+ {
+ id: 'settings',
+ label: formatMessage(labels.profile),
+ path: renderUrl('/settings/profile'),
+ icon: ,
+ },
+ user.isAdmin &&
+ !process.env.cloudMode && {
+ id: 'admin',
+ label: formatMessage(labels.admin),
+ path: '/admin',
+ icon: ,
+ },
+ {
+ id: 'logout',
+ label: formatMessage(labels.logout),
+ path: '/logout',
+ icon: ,
+ separator: true,
+ },
+ ].filter(n => n);
return (
-
+
-
+
-
- {(close: () => void) => (
- handleSelect(key, close)} className={styles.menu}>
- {user.username}
- -
-
-
-
- {formatMessage(labels.profile)}
-
- {!cloudMode && (
- -
-
-
-
- {formatMessage(labels.logout)}
-
- )}
- {`v${CURRENT_VERSION}`}
-
- )}
-
-
+
+
+
+
+ {items.map(({ id, path, label, icon, separator }) => {
+ return (
+
+ {separator && }
+
+
+ {icon}
+ {label}
+
+
+
+ );
+ })}
+
+
+
+
);
}
-
-export default ProfileButton;
diff --git a/src/components/input/RefreshButton.tsx b/src/components/input/RefreshButton.tsx
index 35bfbf3c..b52f830e 100644
--- a/src/components/input/RefreshButton.tsx
+++ b/src/components/input/RefreshButton.tsx
@@ -1,8 +1,7 @@
-import { LoadingButton, Icon, TooltipPopup } from 'react-basics';
+import { Icon, LoadingButton, Tooltip, TooltipTrigger } from '@umami/react-zen';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { RefreshCw } from '@/components/icons';
import { setWebsiteDateRange } from '@/store/websites';
-import { useDateRange } from '@/components/hooks';
-import Icons from '@/components/icons';
-import { useMessages } from '@/components/hooks';
export function RefreshButton({
websiteId,
@@ -12,7 +11,7 @@ export function RefreshButton({
isLoading?: boolean;
}) {
const { formatMessage, labels } = useMessages();
- const { dateRange } = useDateRange(websiteId);
+ const { dateRange } = useDateRange();
function handleClick() {
if (!isLoading && dateRange) {
@@ -21,14 +20,13 @@ export function RefreshButton({
}
return (
-
-
+
+
-
+
-
+ {formatMessage(labels.refresh)}
+
);
}
-
-export default RefreshButton;
diff --git a/src/components/input/ReportEditButton.tsx b/src/components/input/ReportEditButton.tsx
new file mode 100644
index 00000000..b333077a
--- /dev/null
+++ b/src/components/input/ReportEditButton.tsx
@@ -0,0 +1,99 @@
+import {
+ AlertDialog,
+ Button,
+ Icon,
+ Menu,
+ MenuItem,
+ MenuTrigger,
+ Modal,
+ Popover,
+ Row,
+ Text,
+} from '@umami/react-zen';
+import { type ReactNode, useState } from 'react';
+import { useMessages } from '@/components/hooks';
+import { useDeleteQuery } from '@/components/hooks/queries/useDeleteQuery';
+import { Edit, MoreHorizontal, Trash } from '@/components/icons';
+
+export function ReportEditButton({
+ id,
+ name,
+ type,
+ children,
+ onDelete,
+}: {
+ id: string;
+ name: string;
+ type: string;
+ onDelete?: () => void;
+ children: ({ close }: { close: () => void }) => ReactNode;
+}) {
+ const { formatMessage, labels, messages } = useMessages();
+ const [showEdit, setShowEdit] = useState(false);
+ const [showDelete, setShowDelete] = useState(false);
+ const { mutateAsync, touch } = useDeleteQuery(`/reports/${id}`);
+
+ const handleAction = (id: any) => {
+ if (id === 'edit') {
+ setShowEdit(true);
+ } else if (id === 'delete') {
+ setShowDelete(true);
+ }
+ };
+
+ const handleClose = () => {
+ setShowEdit(false);
+ setShowDelete(false);
+ };
+
+ const handleDelete = async () => {
+ await mutateAsync(null, {
+ onSuccess: async () => {
+ touch(`reports:${type}`);
+ setShowDelete(false);
+ onDelete?.();
+ },
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.edit)}
+
+
+
+
+
+ {formatMessage(labels.delete)}
+
+
+
+
+
+ {showEdit && children({ close: handleClose })}
+ {showDelete && (
+
+ {formatMessage(messages.confirmDelete, { target: name })}
+
+ )}
+
+ >
+ );
+}
diff --git a/src/components/input/SegmentFilters.tsx b/src/components/input/SegmentFilters.tsx
new file mode 100644
index 00000000..f03a1dea
--- /dev/null
+++ b/src/components/input/SegmentFilters.tsx
@@ -0,0 +1,42 @@
+import { IconLabel, List, ListItem } from '@umami/react-zen';
+import { Empty } from '@/components/common/Empty';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useWebsiteSegmentsQuery } from '@/components/hooks';
+import { ChartPie, UserPlus } from '@/components/icons';
+
+export interface SegmentFiltersProps {
+ websiteId: string;
+ segmentId: string;
+ type?: string;
+ onChange?: (id: string, type: string) => void;
+}
+
+export function SegmentFilters({
+ websiteId,
+ segmentId,
+ type = 'segment',
+ onChange,
+}: SegmentFiltersProps) {
+ const { data, isLoading, isFetching } = useWebsiteSegmentsQuery(websiteId, { type });
+
+ const handleChange = (id: string) => {
+ onChange?.(id, type);
+ };
+
+ return (
+
+ {data?.data?.length === 0 && }
+ handleChange(id[0])}>
+ {data?.data?.map(item => {
+ return (
+
+ : }>
+ {item.name}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/input/SegmentSaveButton.tsx b/src/components/input/SegmentSaveButton.tsx
new file mode 100644
index 00000000..5f6cac10
--- /dev/null
+++ b/src/components/input/SegmentSaveButton.tsx
@@ -0,0 +1,26 @@
+import { Button, Dialog, DialogTrigger, Icon, Modal, Text } from '@umami/react-zen';
+import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm';
+import { useMessages } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+
+export function SegmentSaveButton({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.segment)}
+
+
+
+ {({ close }) => {
+ return ;
+ }}
+
+
+
+ );
+}
diff --git a/src/components/input/SettingsButton.module.css b/src/components/input/SettingsButton.module.css
deleted file mode 100644
index 8ebd4483..00000000
--- a/src/components/input/SettingsButton.module.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.popup {
- background: var(--base50);
- border: 1px solid var(--base500);
- border-radius: 4px;
- display: flex;
- flex-direction: column;
- position: absolute;
- top: 100%;
- right: 0;
- padding: 20px;
-}
diff --git a/src/components/input/SettingsButton.tsx b/src/components/input/SettingsButton.tsx
index d3dc471f..bd51fb53 100644
--- a/src/components/input/SettingsButton.tsx
+++ b/src/components/input/SettingsButton.tsx
@@ -1,32 +1,84 @@
-import { Button, Icon, PopupTrigger, Popup, Form, FormRow } from 'react-basics';
-import TimezoneSetting from '@/app/(main)/profile/TimezoneSetting';
-import DateRangeSetting from '@/app/(main)/profile/DateRangeSetting';
-import Icons from '@/components/icons';
-import { useMessages } from '@/components/hooks';
-import styles from './SettingsButton.module.css';
+import {
+ Button,
+ Icon,
+ Menu,
+ MenuItem,
+ MenuSection,
+ MenuSeparator,
+ MenuTrigger,
+ Popover,
+} from '@umami/react-zen';
+import type { Key } from 'react';
+import { useConfig, useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
+import {
+ BookText,
+ ExternalLink,
+ LifeBuoy,
+ LockKeyhole,
+ LogOut,
+ Settings,
+ UserCircle,
+} from '@/components/icons';
+import { DOCS_URL } from '@/lib/constants';
export function SettingsButton() {
const { formatMessage, labels } = useMessages();
+ const { user } = useLoginQuery();
+ const { router } = useNavigation();
+ const { cloudMode } = useConfig();
+
+ const handleAction = (id: Key) => {
+ const url = id.toString();
+
+ if (cloudMode) {
+ if (url === '/docs') {
+ window.open(DOCS_URL, '_blank');
+ } else {
+ window.location.href = url;
+ }
+ } else {
+ router.push(url);
+ }
+ };
return (
-
-
+
+
-
+
-
-
-
-
+
+
+
+
+ } label={formatMessage(labels.settings)} />
+ {!cloudMode && user.isAdmin && (
+ } label={formatMessage(labels.admin)} />
+ )}
+ {cloudMode && (
+ <>
+ }
+ label={formatMessage(labels.documentation)}
+ >
+
+
+
+
+ }
+ label={formatMessage(labels.support)}
+ />
+ >
+ )}
+
+ } label={formatMessage(labels.logout)} />
+
+
+
+
);
}
-
-export default SettingsButton;
diff --git a/src/components/input/TeamsButton.module.css b/src/components/input/TeamsButton.module.css
deleted file mode 100644
index 2b1fd549..00000000
--- a/src/components/input/TeamsButton.module.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.button {
- font-weight: 700;
-}
-
-.menu {
- background: var(--base50);
- min-width: 260px;
-}
-
-.heading {
- color: var(--base600);
- font-size: 10px;
- font-weight: 700;
- padding: 8px 16px;
- text-transform: uppercase;
- border-bottom: 1px solid var(--base300);
-}
-
-.selected {
- font-weight: bold;
-}
diff --git a/src/components/input/TeamsButton.tsx b/src/components/input/TeamsButton.tsx
deleted file mode 100644
index f967a64c..00000000
--- a/src/components/input/TeamsButton.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { Key } from 'react';
-import { Text, Icon, Button, Popup, Menu, Item, PopupTrigger, Flexbox } from 'react-basics';
-import classNames from 'classnames';
-import Icons from '@/components/icons';
-import { useLogin, useMessages, useTeams, useTeamUrl } from '@/components/hooks';
-import styles from './TeamsButton.module.css';
-
-export function TeamsButton({
- className,
- showText = true,
- onChange,
-}: {
- className?: string;
- showText?: boolean;
- onChange?: (value: string) => void;
-}) {
- const { user } = useLogin();
- const { formatMessage, labels } = useMessages();
- const { result } = useTeams(user.id);
- const { teamId } = useTeamUrl();
- const team = result?.data?.find(({ id }) => id === teamId);
-
- const handleSelect = (close: () => void, id: Key) => {
- onChange?.((id !== user.id ? id : '') as string);
- close();
- };
-
- if (!result?.count) {
- return null;
- }
-
- return (
-
-
- {teamId ? : }
- {showText && {teamId ? team?.name : user.username} }
-
-
-
-
-
- {(close: () => void) => (
-
- {formatMessage(labels.myAccount)}
- -
-
-
-
-
- {user.username}
-
-
- {formatMessage(labels.team)}
- {result?.data?.map(({ id, name }) => (
- -
-
-
-
-
- {name}
-
-
- ))}
-
- )}
-
-
- );
-}
-
-export default TeamsButton;
diff --git a/src/components/input/ThemeButton.module.css b/src/components/input/ThemeButton.module.css
deleted file mode 100644
index c64647eb..00000000
--- a/src/components/input/ThemeButton.module.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.button {
- width: 50px;
- display: flex;
- justify-content: center;
- align-items: center;
- cursor: pointer;
-}
-
-.button > div {
- display: flex;
- justify-content: center;
- align-items: center;
- position: absolute;
-}
diff --git a/src/components/input/ThemeButton.tsx b/src/components/input/ThemeButton.tsx
deleted file mode 100644
index fd7d79a0..00000000
--- a/src/components/input/ThemeButton.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { useTransition, animated } from '@react-spring/web';
-import { Button, Icon } from 'react-basics';
-import { useTheme } from '@/components/hooks';
-import Icons from '@/components/icons';
-import styles from './ThemeButton.module.css';
-
-export function ThemeButton() {
- const { theme, saveTheme } = useTheme();
-
- const transitions = useTransition(theme, {
- initial: { opacity: 1 },
- from: {
- opacity: 0,
- transform: `translateY(${theme === 'light' ? '20px' : '-20px'}) scale(0.5)`,
- },
- enter: { opacity: 1, transform: 'translateY(0px) scale(1.0)' },
- leave: {
- opacity: 0,
- transform: `translateY(${theme === 'light' ? '-20px' : '20px'}) scale(0.5)`,
- },
- });
-
- function handleClick() {
- saveTheme(theme === 'light' ? 'dark' : 'light');
- }
-
- return (
-
- {transitions((style, item) => (
-
- {item === 'light' ? : }
-
- ))}
-
- );
-}
-
-export default ThemeButton;
diff --git a/src/components/input/WebsiteDateFilter.module.css b/src/components/input/WebsiteDateFilter.module.css
deleted file mode 100644
index c0b94bee..00000000
--- a/src/components/input/WebsiteDateFilter.module.css
+++ /dev/null
@@ -1,24 +0,0 @@
-.container {
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.dropdown {
- min-width: 200px;
-}
-
-.buttons {
- display: flex;
-}
-
-.buttons button:first-child {
- border-start-end-radius: 0;
- border-end-end-radius: 0;
- border-inline-end: 1px solid var(--base400);
-}
-
-.buttons button:last-child {
- border-start-start-radius: 0;
- border-end-start-radius: 0;
-}
diff --git a/src/components/input/WebsiteDateFilter.tsx b/src/components/input/WebsiteDateFilter.tsx
index 97beaf12..18b4f13b 100644
--- a/src/components/input/WebsiteDateFilter.tsx
+++ b/src/components/input/WebsiteDateFilter.tsx
@@ -1,59 +1,102 @@
-import { useDateRange, useLocale } from '@/components/hooks';
+import { Button, Icon, ListItem, Row, Select, Text } from '@umami/react-zen';
import { isAfter } from 'date-fns';
-import { getOffsetDateRange } from '@/lib/date';
-import { Button, Icon, Icons } from 'react-basics';
-import DateFilter from './DateFilter';
-import styles from './WebsiteDateFilter.module.css';
-import { DateRange } from '@/lib/types';
+import { useMemo } from 'react';
+import { useDateRange, useDateRangeQuery, useMessages, useNavigation } from '@/components/hooks';
+import { ChevronRight } from '@/components/icons';
+import { getDateRangeValue } from '@/lib/date';
+import { DateFilter } from './DateFilter';
+
+export interface WebsiteDateFilterProps {
+ websiteId: string;
+ compare?: string;
+ showAllTime?: boolean;
+ showButtons?: boolean;
+ allowCompare?: boolean;
+}
export function WebsiteDateFilter({
websiteId,
showAllTime = true,
-}: {
- websiteId: string;
- showAllTime?: boolean;
-}) {
- const { dir } = useLocale();
- const { dateRange, saveDateRange } = useDateRange(websiteId);
- const { value, startDate, endDate, offset } = dateRange;
- const disableForward =
- value === 'all' || isAfter(getOffsetDateRange(dateRange, 1).startDate, new Date());
+ showButtons = true,
+ allowCompare,
+}: WebsiteDateFilterProps) {
+ const { dateRange, isAllTime, isCustomRange } = useDateRange();
+ const { formatMessage, labels } = useMessages();
+ const {
+ router,
+ updateParams,
+ query: { compare = 'prev', offset = 0 },
+ } = useNavigation();
+ const disableForward = isAllTime || isAfter(dateRange.endDate, new Date());
+ const showCompare = allowCompare && !isAllTime;
- const handleChange = (value: string | DateRange) => {
- saveDateRange(value);
+ const websiteDateRange = useDateRangeQuery(websiteId);
+
+ const handleChange = (date: string) => {
+ if (date === 'all') {
+ router.push(
+ updateParams({
+ date: `${getDateRangeValue(websiteDateRange.startDate, websiteDateRange.endDate)}:all`,
+ offset: undefined,
+ }),
+ );
+ } else {
+ router.push(updateParams({ date, offset: undefined }));
+ }
};
- const handleIncrement = (increment: number) => {
- saveDateRange(getOffsetDateRange(dateRange, increment));
+ const handleIncrement = increment => {
+ router.push(updateParams({ offset: Number(offset) + increment }));
};
+ const handleSelect = (compare: any) => {
+ router.push(updateParams({ compare }));
+ };
+
+ const dateValue = useMemo(() => {
+ return offset !== 0
+ ? getDateRangeValue(dateRange.startDate, dateRange.endDate)
+ : dateRange.value;
+ }, [dateRange]);
return (
-
-
- {value !== 'all' && !value.startsWith('range') && (
-
- handleIncrement(-1)}>
-
-
+
+ {showButtons && !isAllTime && !isCustomRange && (
+
+ handleIncrement(-1)} variant="outline">
+
+
- handleIncrement(1)} disabled={disableForward}>
-
-
+ handleIncrement(1)} variant="outline" isDisabled={disableForward}>
+
+
-
+
)}
-
+
+
+
+ {showCompare && (
+
+ VS
+
+
+ {formatMessage(labels.previousPeriod)}
+ {formatMessage(labels.previousYear)}
+
+
+
+ )}
+
);
}
-
-export default WebsiteDateFilter;
diff --git a/src/components/input/WebsiteFilterButton.tsx b/src/components/input/WebsiteFilterButton.tsx
new file mode 100644
index 00000000..7db850a1
--- /dev/null
+++ b/src/components/input/WebsiteFilterButton.tsx
@@ -0,0 +1,32 @@
+import { useMessages, useNavigation } from '@/components/hooks';
+import { ListFilter } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { FilterEditForm } from '@/components/input/FilterEditForm';
+import { filtersArrayToObject } from '@/lib/params';
+
+export function WebsiteFilterButton({
+ websiteId,
+}: {
+ websiteId: string;
+ position?: 'bottom' | 'top' | 'left' | 'right';
+ alignment?: 'end' | 'center' | 'start';
+}) {
+ const { formatMessage, labels } = useMessages();
+ const { updateParams, router } = useNavigation();
+
+ const handleChange = ({ filters, segment, cohort }: any) => {
+ const params = filtersArrayToObject(filters);
+
+ const url = updateParams({ ...params, segment, cohort });
+
+ router.push(url);
+ };
+
+ return (
+ } label={formatMessage(labels.filter)} variant="outline">
+ {({ close }) => {
+ return ;
+ }}
+
+ );
+}
diff --git a/src/components/input/WebsiteSelect.module.css b/src/components/input/WebsiteSelect.module.css
deleted file mode 100644
index 42e2911a..00000000
--- a/src/components/input/WebsiteSelect.module.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.dropdown {
- max-height: 400px;
- overflow-y: auto;
-}
diff --git a/src/components/input/WebsiteSelect.tsx b/src/components/input/WebsiteSelect.tsx
index 8a7e4ac0..8d81eb9a 100644
--- a/src/components/input/WebsiteSelect.tsx
+++ b/src/components/input/WebsiteSelect.tsx
@@ -1,60 +1,74 @@
-import { useState, Key } from 'react';
-import { Dropdown, Item } from 'react-basics';
-import { useWebsite, useWebsites, useMessages } from '@/components/hooks';
-import Empty from '@/components/common/Empty';
-import styles from './WebsiteSelect.module.css';
+import { ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen';
+import { useState } from 'react';
+import { Empty } from '@/components/common/Empty';
+import {
+ useLoginQuery,
+ useMessages,
+ useUserWebsitesQuery,
+ useWebsiteQuery,
+} from '@/components/hooks';
export function WebsiteSelect({
websiteId,
teamId,
- onSelect,
+ onChange,
+ includeTeams,
+ ...props
}: {
websiteId?: string;
teamId?: string;
- onSelect?: (key: any) => void;
-}) {
- const { formatMessage, labels, messages } = useMessages();
+ includeTeams?: boolean;
+} & SelectProps) {
+ const { formatMessage, messages } = useMessages();
+ const { data: website } = useWebsiteQuery(websiteId);
+ const [name, setName] = useState(website?.name);
const [search, setSearch] = useState('');
- const [selectedId, setSelectedId] = useState(websiteId);
-
- const { data: website } = useWebsite(selectedId as string);
-
- const queryResult = useWebsites({ teamId }, { search, pageSize: 5 });
-
- const renderValue = () => {
- return website?.name;
- };
-
- const renderEmpty = () => {
- return ;
- };
-
- const handleSelect = (value: any) => {
- setSelectedId(value);
- onSelect?.(value);
- };
+ const { user } = useLoginQuery();
+ const { data, isLoading } = useUserWebsitesQuery(
+ { userId: user?.id, teamId },
+ { search, pageSize: 10, includeTeams },
+ );
+ const listItems: { id: string; name: string }[] = data?.data || [];
const handleSearch = (value: string) => {
setSearch(value);
};
+ const handleOpenChange = () => {
+ setSearch('');
+ };
+
+ const handleChange = (id: string) => {
+ setName(listItems.find(item => item.id === id)?.name);
+ onChange(id);
+ };
+
+ const renderValue = () => {
+ return (
+
+ {name}
+
+ );
+ };
+
return (
- ,
+ style: { maxHeight: '400px' },
+ }}
>
- {({ id, name }) => - {name}
}
-
+ {({ id, name }: any) => {name} }
+
);
}
-
-export default WebsiteSelect;
diff --git a/src/components/layout/Grid.module.css b/src/components/layout/Grid.module.css
deleted file mode 100644
index 7a2b7124..00000000
--- a/src/components/layout/Grid.module.css
+++ /dev/null
@@ -1,76 +0,0 @@
-.grid {
- display: grid;
-}
-
-.row {
- display: grid;
- grid-template-columns: repeat(6, 1fr);
- border-top: 1px solid var(--base300);
-}
-
-.row.compare {
- grid-template-columns: max-content 1fr 1fr;
-}
-
-.col {
- padding: 20px;
- min-height: 430px;
- border-inline-start: 1px solid var(--base300);
-}
-
-.col:first-child {
- border-inline-start: 0;
- padding-inline-start: 0;
-}
-
-.col:last-child {
- padding-inline-end: 0;
-}
-
-.col.one {
- grid-column: span 6;
-}
-
-.col.two {
- grid-column: span 3;
-}
-
-.col.three {
- grid-column: span 2;
-}
-
-.col.two-one:first-child {
- grid-column: span 4;
-}
-
-.col.two-one:last-child {
- grid-column: span 2;
-}
-
-.col.one-two:first-child {
- grid-column: span 2;
-}
-
-.col.one-two:last-child {
- grid-column: span 4;
-}
-
-@media only screen and (max-width: 992px) {
- .row {
- border: 0;
- }
-
- .row > .col {
- border-top: 1px solid var(--base300);
- border-inline-start: 0;
- border-inline-end: 0;
- padding: 20px 0;
- }
-
- .col.two,
- .col.three,
- .col.one-two,
- .col.two-one {
- grid-column: span 6 !important;
- }
-}
diff --git a/src/components/layout/Grid.tsx b/src/components/layout/Grid.tsx
deleted file mode 100644
index ec7f4fda..00000000
--- a/src/components/layout/Grid.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { CSSProperties } from 'react';
-import classNames from 'classnames';
-import { mapChildren } from 'react-basics';
-// eslint-disable-next-line css-modules/no-unused-class
-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' | 'compare';
- className?: string;
- children?: any;
-}) {
- const { columns = 'two', className, children, ...otherProps } = props;
- return (
-
- {mapChildren(children, child => {
- return
{child}
;
- })}
-
- );
-}
diff --git a/src/components/layout/MenuLayout.module.css b/src/components/layout/MenuLayout.module.css
deleted file mode 100644
index d2d1b165..00000000
--- a/src/components/layout/MenuLayout.module.css
+++ /dev/null
@@ -1,31 +0,0 @@
-.layout {
- display: grid;
- grid-template-columns: max-content 1fr;
- gap: 20px;
-}
-
-.menu {
- width: 240px;
- padding-top: 34px;
- padding-inline-end: 20px;
-}
-
-.content {
- display: flex;
- flex-direction: column;
- min-height: 50vh;
-}
-
-@media only screen and (max-width: 992px) {
- .layout {
- grid-template-columns: 1fr;
- }
-
- .menu {
- display: none;
- }
-
- .content {
- margin-top: 20px;
- }
-}
diff --git a/src/components/layout/MenuLayout.tsx b/src/components/layout/MenuLayout.tsx
deleted file mode 100644
index 1465c062..00000000
--- a/src/components/layout/MenuLayout.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { ReactNode } from 'react';
-import { usePathname } from 'next/navigation';
-import SideNav from '@/components/layout/SideNav';
-import styles from './MenuLayout.module.css';
-
-export function MenuLayout({ items = [], children }: { items: any[]; children: ReactNode }) {
- const pathname = usePathname();
- const cloudMode = !!process.env.cloudMode;
-
- const getKey = () => items.find(({ url }) => pathname === url)?.key;
-
- return (
-
- {!cloudMode && (
-
-
-
- )}
-
{children}
-
- );
-}
-
-export default MenuLayout;
diff --git a/src/components/layout/NavGroup.module.css b/src/components/layout/NavGroup.module.css
deleted file mode 100644
index 4979210a..00000000
--- a/src/components/layout/NavGroup.module.css
+++ /dev/null
@@ -1,80 +0,0 @@
-.group {
- display: flex;
- flex-direction: column;
- width: 100%;
-}
-
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- color: var(--base600);
- font-size: 11px;
- font-weight: 600;
- padding: 10px 20px;
- text-transform: uppercase;
- cursor: pointer;
-}
-
-.body {
- display: none;
-}
-
-.expanded .body {
- display: block;
-}
-
-.item {
- position: relative;
- display: flex;
- flex-direction: row;
- align-items: center;
- border-inline-end: 2px solid var(--base200);
- padding: 1rem 2rem;
- gap: var(--size500);
- font-weight: 600;
- width: 200px;
- margin-inline-end: -2px;
-}
-
-a.item {
- color: var(--base700);
-}
-
-.item.selected {
- color: var(--base900);
- border-inline-end-color: var(--primary400);
- background: var(--blue100);
-}
-
-.item:hover {
- color: var(--base900);
-}
-
-.minimized .text,
-.minimized .header {
- display: none;
-}
-
-.minimized .item {
- width: 60px;
- padding: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.divider:before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- margin: auto;
- border-top: 1px solid var(--base300);
- width: 160px;
-}
-
-.minimized .divider:before {
- width: 60px;
-}
diff --git a/src/components/layout/NavGroup.tsx b/src/components/layout/NavGroup.tsx
deleted file mode 100644
index 723f9a7e..00000000
--- a/src/components/layout/NavGroup.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { useState } from 'react';
-import { Icon, Text, TooltipPopup } from 'react-basics';
-import classNames from 'classnames';
-import { usePathname } from 'next/navigation';
-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);
-
- const handleExpand = () => setExpanded(state => !state);
-
- return (
-
- {title && (
-
- {title}
-
-
-
-
- )}
-
- {items.map(({ label, url, icon, divider }) => {
- return (
-
-
- {icon}
- {label}
-
-
- );
- })}
-
-
- );
-}
-
-export default NavGroup;
diff --git a/src/components/layout/Page.module.css b/src/components/layout/Page.module.css
deleted file mode 100644
index 3b9a4581..00000000
--- a/src/components/layout/Page.module.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.page {
- flex: 1;
- display: flex;
- flex-direction: column;
- position: relative;
- width: 100%;
- max-width: 1320px;
- margin: 0 auto;
- padding: 0 20px;
- min-height: calc(100vh - 60px);
- min-height: calc(100dvh - 60px);
-}
diff --git a/src/components/layout/Page.tsx b/src/components/layout/Page.tsx
deleted file mode 100644
index c06054b4..00000000
--- a/src/components/layout/Page.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-'use client';
-import { ReactNode } from 'react';
-import classNames from 'classnames';
-import { Banner, Loading } from 'react-basics';
-import { useMessages } from '@/components/hooks';
-import styles from './Page.module.css';
-
-export function Page({
- className,
- error,
- isLoading,
- children,
-}: {
- className?: string;
- error?: unknown;
- isLoading?: boolean;
- children?: ReactNode;
-}) {
- const { formatMessage, messages } = useMessages();
-
- if (error) {
- return {formatMessage(messages.error)} ;
- }
-
- if (isLoading) {
- return ;
- }
-
- return {children}
;
-}
-
-export default Page;
diff --git a/src/components/layout/PageHeader.module.css b/src/components/layout/PageHeader.module.css
deleted file mode 100644
index b92bb1db..00000000
--- a/src/components/layout/PageHeader.module.css
+++ /dev/null
@@ -1,48 +0,0 @@
-.header {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- align-content: center;
- align-self: stretch;
- flex-wrap: wrap;
- height: 100px;
-}
-
-.header a {
- color: var(--base600);
-}
-
-.header a:hover {
- color: var(--base900);
-}
-
-.title {
- display: flex;
- align-items: center;
- font-size: 24px;
- font-weight: 700;
- gap: 20px;
- height: 60px;
- flex: 1;
-}
-
-.breadcrumb {
- padding-top: 20px;
-}
-
-.icon {
- color: var(--base700);
- margin-inline-end: 1rem;
-}
-
-.actions {
- display: flex;
- justify-content: flex-end;
-}
-
-@media only screen and (max-width: 992px) {
- .header {
- margin-bottom: 10px;
- }
-}
diff --git a/src/components/layout/PageHeader.tsx b/src/components/layout/PageHeader.tsx
deleted file mode 100644
index 53f1db9f..00000000
--- a/src/components/layout/PageHeader.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import classNames from 'classnames';
-import React, { ReactNode } from 'react';
-import { Icon } from 'react-basics';
-import styles from './PageHeader.module.css';
-
-export function PageHeader({
- title,
- icon,
- className,
- breadcrumb,
- children,
-}: {
- title?: ReactNode;
- icon?: ReactNode;
- className?: string;
- breadcrumb?: ReactNode;
- children?: ReactNode;
-}) {
- return (
- <>
- {breadcrumb}
-
- {icon && (
-
- {icon}
-
- )}
-
- {title &&
{title}
}
-
{children}
-
- >
- );
-}
-
-export default PageHeader;
diff --git a/src/components/layout/SideNav.module.css b/src/components/layout/SideNav.module.css
deleted file mode 100644
index 5d9af915..00000000
--- a/src/components/layout/SideNav.module.css
+++ /dev/null
@@ -1,20 +0,0 @@
-.menu {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.item a {
- color: var(--font-color100);
- flex: 1;
- padding: var(--size300) var(--size600);
-}
-
-.item {
- padding: 0;
- border-radius: var(--border-radius);
-}
-
-.selected {
- font-weight: 700;
-}
diff --git a/src/components/layout/SideNav.tsx b/src/components/layout/SideNav.tsx
deleted file mode 100644
index 0b5c9856..00000000
--- a/src/components/layout/SideNav.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import classNames from 'classnames';
-import { Menu, Item } from 'react-basics';
-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,
- shallow = true,
- scroll = false,
- className,
- onSelect = () => {},
-}: SideNavProps) {
- const pathname = usePathname();
- return (
-
- {({ key, label, url }) => (
- -
-
- {label}
-
-
- )}
-
- );
-}
-
-export default SideNav;
diff --git a/src/components/messages.ts b/src/components/messages.ts
index 19912b05..0438c06e 100644
--- a/src/components/messages.ts
+++ b/src/components/messages.ts
@@ -18,7 +18,7 @@ export const labels = defineMessages({
user: { id: 'label.user', defaultMessage: 'User' },
viewOnly: { id: 'label.view-only', defaultMessage: 'View only' },
manage: { id: 'label.manage', defaultMessage: 'Manage' },
- admin: { id: 'label.admin', defaultMessage: 'Administrator' },
+ admin: { id: 'label.admin', defaultMessage: 'Admin' },
confirm: { id: 'label.confirm', defaultMessage: 'Confirm' },
details: { id: 'label.details', defaultMessage: 'Details' },
website: { id: 'label.website', defaultMessage: 'Website' },
@@ -51,6 +51,7 @@ export const labels = defineMessages({
data: { id: 'label.data', defaultMessage: 'Data' },
trackingCode: { id: 'label.tracking-code', defaultMessage: 'Tracking code' },
shareUrl: { id: 'label.share-url', defaultMessage: 'Share URL' },
+ action: { id: 'label.action', defaultMessage: 'Action' },
actions: { id: 'label.actions', defaultMessage: 'Actions' },
domain: { id: 'label.domain', defaultMessage: 'Domain' },
websiteId: { id: 'label.website-id', defaultMessage: 'Website ID' },
@@ -73,11 +74,13 @@ export const labels = defineMessages({
language: { id: 'label.language', defaultMessage: 'Language' },
theme: { id: 'label.theme', defaultMessage: 'Theme' },
profile: { id: 'label.profile', defaultMessage: 'Profile' },
+ profiles: { id: 'label.profiles', defaultMessage: 'Profiles' },
dashboard: { id: 'label.dashboard', defaultMessage: 'Dashboard' },
more: { id: 'label.more', defaultMessage: 'More' },
realtime: { id: 'label.realtime', defaultMessage: 'Realtime' },
queries: { id: 'label.queries', defaultMessage: 'Queries' },
teams: { id: 'label.teams', defaultMessage: 'Teams' },
+ teamSettings: { id: 'label.team-settings', defaultMessage: 'Team settings' },
analytics: { id: 'label.analytics', defaultMessage: 'Analytics' },
login: { id: 'label.login', defaultMessage: 'Login' },
logout: { id: 'label.logout', defaultMessage: 'Logout' },
@@ -87,11 +90,12 @@ export const labels = defineMessages({
deleteTeam: { id: 'label.delete-team', defaultMessage: 'Delete team' },
leaveTeam: { id: 'label.leave-team', defaultMessage: 'Leave team' },
refresh: { id: 'label.refresh', defaultMessage: 'Refresh' },
+ page: { id: 'label.page', defaultMessage: 'Page' },
pages: { id: 'label.pages', defaultMessage: 'Pages' },
- entry: { id: 'label.entry', defaultMessage: 'Entry path' },
- exit: { id: 'label.exit', defaultMessage: 'Exit path' },
+ entry: { id: 'label.entry', defaultMessage: 'Entry' },
+ exit: { id: 'label.exit', defaultMessage: 'Exit' },
referrers: { id: 'label.referrers', defaultMessage: 'Referrers' },
- hosts: { id: 'label.hosts', defaultMessage: 'Hosts' },
+ screen: { id: 'label.screen', defaultMessage: 'Screen' },
screens: { id: 'label.screens', defaultMessage: 'Screens' },
browsers: { id: 'label.browsers', defaultMessage: 'Browsers' },
os: { id: 'label.os', defaultMessage: 'OS' },
@@ -99,11 +103,14 @@ export const labels = defineMessages({
countries: { id: 'label.countries', defaultMessage: 'Countries' },
languages: { id: 'label.languages', defaultMessage: 'Languages' },
tags: { id: 'label.tags', defaultMessage: 'Tags' },
+ segments: { id: 'label.segments', defaultMessage: 'Segments' },
+ cohorts: { id: 'label.cohorts', defaultMessage: 'Cohorts' },
count: { id: 'label.count', defaultMessage: 'Count' },
average: { id: 'label.average', defaultMessage: 'Average' },
sum: { id: 'label.sum', defaultMessage: 'Sum' },
event: { id: 'label.event', defaultMessage: 'Event' },
events: { id: 'label.events', defaultMessage: 'Events' },
+ eventName: { id: 'label.event-name', defaultMessage: 'Event name' },
query: { id: 'label.query', defaultMessage: 'Query' },
queryParameters: { id: 'label.query-parameters', defaultMessage: 'Query parameters' },
back: { id: 'label.back', defaultMessage: 'Back' },
@@ -128,6 +135,7 @@ export const labels = defineMessages({
selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' },
selectRole: { id: 'label.select-role', defaultMessage: 'Select role' },
selectDate: { id: 'label.select-date', defaultMessage: 'Select date' },
+ selectFilter: { id: 'label.select-filter', defaultMessage: 'Select filter' },
all: { id: 'label.all', defaultMessage: 'All' },
session: { id: 'label.session', defaultMessage: 'Session' },
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
@@ -155,6 +163,7 @@ export const labels = defineMessages({
eventData: { id: 'label.event-data', defaultMessage: 'Event data' },
sessionData: { id: 'label.session-data', defaultMessage: 'Session data' },
funnel: { id: 'label.funnel', defaultMessage: 'Funnel' },
+ funnels: { id: 'label.funnels', defaultMessage: 'Funnels' },
funnelDescription: {
id: 'label.funnel-description',
defaultMessage: 'Understand the conversion and drop-off rate of users.',
@@ -171,8 +180,6 @@ export const labels = defineMessages({
},
currency: { id: 'label.currency', defaultMessage: 'Currency' },
model: { id: 'label.model', defaultMessage: 'Model' },
- url: { id: 'label.url', defaultMessage: 'URL' },
- urls: { id: 'label.urls', defaultMessage: 'URLs' },
path: { id: 'label.path', defaultMessage: 'Path' },
paths: { id: 'label.paths', defaultMessage: 'Paths' },
add: { id: 'label.add', defaultMessage: 'Add' },
@@ -200,8 +207,14 @@ export const labels = defineMessages({
lessThanEquals: { id: 'label.less-than-equals', defaultMessage: 'Less than or equals' },
contains: { id: 'label.contains', defaultMessage: 'Contains' },
doesNotContain: { id: 'label.does-not-contain', defaultMessage: 'Does not contain' },
+ includes: { id: 'label.includes', defaultMessage: 'Includes' },
+ doesNotInclude: { id: 'label.does-not-include', defaultMessage: 'Does not include' },
before: { id: 'label.before', defaultMessage: 'Before' },
after: { id: 'label.after', defaultMessage: 'After' },
+ isTrue: { id: 'label.is-true', defaultMessage: 'Is true' },
+ isFalse: { id: 'label.is-false', defaultMessage: 'Is false' },
+ exists: { id: 'label.exists', defaultMessage: 'Exists' },
+ doesNotExist: { id: 'label.doest-not-exist', defaultMessage: 'Does not exist' },
total: { id: 'label.total', defaultMessage: 'Total' },
min: { id: 'label.min', defaultMessage: 'Min' },
max: { id: 'label.max', defaultMessage: 'Max' },
@@ -209,6 +222,7 @@ export const labels = defineMessages({
value: { id: 'label.value', defaultMessage: 'Value' },
overview: { id: 'label.overview', defaultMessage: 'Overview' },
totalRecords: { id: 'label.total-records', defaultMessage: 'Total records' },
+ insight: { id: 'label.insight', defaultMessage: 'Insight' },
insights: { id: 'label.insights', defaultMessage: 'Insights' },
insightsDescription: {
id: 'label.insights-description',
@@ -221,7 +235,7 @@ export const labels = defineMessages({
},
dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' },
referrer: { id: 'label.referrer', defaultMessage: 'Referrer' },
- host: { id: 'label.host', defaultMessage: 'Host' },
+ hostname: { id: 'label.hostname', defaultMessage: 'Hostname' },
country: { id: 'label.country', defaultMessage: 'Country' },
region: { id: 'label.region', defaultMessage: 'Region' },
city: { id: 'label.city', defaultMessage: 'City' },
@@ -229,6 +243,8 @@ export const labels = defineMessages({
device: { id: 'label.device', defaultMessage: 'Device' },
pageTitle: { id: 'label.pageTitle', defaultMessage: 'Page title' },
tag: { id: 'label.tag', defaultMessage: 'Tag' },
+ segment: { id: 'label.segment', defaultMessage: 'Segment' },
+ cohort: { id: 'label.cohort', defaultMessage: 'Cohort' },
day: { id: 'label.day', defaultMessage: 'Day' },
date: { id: 'label.date', defaultMessage: 'Date' },
pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' },
@@ -255,16 +271,13 @@ export const labels = defineMessages({
id: 'message.triggered-event',
defaultMessage: 'Triggered event',
},
- visitorsDroppedOff: {
- id: 'message.visitors-dropped-off',
- defaultMessage: 'Visitors dropped off',
- },
utm: { id: 'label.utm', defaultMessage: 'UTM' },
utmDescription: {
id: 'label.utm-description',
defaultMessage: 'Track your campaigns through UTM parameters.',
},
- conversionStep: { id: 'label.conversion-step', defaultMessage: 'Conversion Step' },
+ conversionStep: { id: 'label.conversion-step', defaultMessage: 'Conversion step' },
+ conversionRate: { id: 'label.conversion-rate', defaultMessage: 'Conversion rate' },
steps: { id: 'label.steps', defaultMessage: 'Steps' },
startStep: { id: 'label.start-step', defaultMessage: 'Start Step' },
endStep: { id: 'label.end-step', defaultMessage: 'End Step' },
@@ -276,10 +289,12 @@ export const labels = defineMessages({
defaultMessage: 'Track your goals for pageviews and events.',
},
journey: { id: 'label.journey', defaultMessage: 'Journey' },
+ journeys: { id: 'label.journeys', defaultMessage: 'Journeys' },
journeyDescription: {
id: 'label.journey-description',
defaultMessage: 'Understand how users navigate through your website.',
},
+ compareDates: { id: 'label.compare-dates', defaultMessage: 'Compare dates' },
compare: { id: 'label.compare', defaultMessage: 'Compare' },
current: { id: 'label.current', defaultMessage: 'Current' },
previous: { id: 'label.previous', defaultMessage: 'Previous' },
@@ -288,6 +303,7 @@ export const labels = defineMessages({
lastSeen: { id: 'label.last-seen', defaultMessage: 'Last seen' },
firstSeen: { id: 'label.first-seen', defaultMessage: 'First seen' },
properties: { id: 'label.properties', defaultMessage: 'Properties' },
+ channel: { id: 'label.channel', defaultMessage: 'Channel' },
channels: { id: 'label.channels', defaultMessage: 'Channels' },
sources: { id: 'label.sources', defaultMessage: 'Sources' },
medium: { id: 'label.medium', defaultMessage: 'Medium' },
@@ -310,14 +326,52 @@ export const labels = defineMessages({
paidVideo: { id: 'label.paid-video', defaultMessage: 'Paid video' },
grouped: { id: 'label.grouped', defaultMessage: 'Grouped' },
other: { id: 'label.other', defaultMessage: 'Other' },
+ boards: { id: 'label.boards', defaultMessage: 'Boards' },
+ apply: { id: 'label.apply', defaultMessage: 'Apply' },
+ link: { id: 'label.link', defaultMessage: 'Link' },
+ links: { id: 'label.links', defaultMessage: 'Links' },
+ pixel: { id: 'label.pixel', defaultMessage: 'Pixel' },
+ pixels: { id: 'label.pixels', defaultMessage: 'Pixels' },
+ addBoard: { id: 'label.add-board', defaultMessage: 'Add board' },
+ addLink: { id: 'label.add-link', defaultMessage: 'Add link' },
+ addPixel: { id: 'label.add-pixel', defaultMessage: 'Add pixel' },
+ maximize: { id: 'label.maximize', defaultMessage: 'Maximize' },
+ remaining: { id: 'label.remaining', defaultMessage: 'Remaining' },
+ conversion: { id: 'label.conversion', defaultMessage: 'Conversion' },
+ firstClick: { id: 'label.first-click', defaultMessage: 'First click' },
+ lastClick: { id: 'label.last-click', defaultMessage: 'Last click' },
+ online: { id: 'label.online', defaultMessage: 'Online' },
+ preferences: { id: 'label.preferences', defaultMessage: 'Preferences' },
+ location: { id: 'label.location', defaultMessage: 'Location' },
+ chart: { id: 'label.chart', defaultMessage: 'Chart' },
+ table: { id: 'label.table', defaultMessage: 'Table' },
+ download: { id: 'label.download', defaultMessage: 'Download' },
+ traffic: { id: 'label.traffic', defaultMessage: 'Traffic' },
+ behavior: { id: 'label.behavior', defaultMessage: 'Behavior' },
+ growth: { id: 'label.growth', defaultMessage: 'Growth' },
+ account: { id: 'label.account', defaultMessage: 'Account' },
+ application: { id: 'label.application', defaultMessage: 'Application' },
+ saveSegment: { id: 'label.save-segment', defaultMessage: 'Save as segment' },
+ saveCohort: { id: 'label.save-cohort', defaultMessage: 'Save as cohort' },
+ analysis: { id: 'label.analysis', defaultMessage: 'Analysis' },
+ destinationUrl: { id: 'label.destination-url', defaultMessage: 'Destination URL' },
+ audience: { id: 'label.audience', defaultMessage: 'Audience' },
+ invalidUrl: { id: 'label.invalid-url', defaultMessage: 'Invalid URL' },
+ environment: { id: 'label.environment', defaultMessage: 'Environment' },
+ criteria: { id: 'label.criteria', defaultMessage: 'Criteria' },
+ share: { id: 'label.share', defaultMessage: 'Share' },
+ support: { id: 'label.support', defaultMessage: 'Support' },
+ documentation: { id: 'label.documentation', defaultMessage: 'Documentation' },
+ switchAccount: { id: 'label.switch-account', defaultMessage: 'Switch account' },
});
export const messages = defineMessages({
error: { id: 'message.error', defaultMessage: 'Something went wrong.' },
- saved: { id: 'message.saved', defaultMessage: 'Saved.' },
+ saved: { id: 'message.saved', defaultMessage: 'Saved successfully.' },
noUsers: { id: 'message.no-users', defaultMessage: 'There are no users.' },
userDeleted: { id: 'message.user-deleted', defaultMessage: 'User deleted.' },
noDataAvailable: { id: 'message.no-data-available', defaultMessage: 'No data available.' },
+ nothingSelected: { id: 'message.nothing-selected', defaultMessage: 'Nothing selected.' },
confirmReset: {
id: 'message.confirm-reset',
defaultMessage: 'Are you sure you want to reset {target}?',
@@ -441,4 +495,24 @@ export const messages = defineMessages({
id: 'message.transfer-user-website-to-team',
defaultMessage: 'Select the team to transfer this website to.',
},
+ unauthorized: {
+ id: 'message.unauthorized',
+ defaultMessage: 'Unauthorized',
+ },
+ badRequest: {
+ id: 'message.bad-request',
+ defaultMessage: 'Bad request',
+ },
+ forbidden: {
+ id: 'message.forbidden',
+ defaultMessage: 'Forbidden',
+ },
+ notFound: {
+ id: 'message.not-found',
+ defaultMessage: 'Not found',
+ },
+ serverError: {
+ id: 'message.sever-error',
+ defaultMessage: 'Server error',
+ },
});
diff --git a/src/components/metrics/ActiveUsers.module.css b/src/components/metrics/ActiveUsers.module.css
deleted file mode 100644
index 4a984725..00000000
--- a/src/components/metrics/ActiveUsers.module.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.container {
- display: flex;
- align-items: center;
- margin-inline-start: 20px;
-}
-
-.text {
- display: flex;
- white-space: nowrap;
- font-size: var(--font-size-md);
- font-weight: 400;
-}
diff --git a/src/components/metrics/ActiveUsers.tsx b/src/components/metrics/ActiveUsers.tsx
index 50c676ab..a4bc7da2 100644
--- a/src/components/metrics/ActiveUsers.tsx
+++ b/src/components/metrics/ActiveUsers.tsx
@@ -1,8 +1,7 @@
+import { StatusLight, Text } from '@umami/react-zen';
import { useMemo } from 'react';
-import { StatusLight } from 'react-basics';
-import { useApi } from '@/components/hooks';
-import { useMessages } from '@/components/hooks';
-import styles from './ActiveUsers.module.css';
+import { LinkButton } from '@/components/common/LinkButton';
+import { useActyiveUsersQuery, useMessages } from '@/components/hooks';
export function ActiveUsers({
websiteId,
@@ -13,14 +12,8 @@ export function ActiveUsers({
value?: number;
refetchInterval?: number;
}) {
- const { formatMessage, messages } = useMessages();
- const { get, useQuery } = useApi();
- const { data } = useQuery({
- queryKey: ['websites:active', websiteId],
- queryFn: () => get(`/websites/${websiteId}/active`),
- enabled: !!websiteId,
- refetchInterval,
- });
+ const { formatMessage, labels } = useMessages();
+ const { data } = useActyiveUsersQuery(websiteId, { refetchInterval });
const count = useMemo(() => {
if (websiteId) {
@@ -35,10 +28,12 @@ export function ActiveUsers({
}
return (
-
- {formatMessage(messages.activeUsers, { x: count })}
-
+
+
+
+ {count} {formatMessage(labels.online)}
+
+
+
);
}
-
-export default ActiveUsers;
diff --git a/src/components/metrics/BrowsersTable.tsx b/src/components/metrics/BrowsersTable.tsx
deleted file mode 100644
index 500686b1..00000000
--- a/src/components/metrics/BrowsersTable.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import FilterLink from '@/components/common/FilterLink';
-import MetricsTable, { MetricsTableProps } from '@/components/metrics/MetricsTable';
-import { useMessages } from '@/components/hooks';
-import { useFormat } from '@/components/hooks';
-import TypeIcon from '@/components/common/TypeIcon';
-
-export function BrowsersTable(props: MetricsTableProps) {
- const { formatMessage, labels } = useMessages();
- const { formatBrowser } = useFormat();
-
- function renderLink({ x: browser }) {
- return (
-
-
-
- );
- }
-
- return (
-
- );
-}
-
-export default BrowsersTable;
diff --git a/src/components/metrics/ChangeLabel.module.css b/src/components/metrics/ChangeLabel.module.css
deleted file mode 100644
index 802a9931..00000000
--- a/src/components/metrics/ChangeLabel.module.css
+++ /dev/null
@@ -1,26 +0,0 @@
-.label {
- display: flex;
- align-items: center;
- gap: 5px;
- font-size: 13px;
- font-weight: 700;
- padding: 0.1em 0.5em;
- border-radius: 5px;
- color: var(--base500);
- align-self: flex-start;
-}
-
-.positive {
- color: var(--green700);
- background: var(--green100);
-}
-
-.negative {
- color: var(--red700);
- background: var(--red100);
-}
-
-.neutral {
- color: var(--base700);
- background: var(--base100);
-}
diff --git a/src/components/metrics/ChangeLabel.tsx b/src/components/metrics/ChangeLabel.tsx
index 7e7cb77b..192f0ff2 100644
--- a/src/components/metrics/ChangeLabel.tsx
+++ b/src/components/metrics/ChangeLabel.tsx
@@ -1,46 +1,60 @@
-import classNames from 'classnames';
-import { Icon, Icons } from 'react-basics';
-import { ReactNode } from 'react';
-import styles from './ChangeLabel.module.css';
+import { Icon, Row, type RowProps, Text } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { ArrowRight } from '@/components/icons';
+
+const STYLES = {
+ positive: {
+ color: `var(--success-color)`,
+ background: `color-mix(in srgb, var(--success-color), var(--background-color) 95%)`,
+ },
+ negative: {
+ color: `var(--danger-color)`,
+ background: `color-mix(in srgb, var(--danger-color), var(--background-color) 95%)`,
+ },
+ neutral: {
+ color: `var(--font-color-muted)`,
+ background: `var(--base-color-2)`,
+ },
+};
export function ChangeLabel({
value,
size,
- title,
reverseColors,
- className,
children,
+ ...props
}: {
value: number;
size?: 'xs' | 'sm' | 'md' | 'lg';
title?: string;
reverseColors?: boolean;
showPercentage?: boolean;
- className?: string;
children?: ReactNode;
-}) {
+} & RowProps) {
const positive = value >= 0;
const negative = value < 0;
- const neutral = value === 0 || isNaN(value);
+ const neutral = value === 0 || Number.isNaN(value);
const good = reverseColors ? negative : positive;
+ const style =
+ STYLES[good && 'positive'] || STYLES[!good && 'negative'] || STYLES[neutral && 'neutral'];
+
return (
-
{!neutral && (
-
+
)}
- {children || value}
-
+ {children || value}
+
);
}
-
-export default ChangeLabel;
diff --git a/src/components/metrics/ChannelsTable.tsx b/src/components/metrics/ChannelsTable.tsx
deleted file mode 100644
index d2dc207f..00000000
--- a/src/components/metrics/ChannelsTable.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import MetricsTable, { MetricsTableProps } from '@/components/metrics/MetricsTable';
-import { useMessages } from '@/components/hooks';
-
-export function ChannelsTable(props: MetricsTableProps) {
- const { formatMessage, labels } = useMessages();
-
- const renderLabel = ({ x }) => {
- return formatMessage(labels[x]);
- };
-
- return (
-
- );
-}
-
-export default ChannelsTable;
diff --git a/src/components/metrics/CitiesTable.tsx b/src/components/metrics/CitiesTable.tsx
deleted file mode 100644
index 1e5fc735..00000000
--- a/src/components/metrics/CitiesTable.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import { emptyFilter } from '@/lib/filters';
-import FilterLink from '@/components/common/FilterLink';
-import { useMessages } from '@/components/hooks';
-import { useFormat } from '@/components/hooks';
-
-export function CitiesTable(props: MetricsTableProps) {
- const { formatMessage, labels } = useMessages();
- const { formatCity } = useFormat();
-
- const renderLink = ({ x: city, country }) => {
- return (
-
- {country && (
-
- )}
-
- );
- };
-
- return (
-
- );
-}
-
-export default CitiesTable;
diff --git a/src/components/metrics/CountriesTable.tsx b/src/components/metrics/CountriesTable.tsx
deleted file mode 100644
index cdd05115..00000000
--- a/src/components/metrics/CountriesTable.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import FilterLink from '@/components/common/FilterLink';
-import { useCountryNames } from '@/components/hooks';
-import { useLocale, useMessages, useFormat } from '@/components/hooks';
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import TypeIcon from '@/components/common/TypeIcon';
-
-export function CountriesTable({ ...props }: MetricsTableProps) {
- const { locale } = useLocale();
- const { countryNames } = useCountryNames(locale);
- const { formatMessage, labels } = useMessages();
- const { formatCountry } = useFormat();
-
- const renderLink = ({ x: code }) => {
- return (
-
-
-
- );
- };
-
- return (
-
- );
-}
-
-export default CountriesTable;
diff --git a/src/components/metrics/DatePickerForm.module.css b/src/components/metrics/DatePickerForm.module.css
deleted file mode 100644
index 7168fa0d..00000000
--- a/src/components/metrics/DatePickerForm.module.css
+++ /dev/null
@@ -1,44 +0,0 @@
-.container {
- display: flex;
- flex-direction: column;
- max-width: 100vw;
-}
-
-.calendars {
- display: flex;
- justify-content: center;
-}
-
-.calendars > div + div {
- margin-inline-start: 20px;
- padding-inline-start: 20px;
- border-inline-start: 1px solid var(--base300);
-}
-
-.filter {
- display: flex;
- justify-content: center;
- align-items: center;
- margin-bottom: 20px;
-}
-
-.buttons {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- margin-top: 20px;
-}
-
-@media only screen and (max-width: 768px) {
- .calendars {
- flex-direction: column;
- }
-
- .calendars > div + div {
- padding: 0;
- margin-inline-start: 0;
- margin-top: 20px;
- border: 0;
- }
-}
diff --git a/src/components/metrics/DatePickerForm.tsx b/src/components/metrics/DatePickerForm.tsx
index d1a5c7db..59d17093 100644
--- a/src/components/metrics/DatePickerForm.tsx
+++ b/src/components/metrics/DatePickerForm.tsx
@@ -1,10 +1,10 @@
+import { Button, Calendar, Column, Row, ToggleGroup, ToggleGroupItem } from '@umami/react-zen';
+import { endOfDay, isAfter, isBefore, isSameDay, startOfDay } from 'date-fns';
import { useState } from 'react';
-import { Button, ButtonGroup, Calendar } from 'react-basics';
-import { isAfter, isBefore, isSameDay, startOfDay, endOfDay } from 'date-fns';
-import { useLocale } from '@/components/hooks';
-import { FILTER_DAY, FILTER_RANGE } from '@/lib/constants';
import { useMessages } from '@/components/hooks';
-import styles from './DatePickerForm.module.css';
+
+const FILTER_DAY = 'filter-day';
+const FILTER_RANGE = 'filter-range';
export function DatePickerForm({
startDate: defaultStartDate,
@@ -14,73 +14,61 @@ export function DatePickerForm({
onChange,
onClose,
}) {
- const [selected, setSelected] = useState(
+ const [selected, setSelected] = useState([
isSameDay(defaultStartDate, defaultEndDate) ? FILTER_DAY : FILTER_RANGE,
- );
- const [singleDate, setSingleDate] = useState(defaultStartDate);
- const [startDate, setStartDate] = useState(defaultStartDate);
- const [endDate, setEndDate] = useState(defaultEndDate);
- const { dateLocale } = useLocale();
+ ]);
+ const [date, setDate] = useState(defaultStartDate || new Date());
+ const [startDate, setStartDate] = useState(defaultStartDate || new Date());
+ const [endDate, setEndDate] = useState(defaultEndDate || new Date());
const { formatMessage, labels } = useMessages();
- const disabled =
- selected === FILTER_DAY
- ? isAfter(minDate, singleDate) && isBefore(maxDate, singleDate)
- : isAfter(startDate, endDate);
+ const disabled = selected.includes(FILTER_DAY)
+ ? isAfter(minDate, date) && isBefore(maxDate, date)
+ : isAfter(startDate, endDate);
const handleSave = () => {
- if (selected === FILTER_DAY) {
- onChange(`range:${startOfDay(singleDate).getTime()}:${endOfDay(singleDate).getTime()}`);
+ if (selected.includes(FILTER_DAY)) {
+ onChange(`range:${startOfDay(date).getTime()}:${endOfDay(date).getTime()}`);
} else {
onChange(`range:${startOfDay(startDate).getTime()}:${endOfDay(endDate).getTime()}`);
}
};
return (
-
-
- setSelected(key as any)}>
- {formatMessage(labels.singleDay)}
- {formatMessage(labels.dateRange)}
-
-
-
- {selected === FILTER_DAY && (
-
+
+
+
+ {formatMessage(labels.singleDay)}
+ {formatMessage(labels.dateRange)}
+
+
+
+ {selected.includes(FILTER_DAY) && (
+
)}
- {selected === FILTER_RANGE && (
- <>
+ {selected.includes(FILTER_RANGE) && (
+
- >
+
)}
-
-
-
- {formatMessage(labels.save)}
+
+
+ {formatMessage(labels.cancel)}
+
+ {formatMessage(labels.apply)}
- {formatMessage(labels.cancel)}
-
-
+
+
);
}
-
-export default DatePickerForm;
diff --git a/src/components/metrics/DevicesTable.tsx b/src/components/metrics/DevicesTable.tsx
deleted file mode 100644
index ed327c33..00000000
--- a/src/components/metrics/DevicesTable.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import FilterLink from '@/components/common/FilterLink';
-import { useMessages } from '@/components/hooks';
-import { useFormat } from '@/components/hooks';
-import TypeIcon from '@/components/common/TypeIcon';
-
-export function DevicesTable(props: MetricsTableProps) {
- const { formatMessage, labels } = useMessages();
- const { formatDevice } = useFormat();
-
- function renderLink({ x: device }) {
- return (
-
-
-
- );
- }
-
- return (
-
- );
-}
-
-export default DevicesTable;
diff --git a/src/components/metrics/EventData.tsx b/src/components/metrics/EventData.tsx
new file mode 100644
index 00000000..48d21c57
--- /dev/null
+++ b/src/components/metrics/EventData.tsx
@@ -0,0 +1,22 @@
+import { Column, Grid, Label, Text } from '@umami/react-zen';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useEventDataQuery } from '@/components/hooks';
+
+export function EventData({ websiteId, eventId }: { websiteId: string; eventId: string }) {
+ const { data, isLoading, error } = useEventDataQuery(websiteId, eventId);
+
+ return (
+
+
+ {data?.map(({ dataKey, stringValue }) => {
+ return (
+
+ {dataKey}
+ {stringValue}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/metrics/EventsChart.module.css b/src/components/metrics/EventsChart.module.css
deleted file mode 100644
index d586bead..00000000
--- a/src/components/metrics/EventsChart.module.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.chart {
- display: flex;
-}
diff --git a/src/components/metrics/EventsChart.tsx b/src/components/metrics/EventsChart.tsx
index 594a69f7..3a53ba9a 100644
--- a/src/components/metrics/EventsChart.tsx
+++ b/src/components/metrics/EventsChart.tsx
@@ -1,26 +1,33 @@
-import { useMemo, useState, useEffect } from 'react';
import { colord } from 'colord';
-import BarChart from '@/components/charts/BarChart';
-import { useDateRange, useLocale, useWebsiteEventsSeries } from '@/components/hooks';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { BarChart, type BarChartProps } from '@/components/charts/BarChart';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import {
+ useDateRange,
+ useLocale,
+ useTimezone,
+ useWebsiteEventsSeriesQuery,
+} from '@/components/hooks';
import { renderDateLabels } from '@/lib/charts';
import { CHART_COLORS } from '@/lib/constants';
+import { generateTimeSeries } from '@/lib/date';
-export interface EventsChartProps {
+export interface EventsChartProps extends BarChartProps {
websiteId: string;
- className?: string;
focusLabel?: string;
}
-export function EventsChart({ websiteId, className, focusLabel }: EventsChartProps) {
+export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
+ const { timezone } = useTimezone();
const {
- dateRange: { startDate, endDate, unit, value },
- } = useDateRange(websiteId);
- const { locale } = useLocale();
- const { data, isLoading } = useWebsiteEventsSeries(websiteId);
+ dateRange: { startDate, endDate, unit },
+ } = useDateRange({ timezone: timezone });
+ const { locale, dateLocale } = useLocale();
+ const { data, isLoading, error } = useWebsiteEventsSeriesQuery(websiteId);
const [label, setLabel] = useState(focusLabel);
- const chartData = useMemo(() => {
- if (!data) return [];
+ const chartData: any = useMemo(() => {
+ if (!data) return;
const map = (data as any[]).reduce((obj, { x, t, y }) => {
if (!obj[x]) {
@@ -32,20 +39,32 @@ export function EventsChart({ websiteId, className, focusLabel }: EventsChartPro
return obj;
}, {});
- return {
- datasets: Object.keys(map).map((key, index) => {
- const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
- return {
- label: key,
- data: map[key],
- lineTension: 0,
- backgroundColor: color.alpha(0.6).toRgbString(),
- borderColor: color.alpha(0.7).toRgbString(),
- borderWidth: 1,
- };
- }),
- focusLabel,
- };
+ if (!map || Object.keys(map).length === 0) {
+ return {
+ datasets: [
+ {
+ data: generateTimeSeries([], startDate, endDate, unit, dateLocale),
+ lineTension: 0,
+ borderWidth: 1,
+ },
+ ],
+ };
+ } else {
+ return {
+ datasets: Object.keys(map).map((key, index) => {
+ const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
+ return {
+ label: key,
+ data: generateTimeSeries(map[key], startDate, endDate, unit, dateLocale),
+ lineTension: 0,
+ backgroundColor: color.alpha(0.6).toRgbString(),
+ borderColor: color.alpha(0.7).toRgbString(),
+ borderWidth: 1,
+ };
+ }),
+ focusLabel,
+ };
+ }
}, [data, startDate, endDate, unit, focusLabel]);
useEffect(() => {
@@ -54,19 +73,21 @@ export function EventsChart({ websiteId, className, focusLabel }: EventsChartPro
}
}, [focusLabel]);
+ const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]);
+
return (
-
+
+ {chartData && (
+
+ )}
+
);
}
-
-export default EventsChart;
diff --git a/src/components/metrics/EventsTable.tsx b/src/components/metrics/EventsTable.tsx
deleted file mode 100644
index 45b81094..00000000
--- a/src/components/metrics/EventsTable.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import { useMessages } from '@/components/hooks';
-
-export interface EventsTableProps extends MetricsTableProps {
- onLabelClick?: (value: string) => void;
-}
-
-export function EventsTable({ onLabelClick, ...props }: EventsTableProps) {
- const { formatMessage, labels } = useMessages();
-
- const handleDataLoad = (data: any) => {
- props.onDataLoad?.(data);
- };
-
- const renderLabel = ({ x: label }) => {
- if (onLabelClick) {
- return (
- onLabelClick(label)} style={{ cursor: 'pointer' }}>
- {label}
-
- );
- }
-
- return label;
- };
-
- return (
-
- );
-}
-
-export default EventsTable;
diff --git a/src/components/metrics/FilterTags.module.css b/src/components/metrics/FilterTags.module.css
deleted file mode 100644
index ea7714f4..00000000
--- a/src/components/metrics/FilterTags.module.css
+++ /dev/null
@@ -1,60 +0,0 @@
-.filters {
- display: flex;
- align-items: center;
- gap: 10px;
- background: var(--base75);
- padding: 10px 20px;
- border: 1px solid var(--base400);
- border-radius: 8px;
- margin-bottom: 20px;
- flex-wrap: wrap;
-}
-
-.label {
- font-weight: 700;
-}
-
-.tag {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 4px;
- font-size: 12px;
- background: var(--base50);
- border: 1px solid var(--base400);
- border-radius: var(--border-radius);
- box-shadow: 1px 1px 1px var(--base500);
- padding: 6px 14px;
- cursor: pointer;
-}
-
-.tag:hover {
- background: var(--base100);
-}
-
-.close {
- font-weight: 700;
- align-self: center;
- margin-left: auto;
-}
-
-.name,
-.value {
- color: var(--base700);
- font-weight: 700;
-}
-
-.operator {
- text-transform: lowercase;
- font-weight: 900;
-}
-
-.icon {
- margin-left: 10px;
- padding: 2px;
- border-radius: 100%;
-}
-
-.icon:hover {
- background: var(--base200);
-}
diff --git a/src/components/metrics/FilterTags.tsx b/src/components/metrics/FilterTags.tsx
deleted file mode 100644
index fcba3c9e..00000000
--- a/src/components/metrics/FilterTags.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { MouseEvent } from 'react';
-import { Button, Icon, Icons, Popup, PopupTrigger, Text } from 'react-basics';
-import {
- useDateRange,
- useFields,
- useNavigation,
- useMessages,
- useFormat,
- useFilters,
-} from '@/components/hooks';
-import PopupForm from '@/app/(main)/reports/[reportId]/PopupForm';
-import FieldFilterEditForm from '@/app/(main)/reports/[reportId]/FieldFilterEditForm';
-import { OPERATOR_PREFIXES } from '@/lib/constants';
-import { isSearchOperator, parseParameterValue } from '@/lib/params';
-import styles from './FilterTags.module.css';
-import WebsiteFilterButton from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
-
-export function FilterTags({
- websiteId,
- params,
-}: {
- websiteId: string;
- params: { [key: string]: string };
-}) {
- const { formatMessage, labels } = useMessages();
- const { formatValue } = useFormat();
- const { dateRange } = useDateRange(websiteId);
- const {
- router,
- renderUrl,
- query: { view },
- } = useNavigation();
- const { fields } = useFields();
- const { operatorLabels } = useFilters();
- const { startDate, endDate } = dateRange;
-
- if (Object.keys(params).filter(key => params[key]).length === 0) {
- return null;
- }
-
- const handleCloseFilter = (param: string, e: MouseEvent) => {
- e.stopPropagation();
- router.push(renderUrl({ [param]: undefined }));
- };
-
- const handleResetFilter = () => {
- router.push(renderUrl({ view }, true));
- };
-
- const handleChangeFilter = (
- values: { name: string; operator: string; value: string },
- close: () => void,
- ) => {
- const { name, operator, value } = values;
- const prefix = OPERATOR_PREFIXES[operator];
-
- router.push(renderUrl({ [name]: prefix + value }));
- close();
- };
-
- return (
-
-
{formatMessage(labels.filters)}
- {Object.keys(params).map(key => {
- if (!params[key]) {
- return null;
- }
- const label = fields.find(f => f.name === key)?.label;
- const { operator, value } = parseParameterValue(params[key]);
- const paramValue = isSearchOperator(operator) ? value : formatValue(value, key);
-
- return (
-
-
- {label}
- {operatorLabels[operator]}
- {paramValue}
- handleCloseFilter(key, e)}>
-
-
-
-
- {(close: () => void) => {
- return (
-
- handleChangeFilter(values, close)}
- />
-
- );
- }}
-
-
- );
- })}
-
-
-
-
-
- {formatMessage(labels.clearAll)}
-
-
- );
-}
-
-export default FilterTags;
diff --git a/src/components/metrics/HostsTable.tsx b/src/components/metrics/HostsTable.tsx
deleted file mode 100644
index e034b970..00000000
--- a/src/components/metrics/HostsTable.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import FilterLink from '@/components/common/FilterLink';
-import { useMessages } from '@/components/hooks';
-import { Flexbox } from 'react-basics';
-
-export function HostsTable(props: MetricsTableProps) {
- const { formatMessage, labels } = useMessages();
-
- const renderLink = ({ x: host }) => {
- return (
-
-
-
- );
- };
-
- return (
- <>
-
- >
- );
-}
-
-export default HostsTable;
diff --git a/src/components/metrics/LanguagesTable.tsx b/src/components/metrics/LanguagesTable.tsx
deleted file mode 100644
index 3ced249e..00000000
--- a/src/components/metrics/LanguagesTable.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import { percentFilter } from '@/lib/filters';
-import { useLocale } from '@/components/hooks';
-import { useMessages } from '@/components/hooks';
-import { useFormat } from '@/components/hooks';
-
-export function LanguagesTable({
- onDataLoad,
- ...props
-}: { onDataLoad: (data: any) => void } & MetricsTableProps) {
- const { formatMessage, labels } = useMessages();
- const { locale } = useLocale();
- const { formatLanguage } = useFormat();
-
- const renderLabel = ({ x }) => {
- return {formatLanguage(x)}
;
- };
-
- return (
- onDataLoad?.(percentFilter(data))}
- renderLabel={renderLabel}
- searchFormattedValues={true}
- />
- );
-}
-
-export default LanguagesTable;
diff --git a/src/components/metrics/Legend.module.css b/src/components/metrics/Legend.module.css
deleted file mode 100644
index dea515f3..00000000
--- a/src/components/metrics/Legend.module.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.legend {
- display: flex;
- justify-content: center;
- flex-wrap: wrap;
- padding: 20px 0;
-}
-
-.label {
- display: flex;
- align-items: center;
- font-size: var(--font-size-sm);
- cursor: pointer;
-}
-
-.label + .label {
- margin-inline-start: 20px;
-}
-
-.hidden {
- color: var(--base400);
-}
diff --git a/src/components/metrics/Legend.tsx b/src/components/metrics/Legend.tsx
index 77442957..34ddb5a0 100644
--- a/src/components/metrics/Legend.tsx
+++ b/src/components/metrics/Legend.tsx
@@ -1,8 +1,6 @@
-import { StatusLight } from 'react-basics';
+import { Row, StatusLight, Text } from '@umami/react-zen';
+import type { LegendItem } from 'chart.js/auto';
import { colord } from 'colord';
-import classNames from 'classnames';
-import { LegendItem } from 'chart.js/auto';
-import styles from './Legend.module.css';
export function Legend({
items = [],
@@ -16,23 +14,26 @@ export function Legend({
}
return (
-
+
{items.map(item => {
const { text, fillStyle, hidden } = item;
const color = colord(fillStyle);
return (
- onClick(item)}
- >
- {text}
-
+ onClick(item)}>
+
+
+ {text}
+
+
+
);
})}
-
+
);
}
-
-export default Legend;
diff --git a/src/components/metrics/ListTable.module.css b/src/components/metrics/ListTable.module.css
deleted file mode 100644
index 405819b1..00000000
--- a/src/components/metrics/ListTable.module.css
+++ /dev/null
@@ -1,110 +0,0 @@
-.table {
- position: relative;
- display: grid;
- grid-template-rows: fit-content(100%) auto;
- overflow: hidden;
- flex: 1;
-}
-
-.body {
- position: relative;
- height: 100%;
- overflow: auto;
-}
-
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- line-height: 40px;
-}
-
-.title {
- display: flex;
- font-weight: 600;
-}
-
-.metric {
- font-weight: 600;
- text-align: center;
- width: 100px;
-}
-
-.row {
- position: relative;
- height: 30px;
- line-height: 30px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 5px;
- overflow: hidden;
- border-radius: 4px;
-}
-
-.row:hover {
- background-color: var(--base75);
-}
-
-.label {
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- flex: 2;
- padding-left: 10px;
-}
-
-.label a {
- color: inherit;
- text-decoration: none;
-}
-
-.label a:hover {
- color: var(--primary400);
-}
-
-.label:empty {
- color: #b3b3b3;
-}
-
-.label:empty:before {
- content: 'Unknown';
-}
-
-.value {
- display: flex;
- align-items: center;
- gap: 10px;
- text-align: end;
- margin-inline-end: 5px;
- font-weight: 600;
-}
-
-.percent {
- position: relative;
- width: 50px;
- color: var(--base600);
- border-inline-start: 1px solid var(--base600);
- padding-inline-start: 10px;
- z-index: 1;
-}
-
-.bar {
- position: absolute;
- top: 0;
- left: 0;
- height: 30px;
- opacity: 0.1;
- background: var(--primary400);
- z-index: -1;
-}
-
-.empty {
- min-height: 200px;
-}
-
-@media only screen and (max-width: 992px) {
- .body {
- height: auto;
- }
-}
diff --git a/src/components/metrics/ListTable.tsx b/src/components/metrics/ListTable.tsx
index f5dbbee5..f233bfe7 100644
--- a/src/components/metrics/ListTable.tsx
+++ b/src/components/metrics/ListTable.tsx
@@ -1,21 +1,27 @@
-import Empty from '@/components/common/Empty';
-import { useMessages } from '@/components/hooks';
-import { formatLongCurrency, formatLongNumber } from '@/lib/format';
-import { animated, config, useSpring } from '@react-spring/web';
-import classNames from 'classnames';
-import { ReactNode } from 'react';
+import { config, useSpring } from '@react-spring/web';
+import { Column, Grid, Row, Text } from '@umami/react-zen';
+import type { ReactNode } from 'react';
import { FixedSizeList } from 'react-window';
-import styles from './ListTable.module.css';
+import { AnimatedDiv } from '@/components/common/AnimatedDiv';
+import { Empty } from '@/components/common/Empty';
+import { useMessages, useMobile } from '@/components/hooks';
+import { formatLongCurrency, formatLongNumber } from '@/lib/format';
const ITEM_SIZE = 30;
+interface ListData {
+ label: string;
+ count: number;
+ percent: number;
+}
+
export interface ListTableProps {
- data?: any[];
+ data?: ListData[];
title?: string;
metric?: string;
className?: string;
- renderLabel?: (row: any, index: number) => ReactNode;
- renderChange?: (row: any, index: number) => ReactNode;
+ renderLabel?: (data: ListData, index: number) => ReactNode;
+ renderChange?: (data: ListData, index: number) => ReactNode;
animate?: boolean;
virtualize?: boolean;
showPercentage?: boolean;
@@ -27,7 +33,6 @@ export function ListTable({
data = [],
title,
metric,
- className,
renderLabel,
renderChange,
animate = true,
@@ -37,36 +42,40 @@ export function ListTable({
currency,
}: ListTableProps) {
const { formatMessage, labels } = useMessages();
+ const { isPhone } = useMobile();
- const getRow = (row: { x: any; y: any; z: any }, index: number) => {
- const { x: label, y: value, z: percent } = row;
+ const getRow = (row: ListData, index: number) => {
+ const { label, count, percent } = row;
return (
);
};
- const Row = ({ index, style }) => {
+ const ListTableRow = ({ index, style }) => {
return {getRow(data[index], index)}
;
};
return (
-
-
-
- {data?.length === 0 && }
+
+
+ {title}
+
+ {metric}
+
+
+
+ {data?.length === 0 && }
{virtualize && data.length > 0 ? (
- {Row}
+ {ListTableRow}
) : (
data.map(getRow)
)}
-
-
+
+
);
}
@@ -92,33 +101,52 @@ const AnimatedRow = ({
animate,
showPercentage = true,
currency,
+ isPhone,
}) => {
const props = useSpring({
width: percent,
- y: value,
+ y: !Number.isNaN(value) ? value : 0,
from: { width: 0, y: 0 },
config: animate ? config.default : { duration: 0 },
});
return (
-
-
{label}
-
+
+
+
+ {label}
+
+
+
{change}
-
- {currency
- ? props.y?.to(n => formatLongCurrency(n, currency))
- : props.y?.to(formatLongNumber)}
-
-
+
+
+ {currency
+ ? props.y?.to(n => formatLongCurrency(n, currency))
+ : props.y?.to(formatLongNumber)}
+
+
+
{showPercentage && (
-
-
`${n}%`) }} />
- {props.width.to(n => `${n?.toFixed?.(0)}%`)}
-
+
+ {props.width.to(n => `${n?.toFixed?.(0)}%`)}
+
)}
-
+
);
};
-
-export default ListTable;
diff --git a/src/components/metrics/MetricCard.module.css b/src/components/metrics/MetricCard.module.css
deleted file mode 100644
index 93e6c6d7..00000000
--- a/src/components/metrics/MetricCard.module.css
+++ /dev/null
@@ -1,37 +0,0 @@
-.card {
- display: flex;
- flex-direction: column;
- justify-content: center;
- min-width: 150px;
-}
-
-.card.compare .change {
- font-size: 16px;
- margin: 10px 0;
-}
-
-.card:first-child {
- padding-left: 0;
-}
-
-.card:last-child {
- border: 0;
-}
-
-.value {
- font-size: 36px;
- font-weight: 700;
- white-space: nowrap;
- color: var(--base900);
- line-height: 1.5;
-}
-
-.value.prev {
- color: var(--base800);
-}
-
-.label {
- font-weight: 700;
- white-space: nowrap;
- color: var(--base800);
-}
diff --git a/src/components/metrics/MetricCard.tsx b/src/components/metrics/MetricCard.tsx
index 41766167..d15bcf13 100644
--- a/src/components/metrics/MetricCard.tsx
+++ b/src/components/metrics/MetricCard.tsx
@@ -1,8 +1,8 @@
-import classNames from 'classnames';
-import { useSpring, animated } from '@react-spring/web';
+import { useSpring } from '@react-spring/web';
+import { Column, Text } from '@umami/react-zen';
+import { AnimatedDiv } from '@/components/common/AnimatedDiv';
+import { ChangeLabel } from '@/components/metrics/ChangeLabel';
import { formatNumber } from '@/lib/format';
-import ChangeLabel from '@/components/metrics/ChangeLabel';
-import styles from './MetricCard.module.css';
export interface MetricCardProps {
value: number;
@@ -13,8 +13,6 @@ export interface MetricCardProps {
formatValue?: (n: any) => string;
showLabel?: boolean;
showChange?: boolean;
- showPrevious?: boolean;
- className?: string;
}
export const MetricCard = ({
@@ -25,38 +23,34 @@ export const MetricCard = ({
formatValue = formatNumber,
showLabel = true,
showChange = false,
- showPrevious = false,
- className,
}: MetricCardProps) => {
const diff = value - change;
const pct = ((value - diff) / diff) * 100;
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } });
- const prevProps = useSpring({ x: Number(diff) || 0, from: { x: 0 } });
return (
-
- {showLabel &&
{label}
}
-
- {props?.x?.to(x => formatValue(x))}
-
+
+ {showLabel && (
+
+ {label}
+
+ )}
+
+ {props?.x?.to(x => formatValue(x))}
+
{showChange && (
-
- {changeProps?.x?.to(x => `${Math.abs(~~x)}%`)}
+
+ {changeProps?.x?.to(x => `${Math.abs(~~x)}%`)}
)}
- {showPrevious && (
-
- {prevProps?.x?.to(x => formatValue(x))}
-
- )}
-
+
);
};
-
-export default MetricCard;
diff --git a/src/components/metrics/MetricLabel.tsx b/src/components/metrics/MetricLabel.tsx
new file mode 100644
index 00000000..31c331f5
--- /dev/null
+++ b/src/components/metrics/MetricLabel.tsx
@@ -0,0 +1,142 @@
+import { Row } from '@umami/react-zen';
+import { Favicon } from '@/components/common/Favicon';
+import { FilterLink } from '@/components/common/FilterLink';
+import { TypeIcon } from '@/components/common/TypeIcon';
+import {
+ useCountryNames,
+ useFormat,
+ useLocale,
+ useMessages,
+ useRegionNames,
+} from '@/components/hooks';
+import { GROUPED_DOMAINS } from '@/lib/constants';
+
+export interface MetricLabelProps {
+ type: string;
+ data: any;
+ onClick?: () => void;
+}
+
+export function MetricLabel({ type, data }: MetricLabelProps) {
+ const { formatMessage, labels } = useMessages();
+ const { formatValue, formatCity } = useFormat();
+ const { locale } = useLocale();
+ const { countryNames } = useCountryNames(locale);
+ const { getRegionName } = useRegionNames(locale);
+
+ const { label, country, domain } = data;
+
+ switch (type) {
+ case 'browser':
+ case 'os':
+ return (
+ }
+ />
+ );
+
+ case 'channel':
+ return formatMessage(labels[label]);
+
+ case 'city':
+ return (
+
+ )
+ }
+ />
+ );
+
+ case 'region':
+ return (
+ }
+ />
+ );
+
+ case 'country':
+ return (
+ }
+ />
+ );
+
+ case 'path':
+ case 'entry':
+ case 'exit':
+ return (
+
+ );
+
+ case 'device':
+ return (
+ }
+ />
+ );
+
+ case 'referrer':
+ return (
+ }
+ />
+ );
+
+ case 'domain':
+ if (label === 'Other') {
+ return `(${formatMessage(labels.other)})`;
+ } else {
+ const name = GROUPED_DOMAINS.find(({ domain }) => domain === label)?.name;
+
+ if (!name) {
+ return null;
+ }
+
+ return (
+
+
+ {name}
+
+ );
+ }
+
+ case 'language':
+ return formatValue(label, 'language');
+
+ default:
+ return ;
+ }
+}
diff --git a/src/components/metrics/MetricsBar.module.css b/src/components/metrics/MetricsBar.module.css
deleted file mode 100644
index dadee9ef..00000000
--- a/src/components/metrics/MetricsBar.module.css
+++ /dev/null
@@ -1,13 +0,0 @@
-.bar {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(140px, max-content));
- gap: 20px;
- width: 100%;
- position: relative;
-}
-
-@media screen and (max-width: 768px) {
- .bar {
- grid-template-columns: 1fr 1fr;
- }
-}
diff --git a/src/components/metrics/MetricsBar.tsx b/src/components/metrics/MetricsBar.tsx
index 6e9f22de..850c6bc9 100644
--- a/src/components/metrics/MetricsBar.tsx
+++ b/src/components/metrics/MetricsBar.tsx
@@ -1,31 +1,14 @@
-import { ReactNode } from 'react';
-import { Loading, cloneChildren } from 'react-basics';
-import ErrorMessage from '@/components/common/ErrorMessage';
-import { formatLongNumber } from '@/lib/format';
-import styles from './MetricsBar.module.css';
+import { Grid, type GridProps } from '@umami/react-zen';
+import type { ReactNode } from 'react';
-export interface MetricsBarProps {
- isLoading?: boolean;
- isFetched?: boolean;
- error?: unknown;
+export interface MetricsBarProps extends GridProps {
children?: ReactNode;
}
-export function MetricsBar({ children, isLoading, isFetched, error }: MetricsBarProps) {
- const formatFunc = n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`);
-
+export function MetricsBar({ children, ...props }: MetricsBarProps) {
return (
-
- {isLoading && !isFetched && }
- {error && }
- {!isLoading &&
- !error &&
- isFetched &&
- cloneChildren(children, child => {
- return { format: child.props.format || formatFunc };
- })}
-
+
+ {children}
+
);
}
-
-export default MetricsBar;
diff --git a/src/components/metrics/MetricsExpandedTable.tsx b/src/components/metrics/MetricsExpandedTable.tsx
new file mode 100644
index 00000000..f24c952d
--- /dev/null
+++ b/src/components/metrics/MetricsExpandedTable.tsx
@@ -0,0 +1,139 @@
+import { Button, Column, DataColumn, DataTable, Icon, Row, SearchField } from '@umami/react-zen';
+import { type ReactNode, useState } from 'react';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useMessages, useWebsiteExpandedMetricsQuery } from '@/components/hooks';
+import { X } from '@/components/icons';
+import { DownloadButton } from '@/components/input/DownloadButton';
+import { MetricLabel } from '@/components/metrics/MetricLabel';
+import { SESSION_COLUMNS } from '@/lib/constants';
+import { formatShortTime } from '@/lib/format';
+
+export interface MetricsExpandedTableProps {
+ websiteId: string;
+ type?: string;
+ title?: string;
+ dataFilter?: (data: any) => any;
+ onSearch?: (search: string) => void;
+ params?: { [key: string]: any };
+ allowSearch?: boolean;
+ allowDownload?: boolean;
+ renderLabel?: (row: any, index: number) => ReactNode;
+ onClose?: () => void;
+ children?: ReactNode;
+}
+
+export function MetricsExpandedTable({
+ websiteId,
+ type,
+ title,
+ params,
+ allowSearch = true,
+ allowDownload = true,
+ onClose,
+ children,
+}: MetricsExpandedTableProps) {
+ const [search, setSearch] = useState('');
+ const { formatMessage, labels } = useMessages();
+ const isType = ['browser', 'country', 'device', 'os'].includes(type);
+ const showBounceDuration = SESSION_COLUMNS.includes(type);
+
+ const { data, isLoading, isFetching, error } = useWebsiteExpandedMetricsQuery(websiteId, {
+ type,
+ search: isType ? undefined : search,
+ ...params,
+ });
+
+ const items = data?.map(({ name, ...props }) => ({ label: name, ...props }));
+
+ return (
+ <>
+
+ {allowSearch && }
+
+ {children}
+ {allowDownload && }
+ {onClose && (
+
+
+
+
+
+ )}
+
+
+
+
+ {items && (
+
+
+ {row => (
+
+
+
+ )}
+
+
+ {row => row?.visitors?.toLocaleString()}
+
+
+ {row => row?.visits?.toLocaleString()}
+
+
+ {row => row?.pageviews?.toLocaleString()}
+
+ {showBounceDuration && [
+
+ {row => {
+ const n = (Math.min(row?.visits, row?.bounces) / row?.visits) * 100;
+ return `${Math.round(+n)}%`;
+ }}
+ ,
+
+
+ {row => {
+ const n = row?.totaltime / row?.visits;
+ return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
+ }}
+ ,
+ ]}
+
+ )}
+
+
+ >
+ );
+}
diff --git a/src/components/metrics/MetricsTable.module.css b/src/components/metrics/MetricsTable.module.css
deleted file mode 100644
index f04d9ae4..00000000
--- a/src/components/metrics/MetricsTable.module.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.container {
- position: relative;
- min-height: 430px;
- display: flex;
- flex-direction: column;
- flex: 1;
-}
-
-.actions {
- display: flex;
- gap: 20px;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 10px;
-}
-
-.footer {
- display: flex;
- justify-content: center;
-}
-
-.search {
- max-width: 300px;
-}
-
-@media only screen and (max-width: 992px) {
- .container {
- min-height: auto;
- }
-
- .actions {
- flex-direction: column;
- }
-
- .search {
- max-width: 100%;
- }
-}
diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx
index 616262cb..e99bd216 100644
--- a/src/components/metrics/MetricsTable.tsx
+++ b/src/components/metrics/MetricsTable.tsx
@@ -1,66 +1,42 @@
-import { ReactNode, useMemo, useState } from 'react';
-import { Loading, Icon, Text, SearchField } from 'react-basics';
-import classNames from 'classnames';
-import ErrorMessage from '@/components/common/ErrorMessage';
-import LinkButton from '@/components/common/LinkButton';
-import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants';
+import { Grid, Icon, Row, Text } from '@umami/react-zen';
+import { useEffect, useMemo } from 'react';
+import { LinkButton } from '@/components/common/LinkButton';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useMessages, useNavigation, useWebsiteMetricsQuery } from '@/components/hooks';
+import { Maximize } from '@/components/icons';
+import { MetricLabel } from '@/components/metrics/MetricLabel';
import { percentFilter } from '@/lib/filters';
-import {
- useNavigation,
- useWebsiteMetrics,
- useMessages,
- useLocale,
- useFormat,
-} from '@/components/hooks';
-import Icons from '@/components/icons';
-import ListTable, { ListTableProps } from './ListTable';
-import styles from './MetricsTable.module.css';
+import { ListTable, type ListTableProps } from './ListTable';
export interface MetricsTableProps extends ListTableProps {
websiteId: string;
- type?: string;
- className?: string;
+ type: string;
dataFilter?: (data: any) => any;
limit?: number;
- delay?: number;
- onDataLoad?: (data: any) => void;
- onSearch?: (search: string) => void;
- allowSearch?: boolean;
- searchFormattedValues?: boolean;
showMore?: boolean;
- params?: { [key: string]: any };
- children?: ReactNode;
+ filterLink?: boolean;
+ params?: Record;
+ onDataLoad?: (data: any) => void;
}
export function MetricsTable({
websiteId,
type,
- className,
dataFilter,
limit,
- onDataLoad,
- delay = null,
- allowSearch = false,
- searchFormattedValues = false,
- showMore = true,
+ showMore = false,
+ filterLink = true,
params,
- children,
+ onDataLoad,
...props
}: MetricsTableProps) {
- const [search, setSearch] = useState('');
- const { formatValue } = useFormat();
- const { renderUrl } = useNavigation();
+ const { updateParams } = useNavigation();
const { formatMessage, labels } = useMessages();
- const { dir } = useLocale();
-
- const { data, isLoading, isFetched, error } = useWebsiteMetrics(
- websiteId,
- { type, limit, search: searchFormattedValues ? undefined : search, ...params },
- {
- retryDelay: delay || DEFAULT_ANIMATION_DURATION,
- onDataLoad,
- },
- );
+ const { data, isLoading, isFetching, error } = useWebsiteMetricsQuery(websiteId, {
+ type,
+ limit,
+ ...params,
+ });
const filteredData = useMemo(() => {
if (data) {
@@ -76,52 +52,44 @@ export function MetricsTable({
}
}
- if (searchFormattedValues && search) {
- items = items.filter(({ x, ...data }) => {
- const value = formatValue(x, type, data);
-
- return value?.toLowerCase().includes(search.toLowerCase());
- });
- }
-
items = percentFilter(items);
- return items;
+ return items.map(({ x, y, z, ...props }) => ({ label: x, count: y, percent: z, ...props }));
}
return [];
- }, [data, dataFilter, search, limit, formatValue, type]);
+ }, [data, dataFilter, limit, type]);
+
+ useEffect(() => {
+ if (data) {
+ onDataLoad?.(data);
+ }
+ }, [data]);
+
+ const renderLabel = (row: any) => {
+ return filterLink ? : row.label;
+ };
return (
-
- {error &&
}
-
- {allowSearch && (
-
+
+
+ {data && }
+ {showMore && limit && (
+
+
+
+
+
+ {formatMessage(labels.more)}
+
+
)}
- {children}
-
- {data && !error && (
-
- )}
- {!data && isLoading && !isFetched &&
}
-
- {showMore && data && !error && limit && (
-
- {formatMessage(labels.more)}
-
-
-
-
- )}
-
-
+
+
);
}
-
-export default MetricsTable;
diff --git a/src/components/metrics/OSTable.tsx b/src/components/metrics/OSTable.tsx
deleted file mode 100644
index 37b79549..00000000
--- a/src/components/metrics/OSTable.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import FilterLink from '@/components/common/FilterLink';
-import { useMessages, useFormat } from '@/components/hooks';
-import TypeIcon from '@/components/common/TypeIcon';
-
-export function OSTable(props: MetricsTableProps) {
- const { formatMessage, labels } = useMessages();
- const { formatOS } = useFormat();
-
- function renderLink({ x: os }) {
- return (
-
-
-
- );
- }
-
- return (
-
- );
-}
-
-export default OSTable;
diff --git a/src/components/metrics/PagesTable.tsx b/src/components/metrics/PagesTable.tsx
deleted file mode 100644
index 8163b3d9..00000000
--- a/src/components/metrics/PagesTable.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
-import FilterButtons from '@/components/common/FilterButtons';
-import FilterLink from '@/components/common/FilterLink';
-import { useMessages, useNavigation } from '@/components/hooks';
-import { emptyFilter } from '@/lib/filters';
-import { useContext } from 'react';
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-
-export interface PagesTableProps extends MetricsTableProps {
- allowFilter?: boolean;
-}
-
-export function PagesTable({ allowFilter, ...props }: PagesTableProps) {
- const {
- router,
- renderUrl,
- query: { view = 'url' },
- } = useNavigation();
- const { formatMessage, labels } = useMessages();
- const { domain } = useContext(WebsiteContext);
-
- const handleSelect = (key: any) => {
- router.push(renderUrl({ view: key }), { scroll: false });
- };
-
- const buttons = [
- {
- label: formatMessage(labels.path),
- key: 'url',
- },
- {
- label: formatMessage(labels.entry),
- key: 'entry',
- },
- {
- label: formatMessage(labels.exit),
- key: 'exit',
- },
- {
- label: formatMessage(labels.title),
- key: 'title',
- },
- ];
-
- const renderLink = ({ x }) => {
- return (
-
- );
- };
-
- return (
-
- {allowFilter && }
-
- );
-}
-
-export default PagesTable;
diff --git a/src/components/metrics/PageviewsChart.tsx b/src/components/metrics/PageviewsChart.tsx
index 6fa3285f..b83f8dc3 100644
--- a/src/components/metrics/PageviewsChart.tsx
+++ b/src/components/metrics/PageviewsChart.tsx
@@ -1,9 +1,12 @@
-import { useMemo } from 'react';
-import BarChart, { BarChartProps } from '@/components/charts/BarChart';
-import { useLocale, useTheme, useMessages } from '@/components/hooks';
+import { useTheme } from '@umami/react-zen';
+import { useCallback, useMemo } from 'react';
+import { BarChart, type BarChartProps } from '@/components/charts/BarChart';
+import { useLocale, useMessages } from '@/components/hooks';
import { renderDateLabels } from '@/lib/charts';
+import { getThemeColors } from '@/lib/colors';
+import { generateTimeSeries } from '@/lib/date';
-export interface PagepageviewsChartProps extends BarChartProps {
+export interface PageviewsChartProps extends BarChartProps {
data: {
pageviews: any[];
sessions: any[];
@@ -13,38 +16,36 @@ export interface PagepageviewsChartProps extends BarChartProps {
};
};
unit: string;
- isLoading?: boolean;
- isAllTime?: boolean;
}
-export function PagepageviewsChart({
- data,
- unit,
- isLoading,
- isAllTime,
- ...props
-}: PagepageviewsChartProps) {
+export function PageviewsChart({ data, unit, minDate, maxDate, ...props }: PageviewsChartProps) {
const { formatMessage, labels } = useMessages();
- const { colors } = useTheme();
- const { locale } = useLocale();
+ const { theme } = useTheme();
+ const { locale, dateLocale } = useLocale();
+ const { colors } = useMemo(() => getThemeColors(theme), [theme]);
- const chartData = useMemo(() => {
- if (!data) {
- return {};
- }
+ const chartData: any = useMemo(() => {
+ if (!data) return;
return {
+ __id: Date.now(),
datasets: [
{
+ type: 'bar',
label: formatMessage(labels.visitors),
- data: data.sessions,
+ data: generateTimeSeries(data.sessions, minDate, maxDate, unit, dateLocale),
borderWidth: 1,
+ barPercentage: 0.9,
+ categoryPercentage: 0.9,
...colors.chart.visitors,
order: 3,
},
{
+ type: 'bar',
label: formatMessage(labels.views),
- data: data.pageviews,
+ data: generateTimeSeries(data.pageviews, minDate, maxDate, unit, dateLocale),
+ barPercentage: 0.9,
+ categoryPercentage: 0.9,
borderWidth: 1,
...colors.chart.views,
order: 4,
@@ -54,7 +55,13 @@ export function PagepageviewsChart({
{
type: 'line',
label: `${formatMessage(labels.views)} (${formatMessage(labels.previous)})`,
- data: data.compare.pageviews,
+ data: generateTimeSeries(
+ data.compare.pageviews,
+ minDate,
+ maxDate,
+ unit,
+ dateLocale,
+ ),
borderWidth: 2,
backgroundColor: '#8601B0',
borderColor: '#8601B0',
@@ -63,7 +70,7 @@ export function PagepageviewsChart({
{
type: 'line',
label: `${formatMessage(labels.visitors)} (${formatMessage(labels.previous)})`,
- data: data.compare.sessions,
+ data: generateTimeSeries(data.compare.sessions, minDate, maxDate, unit, dateLocale),
borderWidth: 2,
backgroundColor: '#f15bb5',
borderColor: '#f15bb5',
@@ -75,16 +82,17 @@ export function PagepageviewsChart({
};
}, [data, locale]);
+ const renderXLabel = useCallback(renderDateLabels(unit, locale), [unit, locale]);
+
return (
);
}
-
-export default PagepageviewsChart;
diff --git a/src/components/metrics/QueryParametersTable.module.css b/src/components/metrics/QueryParametersTable.module.css
deleted file mode 100644
index 47b20a90..00000000
--- a/src/components/metrics/QueryParametersTable.module.css
+++ /dev/null
@@ -1,16 +0,0 @@
-.item {
- display: inline-flex;
- align-items: center;
- line-height: 26px;
-}
-
-.param {
- padding: 0 8px;
- color: var(--primary400);
- background: var(--blue100);
- border-radius: 4px;
-}
-
-.value {
- padding: 0 8px;
-}
diff --git a/src/components/metrics/QueryParametersTable.tsx b/src/components/metrics/QueryParametersTable.tsx
deleted file mode 100644
index 26f01faf..00000000
--- a/src/components/metrics/QueryParametersTable.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { useState } from 'react';
-import FilterButtons from '@/components/common/FilterButtons';
-import { emptyFilter, paramFilter } from '@/lib/filters';
-import { FILTER_RAW, FILTER_COMBINED } from '@/lib/constants';
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import { useMessages } from '@/components/hooks';
-import styles from './QueryParametersTable.module.css';
-
-const filters = {
- [FILTER_RAW]: emptyFilter,
- [FILTER_COMBINED]: [emptyFilter, paramFilter],
-};
-
-export function QueryParametersTable({
- allowFilter,
- ...props
-}: { allowFilter: boolean } & MetricsTableProps) {
- const [filter, setFilter] = useState(FILTER_COMBINED);
- const { formatMessage, labels } = useMessages();
-
- const buttons = [
- {
- label: formatMessage(labels.filterCombined),
- key: FILTER_COMBINED,
- },
- { label: formatMessage(labels.filterRaw), key: FILTER_RAW },
- ];
-
- return (
-
- filter === FILTER_RAW ? (
- x
- ) : (
-
- )
- }
- delay={0}
- >
- {allowFilter && }
-
- );
-}
-
-export default QueryParametersTable;
diff --git a/src/components/metrics/RealtimeChart.tsx b/src/components/metrics/RealtimeChart.tsx
index f5697caa..f42b96da 100644
--- a/src/components/metrics/RealtimeChart.tsx
+++ b/src/components/metrics/RealtimeChart.tsx
@@ -1,8 +1,9 @@
+import { isBefore, startOfMinute, subMinutes } from 'date-fns';
import { useMemo, useRef } from 'react';
-import { startOfMinute, subMinutes, isBefore } from 'date-fns';
-import PageviewsChart from './PageviewsChart';
+import { useTimezone } from '@/components/hooks';
import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from '@/lib/constants';
-import { RealtimeData } from '@/lib/types';
+import type { RealtimeData } from '@/lib/types';
+import { PageviewsChart } from './PageviewsChart';
export interface RealtimeChartProps {
data: RealtimeData;
@@ -11,9 +12,11 @@ export interface RealtimeChartProps {
}
export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) {
+ const { formatSeriesTimezone, fromUtc, timezone } = useTimezone();
const endDate = startOfMinute(new Date());
const startDate = subMinutes(endDate, REALTIME_RANGE);
const prevEndDate = useRef(endDate);
+ const prevData = useRef(null);
const chartData = useMemo(() => {
if (!data) {
@@ -21,30 +24,36 @@ export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) {
}
return {
- pageviews: data.series.views,
- sessions: data.series.visitors,
+ pageviews: formatSeriesTimezone(data.series.views, 'x', timezone),
+ sessions: formatSeriesTimezone(data.series.visitors, 'x', timezone),
};
}, [data, startDate, endDate, unit]);
- // Don't animate the bars shifting over because it looks weird
const animationDuration = useMemo(() => {
+ // Don't animate the bars shifting over because it looks weird
if (isBefore(prevEndDate.current, endDate)) {
prevEndDate.current = endDate;
return 0;
}
+
+ // Don't animate when data hasn't changed
+ const serialized = JSON.stringify(chartData);
+ if (prevData.current === serialized) {
+ return 0;
+ }
+ prevData.current = serialized;
+
return DEFAULT_ANIMATION_DURATION;
- }, [endDate]);
+ }, [endDate, chartData]);
return (
);
}
-
-export default RealtimeChart;
diff --git a/src/components/metrics/ReferrersTable.tsx b/src/components/metrics/ReferrersTable.tsx
deleted file mode 100644
index 89729418..00000000
--- a/src/components/metrics/ReferrersTable.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import FilterLink from '@/components/common/FilterLink';
-import Favicon from '@/components/common/Favicon';
-import { useMessages, useNavigation } from '@/components/hooks';
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import FilterButtons from '@/components/common/FilterButtons';
-import thenby from 'thenby';
-import { GROUPED_DOMAINS } from '@/lib/constants';
-import { Flexbox } from 'react-basics';
-
-export interface ReferrersTableProps extends MetricsTableProps {
- allowFilter?: boolean;
-}
-
-export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) {
- const {
- router,
- renderUrl,
- query: { view = 'referrer' },
- } = useNavigation();
- const { formatMessage, labels } = useMessages();
-
- const handleSelect = (key: any) => {
- router.push(renderUrl({ view: key }), { scroll: false });
- };
-
- const buttons = [
- {
- label: formatMessage(labels.domain),
- key: 'referrer',
- },
- {
- label: formatMessage(labels.grouped),
- key: 'grouped',
- },
- ];
-
- const renderLink = ({ x: referrer }) => {
- if (view === 'grouped') {
- if (referrer === '_other') {
- return `(${formatMessage(labels.other)})`;
- } else {
- return (
-
-
- {GROUPED_DOMAINS.find(({ domain }) => domain === referrer)?.name}
-
- );
- }
- }
-
- return (
-
-
-
- );
- };
-
- const getDomain = (x: string) => {
- for (const { domain, match } of GROUPED_DOMAINS) {
- if (Array.isArray(match) ? match.some(str => x.includes(str)) : x.includes(match)) {
- return domain;
- }
- }
- return '_other';
- };
-
- const groupedFilter = (data: any[]) => {
- const groups = { _other: 0 };
-
- for (const { x, y } of data) {
- const domain = getDomain(x);
- if (!groups[domain]) {
- groups[domain] = 0;
- }
- groups[domain] += +y;
- }
-
- return Object.keys(groups)
- .map((key: any) => ({ x: key, y: groups[key] }))
- .sort(thenby.firstBy('y', -1));
- };
-
- return (
- <>
-
- {allowFilter && (
-
- )}
-
- >
- );
-}
-
-export default ReferrersTable;
diff --git a/src/components/metrics/RegionsTable.tsx b/src/components/metrics/RegionsTable.tsx
deleted file mode 100644
index 0b7e3bdf..00000000
--- a/src/components/metrics/RegionsTable.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import FilterLink from '@/components/common/FilterLink';
-import { emptyFilter } from '@/lib/filters';
-import { useMessages, useLocale, useRegionNames } from '@/components/hooks';
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import TypeIcon from '@/components/common/TypeIcon';
-
-export function RegionsTable(props: MetricsTableProps) {
- const { locale } = useLocale();
- const { formatMessage, labels } = useMessages();
- const { getRegionName } = useRegionNames(locale);
-
- const renderLink = ({ x: code, country }) => {
- return (
-
-
-
- );
- };
-
- return (
-
- );
-}
-
-export default RegionsTable;
diff --git a/src/components/metrics/ScreenTable.tsx b/src/components/metrics/ScreenTable.tsx
deleted file mode 100644
index c2a19caa..00000000
--- a/src/components/metrics/ScreenTable.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import { useMessages } from '@/components/hooks';
-
-export function ScreenTable(props: MetricsTableProps) {
- const { formatMessage, labels } = useMessages();
-
- return (
-
- );
-}
-
-export default ScreenTable;
diff --git a/src/components/metrics/TagsTable.tsx b/src/components/metrics/TagsTable.tsx
deleted file mode 100644
index e915f873..00000000
--- a/src/components/metrics/TagsTable.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import MetricsTable, { MetricsTableProps } from './MetricsTable';
-import FilterLink from '@/components/common/FilterLink';
-import { useMessages } from '@/components/hooks';
-import { Flexbox } from 'react-basics';
-
-export function TagsTable(props: MetricsTableProps) {
- const { formatMessage, labels } = useMessages();
-
- const renderLink = ({ x: tag }) => {
- return (
-
-
-
- );
- };
-
- return (
- <>
-
- >
- );
-}
-
-export default TagsTable;
diff --git a/src/components/metrics/WeeklyTraffic.tsx b/src/components/metrics/WeeklyTraffic.tsx
new file mode 100644
index 00000000..90e47c63
--- /dev/null
+++ b/src/components/metrics/WeeklyTraffic.tsx
@@ -0,0 +1,112 @@
+import { Focusable, Grid, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
+import { addHours, format, startOfDay } from 'date-fns';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useLocale, useMessages, useWeeklyTrafficQuery } from '@/components/hooks';
+import { getDayOfWeekAsDate } from '@/lib/date';
+
+export function WeeklyTraffic({ websiteId }: { websiteId: string }) {
+ const { data, isLoading, error } = useWeeklyTrafficQuery(websiteId);
+ const { dateLocale } = useLocale();
+ const { labels, formatMessage } = useMessages();
+ const { weekStartsOn } = dateLocale.options;
+ const daysOfWeek = Array(7)
+ .fill(weekStartsOn)
+ .map((d, i) => (d + i) % 7);
+
+ const [, max = 1] = data
+ ? data.reduce((arr: number[], hours: number[], index: number) => {
+ const min = Math.min(...hours);
+ const max = Math.max(...hours);
+
+ if (index === 0) {
+ return [min, max];
+ }
+
+ if (min < arr[0]) {
+ arr[0] = min;
+ }
+
+ if (max > arr[1]) {
+ arr[1] = max;
+ }
+
+ return arr;
+ }, [])
+ : [];
+
+ return (
+
+
+ {data && (
+ <>
+
+
+ {Array(24)
+ .fill(null)
+ .map((_, i) => {
+ const label = format(addHours(startOfDay(new Date()), i), 'haaa', {
+ locale: dateLocale,
+ });
+ return (
+
+
+ {label}
+
+
+ );
+ })}
+
+ {daysOfWeek.map((index: number) => {
+ const day = data[index];
+ return (
+
+
+
+ {format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })}
+
+
+ {day?.map((count: number, j) => {
+ const pct = max ? count / max : 0;
+ return (
+
+
+
+
+
+
+ {`${formatMessage(
+ labels.visitors,
+ )}: ${count}`}
+
+ );
+ })}
+
+ );
+ })}
+ >
+ )}
+
+
+ );
+}
diff --git a/src/components/metrics/WorldMap.module.css b/src/components/metrics/WorldMap.module.css
deleted file mode 100644
index c2528038..00000000
--- a/src/components/metrics/WorldMap.module.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.container {
- overflow: hidden;
- position: relative;
-}
diff --git a/src/components/metrics/WorldMap.tsx b/src/components/metrics/WorldMap.tsx
index a377bfc9..3c8fadb8 100644
--- a/src/components/metrics/WorldMap.tsx
+++ b/src/components/metrics/WorldMap.tsx
@@ -1,42 +1,37 @@
-import { useState, useMemo, HTMLAttributes } from 'react';
-import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
-import classNames from 'classnames';
+import { Column, type ColumnProps, FloatingTooltip, useTheme } from '@umami/react-zen';
import { colord } from 'colord';
-import HoverTooltip from '@/components/common/HoverTooltip';
+import { useMemo, useState } from 'react';
+import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
+import {
+ useCountryNames,
+ useLocale,
+ useMessages,
+ useWebsiteMetricsQuery,
+} from '@/components/hooks';
+import { getThemeColors } from '@/lib/colors';
import { ISO_COUNTRIES, MAP_FILE } from '@/lib/constants';
-import { useDateRange, useTheme, useWebsiteMetrics } from '@/components/hooks';
-import { useCountryNames } from '@/components/hooks';
-import { useLocale } from '@/components/hooks';
-import { useMessages } from '@/components/hooks';
-import { formatLongNumber } from '@/lib/format';
import { percentFilter } from '@/lib/filters';
-import styles from './WorldMap.module.css';
+import { formatLongNumber } from '@/lib/format';
-export function WorldMap({
- websiteId,
- data,
- className,
- ...props
-}: {
+export interface WorldMapProps extends ColumnProps {
websiteId?: string;
data?: any[];
- className?: string;
-} & HTMLAttributes) {
+}
+
+export function WorldMap({ websiteId, data, ...props }: WorldMapProps) {
const [tooltip, setTooltipPopup] = useState();
- const { theme, colors } = useTheme();
+ const { theme } = useTheme();
+ const { colors } = getThemeColors(theme);
const { locale } = useLocale();
const { formatMessage, labels } = useMessages();
const { countryNames } = useCountryNames(locale);
const visitorsLabel = formatMessage(labels.visitors).toLocaleLowerCase(locale);
const unknownLabel = formatMessage(labels.unknown);
- const {
- dateRange: { startDate, endDate },
- } = useDateRange(websiteId);
- const { data: mapData } = useWebsiteMetrics(websiteId, {
+
+ const { data: mapData } = useWebsiteMetricsQuery(websiteId, {
type: 'country',
- startAt: +startDate,
- endAt: +endDate,
});
+
const metrics = useMemo(
() => (data || mapData ? percentFilter((data || mapData) as any[]) : []),
[data, mapData],
@@ -70,11 +65,11 @@ export function WorldMap({
};
return (
-
@@ -104,9 +99,7 @@ export function WorldMap({
- {tooltip && {tooltip} }
-
+ {tooltip && {tooltip} }
+
);
}
-
-export default WorldMap;
diff --git a/src/components/svg/AddUser.tsx b/src/components/svg/AddUser.tsx
new file mode 100644
index 00000000..d1eb5095
--- /dev/null
+++ b/src/components/svg/AddUser.tsx
@@ -0,0 +1,16 @@
+import type { SVGProps } from 'react';
+
+const SvgAddUser = (props: SVGProps) => (
+
+
+
+
+);
+export default SvgAddUser;
diff --git a/src/components/svg/BarChart.tsx b/src/components/svg/BarChart.tsx
new file mode 100644
index 00000000..96ebe000
--- /dev/null
+++ b/src/components/svg/BarChart.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgBarChart = (props: SVGProps) => (
+
+
+
+);
+export default SvgBarChart;
diff --git a/src/components/svg/Bars.tsx b/src/components/svg/Bars.tsx
new file mode 100644
index 00000000..1ce88f72
--- /dev/null
+++ b/src/components/svg/Bars.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgBars = (props: SVGProps) => (
+
+
+
+);
+export default SvgBars;
diff --git a/src/components/svg/Bolt.tsx b/src/components/svg/Bolt.tsx
new file mode 100644
index 00000000..23b1e76b
--- /dev/null
+++ b/src/components/svg/Bolt.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgBolt = (props: SVGProps) => (
+
+
+
+);
+export default SvgBolt;
diff --git a/src/components/svg/Bookmark.tsx b/src/components/svg/Bookmark.tsx
new file mode 100644
index 00000000..089f61fe
--- /dev/null
+++ b/src/components/svg/Bookmark.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgBookmark = (props: SVGProps) => (
+
+
+
+);
+export default SvgBookmark;
diff --git a/src/components/svg/Calendar.tsx b/src/components/svg/Calendar.tsx
new file mode 100644
index 00000000..dfb848a9
--- /dev/null
+++ b/src/components/svg/Calendar.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgCalendar = (props: SVGProps) => (
+
+
+
+);
+export default SvgCalendar;
diff --git a/src/components/svg/Change.tsx b/src/components/svg/Change.tsx
new file mode 100644
index 00000000..935a2f7a
--- /dev/null
+++ b/src/components/svg/Change.tsx
@@ -0,0 +1,13 @@
+import type { SVGProps } from 'react';
+
+const SvgChange = (props: SVGProps) => (
+
+
+
+);
+export default SvgChange;
diff --git a/src/components/svg/Clock.tsx b/src/components/svg/Clock.tsx
new file mode 100644
index 00000000..2dfa6a6e
--- /dev/null
+++ b/src/components/svg/Clock.tsx
@@ -0,0 +1,12 @@
+import type { SVGProps } from 'react';
+
+const SvgClock = (props: SVGProps) => (
+
+
+
+
+
+
+
+);
+export default SvgClock;
diff --git a/src/components/svg/Compare.tsx b/src/components/svg/Compare.tsx
new file mode 100644
index 00000000..3434461a
--- /dev/null
+++ b/src/components/svg/Compare.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgCompare = (props: SVGProps) => (
+
+
+
+);
+export default SvgCompare;
diff --git a/src/components/svg/Dashboard.tsx b/src/components/svg/Dashboard.tsx
new file mode 100644
index 00000000..5696244f
--- /dev/null
+++ b/src/components/svg/Dashboard.tsx
@@ -0,0 +1,21 @@
+import type { SVGProps } from 'react';
+
+const SvgDashboard = (props: SVGProps) => (
+
+
+
+
+
+
+);
+export default SvgDashboard;
diff --git a/src/components/svg/Download.tsx b/src/components/svg/Download.tsx
new file mode 100644
index 00000000..5f58724c
--- /dev/null
+++ b/src/components/svg/Download.tsx
@@ -0,0 +1,9 @@
+import type { SVGProps } from 'react';
+
+const SvgDownload = (props: SVGProps) => (
+
+
+
+
+);
+export default SvgDownload;
diff --git a/src/components/svg/Expand.tsx b/src/components/svg/Expand.tsx
new file mode 100644
index 00000000..a0f472e5
--- /dev/null
+++ b/src/components/svg/Expand.tsx
@@ -0,0 +1,18 @@
+import type { SVGProps } from 'react';
+
+const SvgExpand = (props: SVGProps) => (
+
+
+
+);
+export default SvgExpand;
diff --git a/src/components/svg/Export.tsx b/src/components/svg/Export.tsx
new file mode 100644
index 00000000..5c1ef14d
--- /dev/null
+++ b/src/components/svg/Export.tsx
@@ -0,0 +1,12 @@
+import type { SVGProps } from 'react';
+
+const SvgExport = (props: SVGProps) => (
+
+
+
+
+
+
+
+);
+export default SvgExport;
diff --git a/src/components/svg/Flag.tsx b/src/components/svg/Flag.tsx
new file mode 100644
index 00000000..34af943a
--- /dev/null
+++ b/src/components/svg/Flag.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgFlag = (props: SVGProps) => (
+
+
+
+);
+export default SvgFlag;
diff --git a/src/components/svg/Funnel.tsx b/src/components/svg/Funnel.tsx
new file mode 100644
index 00000000..63cf47d7
--- /dev/null
+++ b/src/components/svg/Funnel.tsx
@@ -0,0 +1,18 @@
+import type { SVGProps } from 'react';
+
+const SvgFunnel = (props: SVGProps) => (
+
+
+
+
+
+
+);
+export default SvgFunnel;
diff --git a/src/components/svg/Gear.tsx b/src/components/svg/Gear.tsx
new file mode 100644
index 00000000..539b838c
--- /dev/null
+++ b/src/components/svg/Gear.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgGear = (props: SVGProps) => (
+
+
+
+);
+export default SvgGear;
diff --git a/src/components/svg/Globe.tsx b/src/components/svg/Globe.tsx
new file mode 100644
index 00000000..385017d4
--- /dev/null
+++ b/src/components/svg/Globe.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgGlobe = (props: SVGProps) => (
+
+
+
+);
+export default SvgGlobe;
diff --git a/src/components/svg/Lightbulb.tsx b/src/components/svg/Lightbulb.tsx
new file mode 100644
index 00000000..8d86170e
--- /dev/null
+++ b/src/components/svg/Lightbulb.tsx
@@ -0,0 +1,15 @@
+import type { SVGProps } from 'react';
+
+const SvgLightbulb = (props: SVGProps) => (
+
+
+
+
+);
+export default SvgLightbulb;
diff --git a/src/components/svg/Lightning.tsx b/src/components/svg/Lightning.tsx
new file mode 100644
index 00000000..9539a961
--- /dev/null
+++ b/src/components/svg/Lightning.tsx
@@ -0,0 +1,33 @@
+import type { SVGProps } from 'react';
+
+const SvgLightning = (props: SVGProps) => (
+
+
+
+
+
+
+
+
+
+
+);
+export default SvgLightning;
diff --git a/src/components/svg/Link.tsx b/src/components/svg/Link.tsx
new file mode 100644
index 00000000..4ce88e78
--- /dev/null
+++ b/src/components/svg/Link.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgLink = (props: SVGProps) => (
+
+
+
+);
+export default SvgLink;
diff --git a/src/components/svg/Location.tsx b/src/components/svg/Location.tsx
new file mode 100644
index 00000000..0fd7d165
--- /dev/null
+++ b/src/components/svg/Location.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgLocation = (props: SVGProps) => (
+
+
+
+);
+export default SvgLocation;
diff --git a/src/components/svg/Lock.tsx b/src/components/svg/Lock.tsx
new file mode 100644
index 00000000..2b62eb9e
--- /dev/null
+++ b/src/components/svg/Lock.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgLock = (props: SVGProps) => (
+
+
+
+);
+export default SvgLock;
diff --git a/src/components/svg/Logo.tsx b/src/components/svg/Logo.tsx
new file mode 100644
index 00000000..eb9fdf5b
--- /dev/null
+++ b/src/components/svg/Logo.tsx
@@ -0,0 +1,17 @@
+import type { SVGProps } from 'react';
+
+const SvgLogo = (props: SVGProps) => (
+
+
+
+
+);
+export default SvgLogo;
diff --git a/src/components/svg/LogoWhite.tsx b/src/components/svg/LogoWhite.tsx
new file mode 100644
index 00000000..fb8c5f9b
--- /dev/null
+++ b/src/components/svg/LogoWhite.tsx
@@ -0,0 +1,26 @@
+import type { SVGProps } from 'react';
+
+const SvgLogoWhite = (props: SVGProps) => (
+
+
+
+
+);
+export default SvgLogoWhite;
diff --git a/src/components/svg/Magnet.tsx b/src/components/svg/Magnet.tsx
new file mode 100644
index 00000000..88b0f03a
--- /dev/null
+++ b/src/components/svg/Magnet.tsx
@@ -0,0 +1,15 @@
+import type { SVGProps } from 'react';
+
+const SvgMagnet = (props: SVGProps) => (
+
+
+
+);
+export default SvgMagnet;
diff --git a/src/components/svg/Money.tsx b/src/components/svg/Money.tsx
new file mode 100644
index 00000000..7d7b1e56
--- /dev/null
+++ b/src/components/svg/Money.tsx
@@ -0,0 +1,15 @@
+import type { SVGProps } from 'react';
+
+const SvgMoney = (props: SVGProps) => (
+
+
+
+
+);
+export default SvgMoney;
diff --git a/src/components/svg/Moon.tsx b/src/components/svg/Moon.tsx
new file mode 100644
index 00000000..40e3e8b9
--- /dev/null
+++ b/src/components/svg/Moon.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgMoon = (props: SVGProps) => (
+
+
+
+);
+export default SvgMoon;
diff --git a/src/components/svg/Network.tsx b/src/components/svg/Network.tsx
new file mode 100644
index 00000000..15941a99
--- /dev/null
+++ b/src/components/svg/Network.tsx
@@ -0,0 +1,15 @@
+import type { SVGProps } from 'react';
+
+const SvgNetwork = (props: SVGProps) => (
+
+
+
+);
+export default SvgNetwork;
diff --git a/src/components/svg/Nodes.tsx b/src/components/svg/Nodes.tsx
new file mode 100644
index 00000000..1adfcb84
--- /dev/null
+++ b/src/components/svg/Nodes.tsx
@@ -0,0 +1,12 @@
+import type { SVGProps } from 'react';
+
+const SvgNodes = (props: SVGProps) => (
+
+
+
+);
+export default SvgNodes;
diff --git a/src/components/svg/Overview.tsx b/src/components/svg/Overview.tsx
new file mode 100644
index 00000000..67e6af14
--- /dev/null
+++ b/src/components/svg/Overview.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgOverview = (props: SVGProps) => (
+
+
+
+);
+export default SvgOverview;
diff --git a/src/components/svg/Path.tsx b/src/components/svg/Path.tsx
new file mode 100644
index 00000000..7538ba44
--- /dev/null
+++ b/src/components/svg/Path.tsx
@@ -0,0 +1,15 @@
+import type { SVGProps } from 'react';
+
+const SvgPath = (props: SVGProps) => (
+
+
+
+);
+export default SvgPath;
diff --git a/src/components/svg/Profile.tsx b/src/components/svg/Profile.tsx
new file mode 100644
index 00000000..c955fce2
--- /dev/null
+++ b/src/components/svg/Profile.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgProfile = (props: SVGProps) => (
+
+
+
+);
+export default SvgProfile;
diff --git a/src/components/svg/Pushpin.tsx b/src/components/svg/Pushpin.tsx
new file mode 100644
index 00000000..d19e98ea
--- /dev/null
+++ b/src/components/svg/Pushpin.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgPushpin = (props: SVGProps) => (
+
+
+
+);
+export default SvgPushpin;
diff --git a/src/components/svg/Redo.tsx b/src/components/svg/Redo.tsx
new file mode 100644
index 00000000..04c389f6
--- /dev/null
+++ b/src/components/svg/Redo.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgRedo = (props: SVGProps) => (
+
+
+
+);
+export default SvgRedo;
diff --git a/src/components/svg/Reports.tsx b/src/components/svg/Reports.tsx
new file mode 100644
index 00000000..b5489668
--- /dev/null
+++ b/src/components/svg/Reports.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgReports = (props: SVGProps) => (
+
+
+
+);
+export default SvgReports;
diff --git a/src/components/svg/Security.tsx b/src/components/svg/Security.tsx
new file mode 100644
index 00000000..d075a938
--- /dev/null
+++ b/src/components/svg/Security.tsx
@@ -0,0 +1,16 @@
+import type { SVGProps } from 'react';
+
+const SvgSecurity = (props: SVGProps) => (
+
+
+
+
+);
+export default SvgSecurity;
diff --git a/src/components/svg/Speaker.tsx b/src/components/svg/Speaker.tsx
new file mode 100644
index 00000000..eb724ae9
--- /dev/null
+++ b/src/components/svg/Speaker.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgSpeaker = (props: SVGProps) => (
+
+
+
+);
+export default SvgSpeaker;
diff --git a/src/components/svg/Sun.tsx b/src/components/svg/Sun.tsx
new file mode 100644
index 00000000..61880f5c
--- /dev/null
+++ b/src/components/svg/Sun.tsx
@@ -0,0 +1,9 @@
+import type { SVGProps } from 'react';
+
+const SvgSun = (props: SVGProps) => (
+
+
+
+
+);
+export default SvgSun;
diff --git a/src/components/svg/Switch.tsx b/src/components/svg/Switch.tsx
new file mode 100644
index 00000000..0196d850
--- /dev/null
+++ b/src/components/svg/Switch.tsx
@@ -0,0 +1,19 @@
+import type { SVGProps } from 'react';
+
+const SvgSwitch = (props: SVGProps) => (
+
+
+
+);
+export default SvgSwitch;
diff --git a/src/components/svg/Tag.tsx b/src/components/svg/Tag.tsx
new file mode 100644
index 00000000..2ff51f4d
--- /dev/null
+++ b/src/components/svg/Tag.tsx
@@ -0,0 +1,16 @@
+import type { SVGProps } from 'react';
+
+const SvgTag = (props: SVGProps) => (
+
+
+
+
+);
+export default SvgTag;
diff --git a/src/components/svg/Target.tsx b/src/components/svg/Target.tsx
new file mode 100644
index 00000000..3fe76d21
--- /dev/null
+++ b/src/components/svg/Target.tsx
@@ -0,0 +1,21 @@
+import type { SVGProps } from 'react';
+
+const SvgTarget = (props: SVGProps) => (
+
+
+
+
+
+);
+export default SvgTarget;
diff --git a/src/components/svg/Visitor.tsx b/src/components/svg/Visitor.tsx
new file mode 100644
index 00000000..16db5858
--- /dev/null
+++ b/src/components/svg/Visitor.tsx
@@ -0,0 +1,8 @@
+import type { SVGProps } from 'react';
+
+const SvgVisitor = (props: SVGProps) => (
+
+
+
+);
+export default SvgVisitor;
diff --git a/src/components/svg/Website.tsx b/src/components/svg/Website.tsx
new file mode 100644
index 00000000..20a18a49
--- /dev/null
+++ b/src/components/svg/Website.tsx
@@ -0,0 +1,13 @@
+import type { SVGProps } from 'react';
+
+const SvgWebsite = (props: SVGProps) => (
+
+
+
+);
+export default SvgWebsite;
diff --git a/src/components/svg/index.ts b/src/components/svg/index.ts
new file mode 100644
index 00000000..76756af4
--- /dev/null
+++ b/src/components/svg/index.ts
@@ -0,0 +1,37 @@
+export { default as AddUser } from './AddUser';
+export { default as BarChart } from './BarChart';
+export { default as Bars } from './Bars';
+export { default as Bolt } from './Bolt';
+export { default as Bookmark } from './Bookmark';
+export { default as Change } from './Change';
+export { default as Compare } from './Compare';
+export { default as Dashboard } from './Dashboard';
+export { default as Download } from './Download';
+export { default as Expand } from './Expand';
+export { default as Export } from './Export';
+export { default as Flag } from './Flag';
+export { default as Funnel } from './Funnel';
+export { default as Gear } from './Gear';
+export { default as Lightbulb } from './Lightbulb';
+export { default as Lightning } from './Lightning';
+export { default as Location } from './Location';
+export { default as Lock } from './Lock';
+export { default as Logo } from './Logo';
+export { default as LogoWhite } from './LogoWhite';
+export { default as Magnet } from './Magnet';
+export { default as Money } from './Money';
+export { default as Network } from './Network';
+export { default as Nodes } from './Nodes';
+export { default as Overview } from './Overview';
+export { default as Path } from './Path';
+export { default as Profile } from './Profile';
+export { default as Pushpin } from './Pushpin';
+export { default as Redo } from './Redo';
+export { default as Reports } from './Reports';
+export { default as Security } from './Security';
+export { default as Speaker } from './Speaker';
+export { default as Switch } from './Switch';
+export { default as Tag } from './Tag';
+export { default as Target } from './Target';
+export { default as Visitor } from './Visitor';
+export { default as Website } from './Website';
diff --git a/src/declaration.d.ts b/src/declaration.d.ts
index 7dff68b8..14bae12a 100644
--- a/src/declaration.d.ts
+++ b/src/declaration.d.ts
@@ -1,6 +1,18 @@
+declare module '*.css';
+declare module '*.svg';
+declare module '*.json';
declare module 'bcryptjs';
declare module 'chartjs-adapter-date-fns';
declare module 'cors';
+declare module 'date-fns-tz';
declare module 'debug';
+declare module 'fs-extra';
declare module 'jsonwebtoken';
declare module 'md5';
+declare module 'papaparse';
+declare module 'prettier';
+declare module 'react-simple-maps';
+declare module 'semver';
+declare module 'tsup';
+declare module 'uuid';
+declare module '@umami/esbuild-plugin-css-modules';
diff --git a/src/index.ts b/src/index.ts
index e7b0e6c6..907c5623 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,64 +1,82 @@
-export * from '@/components/hooks';
+export * from '@/app/(main)/settings/preferences/LanguageSetting';
+export * from '@/app/(main)/settings/preferences/PreferenceSettings';
+export * from '@/app/(main)/settings/preferences/PreferencesPage';
+export * from '@/app/(main)/settings/preferences/ThemeSetting';
+export * from '@/app/(main)/teams/[teamId]/TeamDeleteForm';
+export * from '@/app/(main)/teams/[teamId]/TeamEditForm';
+export * from '@/app/(main)/teams/[teamId]/TeamManage';
+export * from '@/app/(main)/teams/[teamId]/TeamMemberEditButton';
+export * from '@/app/(main)/teams/[teamId]/TeamMemberEditForm';
+export * from '@/app/(main)/teams/[teamId]/TeamMemberRemoveButton';
+export * from '@/app/(main)/teams/[teamId]/TeamMembersDataTable';
+export * from '@/app/(main)/teams/[teamId]/TeamMembersTable';
+export * from '@/app/(main)/teams/[teamId]/TeamSettings';
+export * from '@/app/(main)/teams/[teamId]/TeamWebsiteRemoveButton';
+export * from '@/app/(main)/teams/[teamId]/TeamWebsitesDataTable';
+export * from '@/app/(main)/teams/[teamId]/TeamWebsitesTable';
-export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton';
-export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm';
-export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton';
-export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable';
-export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMembersTable';
+export * from '@/app/(main)/teams/TeamAddForm';
+export * from '@/app/(main)/teams/TeamJoinForm';
+export * from '@/app/(main)/teams/TeamLeaveButton';
+export * from '@/app/(main)/teams/TeamLeaveForm';
+export * from '@/app/(main)/teams/TeamProvider';
+export * from '@/app/(main)/teams/TeamsAddButton';
+export * from '@/app/(main)/teams/TeamsDataTable';
+export * from '@/app/(main)/teams/TeamsHeader';
+export * from '@/app/(main)/teams/TeamsJoinButton';
+export * from '@/app/(main)/teams/TeamsTable';
+export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteData';
+export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm';
+export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteEditForm';
+export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteResetForm';
+export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings';
+export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteShareForm';
+export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteTrackingCode';
-export * from '@/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm';
-export * from '@/app/(main)/teams/[teamId]/settings/team/TeamDetails';
-export * from '@/app/(main)/teams/[teamId]/settings/team/TeamEditForm';
-export * from '@/app/(main)/teams/[teamId]/settings/team/TeamManage';
+export * from '@/app/(main)/websites/WebsiteAddButton';
+export * from '@/app/(main)/websites/WebsiteAddForm';
+export * from '@/app/(main)/websites/WebsiteProvider';
+export * from '@/app/(main)/websites/WebsitesDataTable';
+export * from '@/app/(main)/websites/WebsitesHeader';
+export * from '@/app/(main)/websites/WebsitesTable';
-export * from '@/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton';
-export * from '@/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable';
-export * from '@/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable';
-
-export * from '@/app/(main)/settings/teams/TeamAddForm';
-export * from '@/app/(main)/settings/teams/TeamJoinForm';
-export * from '@/app/(main)/settings/teams/TeamLeaveButton';
-export * from '@/app/(main)/settings/teams/TeamLeaveForm';
-export * from '@/app/(main)/settings/teams/TeamsAddButton';
-export * from '@/app/(main)/settings/teams/TeamsDataTable';
-export * from '@/app/(main)/settings/teams/TeamsHeader';
-export * from '@/app/(main)/settings/teams/TeamsJoinButton';
-export * from '@/app/(main)/settings/teams/TeamsTable';
-export * from '@/app/(main)/settings/teams/WebsiteTags';
-
-export * from '@/app/(main)/settings/websites/[websiteId]/ShareUrl';
-export * from '@/app/(main)/settings/websites/[websiteId]/TrackingCode';
-export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteData';
-export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm';
-export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteEditForm';
-export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteResetForm';
-export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteSettings';
-
-export * from '@/app/(main)/settings/websites/WebsiteAddButton';
-export * from '@/app/(main)/settings/websites/WebsiteAddForm';
-export * from '@/app/(main)/settings/websites/WebsitesDataTable';
-export * from '@/app/(main)/settings/websites/WebsitesHeader';
-export * from '@/app/(main)/settings/websites/WebsitesTable';
-
-export * from '@/app/(main)/teams/[teamId]/TeamProvider';
-export * from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
+export * from '@/components/charts/BarChart';
+export * from '@/components/charts/BubbleChart';
+export * from '@/components/charts/Chart';
+export * from '@/components/charts/ChartTooltip';
+export * from '@/components/charts/PieChart';
+export * from '@/components/common/ActionForm';
export * from '@/components/common/ConfirmationForm';
-export * from '@/components/common/DataTable';
+export * from '@/components/common/DataGrid';
+export * from '@/components/common/DateDisplay';
+export * from '@/components/common/DateDistance';
export * from '@/components/common/Empty';
+export * from '@/components/common/EmptyPlaceholder';
export * from '@/components/common/ErrorBoundary';
export * from '@/components/common/ErrorMessage';
+export * from '@/components/common/ExternalLink';
export * from '@/components/common/Favicon';
-export * from '@/components/common/FilterButtons';
-export * from '@/components/common/FilterLink';
-export * from '@/components/common/HamburgerButton';
-export * from '@/components/common/HoverTooltip';
export * from '@/components/common/LinkButton';
-export * from '@/components/common/MobileMenu';
+export * from '@/components/common/LoadingPanel';
+export * from '@/components/common/PageBody';
+export * from '@/components/common/PageHeader';
export * from '@/components/common/Pager';
+export * from '@/components/common/Panel';
+export * from '@/components/common/SectionHeader';
+export * from '@/components/common/SideMenu';
export * from '@/components/common/TypeConfirmationForm';
-
-export * from '@/components/input/TeamsButton';
-export * from '@/components/input/ThemeButton';
-
-export { ROLES } from '@/lib/constants';
+export * from '@/components/hooks';
+export * from '@/components/input/DateFilter';
+export * from '@/components/input/DialogButton';
+export * from '@/components/input/DownloadButton';
+export * from '@/components/input/ExportButton';
+export * from '@/components/input/FilterButtons';
+export * from '@/components/input/NavButton';
+export * from '@/components/input/ProfileButton';
+export * from '@/components/input/WebsiteSelect';
+export * from '@/components/metrics/ChangeLabel';
+export * from '@/components/metrics/ListTable';
+export * from '@/components/metrics/MetricCard';
+export * from '@/components/metrics/MetricLabel';
+export * from '@/components/metrics/MetricsBar';
diff --git a/src/lang/am-ET.json b/src/lang/am-ET.json
deleted file mode 100644
index ab311218..00000000
--- a/src/lang/am-ET.json
+++ /dev/null
@@ -1,279 +0,0 @@
-{
- "label.access-code": "Access code",
- "label.actions": "Actions",
- "label.activity": "Activity log",
- "label.add": "Add",
- "label.add-description": "Add description",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
- "label.add-website": "Add website",
- "label.admin": "Administrator",
- "label.after": "After",
- "label.all": "All",
- "label.all-time": "All time",
- "label.analytics": "Analytics",
- "label.average": "Average",
- "label.back": "Back",
- "label.before": "Before",
- "label.bounce-rate": "Bounce rate",
- "label.breakdown": "Breakdown",
- "label.browser": "Browser",
- "label.browsers": "Browsers",
- "label.cancel": "Cancel",
- "label.change-password": "Change password",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
- "label.confirm-password": "Confirm password",
- "label.contains": "Contains",
- "label.continue": "Continue",
- "label.count": "Count",
- "label.countries": "Countries",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
- "label.current-password": "Current password",
- "label.custom-range": "Custom range",
- "label.dashboard": "Dashboard",
- "label.data": "Data",
- "label.date": "Date",
- "label.date-range": "Date range",
- "label.day": "Day",
- "label.default-date-range": "Default date range",
- "label.delete": "Delete",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
- "label.delete-website": "Delete website",
- "label.description": "Description",
- "label.desktop": "Desktop",
- "label.details": "Details",
- "label.device": "Device",
- "label.devices": "Devices",
- "label.dismiss": "Dismiss",
- "label.does-not-contain": "Does not contain",
- "label.domain": "Domain",
- "label.dropoff": "Dropoff",
- "label.edit": "Edit",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
- "label.enable-share-url": "Enable share URL",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event Data",
- "label.events": "Events",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
- "label.filter": "Filter",
- "label.filter-combined": "Combined",
- "label.filter-raw": "Raw",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
- "label.language": "Language",
- "label.languages": "Languages",
- "label.laptop": "Laptop",
- "label.last-days": "Last {x} days",
- "label.last-hours": "Last {x} hours",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
- "label.login": "Login",
- "label.logout": "Logout",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
- "label.mobile": "Mobile",
- "label.more": "More",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
- "label.name": "Name",
- "label.new-password": "New password",
- "label.none": "None",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
- "label.ok": "OK",
- "label.os": "OS",
- "label.overview": "Overview",
- "label.owner": "Owner",
- "label.page-of": "Page {current} of {total}",
- "label.page-views": "Page views",
- "label.pageTitle": "Page title",
- "label.pages": "Pages",
- "label.password": "Password",
- "label.path": "Path",
- "label.paths": "Paths",
- "label.powered-by": "Powered by {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
- "label.profile": "Profile",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
- "label.query": "Query",
- "label.query-parameters": "Query parameters",
- "label.realtime": "Realtime",
- "label.referrer": "Referrer",
- "label.referrers": "Referrers",
- "label.refresh": "Refresh",
- "label.regenerate": "Regenerate",
- "label.region": "Region",
- "label.regions": "Regions",
- "label.remove": "Remove",
- "label.remove-member": "Remove member",
- "label.reports": "Reports",
- "label.required": "Required",
- "label.reset": "Reset",
- "label.reset-website": "Reset statistics",
- "label.retention": "Retention",
- "label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
- "label.role": "Role",
- "label.run-query": "Run query",
- "label.save": "Save",
- "label.screens": "Screens",
- "label.search": "Search",
- "label.select": "Select",
- "label.select-date": "Select date",
- "label.select-role": "Select role",
- "label.select-website": "Select website",
- "label.session": "Session",
- "label.sessions": "Sessions",
- "label.settings": "Settings",
- "label.share-url": "Share URL",
- "label.single-day": "Single day",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
- "label.sum": "Sum",
- "label.tablet": "Tablet",
- "label.team": "Team",
- "label.team-id": "Team ID",
- "label.team-manager": "Team manager",
- "label.team-member": "Team member",
- "label.team-name": "Team name",
- "label.team-owner": "Team owner",
- "label.team-view-only": "Team view only",
- "label.team-websites": "Team websites",
- "label.teams": "Teams",
- "label.theme": "Theme",
- "label.this-month": "This month",
- "label.this-week": "This week",
- "label.this-year": "This year",
- "label.timezone": "Timezone",
- "label.title": "Title",
- "label.today": "Today",
- "label.toggle-charts": "Toggle charts",
- "label.total": "Total",
- "label.total-records": "Total records",
- "label.tracking-code": "Tracking code",
- "label.transactions": "Transactions",
- "label.transfer": "Transfer",
- "label.transfer-website": "Transfer website",
- "label.true": "True",
- "label.type": "Type",
- "label.unique": "Unique",
- "label.unique-visitors": "Unique visitors",
- "label.uniqueCustomers": "Unique Customers",
- "label.unknown": "Unknown",
- "label.untitled": "Untitled",
- "label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
- "label.user": "User",
- "label.user-property": "User Property",
- "label.username": "Username",
- "label.users": "Users",
- "label.utm": "UTM",
- "label.utm-description": "Track your campaigns through UTM parameters.",
- "label.value": "Value",
- "label.view": "View",
- "label.view-details": "View details",
- "label.view-only": "View only",
- "label.views": "Views",
- "label.views-per-visit": "Views per visit",
- "label.visit-duration": "Visit duration",
- "label.visitors": "Visitors",
- "label.visits": "Visits",
- "label.website": "Website",
- "label.website-id": "Website ID",
- "label.websites": "Websites",
- "label.window": "Window",
- "label.yesterday": "Yesterday",
- "message.action-confirmation": "Type {confirmation} in the box below to confirm.",
- "message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
- "message.collected-data": "Collected data",
- "message.confirm-delete": "Are you sure you want to delete {target}?",
- "message.confirm-leave": "Are you sure you want to leave {target}?",
- "message.confirm-remove": "Are you sure you want to remove {target}?",
- "message.confirm-reset": "Are you sure you want to reset {target}'s statistics?",
- "message.delete-team-warning": "Deleting a team will also delete all team websites.",
- "message.delete-website-warning": "All website data will be deleted.",
- "message.error": "Something went wrong.",
- "message.event-log": "{event} on {url}",
- "message.go-to-settings": "Go to settings",
- "message.incorrect-username-password": "Incorrect username/password.",
- "message.invalid-domain": "Invalid domain. Do not include http/https.",
- "message.min-password-length": "Minimum length of {n} characters",
- "message.new-version-available": "A new version of Umami {version} is available!",
- "message.no-data-available": "No data available.",
- "message.no-event-data": "No event data is available.",
- "message.no-match-password": "Passwords do not match.",
- "message.no-results-found": "No results were found.",
- "message.no-team-websites": "This team does not have any websites.",
- "message.no-teams": "You have not created any teams.",
- "message.no-users": "There are no users.",
- "message.no-websites-configured": "You do not have any websites configured.",
- "message.page-not-found": "Page not found.",
- "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
- "message.reset-website-warning": "All statistics for this website will be deleted, but your settings will remain intact.",
- "message.saved": "Saved.",
- "message.share-url": "This is the publicly shared URL for {target}.",
- "message.team-already-member": "You are already a member of the team.",
- "message.team-not-found": "Team not found.",
- "message.team-websites-info": "Websites can be viewed by anyone on the team.",
- "message.tracking-code": "To track stats for this website, place the following code in the ... section of your HTML.",
- "message.transfer-team-website-to-user": "Transfer this website to your account?",
- "message.transfer-user-website-to-team": "Select the team to transfer this website to.",
- "message.transfer-website": "Transfer website ownership to your account or another team.",
- "message.triggered-event": "Triggered event",
- "message.user-deleted": "User deleted.",
- "message.viewed-page": "Viewed page",
- "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
-}
diff --git a/src/lang/ar-SA.json b/src/lang/ar-SA.json
index 981fea84..5b5cfa9f 100644
--- a/src/lang/ar-SA.json
+++ b/src/lang/ar-SA.json
@@ -3,33 +3,47 @@
"label.actions": "الإجراءات",
"label.activity": "سجل الأحداث",
"label.add": "أضِف",
+ "label.add-board": "أضف لوحة",
"label.add-description": "أضِف وصف",
"label.add-member": "أضِف عضو",
- "label.add-step": "Add step",
+ "label.add-step": "إضافة خطوة",
"label.add-website": "إضافة موقع",
"label.admin": "مدير",
+ "label.affiliate": "Affiliate",
"label.after": "يعد",
"label.all": "الكل",
"label.all-time": "كل الوقت",
"label.analytics": "تحليلات",
+ "label.apply": "تطبيق",
+ "label.attribution": "الإسناد",
+ "label.attribution-description": "شاهد كيف يتفاعل المستخدمون مع حملاتك التسويقية وما الذي يحفز التحويلات.",
"label.average": "المتوسط",
"label.back": "للخلف",
"label.before": "قبل",
+ "label.boards": "لوحات",
"label.bounce-rate": "معدل الارتداد",
"label.breakdown": "التصنيف",
"label.browser": "المتصفح",
"label.browsers": "المتصفحات",
- "label.cancel": "ألغِ",
+ "label.campaigns": "حملات",
+ "label.cancel": "إلغاء",
"label.change-password": "تغيير كلمة المرور",
+ "label.channels": "قنوات",
"label.cities": "المدن",
"label.city": "المدينة",
"label.clear-all": "مسح الكل",
- "label.compare": "Compare",
+ "label.cohort": "مجموعة",
+ "label.compare": "المقارنة",
+ "label.compare-dates": "قارن التواريخ",
"label.confirm": "تأكيد",
"label.confirm-password": "تأكيد كلمة المرور",
- "label.contains": "يحتوي",
+ "label.contains": "يحتوي على",
+ "label.content": "المحتوى",
"label.continue": "تابع",
- "label.count": "Count",
+ "label.conversion": "تحويل",
+ "label.conversion-rate": "معدل التحويل",
+ "label.conversion-step": "خطوة التحويل",
+ "label.count": "العدد",
"label.countries": "الدول",
"label.country": "الدولة",
"label.create": "أنشِئ",
@@ -38,10 +52,11 @@
"label.create-user": "أنشِئ مستخدم",
"label.created": "أُنشئت",
"label.created-by": "أُنشئ من قبل",
- "label.current": "Current",
+ "label.currency": "العملة",
+ "label.current": "الحالي",
"label.current-password": "كلمة المرور الحالية",
"label.custom-range": "فترة مخصّصة",
- "label.dashboard": "الشاشة الرئيسية",
+ "label.dashboard": "لوحة التحكم",
"label.data": "البيانات",
"label.date": "التاريخ",
"label.date-range": "فترة مخصّصة",
@@ -57,20 +72,27 @@
"label.details": "تفاصيل",
"label.device": "الجهاز",
"label.devices": "الأجهزة",
+ "label.direct": "مباشر",
"label.dismiss": "تجاهل",
- "label.does-not-contain": "لا يحتوي",
+ "label.distinct-id": "معرّف مميز",
+ "label.does-not-contain": "لا يحتوي على",
+ "label.does-not-include": "لا يتضمن",
+ "label.doest-not-exist": "غير موجود",
"label.domain": "النطاق",
"label.dropoff": "إنزال",
- "label.edit": "عدّل",
+ "label.edit": "تعديل",
"label.edit-dashboard": "عدّل لوحة التحكم",
"label.edit-member": "عدّل العضو",
+ "label.email": "Email",
"label.enable-share-url": "فعّل مشاركة الرابط",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
+ "label.end-step": "الخطوة الأخيرة",
+ "label.entry": "رابط الدخول",
"label.event": "الحدث",
"label.event-data": "تاريخ الحدث",
+ "label.event-name": "اسم الحدث",
"label.events": "الأحداث",
- "label.exit": "Exit URL",
+ "label.exists": "موجود",
+ "label.exit": "رابط المغادرة",
"label.false": "خطأ",
"label.field": "الحقل",
"label.fields": "الحقول",
@@ -78,81 +100,108 @@
"label.filter-combined": "مُجمّعة",
"label.filter-raw": "خام",
"label.filters": "التصفيات",
- "label.first-seen": "First seen",
+ "label.first-click": "النقرة الأولى",
+ "label.first-seen": "أول ظهور",
"label.funnel": "قمع",
"label.funnel-description": "فهم معدل التحويل والانقطاع عن المستخدمين.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
+ "label.funnels": "قمعات",
+ "label.goal": "الهدف",
+ "label.goals": "الأهداف",
+ "label.goals-description": "تابع تحقق أهدافك المرتبطة بمشاهدات الصفحات والأحداث.",
"label.greater-than": "أكبَر مِن",
"label.greater-than-equals": "أكبَر مِن أو يساوي",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "مجمع",
+ "label.hostname": "اسم المضيف",
+ "label.includes": "يتضمن",
+ "label.insight": "رؤية معمقة",
"label.insights": "نتائج التحليلات",
"label.insights-description": "تعمق في بياناتك باستخدام الشرائح والتصفيات.",
- "label.is": "هو",
- "label.is-not": "لم",
+ "label.is": "يساوي",
+ "label.is-false": "غير صحيح",
+ "label.is-not": "لا يساوي",
"label.is-not-set": "لم ضُبط",
"label.is-set": "ضُبط",
+ "label.is-true": "صحيح",
"label.join": "انضم",
"label.join-team": "انضم للفريق",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.journey": "رحلة المستخدم",
+ "label.journey-description": "تعرّف على كيفية تنقّل المستخدمين داخل موقعك.",
+ "label.journeys": "رحلات المستخدم",
"label.language": "اللغة",
"label.languages": "اللغات",
"label.laptop": "لابتوب",
+ "label.last-click": "النقرة الأخيرة",
"label.last-days": "آخر {x} يوم/ايام",
"label.last-hours": "آخر {x} ساعة",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
+ "label.last-months": "آخر {x} شهر/أشهر",
+ "label.last-seen": "آخر ظهور",
"label.leave": "غادر",
"label.leave-team": "مغادرة المجموعة",
"label.less-than": "أقل مِن",
"label.less-than-equals": "أقل مِن أو يساوي",
+ "label.links": "روابط",
"label.login": "تسجيل الدخول",
"label.logout": "تسجيل الخروج",
"label.manage": "التحكم",
- "label.manager": "Manager",
+ "label.manager": "مدير",
"label.max": "الحد الأقصى",
+ "label.maximize": "توسيع",
+ "label.medium": "وسيط",
"label.member": "عضو",
"label.members": "الأعضاء",
"label.min": "الحد الأدنى",
"label.mobile": "جوال",
+ "label.model": "نموذج",
"label.more": "المزيد",
"label.my-account": "حسابي",
"label.my-websites": "مواقعي",
"label.name": "الاسم",
"label.new-password": "كلمة مرور جديدة",
- "label.none": "غير معرّف",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
+ "label.none": "لا شيء",
+ "label.number-of-records": "{x} {x, plural, one {سجل} other {سجلات}}",
"label.ok": "نعم",
+ "label.online": "Online",
+ "label.organic-search": "بحث عضوي",
+ "label.organic-shopping": "تسوق عضوي",
+ "label.organic-social": "اجتماعي عضوي",
+ "label.organic-video": "فيديو عضوي",
"label.os": "نظام التشغيل",
+ "label.other": "أخرى",
"label.overview": "نظرة عامة",
"label.owner": "المالك",
+ "label.page": "صفحة",
"label.page-of": "صفحة {current} من {total}",
"label.page-views": "مشاهدات الصفحة",
"label.pageTitle": "عنوان الصفحة",
- "label.pages": "الصفحات",
+ "label.pages": "صفحات",
+ "label.paid-ads": "إعلانات مدفوعة",
+ "label.paid-search": "بحث مدفوع",
+ "label.paid-shopping": "تسوق مدفوع",
+ "label.paid-social": "اجتماعي مدفوع",
+ "label.paid-video": "فيديو مدفوع",
"label.password": "كلمة المرور",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "مسار",
+ "label.paths": "مسارات",
+ "label.pixels": "بكسلات",
"label.powered-by": "مشغل بواسطة {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "السابق",
+ "label.previous-period": "الفترة السابقة",
+ "label.previous-year": "العام السابق",
"label.profile": "الملف الشخصي",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "خصائص",
+ "label.property": "خاصية",
"label.queries": "استعلامات",
"label.query": "استعلام",
- "label.query-parameters": "متغيرات الرابط",
+ "label.query-parameters": "معاملات الاستعلام",
"label.realtime": "الوقت الفعلي",
+ "label.referral": "إحالة",
"label.referrer": "المرجع",
"label.referrers": "التحويلات",
"label.refresh": "تحديث",
"label.regenerate": "إعادة توليد",
"label.region": "المنطقة",
"label.regions": "المناطق",
+ "label.remaining": "متبقي",
"label.remove": "أزِل",
"label.remove-member": "احذف عضو",
"label.reports": "التقارير",
@@ -161,9 +210,8 @@
"label.reset-website": "اعادة تعيين الإحصائيات",
"label.retention": "الاحتفاظ",
"label.retention-description": "قس مدى ثبات موقعك على الويب من خلال تتبع عدد مرات عودة المستخدمين.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "الإيرادات",
+ "label.revenue-description": "قم بإلقاء نظرة على بيانات إيراداتك وكيفية إنفاق المستخدمين.",
"label.role": "الصلاحية",
"label.run-query": "شغّل الاستعلام",
"label.save": "حفظ",
@@ -171,26 +219,35 @@
"label.search": "بحث",
"label.select": "اختر",
"label.select-date": "حدد التاريخ",
+ "label.select-filter": "اختر تصفية",
"label.select-role": "حدد الدور",
"label.select-website": "حدد موقع",
- "label.session": "Session",
+ "label.session": "الزيارة",
+ "label.session-data": "بيانات الجلسة",
"label.sessions": "الزيارات",
"label.settings": "الإعدادات",
+ "label.share": "مشاركة",
"label.share-url": "مشاركة الرابط",
"label.single-day": "يوم واحد",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
+ "label.sms": "SMS",
+ "label.sources": "مصادر",
+ "label.start-step": "الخطوة الأولى",
+ "label.steps": "الخطوات",
"label.sum": "المجموع",
"label.tablet": "تابلت",
+ "label.tag": "الوسم",
+ "label.tags": "الوسوم",
"label.team": "الفريق",
"label.team-id": "معرّف الفريق",
- "label.team-manager": "Team manager",
+ "label.team-manager": "مدير الفريق",
"label.team-member": "عضو الفريق",
"label.team-name": "اسم الفريق",
"label.team-owner": "مدير الفريق",
- "label.team-view-only": "Team view only",
+ "label.team-settings": "إعدادات الفريق",
+ "label.team-view-only": "عرض الفريق فقط",
"label.team-websites": "مواقع الفريق",
"label.teams": "الفرق",
+ "label.terms": "مصطلحات",
"label.theme": "السمة",
"label.this-month": "الشهر الحالي",
"label.this-week": "الاسبوع الحالي",
@@ -202,42 +259,41 @@
"label.total": "الإجمالي",
"label.total-records": "إجمالي السجلات",
"label.tracking-code": "كود التتبع",
- "label.transactions": "Transactions",
- "label.transfer": "Transfer",
+ "label.transactions": "المعاملات",
+ "label.transfer": "نقل",
"label.transfer-website": "انقل الموقع",
"label.true": "حقيقي",
"label.type": "النوع",
"label.unique": "فريد",
"label.unique-visitors": "زائرون فريدون",
- "label.uniqueCustomers": "Unique Customers",
+ "label.uniqueCustomers": "العملاء الفريدون",
"label.unknown": "غير معروف",
"label.untitled": "بدون عنوان",
- "label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
+ "label.update": "تحديث",
"label.user": "المستخدم",
- "label.user-property": "User Property",
"label.username": "اسم المستخدم",
"label.users": "المستخدمين",
"label.utm": "UTM",
- "label.utm-description": "Track your campaigns through UTM parameters.",
+ "label.utm-description": "تابع حملاتك التسويقية باستخدام معلمات UTM.",
"label.value": "القيمة",
"label.view": "عرض",
"label.view-details": "عرض التفاصيل",
"label.view-only": "عرض فقط",
"label.views": "المشاهدات",
- "label.views-per-visit": "Views per visit",
+ "label.views-per-visit": "مشاهدات لكل زيارة",
"label.visit-duration": "متوسط وقت الزيارة",
"label.visitors": "الزوار",
- "label.visits": "Visits",
+ "label.visits": "الزيارات",
"label.website": "الموقع",
"label.website-id": "معرّف الموقع",
"label.websites": "المواقع",
"label.window": "النافذة",
"label.yesterday": "الأمس",
+ "label.behavior": "السلوك",
"message.action-confirmation": "اكتب {confirmation} في المربع أدناه للتأكيد.",
"message.active-users": "{x} حاليا {x, plural, one {زائر واحد} other {زوار}}",
- "message.collected-data": "Collected data",
+ "message.bad-request": "Bad request",
+ "message.collected-data": "البيانات المجمعة",
"message.confirm-delete": "هل أنت متأكد من حذف {target}?",
"message.confirm-leave": "هل أنت متأكد من مغادرة {target}?",
"message.confirm-remove": "هل انت متأكد من حذف {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "سيتم حذف كافة بيانات الموقع.",
"message.error": "حدث خطأ ما.",
"message.event-log": "{event} في {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "الذهاب إلى الإعدادات",
"message.incorrect-username-password": "اسم المستخدم او كلمة المرور غير صحيحة.",
"message.invalid-domain": "النطاق غير صحيح",
@@ -259,11 +316,14 @@
"message.no-teams": "لم تنشِئ اي فرق.",
"message.no-users": "لا يوجد مستخدمين.",
"message.no-websites-configured": "لم تقم بإعداد اي موقع.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "الصفحة غير موجودة.",
"message.reset-website": "لإعادة ضبط موقع الويب هذا، اكتب {confirmation} في المربع أدناه للتأكيد.",
- "message.reset-website-warning": "سيتم اعادة تعيين كافة الإحصائيات لهذا الموقع، لكن لن يتم تعيير كود التتبع",
+ "message.reset-website-warning": "سيتم اعادة تعيين كافة الإحصائيات لهذا الموقع، لكن لن يتم تغيير كود التتبع",
"message.saved": "تم الحفظ بنجاح.",
- "message.share-url": "هذا الرابط الذي تم مشاركته بشكل عام لـ {target}.",
+ "message.sever-error": "Server error",
+ "message.share-url": "إحصائيات موقعك متاحة للجميع على الرابط التالي:",
"message.team-already-member": "أنت عضو في الفريق",
"message.team-not-found": "لم يتم العثور على الفريق",
"message.team-websites-info": "يمكن مشاهدة الموقع من اي عضو في الفريق.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "اختر الفريق الذي تريد نقل الموقع إليه.",
"message.transfer-website": "نقل ملكية الموقع لحسابك أو فريق أخر.",
"message.triggered-event": "أُطلق الحدث",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "تم حذف المستخدم.",
"message.viewed-page": "شوهدت الصفحة",
- "message.visitor-log": "زائر من {country} يستخدم {browser} على {os} {device}",
- "message.visitors-dropped-off": "أنخفض عدد الزوار"
+ "message.visitor-log": "زائر من {country} يستخدم {browser} على {os} {device}"
}
diff --git a/src/lang/be-BY.json b/src/lang/be-BY.json
index b417afaa..1a866a92 100644
--- a/src/lang/be-BY.json
+++ b/src/lang/be-BY.json
@@ -3,32 +3,47 @@
"label.actions": "Дзеянні",
"label.activity": "Журнал актыўнасці",
"label.add": "Дадаць",
+ "label.add-board": "Дадаць дошку",
"label.add-description": "Дадаць апісанне",
"label.add-member": "Дадаць удзельніка",
"label.add-step": "Дадаць крок",
"label.add-website": "Дадаць сайт",
"label.admin": "Адміністратар",
+ "label.affiliate": "Партнёр",
"label.after": "Пасля",
"label.all": "Усё",
"label.all-time": "Увесь час",
"label.analytics": "Аналітыка",
+ "label.apply": "Ужыць",
+ "label.attribution": "Атрыбуцыя",
+ "label.attribution-description": "Глядзіце, як карыстальнікі ўзаемадзейнічаюць з вашым маркетынгам і што прыводзіць да канверсій.",
"label.average": "Сярэдняе",
"label.back": "Назад",
"label.before": "Да",
+ "label.behavior": "Паводзіны",
+ "label.boards": "Дошкі",
"label.bounce-rate": "Паказчык адмоваў",
"label.breakdown": "Разбіўка",
"label.browser": "Браўзер",
"label.browsers": "Браўзеры",
+ "label.campaigns": "Кампаніі",
"label.cancel": "Адмена",
"label.change-password": "Змяніць пароль",
+ "label.channels": "Каналы",
"label.cities": "Гарады",
"label.city": "Горад",
"label.clear-all": "Ачысціць усё",
+ "label.cohort": "Кагорта",
"label.compare": "Параўнаць",
+ "label.compare-dates": "Параўнаць даты",
"label.confirm": "Падцвердзіць",
"label.confirm-password": "Падцвердзіць пароль",
"label.contains": "Уключае",
+ "label.content": "Змест",
"label.continue": "Працягнуць",
+ "label.conversion": "Канверсія",
+ "label.conversion-rate": "Канверсійная стаўка",
+ "label.conversion-step": "Крок канверсіі",
"label.count": "Колькасць",
"label.countries": "Краіны",
"label.country": "Краіна",
@@ -38,6 +53,7 @@
"label.create-user": "Стварыць карыстальніка",
"label.created": "Створана",
"label.created-by": "Створана",
+ "label.currency": "Валюта",
"label.current": "Цяперашні",
"label.current-password": "Цяперашні пароль",
"label.custom-range": "Іншы дыяпазон",
@@ -57,19 +73,26 @@
"label.details": "Дэталі",
"label.device": "Прылада",
"label.devices": "Прылады",
+ "label.direct": "Прама",
"label.dismiss": "Адхіліць",
+ "label.distinct-id": "Унікальны ID",
"label.does-not-contain": "Не ўключае",
+ "label.does-not-include": "Не ўключае",
+ "label.doest-not-exist": "Не існуе",
"label.domain": "Дамен",
"label.dropoff": "Адмовы",
"label.edit": "Змяніць",
"label.edit-dashboard": "Змяніць інфармацыйную панэль",
"label.edit-member": "Рэдагаваць удзельніка",
+ "label.email": "Email",
"label.enable-share-url": "Дазволіць дзяліцца спасылкай",
"label.end-step": "Канчатковы крок",
"label.entry": "URL уваходу",
"label.event": "Падзея",
"label.event-data": "Дадзеныя падзеі",
+ "label.event-name": "Назва падзеі",
"label.events": "Падзеі",
+ "label.exists": "Існуе",
"label.exit": "URL выхаду",
"label.false": "Ложна",
"label.field": "Поле",
@@ -78,29 +101,37 @@
"label.filter-combined": "Камбініраваны",
"label.filter-raw": "Сырыя",
"label.filters": "Фільтры",
- "label.first-seen": "First seen",
+ "label.first-click": "Першы клік",
+ "label.first-seen": "Першы раз убачана",
"label.funnel": "Варонка",
"label.funnel-description": "Разумець паказчыкі канверсіі і адмоваў.",
+ "label.funnels": "Варонкі",
"label.goal": "Мэта",
"label.goals": "Мэты",
"label.goals-description": "Сачыць за мэтамі па праглядах старонак і падзеях.",
"label.greater-than": "Больш чым",
"label.greater-than-equals": "Больш чым або роўна",
- "label.host": "Хост",
- "label.hosts": "Хасты",
+ "label.grouped": "Групаваны",
+ "label.hostname": "Імя хаста",
+ "label.includes": "Уключае",
+ "label.insight": "Інсайт",
"label.insights": "Інсайты",
"label.insights-description": "Даследваць дадзеныя з дапамогай сегментаў і фільтраў.",
"label.is": "З'яўляецца",
+ "label.is-false": "Ложна",
"label.is-not": "Не з'яўляецца",
"label.is-not-set": "Не ўстаноўлена",
"label.is-set": "Устаноўлена",
+ "label.is-true": "Праўда",
"label.join": "Далучыцца",
"label.join-team": "Далучыцца да каманды",
"label.journey": "Маршрут карыстальніка",
"label.journey-description": "Разумець як карыстальнікі навігуюць па сайце.",
+ "label.journeys": "Маршруты",
"label.language": "Мова",
"label.languages": "Мовы",
"label.laptop": "Ноўтбук",
+ "label.last-click": "Апошні клік",
"label.last-days": "Апошнія {x} дзён",
"label.last-hours": "Апошнія {x} гадзіны",
"label.last-months": "Апошнія {x} месяцаў",
@@ -109,15 +140,19 @@
"label.leave-team": "Пакінуць каманду",
"label.less-than": "Менш чым",
"label.less-than-equals": "Менш чым або роўна",
+ "label.links": "Спасылкі",
"label.login": "Увайсці",
"label.logout": "Выйсці",
"label.manage": "Кіраваць",
"label.manager": "Кіраўнік",
"label.max": "Максімум",
+ "label.maximize": "Разгарнуць",
+ "label.medium": "Сярэдні",
"label.member": "Удзельнік",
"label.members": "Удзельнікі",
"label.min": "Мінімум",
"label.mobile": "Мабільны",
+ "label.model": "Мадэль",
"label.more": "Болей",
"label.my-account": "Мой уліковы запіс",
"label.my-websites": "Мае сайты",
@@ -126,33 +161,48 @@
"label.none": "Няма",
"label.number-of-records": "{x} {x, plural, one {запіс} other {запісаў}}",
"label.ok": "ОК",
+ "label.online": "Online",
+ "label.organic-search": "Арганічны пошук",
+ "label.organic-shopping": "Арганічныя пакупкі",
+ "label.organic-social": "Арганічныя сацыяльныя сеткі",
+ "label.organic-video": "Арганічнае відэа",
"label.os": "Аперацыйная сістэма",
+ "label.other": "Іншае",
"label.overview": "Агляд",
"label.owner": "Уласнік",
+ "label.page": "Старонка",
"label.page-of": "Старонка {current} з {total}",
"label.page-views": "Прагляды старонкі",
"label.pageTitle": "Загаловак старонкі",
"label.pages": "Старонкі",
+ "label.paid-ads": "Платная рэклама",
+ "label.paid-search": "Платаны пошук",
+ "label.paid-shopping": "Платныя пакупкі",
+ "label.paid-social": "Платныя сацыяльныя сеткі",
+ "label.paid-video": "Платнае відэа",
"label.password": "Пароль",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Шлях",
+ "label.paths": "Шляхи",
+ "label.pixels": "Пікселі",
"label.powered-by": "Зроблена {name}",
"label.previous": "Папярэдні",
"label.previous-period": "Папярэдні перыяд",
"label.previous-year": "Папярэдні год",
"label.profile": "Профіль",
- "label.properties": "Properties",
+ "label.properties": "Уласцівасці",
"label.property": "Уласцівасць",
"label.queries": "Запыты",
"label.query": "Запыт",
"label.query-parameters": "Параметры запыту",
"label.realtime": "У рэяльным часе",
+ "label.referral": "Рэферал",
"label.referrer": "Рэферэр",
"label.referrers": "Рэферэры",
"label.refresh": "Аднавіць",
"label.regenerate": "Рэгенераваць",
"label.region": "Рэгіён",
"label.regions": "Рэгіёны",
+ "label.remaining": "Засталося",
"label.remove": "Выдаліць",
"label.remove-member": "Выдаліць удзельніка",
"label.reports": "Справаздачы",
@@ -163,7 +213,6 @@
"label.retention-description": "Ацаніць прыцягальнасць сайта, адсочваючы павяртанні карыстальнікаў.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
"label.role": "Роля",
"label.run-query": "Запусціць запыт",
"label.save": "Захаваць",
@@ -171,26 +220,35 @@
"label.search": "Пошук",
"label.select": "Выбраць",
"label.select-date": "Выбраць дату",
+ "label.select-filter": "Выбраць фільтр",
"label.select-role": "Выбраць ролю",
"label.select-website": "Выбраць сайт",
- "label.session": "Session",
+ "label.session": "Сесія",
+ "label.session-data": "Дадзеныя сесіі",
"label.sessions": "Сесіі",
"label.settings": "Налады",
+ "label.share": "Падзяліцца",
"label.share-url": "Падзяліцца спасылкай",
"label.single-day": "Адзін дзень",
+ "label.sms": "SMS",
+ "label.sources": "Крыніцы",
"label.start-step": "Першы кроку",
"label.steps": "Крокі",
"label.sum": "Сума",
"label.tablet": "Планшэт",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "Каманда",
"label.team-id": "Ідэнтыфікатар каманды",
"label.team-manager": "Кіраўнік каманды",
"label.team-member": "Удзельнік каманды",
"label.team-name": "Назва каманды",
"label.team-owner": "Уласнік каманды",
+ "label.team-settings": "Налады каманды",
"label.team-view-only": "Толькі для каманднага прагляду",
"label.team-websites": "Сайты каманды",
"label.teams": "Каманды",
+ "label.terms": "Тэрміны",
"label.theme": "Тэма",
"label.this-month": "Гэты месяц",
"label.this-week": "Гэты тыдзень",
@@ -213,10 +271,7 @@
"label.unknown": "Невядома",
"label.untitled": "Без назвы",
"label.update": "Абнавіць",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Карыстальнік",
- "label.user-property": "User Property",
"label.username": "Імя карыстальніка",
"label.users": "Карыстальнікі",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Учора",
"message.action-confirmation": "Увядзіце {confirmation} у поле ніжэй, каб пацвердзіць.",
"message.active-users": "{x} цякучых {x, plural, one {наведвальнік} other {наведвальнікаў}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Сабраныя дадзеныя",
"message.confirm-delete": "Вы дакладна хочаце выдаліць {target}?",
"message.confirm-leave": "Вы дакладна хочаце пакінуць {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Усе асацыяваныя дадзеныя будуць таксама выдалены.",
"message.error": "Нешта пайшло не так.",
"message.event-log": "{event} на {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Да налад",
"message.incorrect-username-password": "Некарэктнае імя карыстальніка/пароль.",
"message.invalid-domain": "Некарэктны дамен",
@@ -259,10 +316,13 @@
"message.no-teams": "Вы не стварылі ніводнай каманды.",
"message.no-users": "Няма карыстальнікаў.",
"message.no-websites-configured": "Вы не наладзілі ніводнага сайта.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Старонка не знойдзена.",
"message.reset-website": "Каб скінуць гэты сайт, увядзіце {confirmation} у поле ніжэй для пацверджання.",
"message.reset-website-warning": "Уся статыстыка для гэтага сайта будзе выдалена, але код адсочвання будзе працягваць працаваць.",
"message.saved": "Захавана паспяхова.",
+ "message.sever-error": "Server error",
"message.share-url": "Гэта публічная спасылка для {target}.",
"message.team-already-member": "Вы ўжо ўдзельнік каманды.",
"message.team-not-found": "Каманда не знойдзена.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Выберыце каманду для перадачы гэтага сайта.",
"message.transfer-website": "Перадача сайта на ваш уліковы запіс або іншай камандзе.",
"message.triggered-event": "Падзея якая спрацавала",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Карыстальнік выдалены.",
"message.viewed-page": "Праглядзеў старонку",
- "message.visitor-log": "Наведвальнік з {country} праз {browser} на {os} {device}",
- "message.visitors-dropped-off": "Наведвальнікі сышлі"
+ "message.visitor-log": "Наведвальнік з {country} праз {browser} на {os} {device}"
}
diff --git a/src/lang/bg-BG.json b/src/lang/bg-BG.json
index 1fd87de2..4b0effc8 100644
--- a/src/lang/bg-BG.json
+++ b/src/lang/bg-BG.json
@@ -3,33 +3,48 @@
"label.actions": "Действия",
"label.activity": "Активностти",
"label.add": "Добави",
+ "label.add-board": "Добави дъска",
"label.add-description": "Добави описание",
"label.add-member": "Добави член",
"label.add-step": "Добави стъпка",
"label.add-website": "Добави уебсайт",
"label.admin": "Администратор",
+ "label.affiliate": "Партньор",
"label.after": "След",
"label.all": "Всички",
"label.all-time": "За всички времена",
"label.analytics": "Анализи",
+ "label.apply": "Приложи",
+ "label.attribution": "Атрибуция",
+ "label.attribution-description": "Вижте как потребителите взаимодействат с вашия маркетинг и какво води до конверсии.",
"label.average": "Средно",
"label.back": "Назад",
"label.before": "Преди",
+ "label.behavior": "Поведение",
+ "label.boards": "Дъски",
"label.bounce-rate": "Kоефициент на отказ",
"label.breakdown": "Разбивка",
"label.browser": "Браузър",
"label.browsers": "Браузъри",
+ "label.campaigns": "Кампании",
"label.cancel": "Отмени",
"label.change-password": "Смени парола",
+ "label.channels": "Канали",
"label.cities": "Градове",
"label.city": "Град",
"label.clear-all": "Изчисти всички",
- "label.compare": "Compare",
+ "label.cohort": "Кохорта",
+ "label.compare": "Сравни",
+ "label.compare-dates": "Сравни дати",
"label.confirm": "Потвърди",
"label.confirm-password": "Потвърди парола",
"label.contains": "Съдържа",
+ "label.content": "Съдържание",
"label.continue": "Продължи",
- "label.count": "Count",
+ "label.conversion": "Конверсия",
+ "label.conversion-rate": "Процент на конверсия",
+ "label.conversion-step": "Стъпка на конверсия",
+ "label.count": "Брой",
"label.countries": "Държави",
"label.country": "Държава",
"label.create": "Създай",
@@ -38,7 +53,8 @@
"label.create-user": "Създай потребител",
"label.created": "Създадено",
"label.created-by": "Създадено от",
- "label.current": "Current",
+ "label.currency": "Валута",
+ "label.current": "Текущ",
"label.current-password": "Текуща парола",
"label.custom-range": "Обхват",
"label.dashboard": "Табло",
@@ -57,19 +73,26 @@
"label.details": "Детайли",
"label.device": "Устройство",
"label.devices": "Устройства",
+ "label.direct": "Директно",
"label.dismiss": "Отхвърли",
+ "label.distinct-id": "Уникален ID",
"label.does-not-contain": "Не съдържа",
+ "label.does-not-include": "Не включва",
+ "label.doest-not-exist": "Не съществува",
"label.domain": "Домейн",
"label.dropoff": "Отпадане",
"label.edit": "Редактирай",
"label.edit-dashboard": "Редактирай табло",
"label.edit-member": "Редактирай член",
+ "label.email": "Имейл",
"label.enable-share-url": "Активирай Линк за споделяне",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
+ "label.end-step": "Крайна стъпка",
+ "label.entry": "URL на вход",
"label.event": "Събитие",
"label.event-data": "Данни за събитие",
+ "label.event-name": "Име на събитие",
"label.events": "Събития",
+ "label.exists": "Съществува",
"label.exit": "Exit URL",
"label.false": "Грешно",
"label.field": "Поле",
@@ -78,29 +101,37 @@
"label.filter-combined": "Комбиниран",
"label.filter-raw": "Суров",
"label.filters": "Филтри",
- "label.first-seen": "First seen",
+ "label.first-click": "Първо кликване",
+ "label.first-seen": "Първо видяно",
"label.funnel": "Фуния",
"label.funnel-description": "Разберете процента на конверсия и отпадане на потребителите.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
+ "label.funnels": "Фунии",
+ "label.goal": "Цел",
+ "label.goals": "Цели",
+ "label.goals-description": "Следете целите си за прегледи на страници и събития.",
"label.greater-than": "По-голямо от",
"label.greater-than-equals": "По-голямо или равно на",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Групирано",
+ "label.hostname": "Име на хост",
+ "label.includes": "Включва",
+ "label.insight": "Прозрение",
"label.insights": "Изводи",
"label.insights-description": "Навлезте по-дълбоко в данните си, като използвате сегменти и филтри.",
"label.is": "Е",
+ "label.is-false": "Грешно",
"label.is-not": "Не е",
"label.is-not-set": "Не е зададено",
"label.is-set": "Зададено е",
+ "label.is-true": "Вярно",
"label.join": "Присъедини се",
"label.join-team": "Присъедини се към екип",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.journey": "Пътешествие",
+ "label.journey-description": "Разберете как потребителите навигират във вашия уебсайт.",
+ "label.journeys": "Пътешествия",
"label.language": "Език",
"label.languages": "Езици",
"label.laptop": "Лаптоп",
+ "label.last-click": "Последно кликване",
"label.last-days": "Последните {x} дни",
"label.last-hours": "Последните {x} часа",
"label.last-months": "Последните {x} месеца",
@@ -109,15 +140,19 @@
"label.leave-team": "Напусни екип",
"label.less-than": "По-малко от",
"label.less-than-equals": "По-малко или равно на",
+ "label.links": "Връзки",
"label.login": "Вход",
"label.logout": "Изход",
"label.manage": "Управлявай",
- "label.manager": "Manager",
+ "label.manager": "Мениджър",
"label.max": "Максимум",
+ "label.maximize": "Разшири",
+ "label.medium": "Среден",
"label.member": "Член",
"label.members": "Членове",
"label.min": "Минимум",
"label.mobile": "Мобилен",
+ "label.model": "Модел",
"label.more": "Още",
"label.my-account": "Моят акаунт",
"label.my-websites": "Моите уебсайтове",
@@ -126,33 +161,48 @@
"label.none": "Няма",
"label.number-of-records": "{x} {x, plural, one {един} other {други}}",
"label.ok": "Добре",
+ "label.online": "Online",
+ "label.organic-search": "Органично търсене",
+ "label.organic-shopping": "Органично пазаруване",
+ "label.organic-social": "Органични социални мрежи",
+ "label.organic-video": "Органично видео",
"label.os": "ОС",
+ "label.other": "Друго",
"label.overview": "Общ преглед",
"label.owner": "Собственик",
+ "label.page": "Страница",
"label.page-of": "Страница {current} от {total}",
"label.page-views": "Прегледи на страницата",
"label.pageTitle": "Заглавие на страница",
"label.pages": "Страници",
+ "label.paid-ads": "Платени реклами",
+ "label.paid-search": "Платено търсене",
+ "label.paid-shopping": "Платено пазаруване",
+ "label.paid-social": "Платени социални мрежи",
+ "label.paid-video": "Платено видео",
"label.password": "Парола",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Път",
+ "label.paths": "Пътища",
+ "label.pixels": "Пиксели",
"label.powered-by": "Поддържано от {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
"label.previous-year": "Previous year",
"label.profile": "Профил",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Свойства",
+ "label.property": "Свойство",
"label.queries": "Запитвания",
"label.query": "Запитване",
"label.query-parameters": "Параметри на търсене",
"label.realtime": "В реално време",
+ "label.referral": "Реферал",
"label.referrer": "Референт",
"label.referrers": "Референти",
"label.refresh": "Обнови",
"label.regenerate": "Регенерирай",
"label.region": "Регион",
"label.regions": "Региони",
+ "label.remaining": "Оставащи",
"label.remove": "Премахни",
"label.remove-member": "Премахни член",
"label.reports": "Отчети",
@@ -163,7 +213,6 @@
"label.retention-description": "Измерете привързаността към вашия уебсайт, като проследявате колко често потребителите се връщат.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
"label.role": "Роля",
"label.run-query": "Изпълни запитване",
"label.save": "Запази",
@@ -171,26 +220,35 @@
"label.search": "Търсене",
"label.select": "Избери",
"label.select-date": "Избери дата",
+ "label.select-filter": "Избери филтър",
"label.select-role": "Избери роля",
"label.select-website": "Избери уебсайт",
- "label.session": "Session",
+ "label.session": "Сесия",
+ "label.session-data": "Данни за сесия",
"label.sessions": "Сесии",
"label.settings": "Настройки",
+ "label.share": "Сподели",
"label.share-url": "Сподели Линк",
"label.single-day": "Един ден",
- "label.start-step": "Start Step",
+ "label.sms": "SMS",
+ "label.sources": "Източници",
+ "label.start-step": "Начална стъпка",
"label.steps": "Стъпки",
"label.sum": "Сума",
"label.tablet": "Таблет",
+ "label.tag": "Етикет",
+ "label.tags": "Етикети",
"label.team": "Екип",
"label.team-id": "ID на екип",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Мениджър на екип",
"label.team-member": "Член на екипа",
"label.team-name": "Име на екипа",
"label.team-owner": "Собственик на екипа",
+ "label.team-settings": "Настройки на екипа",
"label.team-view-only": "Видимо само за членове на екипа",
"label.team-websites": "Уебсайтове на екипа",
"label.teams": "Екипи",
+ "label.terms": "Термини",
"label.theme": "Тема",
"label.this-month": "Този месец",
"label.this-week": "Тази седмица",
@@ -213,10 +271,7 @@
"label.unknown": "Неизвестен",
"label.untitled": "Без заглавие",
"label.update": "Актуализирай",
- "label.url": "URL адрес",
- "label.urls": "URL адреси",
"label.user": "Потребител",
- "label.user-property": "User Property",
"label.username": "Потребителско име",
"label.users": "Потребители",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Вчера",
"message.action-confirmation": "Въведете {confirmation} в полето по-долу, за да потвърдите.",
"message.active-users": "{x} {x, plural, one {активен един} other {активни други}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Сигурни ли сте, че искате да изтриете {target}?",
"message.confirm-leave": "Сигурни ли сте, че искате да напуснете {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Всички данни за уебсайта ще бъдат изтрити.",
"message.error": "Възникна грешка.",
"message.event-log": "{event} на {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Отидете в настройките",
"message.incorrect-username-password": "Неправилно потребителско име и/или парола.",
"message.invalid-domain": "Невалиден домейн. Не включвайте http/https.",
@@ -259,10 +316,13 @@
"message.no-teams": "Няма създадени екипи.",
"message.no-users": "Няма потребители.",
"message.no-websites-configured": "Нямате конфигурирани уебсайтове.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Страницата не е намерена",
"message.reset-website": "За да нулирате този уебсайт, въведете {confirmation} в полето по-долу, за да потвърдите.",
"message.reset-website-warning": "Всички статистически данни за този уебсайт ще бъдат изтрити, но вашите настройки ще останат непроменени.",
"message.saved": "Запазено.",
+ "message.sever-error": "Server error",
"message.share-url": "Статистиката за вашия уебсайт е публично достъпна на следния URL адрес:",
"message.team-already-member": "Вече сте член на екипа.",
"message.team-not-found": "Екипът не е намерен.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Изберете екипът на който да бъде прехвърлен уебсайта.",
"message.transfer-website": "Прехвърли собствеността на уебсайта към вашия акаунт или към друг екип.",
"message.triggered-event": "Активирано събитие",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Потребителят е изтрит.",
"message.viewed-page": "Страницата е видяна",
- "message.visitor-log": "Посетител от {country}, използващ {browser} на {os} {device}",
- "message.visitors-dropped-off": "Спад на посетителите"
+ "message.visitor-log": "Посетител от {country}, използващ {browser} на {os} {device}"
}
diff --git a/src/lang/bn-BD.json b/src/lang/bn-BD.json
index 7a22d76c..9b9ad2f4 100644
--- a/src/lang/bn-BD.json
+++ b/src/lang/bn-BD.json
@@ -3,33 +3,48 @@
"label.actions": "অ্যাকশনস",
"label.activity": "একটিভিটি দেখুন",
"label.add": "যুক্ত করুন",
+ "label.add-board": "বোর্ড যুক্ত করুন",
"label.add-description": "বর্ননা যোগ করুন",
"label.add-member": "সদস্য যোগ করুন",
"label.add-step": "পদ যোগ করুন",
"label.add-website": "ওয়েবসাইট যুক্ত করুন",
"label.admin": "অ্যাডমিন",
+ "label.affiliate": "সহযোগী",
"label.after": "পরে",
"label.all": "সবগুলো",
"label.all-time": "সব সময়",
- "label.analytics": "Analytics",
- "label.average": "Average",
+ "label.analytics": "বিশ্লেষণ",
+ "label.apply": "প্রয়োগ করুন",
+ "label.attribution": "অ্যাট্রিবিউশন",
+ "label.attribution-description": "দেখুন ব্যবহারকারীরা কীভাবে আপনার মার্কেটিংয়ের সাথে যুক্ত হয় এবং কীভাবে রূপান্তর ঘটে।",
+ "label.average": "গড়",
"label.back": "পেছনে",
"label.before": "পূর্বে",
+ "label.behavior": "আচরণ",
+ "label.boards": "বোর্ডসমূহ",
"label.bounce-rate": "উপরে উঠার হার",
"label.breakdown": "ভাঙ্গন",
"label.browser": "ব্রাউজার",
"label.browsers": "ব্রাউজার সমূহ",
+ "label.campaigns": "প্রচারণা",
"label.cancel": "বাতিল",
"label.change-password": "পাসওয়ার্ড পরিবর্তন করুন",
+ "label.channels": "চ্যানেলসমূহ",
"label.cities": "শহরসমূহ",
"label.city": "শহর",
"label.clear-all": "সব মুছে ফেলুন",
- "label.compare": "Compare",
+ "label.cohort": "কোহর্ট",
+ "label.compare": "তুলনা করুন",
+ "label.compare-dates": "তারিখ তুলনা করুন",
"label.confirm": "নিশ্চিত করুন",
"label.confirm-password": "পাসওয়ার্ড নিশ্চিত করুন",
"label.contains": "রয়েছে",
+ "label.content": "বিষয়বস্তু",
"label.continue": "পরবর্তিতে",
- "label.count": "Count",
+ "label.conversion": "রূপান্তর",
+ "label.conversion-rate": "রূপান্তর হার",
+ "label.conversion-step": "রূপান্তর ধাপ",
+ "label.count": "গণনা",
"label.countries": "দেশসমূহ",
"label.country": "দেশ",
"label.create": "তৈরি করুন",
@@ -37,122 +52,157 @@
"label.create-team": "দল তৈরি করুন",
"label.create-user": "ব্যবহারকারী তৈরি করুন",
"label.created": "তৈরি করা হয়েছে",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.created-by": "তৈরি করেছেন",
+ "label.currency": "মুদ্রা",
+ "label.current": "বর্তমান",
"label.current-password": "বর্তমান পাসওয়ার্ড",
"label.custom-range": "কাস্টম রেঞ্জ",
"label.dashboard": "ড্যাশবোর্ড",
- "label.data": "Data",
- "label.date": "Date",
+ "label.data": "ডেটা",
+ "label.date": "তারিখ",
"label.date-range": "তারিখের পরিসীমা",
- "label.day": "Day",
+ "label.day": "দিন",
"label.default-date-range": "ডিফল্ট তারিখের পরিসীমা",
"label.delete": "মুছে ফেলুন",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "রিপোর্ট মুছুন",
+ "label.delete-team": "দল মুছুন",
+ "label.delete-user": "ব্যবহারকারী মুছুন",
"label.delete-website": "ওয়েবসাইট মুছুন",
- "label.description": "Description",
+ "label.description": "বর্ণনা",
"label.desktop": "ডেস্কটপ",
- "label.details": "Details",
- "label.device": "Device",
+ "label.details": "বিস্তারিত",
+ "label.device": "ডিভাইস",
"label.devices": "ডিভাইস গুলো",
+ "label.direct": "সরাসরি",
"label.dismiss": "বাতিল",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "স্বতন্ত্র আইডি",
+ "label.does-not-contain": "ধারণ করে না",
+ "label.does-not-include": "অন্তর্ভুক্ত নয়",
+ "label.doest-not-exist": "অস্তিত্ব নেই",
"label.domain": "ডোমেইন",
- "label.dropoff": "Dropoff",
+ "label.dropoff": "ছেড়ে যাওয়া",
"label.edit": "সম্পাদনা করুন",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
+ "label.edit-dashboard": "ড্যাশবোর্ড সম্পাদনা করুন",
+ "label.edit-member": "সদস্য সম্পাদনা করুন",
+ "label.email": "Email",
"label.enable-share-url": "শেয়ার ইউআরএল শেয়ার করুন",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event data",
+ "label.end-step": "শেষ ধাপ",
+ "label.entry": "প্রবেশ URL",
+ "label.event": "ইভেন্ট",
+ "label.event-data": "ইভেন্ট ডেটা",
+ "label.event-name": "ইভেন্টের নাম",
"label.events": "ঘটনা",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
- "label.filter": "Filter",
+ "label.exists": "অস্তিত্ব আছে",
+ "label.exit": "প্রস্থান URL",
+ "label.false": "মিথ্যা",
+ "label.field": "ক্ষেত্র",
+ "label.fields": "ক্ষেত্রসমূহ",
+ "label.filter": "ফিল্টার",
"label.filter-combined": "সম্মিলিত",
"label.filter-raw": "অপরিশোধিত",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.filters": "ফিল্টারসমূহ",
+ "label.first-click": "প্রথম ক্লিক",
+ "label.first-seen": "প্রথম দেখা",
+ "label.funnel": "ফানেল",
+ "label.funnel-description": "ব্যবহারকারীদের রূপান্তর ও ছেড়ে যাওয়ার হার বুঝুন।",
+ "label.funnels": "ফানেলসমূহ",
+ "label.goal": "লক্ষ্য",
+ "label.goals": "লক্ষ্যসমূহ",
+ "label.goals-description": "পৃষ্ঠাদর্শন ও ইভেন্টের লক্ষ্য ট্র্যাক করুন।",
+ "label.greater-than": "এর চেয়ে বেশি",
+ "label.greater-than-equals": "এর চেয়ে বেশি বা সমান",
+ "label.grouped": "গ্রুপ করা",
+ "label.hostname": "হোস্টনেম",
+ "label.includes": "অন্তর্ভুক্ত",
+ "label.insight": "অন্তর্দৃষ্টি",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.is": "হয়",
+ "label.is-false": "মিথ্যা",
+ "label.is-not": "নয়",
+ "label.is-not-set": "নির্ধারিত নয়",
+ "label.is-set": "নির্ধারিত",
+ "label.is-true": "সত্য",
+ "label.join": "যোগ দিন",
+ "label.join-team": "দলে যোগ দিন",
+ "label.journey": "যাত্রা",
+ "label.journey-description": "ব্যবহারকারীরা কীভাবে আপনার ওয়েবসাইটে নেভিগেট করে তা বুঝুন।",
+ "label.journeys": "যাত্রাসমূহ",
"label.language": "ভাষা",
"label.languages": "ভাষা",
"label.laptop": "ল্যাপটপ",
+ "label.last-click": "শেষ ক্লিক",
"label.last-days": "শেষ {x} দিন",
"label.last-hours": "শেষ {x} ঘন্টা",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "শেষ {x} মাস",
+ "label.last-seen": "শেষ দেখা",
+ "label.leave": "ত্যাগ করুন",
+ "label.leave-team": "দল ত্যাগ করুন",
+ "label.less-than": "এর চেয়ে কম",
+ "label.less-than-equals": "এর চেয়ে কম বা সমান",
+ "label.links": "লিঙ্কসমূহ",
"label.login": "লগিন",
"label.logout": "লগ আউট",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
+ "label.manage": "পরিচালনা করুন",
+ "label.manager": "পরিচালক",
+ "label.max": "সর্বাধিক",
+ "label.maximize": "বিস্তৃত করুন",
+ "label.medium": "মাঝারি",
+ "label.member": "সদস্য",
+ "label.members": "সদস্যগণ",
+ "label.min": "সর্বনিম্ন",
"label.mobile": "মুঠোফোন",
+ "label.model": "মডেল",
"label.more": "আরও",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "আমার অ্যাকাউন্ট",
+ "label.my-websites": "আমার ওয়েবসাইটসমূহ",
"label.name": "নাম",
"label.new-password": "নতুন পাসওয়ার্ড",
"label.none": "কিছুই না",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "অর্গানিক সার্চ",
+ "label.organic-shopping": "অর্গানিক শপিং",
+ "label.organic-social": "অর্গানিক সোশ্যাল",
+ "label.organic-video": "অর্গানিক ভিডিও",
"label.os": "OS",
+ "label.other": "অন্যান্য",
"label.overview": "Overview",
"label.owner": "মালিক",
+ "label.page": "পৃষ্ঠা",
"label.page-of": "Page {current} of {total}",
"label.page-views": "পৃষ্ঠা পরিদর্শন গুলো",
"label.pageTitle": "Page title",
"label.pages": "পৃষ্ঠাগুলি",
+ "label.paid-ads": "পেইড বিজ্ঞাপন",
+ "label.paid-search": "পেইড সার্চ",
+ "label.paid-shopping": "পেইড শপিং",
+ "label.paid-social": "পেইড সোশ্যাল",
+ "label.paid-video": "পেইড ভিডিও",
"label.password": "পাসওয়ার্ড",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "পথ",
+ "label.paths": "পথসমূহ",
+ "label.pixels": "পিক্সেল",
"label.powered-by": "{name} দ্বারা চালিত",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "পূর্ববর্তী",
+ "label.previous-period": "পূর্ববর্তী সময়কাল",
+ "label.previous-year": "গত বছর",
"label.profile": "প্রোফাইল",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "বৈশিষ্ট্যসমূহ",
+ "label.property": "বৈশিষ্ট্য",
"label.queries": "Queries",
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.realtime": "সরাসরি",
+ "label.referral": "রেফারেল",
"label.referrer": "Referrer",
"label.referrers": "রেফারার্স",
"label.refresh": "রিফ্রেশ",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "বাকি আছে",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -161,9 +211,8 @@
"label.reset-website": "ওয়েবসাইট রিসেট করুন",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "আয়",
+ "label.revenue-description": "সময়ের সাথে সাথে আপনার আয় দেখুন।",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "সংরক্ষণ",
@@ -171,26 +220,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "ফিল্টার নির্বাচন করুন",
"label.select-role": "Select role",
"label.select-website": "Select website",
- "label.session": "Session",
+ "label.session": "সেশন",
+ "label.session-data": "সেশন ডেটা",
"label.sessions": "Sessions",
"label.settings": "সেটিংস",
- "label.share-url": "ইউআরএল শেয়ার করুন",
+ "label.share": "শেয়ার করুন",
+ "label.share-url": "এটি {target} এর জন্য প্রকাশ্যে শেয়ার করার ইউআরএল।",
"label.single-day": "একদিন",
+ "label.sms": "SMS",
+ "label.sources": "উৎসসমূহ",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "ট্যাবলেট",
- "label.team": "Team",
- "label.team-id": "Team ID",
- "label.team-manager": "Team manager",
- "label.team-member": "Team member",
- "label.team-name": "Team name",
- "label.team-owner": "Team owner",
+ "label.tag": "ট্যাগ",
+ "label.tags": "ট্যাগসমূহ",
+ "label.team": "দল",
+ "label.team-id": "দল আইডি",
+ "label.team-manager": "দল ব্যবস্থাপক",
+ "label.team-member": "দলের সদস্য",
+ "label.team-name": "দলের নাম",
+ "label.team-owner": "দলের মালিক",
+ "label.team-settings": "দলের সেটিংস",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "শর্তাবলী",
"label.theme": "থিম",
"label.this-month": "এই মাস",
"label.this-week": "এই সপ্তাহ",
@@ -213,10 +271,7 @@
"label.unknown": "অজানা",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "ব্যবহারকারীর নাম",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} বর্তমান {x, plural, one {visitor} other {visitors}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "আপনি কি নিশ্চিত যে আপনি {target} মুছতে চান?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "সমস্ত সম্পর্কিত ডেটা পাশাপাশি মুছে ফেলা হবে।",
"message.error": "কিছু ভুল হয়েছে।",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "সেটিংস এ যান",
"message.incorrect-username-password": "ভুল ব্যবহারকারীর নাম/পাসওয়ার্ড।",
"message.invalid-domain": "ভুল ডোমেন",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "কোনও ওয়েবসাইট কনফিগার করা নেই।",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "পৃষ্ঠা খুঁজে পাওয়া যায়নি।",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "এই ওয়েবসাইটের সমস্ত পরিসংখ্যান মুছে ফেলা হবে, তবে আপনার ট্র্যাকিং কোডটি অক্ষত থাকবে।",
"message.saved": "সংরক্ষিত হয়েছে।",
+ "message.sever-error": "Server error",
"message.share-url": "এটি {target} এর জন্য প্রকাশ্যে শেয়ার করার ইউআরএল।",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "{country} থেকে একজন ভিসিটর {ব্রাউজার}, ব্যবহার করছেন {os} {device} এর মধ্যে।",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "{country} থেকে একজন ভিসিটর {ব্রাউজার}, ব্যবহার করছেন {os} {device} এর মধ্যে।"
}
diff --git a/src/lang/bs-BA.json b/src/lang/bs-BA.json
index b9d9e8a8..56848771 100644
--- a/src/lang/bs-BA.json
+++ b/src/lang/bs-BA.json
@@ -3,33 +3,48 @@
"label.actions": "Akcije",
"label.activity": "Log aktivnosti",
"label.add": "Dodaj",
+ "label.add-board": "Dodaj ploču",
"label.add-description": "Dodaj opis",
"label.add-member": "Dodaj člana",
"label.add-step": "Dodaj korak",
"label.add-website": "Dodaj web stranicu",
"label.admin": "Administrator",
+ "label.affiliate": "Partner",
"label.after": "Nakon",
"label.all": "Sve",
"label.all-time": "Cijelo vrijeme",
"label.analytics": "Analitike",
+ "label.apply": "Primijeni",
+ "label.attribution": "Atribucija",
+ "label.attribution-description": "Pogledajte kako korisnici komuniciraju s vašim marketingom i šta dovodi do konverzija.",
"label.average": "Prosjek",
"label.back": "Nazad",
"label.before": "Prije",
- "label.bounce-rate": "Bounce rate",
+ "label.behavior": "Ponašanje",
+ "label.boards": "Ploče",
+ "label.bounce-rate": "Stopa napuštanja",
"label.breakdown": "Pregled po kategorijama",
"label.browser": "Browser",
"label.browsers": "Browseri",
+ "label.campaigns": "Kampanje",
"label.cancel": "Otkaži",
"label.change-password": "Promijeni šifru",
+ "label.channels": "Kanali",
"label.cities": "Gradovi",
"label.city": "Grad",
"label.clear-all": "Očisti sve",
- "label.compare": "Compare",
+ "label.cohort": "Kohorta",
+ "label.compare": "Uporedi",
+ "label.compare-dates": "Uporedi datume",
"label.confirm": "Potvrdi",
"label.confirm-password": "Potvrdi šifru",
"label.contains": "Sadrži",
+ "label.content": "Sadržaj",
"label.continue": "Nastavi",
- "label.count": "Count",
+ "label.conversion": "Konverzija",
+ "label.conversion-rate": "Stopa konverzije",
+ "label.conversion-step": "Korak konverzije",
+ "label.count": "Broj",
"label.countries": "Zemlje",
"label.country": "Zemlja",
"label.create": "Kreiraj",
@@ -38,7 +53,8 @@
"label.create-user": "Kreiraj korisnika",
"label.created": "Kreiraj",
"label.created-by": "Kreirao",
- "label.current": "Current",
+ "label.currency": "Valuta",
+ "label.current": "Trenutno",
"label.current-password": "Trenutna šifra",
"label.custom-range": "Proizvoljni raspon",
"label.dashboard": "Dashboard",
@@ -57,19 +73,26 @@
"label.details": "Detalji",
"label.device": "Uređaj",
"label.devices": "Uređaji",
+ "label.direct": "Direktno",
"label.dismiss": "Odbaci",
+ "label.distinct-id": "Jedinstveni ID",
"label.does-not-contain": "Ne sadrži",
+ "label.does-not-include": "Ne uključuje",
+ "label.doest-not-exist": "Ne postoji",
"label.domain": "Domena",
- "label.dropoff": "Dropoff",
+ "label.dropoff": "Odlazak",
"label.edit": "Uredi",
"label.edit-dashboard": "Uredi dashboard",
"label.edit-member": "Uredi člana",
+ "label.email": "E-mail",
"label.enable-share-url": "Omogući URL za dijeljenje",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
+ "label.end-step": "Završni korak",
+ "label.entry": "URL ulaza",
"label.event": "Događaj",
"label.event-data": "Podaci o događaju",
+ "label.event-name": "Naziv događaja",
"label.events": "Događaji",
+ "label.exists": "Postoji",
"label.exit": "Exit URL",
"label.false": "Ne",
"label.field": "Polje",
@@ -78,29 +101,37 @@
"label.filter-combined": "Kombinovano",
"label.filter-raw": "Sirovo",
"label.filters": "Filtri",
- "label.first-seen": "First seen",
+ "label.first-click": "Prvi klik",
+ "label.first-seen": "Prvi put viđeno",
"label.funnel": "Lijevak",
"label.funnel-description": "Razumite koverziju i drop-off učestalost korisnika.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
+ "label.funnels": "Lijevci",
+ "label.goal": "Cilj",
+ "label.goals": "Ciljevi",
+ "label.goals-description": "Pratite svoje ciljeve za prikaze stranica i događaje.",
"label.greater-than": "Veće od",
"label.greater-than-equals": "Veće od ili jednako",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grupisano",
+ "label.hostname": "Naziv hosta",
+ "label.includes": "Uključuje",
+ "label.insight": "Uvid",
"label.insights": "Uvidi",
"label.insights-description": "Zaronite dublje u vaše podatke korištenjem segmenata i filtera",
"label.is": "Jeste",
+ "label.is-false": "Nije tačno",
"label.is-not": "Nije",
"label.is-not-set": "Nije setano",
"label.is-set": "Jeste setano",
+ "label.is-true": "Tačno",
"label.join": "Učlani se",
"label.join-team": "Učlani se u tim",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.journey": "Putovanje",
+ "label.journey-description": "Saznajte kako korisnici navigiraju vašom web stranicom.",
+ "label.journeys": "Putovanja",
"label.language": "Jezik",
"label.languages": "Jezici",
"label.laptop": "Laptop",
+ "label.last-click": "Zadnji klik",
"label.last-days": "Zadnjih {x} dana",
"label.last-hours": "Zadnjih {x} sati",
"label.last-months": "Zadnjih {x} mjeseci",
@@ -109,61 +140,79 @@
"label.leave-team": "Napusti tim",
"label.less-than": "Manje od",
"label.less-than-equals": "Manje od ili jednako",
+ "label.links": "Linkovi",
"label.login": "Login",
"label.logout": "Logout",
"label.manage": "Manage",
- "label.manager": "Manager",
+ "label.manager": "Menadžer",
"label.max": "Max",
+ "label.maximize": "Proširi",
+ "label.medium": "Srednje",
"label.member": "Član",
"label.members": "Članovi",
"label.min": "Min",
"label.mobile": "Mobile",
+ "label.model": "Model",
"label.more": "Više",
"label.my-account": "Moj račun",
"label.my-websites": "Moje web stranice",
"label.name": "Ime",
"label.new-password": "Nova šifra",
- "label.none": "None",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
+ "label.none": "Nijedno",
+ "label.number-of-records": "{x} {x, plural, one {zapis} other {zapisa}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organska pretraga",
+ "label.organic-shopping": "Organska kupovina",
+ "label.organic-social": "Organske društvene mreže",
+ "label.organic-video": "Organski video",
"label.os": "OS",
+ "label.other": "Drugo",
"label.overview": "Pregled",
"label.owner": "Vlasnik",
+ "label.page": "Stranica",
"label.page-of": "Strana {current} od {total}",
"label.page-views": "Pregleda stranica",
"label.pageTitle": "Naslov stranice",
"label.pages": "Stranice",
+ "label.paid-ads": "Plaćeni oglasi",
+ "label.paid-search": "Plaćena pretraga",
+ "label.paid-shopping": "Plaćena kupovina",
+ "label.paid-social": "Plaćene društvene mreže",
+ "label.paid-video": "Plaćeni video",
"label.password": "Šifra",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Putanja",
+ "label.paths": "Putanje",
+ "label.pixels": "Pikseli",
"label.powered-by": "Omogućeno s {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
"label.previous-year": "Previous year",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queryji",
- "label.query": "Query",
- "label.query-parameters": "Query parametri",
+ "label.properties": "Svojstva",
+ "label.property": "Svojstvo",
+ "label.queries": "Upiti",
+ "label.query": "Upit",
+ "label.query-parameters": "Parametri upita",
"label.realtime": "Realno vrijeme",
- "label.referrer": "Referrer",
- "label.referrers": "Referrers",
- "label.refresh": "Refresh",
+ "label.referral": "Preporuka",
+ "label.referrer": "Preporučilac",
+ "label.referrers": "Preporučioci",
+ "label.refresh": "Osvježi",
"label.regenerate": "Regeneriši",
"label.region": "Region",
"label.regions": "Regioni",
+ "label.remaining": "Preostalo",
"label.remove": "Ukloni",
"label.remove-member": "Ukloni člana",
"label.reports": "Izvještaji",
- "label.required": "Required",
+ "label.required": "Obavezno",
"label.reset": "Resetuj",
"label.reset-website": "Resetuj web stranicu",
- "label.retention": "Retention",
+ "label.retention": "Zadržavanje",
"label.retention-description": "Izmjeri 'ljepljivost' svoje web stranice praćenjem koliko često set korisnici vraćaju.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Prihod",
+ "label.revenue-description": "Pogledajte svoje prihode tokom vremena.",
"label.role": "Rola",
"label.run-query": "Pokreni query",
"label.save": "Sačuvaj",
@@ -171,26 +220,35 @@
"label.search": "Traži",
"label.select": "Odaberi",
"label.select-date": "Odaberi datum",
+ "label.select-filter": "Odaberi filter",
"label.select-role": "Odaberi rolu",
"label.select-website": "Odaberi web stranicu",
- "label.session": "Session",
+ "label.session": "Sesija",
+ "label.session-data": "Podaci o sesiji",
"label.sessions": "Sesije",
"label.settings": "Postavke",
- "label.share-url": "Share URL",
+ "label.share": "Podijeli",
+ "label.share-url": "URL za dijeljenje",
"label.single-day": "Jedan dan",
- "label.start-step": "Start Step",
+ "label.sms": "SMS",
+ "label.sources": "Izvori",
+ "label.start-step": "Početni korak",
"label.steps": "Koraci",
"label.sum": "Suma",
"label.tablet": "Tablet",
+ "label.tag": "Oznaka",
+ "label.tags": "Oznake",
"label.team": "Tim",
"label.team-id": "Tim ID",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Menadžer tima",
"label.team-member": "Član tima",
"label.team-name": "Naziv tima",
"label.team-owner": "Vlasnik tima",
+ "label.team-settings": "Postavke tima",
"label.team-view-only": "Samo tim može vidjeti",
"label.team-websites": "Timske web stranice",
"label.teams": "Timovi",
+ "label.terms": "Pojmovi",
"label.theme": "Teme",
"label.this-month": "Ovaj mjesec",
"label.this-week": "Ova sedmica",
@@ -213,10 +271,7 @@
"label.unknown": "Nepoznato",
"label.untitled": "Bezimeno",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Korisnik",
- "label.user-property": "User Property",
"label.username": "Korisničko ime",
"label.users": "Korisnici",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Jučer",
"message.action-confirmation": "Unesite {confirmation} ispod da potvrdite.",
"message.active-users": "{x} trenutno {x, plural, one {posjetitelj} other {posjetitelja}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Jeste li sigurni da želite obrisati {target}?",
"message.confirm-leave": "Jeste li sigurni da želite napustiti {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Svi podaci web stranice biće obrisani.",
"message.error": "Nešto je pošlo po zlu.",
"message.event-log": "{event} na {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Idi na postavke",
"message.incorrect-username-password": "Pogrešno korisničko ime i/ili šifra.",
"message.invalid-domain": "Nevalidna domena. Ne uključujte http/https.",
@@ -259,10 +316,13 @@
"message.no-teams": "Niste kreirali nijedan tim.",
"message.no-users": "Nema nikakvih korisnika.",
"message.no-websites-configured": "Nemate iskonfigurisanu nijednu web stranicu.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Stranica nije pronađena",
"message.reset-website": "Da resetujete ovu web stranicu, upišite {confirmation} dole da potvrdite.",
"message.reset-website-warning": "Sve statistike o ovoj web stranici će biti obrisane, ali vaše postavke neće biti dirane.",
"message.saved": "Sačuvano.",
+ "message.sever-error": "Server error",
"message.share-url": "Statistike vaše web stranice su javno dostupne na sljedećem URLu:",
"message.team-already-member": "Već ste član tima.",
"message.team-not-found": "Tim nije pronađen.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Odaberite tim u koji želite prebaciti ovu web stranicu.",
"message.transfer-website": "Prebacite vlasništvo web stranice na vaš račun ili drugi tim.",
"message.triggered-event": "Trigerovani događaj",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Korisnik obrisan.",
"message.viewed-page": "Pogledana stranica",
- "message.visitor-log": "Posjetitelj iz {country} koristi {browser} na {os} {device}",
- "message.visitors-dropped-off": "Posjetitelji koji su napustili stranicu"
+ "message.visitor-log": "Posjetitelj iz {country} koristi {browser} na {os} {device}"
}
diff --git a/src/lang/ca-ES.json b/src/lang/ca-ES.json
index 3b633337..ab5444ce 100644
--- a/src/lang/ca-ES.json
+++ b/src/lang/ca-ES.json
@@ -3,32 +3,47 @@
"label.actions": "Accions",
"label.activity": "Registre d'activitat",
"label.add": "Afegir",
+ "label.add-board": "Afegir tauler",
"label.add-description": "Afegir descripció",
"label.add-member": "Afegir membre",
"label.add-step": "Afegir pas",
"label.add-website": "Afegir lloc web",
"label.admin": "Administrador",
+ "label.affiliate": "Afiliat",
"label.after": "Després",
"label.all": "Tots",
"label.all-time": "Sempre",
"label.analytics": "Analítiques",
+ "label.apply": "Aplica",
+ "label.attribution": "Atribució",
+ "label.attribution-description": "Vegeu com els usuaris interactuen amb el vostre màrqueting i què impulsa les conversions.",
"label.average": "Mitjana",
"label.back": "Enrere",
"label.before": "Abans",
+ "label.behavior": "Comportament",
+ "label.boards": "Taulers",
"label.bounce-rate": "Percentatge de rebot",
"label.breakdown": "Desglossament",
"label.browser": "Navegador",
"label.browsers": "Navegadors",
+ "label.campaigns": "Campanyes",
"label.cancel": "Cancel·la",
"label.change-password": "Canvia la contrasenya",
+ "label.channels": "Canals",
"label.cities": "Ciutats",
"label.city": "Ciutat",
"label.clear-all": "Netejar tot",
+ "label.cohort": "Cohort",
"label.compare": "Comparar",
+ "label.compare-dates": "Comparar dates",
"label.confirm": "Confirmar",
"label.confirm-password": "Confirma la contrasenya",
"label.contains": "Conté",
+ "label.content": "Contingut",
"label.continue": "Continuar",
+ "label.conversion": "Conversió",
+ "label.conversion-rate": "Taxa de conversió",
+ "label.conversion-step": "Pas de conversió",
"label.count": "Recompte",
"label.countries": "Països",
"label.country": "País",
@@ -38,6 +53,7 @@
"label.create-user": "Crear usuari",
"label.created": "Creat",
"label.created-by": "Creat Per",
+ "label.currency": "Moneda",
"label.current": "Actual",
"label.current-password": "Contrasenya actual",
"label.custom-range": "Rang personalitzat",
@@ -57,19 +73,26 @@
"label.details": "Detalls",
"label.device": "Dispositiu",
"label.devices": "Dispositius",
+ "label.direct": "Directe",
"label.dismiss": "Descarta",
+ "label.distinct-id": "ID distintiu",
"label.does-not-contain": "No conté",
+ "label.does-not-include": "No inclou",
+ "label.doest-not-exist": "No existeix",
"label.domain": "Domini",
"label.dropoff": "Abandonament",
"label.edit": "Edita",
"label.edit-dashboard": "Edita panell",
"label.edit-member": "Edita membre",
+ "label.email": "Email",
"label.enable-share-url": "Activa l'enllaç per compartir",
"label.end-step": "Pas Final",
"label.entry": "URL d'entrada",
"label.event": "Esdeveniment",
"label.event-data": "Dades de l'esdeveniment",
+ "label.event-name": "Nom de l'esdeveniment",
"label.events": "Esdeveniments",
+ "label.exists": "Existeix",
"label.exit": "URL de sortida",
"label.false": "Fals",
"label.field": "Camp",
@@ -78,29 +101,37 @@
"label.filter-combined": "Combinat",
"label.filter-raw": "En cru",
"label.filters": "Filtres",
+ "label.first-click": "Primer clic",
"label.first-seen": "Vist per primer cop",
"label.funnel": "Embut",
"label.funnel-description": "Entengui la taxa de conversió i abandonament dels usuaris.",
+ "label.funnels": "Embuts",
"label.goal": "Meta",
"label.goals": "Metes",
"label.goals-description": "Feu un seguiment de les seves metes per a pàgines vistes i esdeveniments.",
"label.greater-than": "Més gran que",
"label.greater-than-equals": "Més gran que o igual a",
- "label.host": "Amfitrió",
- "label.hosts": "Amfitrions",
+ "label.grouped": "Agrupat",
+ "label.hostname": "Nom de host",
+ "label.includes": "Inclou",
+ "label.insight": "Visió",
"label.insights": "Insights",
"label.insights-description": "Aprofundeixi en les seves dades mitjançant l'ús de segments i filtres.",
"label.is": "És igual a",
+ "label.is-false": "És fals",
"label.is-not": "No és igual a",
"label.is-not-set": "No està establert",
"label.is-set": "Està establert",
+ "label.is-true": "És cert",
"label.join": "Unir",
"label.join-team": "Unir-se al equip",
"label.journey": "Trajecte",
"label.journey-description": "Entengui com naveguen els usuaris pel seu lloc web.",
+ "label.journeys": "Trajectes",
"label.language": "Idioma",
"label.languages": "Idiomes",
"label.laptop": "Portàtil",
+ "label.last-click": "Últim clic",
"label.last-days": "Últims {x} dies",
"label.last-hours": "Últimes {x} hores",
"label.last-months": "Últims {x} mesos",
@@ -109,15 +140,19 @@
"label.leave-team": "Abandonar equip",
"label.less-than": "Menor que",
"label.less-than-equals": "Menor que o igual a",
+ "label.links": "Enllaços",
"label.login": "Connecta't",
"label.logout": "Desconnecta't",
"label.manage": "Administrar",
- "label.manager": "Manager",
+ "label.manager": "Responsable",
"label.max": "Màx",
+ "label.maximize": "Expandeix",
+ "label.medium": "Mitjà",
"label.member": "Membre",
"label.members": "Membres",
"label.min": "Mín",
"label.mobile": "Mòbil",
+ "label.model": "Model",
"label.more": "Més",
"label.my-account": "El meu compte",
"label.my-websites": "Els meus llocs web",
@@ -126,16 +161,29 @@
"label.none": "Cap",
"label.number-of-records": "{x} {x, plural, one {registre} other {registres}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Cerca orgànica",
+ "label.organic-shopping": "Compra orgànica",
+ "label.organic-social": "Social orgànic",
+ "label.organic-video": "Vídeo orgànic",
"label.os": "SO",
+ "label.other": "Altres",
"label.overview": "Resum",
"label.owner": "Propietari",
+ "label.page": "Pàgina",
"label.page-of": "Pàgina {current} de {total}",
"label.page-views": "Pàgines vistes",
"label.pageTitle": "Títol de la pàgina",
"label.pages": "Pàgines",
+ "label.paid-ads": "Anuncis de pagament",
+ "label.paid-search": "Cerca de pagament",
+ "label.paid-shopping": "Compra de pagament",
+ "label.paid-social": "Social de pagament",
+ "label.paid-video": "Vídeo de pagament",
"label.password": "Contrasenya",
"label.path": "Camí",
"label.paths": "Camins",
+ "label.pixels": "Pixels",
"label.powered-by": "Funciona amb {name}",
"label.previous": "Anterior",
"label.previous-period": "Període anterior",
@@ -147,12 +195,14 @@
"label.query": "Consulta",
"label.query-parameters": "Paràmetres de consulta",
"label.realtime": "Temps real",
+ "label.referral": "Referència",
"label.referrer": "Referent",
"label.referrers": "Referents",
"label.refresh": "Refresca",
"label.regenerate": "Regenerar",
"label.region": "Regió",
"label.regions": "Regions",
+ "label.remaining": "Restant",
"label.remove": "Treure",
"label.remove-member": "Eliminar membre",
"label.reports": "Informes",
@@ -163,7 +213,6 @@
"label.retention-description": "Mesuri la retenció del seu lloc web fent un seguiment de la freqüència amb què tornen els usuaris.",
"label.revenue": "Ingressos",
"label.revenue-description": "Observi els seus ingressos al llarg del temps.",
- "label.revenue-property": "Propietat d'Ingressos",
"label.role": "Rol",
"label.run-query": "Executar consulta",
"label.save": "Desa",
@@ -171,26 +220,35 @@
"label.search": "Buscar",
"label.select": "Seleccionar",
"label.select-date": "Seleccionar data",
+ "label.select-filter": "Seleccionar filtre",
"label.select-role": "Seleccionar rol",
"label.select-website": "Seleccionar lloc web",
"label.session": "Sessió",
+ "label.session-data": "Dades de sessió",
"label.sessions": "Sessions",
"label.settings": "Configuració",
+ "label.share": "Comparteix",
"label.share-url": "Enllaç per compartir",
"label.single-day": "Un sol dia",
+ "label.sms": "SMS",
+ "label.sources": "Fonts",
"label.start-step": "Pas inicial",
"label.steps": "Pasos",
"label.sum": "Suma",
"label.tablet": "Tauleta",
+ "label.tag": "Etiqueta",
+ "label.tags": "Etiquetes",
"label.team": "Equip",
"label.team-id": "ID del equip",
"label.team-manager": "Responsable d'equip",
"label.team-member": "Membre de l'equip",
"label.team-name": "Nom de l'equip",
"label.team-owner": "Propietari de l'equip",
+ "label.team-settings": "Configuració de l'equip",
"label.team-view-only": "Vista només de l'equip",
"label.team-websites": "Llocs web de l'equip",
"label.teams": "Equips",
+ "label.terms": "Termes",
"label.theme": "Tema",
"label.this-month": "Aquest mes",
"label.this-week": "Aquesta setmana",
@@ -213,10 +271,7 @@
"label.unknown": "Desconegut",
"label.untitled": "Sense títol",
"label.update": "Actualitzar",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Usuari",
- "label.user-property": "Propietat d'Usuari",
"label.username": "Nom d'usuari",
"label.users": "Usuaris",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Ahir",
"message.action-confirmation": "Escrigui {confirmation} al cuadre inferior per confirmar.",
"message.active-users": "{x} {x, plural, one {visitant actual} other {visitants actuals}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Dades recol·lectades",
"message.confirm-delete": "Segur que vol esborrar {target}?",
"message.confirm-leave": "Segur que vol abandonar {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "També s'esborraran totes les dades relacionades.",
"message.error": "S'ha produït un error.",
"message.event-log": "{event} a {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Vés a la configuració",
"message.incorrect-username-password": "Nom d'usuari o contrasenya incorrectes.",
"message.invalid-domain": "Domini invàlid",
@@ -259,10 +316,13 @@
"message.no-teams": "No ha creat cap equip.",
"message.no-users": "No hi ha cap usuari.",
"message.no-websites-configured": "No hi ha cap lloc web configurat.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "No s'ha trobat la pàgina.",
"message.reset-website": "Per restablir aquest lloc web, escrigui {confirmation} al cuadre inferior per confirmar.",
"message.reset-website-warning": "S'esborraran totes les estadístiques per aquest lloc web, però el codi de seguiment es mantindrà.",
"message.saved": "S'ha desat amb èxit.",
+ "message.sever-error": "Server error",
"message.share-url": "Aquest és l'enllaç públic per compartir de {target}.",
"message.team-already-member": "Ja és membre d'aquest equip.",
"message.team-not-found": "Equip no trobat.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Seleccioni l'equip al qui transferir aquest lloc web.",
"message.transfer-website": "Transferir la propietat del lloc web al seu compte o a un altre equip.",
"message.triggered-event": "Esdeveniment desencadenat",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Usuari eliminat.",
"message.viewed-page": "Pàgina vista",
- "message.visitor-log": "Visitant de {country} usant {browser} a {os} {device}",
- "message.visitors-dropped-off": "Visitants han sortit"
+ "message.visitor-log": "Visitant de {country} usant {browser} a {os} {device}"
}
diff --git a/src/lang/cs-CZ.json b/src/lang/cs-CZ.json
index 8adc5e36..77d45a79 100644
--- a/src/lang/cs-CZ.json
+++ b/src/lang/cs-CZ.json
@@ -3,32 +3,47 @@
"label.actions": "Akce",
"label.activity": "Log aktivity",
"label.add": "Přidat",
+ "label.add-board": "Přidat nástěnku",
"label.add-description": "Přidat popis",
"label.add-member": "Přidat člena",
"label.add-step": "Přidat krok",
"label.add-website": "Přidat web",
"label.admin": "Administrátor",
+ "label.affiliate": "Partner",
"label.after": "Po",
"label.all": "Vše",
"label.all-time": "Celá doba",
- "label.analytics": "Analytics",
+ "label.analytics": "Analytika",
+ "label.apply": "Použít",
+ "label.attribution": "Atribuce",
+ "label.attribution-description": "Podívejte se, jak uživatelé interagují s vaším marketingem a co vede ke konverzím.",
"label.average": "Průměr",
"label.back": "Zpět",
"label.before": "Před",
+ "label.behavior": "Chování",
+ "label.boards": "Nástěnky",
"label.bounce-rate": "Okamžité opuštění",
- "label.breakdown": "Breakdown",
+ "label.breakdown": "Rozpis",
"label.browser": "Prohlížeč",
"label.browsers": "Prohlížeče",
+ "label.campaigns": "Kampaně",
"label.cancel": "Zrušit",
"label.change-password": "Změnit heslo",
+ "label.channels": "Kanály",
"label.cities": "Města",
"label.city": "Město",
"label.clear-all": "Vyčistit vše",
+ "label.cohort": "Kohorta",
"label.compare": "Porovnat",
+ "label.compare-dates": "Porovnat data",
"label.confirm": "Potvrdit",
"label.confirm-password": "Potvrdit heslo",
"label.contains": "Obsahuje",
+ "label.content": "Obsah",
"label.continue": "Pokračovat",
+ "label.conversion": "Konverze",
+ "label.conversion-rate": "Míra konverze",
+ "label.conversion-step": "Krok konverze",
"label.count": "Počet",
"label.countries": "Státy",
"label.country": "Stát",
@@ -38,6 +53,7 @@
"label.create-user": "Vytvořit uživatele",
"label.created": "Vytvořeno",
"label.created-by": "Created By",
+ "label.currency": "Měna",
"label.current": "Aktuální",
"label.current-password": "Aktuální heslo",
"label.custom-range": "Vlastní rozsah",
@@ -57,141 +73,183 @@
"label.details": "Detaily",
"label.device": "Zařízení",
"label.devices": "Zařízení",
+ "label.direct": "Přímý",
"label.dismiss": "Odejít",
+ "label.distinct-id": "Jedinečné ID",
"label.does-not-contain": "Neobsahuje",
+ "label.does-not-include": "Nezahrnuje",
+ "label.doest-not-exist": "Neexistuje",
"label.domain": "Doména",
- "label.dropoff": "Dropoff",
+ "label.dropoff": "Opuštění",
"label.edit": "Upravit",
"label.edit-dashboard": "Upravit dashboard",
"label.edit-member": "Upravit člena",
+ "label.email": "E-mail",
"label.enable-share-url": "Povolit sdílení URL",
- "label.end-step": "End Step",
+ "label.end-step": "Konečný krok",
"label.entry": "Vstupní URL",
"label.event": "Událost",
- "label.event-data": "Event data",
+ "label.event-data": "Data události",
+ "label.event-name": "Název události",
"label.events": "Události",
+ "label.exists": "Existuje",
"label.exit": "Exit URL",
- "label.false": "False",
+ "label.false": "Nepravda",
"label.field": "Pole",
- "label.fields": "Fields",
+ "label.fields": "Pole",
"label.filter": "Filtr",
"label.filter-combined": "Kombinace",
"label.filter-raw": "Nezpracované",
"label.filters": "Filtry",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
+ "label.first-click": "První kliknutí",
+ "label.first-seen": "Poprvé viděno",
+ "label.funnel": "Trychtýř",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
+ "label.funnels": "Trychtýře",
"label.goal": "Cíl",
"label.goals": "Cíle",
"label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.greater-than": "Větší než",
+ "label.greater-than-equals": "Větší nebo rovno",
+ "label.grouped": "Seskupeno",
+ "label.hostname": "Název hostitele",
+ "label.includes": "Zahrnuje",
+ "label.insight": "Pohled",
+ "label.insights": "Pohledy",
+ "label.insights-description": "Ponořte se hlouběji do svých dat pomocí segmentů a filtrů.",
+ "label.is": "Je",
+ "label.is-false": "Nepravda",
+ "label.is-not": "Není",
+ "label.is-not-set": "Není nastaveno",
+ "label.is-set": "Nastaveno",
+ "label.is-true": "Pravda",
+ "label.join": "Připojit se",
+ "label.join-team": "Připojit se k týmu",
+ "label.journey": "Cesta",
+ "label.journey-description": "Zjistěte, jak uživatelé procházejí vaším webem.",
+ "label.journeys": "Cesty",
"label.language": "Jazyk",
"label.languages": "Jazyky",
"label.laptop": "Přenosný počítač",
+ "label.last-click": "Poslední kliknutí",
"label.last-days": "Posledních {x} dnů",
"label.last-hours": "Posledních {x} hodin",
"label.last-months": "Posledních {x} měsíců",
"label.last-seen": "Last seen",
"label.leave": "Opustit",
"label.leave-team": "Opustit tým",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.less-than": "Méně než",
+ "label.less-than-equals": "Méně nebo rovno",
+ "label.links": "Odkazy",
"label.login": "Přihlásit",
"label.logout": "Odhlásit",
"label.manage": "Spravovat",
"label.manager": "Správce",
"label.max": "Max",
+ "label.maximize": "Rozbalit",
+ "label.medium": "Střední",
"label.member": "Člen",
"label.members": "Členové",
"label.min": "Min",
"label.mobile": "Mobilní telefon",
+ "label.model": "Model",
"label.more": "Více",
"label.my-account": "Můj účet",
"label.my-websites": "Mé weby",
"label.name": "Jméno",
"label.new-password": "Nové heslo",
- "label.none": "None",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
+ "label.none": "Žádný",
+ "label.number-of-records": "{x} {x, plural, one {záznam} other {záznamů}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organické vyhledávání",
+ "label.organic-shopping": "Organický nákup",
+ "label.organic-social": "Organická sociální síť",
+ "label.organic-video": "Organické video",
"label.os": "OS",
+ "label.other": "Jiné",
"label.overview": "Přehled",
"label.owner": "Vlastník",
- "label.page-of": "Page {current} of {total}",
+ "label.page": "Stránka",
+ "label.page-of": "Stránka {current} z {total}",
"label.page-views": "Zobrazení stránek",
"label.pageTitle": "Název stránky",
"label.pages": "Stránky",
+ "label.paid-ads": "Placené reklamy",
+ "label.paid-search": "Placené vyhledávání",
+ "label.paid-shopping": "Placený nákup",
+ "label.paid-social": "Placená sociální síť",
+ "label.paid-video": "Placené video",
"label.password": "Heslo",
"label.path": "Cesta",
"label.paths": "Cesty",
+ "label.pixels": "Pixely",
"label.powered-by": "Běží na {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
"label.previous-year": "Previous year",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
- "label.query": "Query",
- "label.query-parameters": "Query parameters",
+ "label.properties": "Vlastnosti",
+ "label.property": "Vlastnost",
+ "label.queries": "Dotazy",
+ "label.query": "Dotaz",
+ "label.query-parameters": "Parametry dotazu",
"label.realtime": "Aktuálně",
- "label.referrer": "Referrer",
- "label.referrers": "Odkazy",
+ "label.referral": "Doporučení",
+ "label.referrer": "Odkazující",
+ "label.referrers": "Odkazující",
"label.refresh": "Obnovit",
- "label.regenerate": "Regenerate",
+ "label.regenerate": "Regenerovat",
"label.region": "Region",
- "label.regions": "Regions",
- "label.remove": "Remove",
- "label.remove-member": "Remove member",
- "label.reports": "Reports",
- "label.required": "Vyžadováno",
- "label.reset": "Reset",
- "label.reset-website": "Reset statistics",
- "label.retention": "Retention",
- "label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.regions": "Regiony",
+ "label.remaining": "Zbývá",
+ "label.remove": "Odstranit",
+ "label.remove-member": "Odstranit člena",
+ "label.reports": "Hlášení",
+ "label.required": "Povinné",
+ "label.reset": "Resetovat",
+ "label.reset-website": "Resetovat statistiky",
+ "label.retention": "Udržení",
+ "label.retention-description": "Měřte přilnavost svého webu sledováním, jak často se uživatelé vracejí.",
+ "label.revenue": "Příjem",
+ "label.revenue-description": "Podívejte se na své příjmy v průběhu času.",
"label.role": "Role",
- "label.run-query": "Run query",
+ "label.run-query": "Spustit dotaz",
"label.save": "Uložit",
- "label.screens": "Screens",
- "label.search": "Search",
- "label.select": "Select",
- "label.select-date": "Select date",
- "label.select-role": "Select role",
- "label.select-website": "Select website",
- "label.session": "Session",
- "label.sessions": "Sessions",
+ "label.screens": "Obrazovky",
+ "label.search": "Hledat",
+ "label.select": "Vybrat",
+ "label.select-date": "Vybrat datum",
+ "label.select-filter": "Vybrat filtr",
+ "label.select-role": "Vybrat roli",
+ "label.select-website": "Vybrat web",
+ "label.session": "Relace",
+ "label.session-data": "Data relace",
+ "label.sessions": "Relace",
"label.settings": "Nastavení",
- "label.share-url": "Sdílet URL",
+ "label.share": "Sdílet",
+ "label.share-url": "URL pro sdílení",
"label.single-day": "Jeden den",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
- "label.sum": "Sum",
+ "label.sms": "SMS",
+ "label.sources": "Zdroje",
+ "label.start-step": "Počáteční krok",
+ "label.steps": "Kroky",
+ "label.sum": "Součet",
"label.tablet": "Tablet",
- "label.team": "Team",
- "label.team-id": "Team ID",
- "label.team-manager": "Team manager",
- "label.team-member": "Team member",
- "label.team-name": "Team name",
- "label.team-owner": "Team owner",
- "label.team-view-only": "Team view only",
- "label.team-websites": "Team websites",
- "label.teams": "Teams",
- "label.theme": "Theme",
+ "label.tag": "Štítek",
+ "label.tags": "Štítky",
+ "label.team": "Tým",
+ "label.team-id": "ID týmu",
+ "label.team-manager": "Manažer týmu",
+ "label.team-member": "Člen týmu",
+ "label.team-name": "Název týmu",
+ "label.team-owner": "Vlastník týmu",
+ "label.team-settings": "Nastavení týmu",
+ "label.team-view-only": "Pouze pro zobrazení týmu",
+ "label.team-websites": "Weby týmu",
+ "label.teams": "Týmy",
+ "label.terms": "Termíny",
+ "label.theme": "Téma",
"label.this-month": "Tento měsíc",
"label.this-week": "Tento týden",
"label.this-year": "Tento rok",
@@ -213,10 +271,7 @@
"label.unknown": "Neznámý",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Uživatelské jméno",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Včera",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} aktuálně {x, plural, one {návštěvník} other {návštěvníci}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Opravdu smazat {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Všechna související data budou také smazána.",
"message.error": "Něco se pokazilo.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Jít do nastavení",
"message.incorrect-username-password": "Nesprávné jméno/heslo.",
"message.invalid-domain": "Neplatná doména",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "Nemáte nastavený žádný web.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Stránka nenalezena.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.saved": "Úspěšně uloženo.",
+ "message.sever-error": "Server error",
"message.share-url": "Toto je sdílené URL pro {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Návštěvník z {country} s prohlížečem {browser} na {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Návštěvník z {country} s prohlížečem {browser} na {os} {device}"
}
diff --git a/src/lang/da-DK.json b/src/lang/da-DK.json
index 143d079d..f6c447ff 100644
--- a/src/lang/da-DK.json
+++ b/src/lang/da-DK.json
@@ -1,196 +1,254 @@
{
- "label.access-code": "Access code",
+ "label.access-code": "Adgangskode",
"label.actions": "Handlinger",
- "label.activity": "Activity log",
- "label.add": "Add",
- "label.add-description": "Add description",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.activity": "Aktivitetslog",
+ "label.add": "Tilføj",
+ "label.add-board": "Tilføj tavle",
+ "label.add-description": "Tilføj beskrivelse",
+ "label.add-member": "Tilføj medlem",
+ "label.add-step": "Tilføj trin",
"label.add-website": "Tilføj hjemmeside",
"label.admin": "Administrator",
- "label.after": "After",
+ "label.affiliate": "Partner",
+ "label.after": "Efter",
"label.all": "Alle",
"label.all-time": "Altid",
- "label.analytics": "Analytics",
- "label.average": "Average",
+ "label.analytics": "Analyser",
+ "label.apply": "Anvend",
+ "label.attribution": "Attribuering",
+ "label.attribution-description": "Se, hvordan brugere interagerer med din markedsføring, og hvad der driver konverteringer.",
+ "label.average": "Gennemsnit",
"label.back": "Tilbage",
- "label.before": "Before",
+ "label.before": "Før",
+ "label.behavior": "Adfærd",
+ "label.boards": "Tavler",
"label.bounce-rate": "Afvisningsprocent",
- "label.breakdown": "Breakdown",
+ "label.breakdown": "Opdeling",
"label.browser": "Browser",
"label.browsers": "Browsere",
+ "label.campaigns": "Kampagner",
"label.cancel": "Afvis",
"label.change-password": "Skift adgangskode",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
+ "label.channels": "Kanaler",
+ "label.cities": "Byer",
+ "label.city": "By",
+ "label.clear-all": "Ryd alt",
+ "label.cohort": "Kohorte",
+ "label.compare": "Sammenlign",
+ "label.compare-dates": "Sammenlign datoer",
+ "label.confirm": "Bekræft",
"label.confirm-password": "Godkendt adgangskode",
"label.contains": "Contains",
- "label.continue": "Continue",
- "label.count": "Count",
+ "label.content": "Indhold",
+ "label.continue": "Fortsæt",
+ "label.conversion": "Konvertering",
+ "label.conversion-rate": "Konverteringsrate",
+ "label.conversion-step": "Konverteringstrin",
+ "label.count": "Antal",
"label.countries": "Lande",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.country": "Land",
+ "label.create": "Opret",
+ "label.create-report": "Opret rapport",
+ "label.create-team": "Opret team",
+ "label.create-user": "Opret bruger",
+ "label.created": "Oprettet",
+ "label.created-by": "Oprettet af",
+ "label.currency": "Valuta",
+ "label.current": "Nuværende",
"label.current-password": "Nuværende adgangskode",
"label.custom-range": "Tilpasset interval",
"label.dashboard": "Betjeningspanel",
"label.data": "Data",
- "label.date": "Date",
+ "label.date": "Dato",
"label.date-range": "Datointerval",
- "label.day": "Day",
+ "label.day": "Dag",
"label.default-date-range": "Standard datointerval",
"label.delete": "Slet",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "Slet rapport",
+ "label.delete-team": "Slet team",
+ "label.delete-user": "Slet bruger",
"label.delete-website": "Slet hjemmeside",
- "label.description": "Description",
- "label.desktop": "Desktop",
- "label.details": "Details",
- "label.device": "Device",
+ "label.description": "Beskrivelse",
+ "label.desktop": "Skrivebord",
+ "label.details": "Detaljer",
+ "label.device": "Enhed",
"label.devices": "Enheder",
+ "label.direct": "Direkte",
"label.dismiss": "Afvis",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "Unikt ID",
+ "label.does-not-contain": "Indeholder ikke",
+ "label.does-not-include": "Inkluderer ikke",
+ "label.doest-not-exist": "Findes ikke",
"label.domain": "Domæne",
- "label.dropoff": "Dropoff",
+ "label.dropoff": "Frafald",
"label.edit": "Rediger",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
+ "label.edit-dashboard": "Rediger betjeningspanel",
+ "label.edit-member": "Rediger medlem",
+ "label.email": "E-mail",
"label.enable-share-url": "Aktivér delings-URL",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event data",
+ "label.end-step": "Sluttrin",
+ "label.entry": "Indgangs-URL",
+ "label.event": "Hændelse",
+ "label.event-data": "Hændelsesdata",
+ "label.event-name": "Hændelsesnavn",
"label.events": "Hændelser",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
+ "label.exists": "Findes",
+ "label.exit": "Udgangs-URL",
+ "label.false": "Falsk",
+ "label.field": "Felt",
+ "label.fields": "Felter",
"label.filter": "Filter",
"label.filter-combined": "Kombineret",
"label.filter-raw": "Rå",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.filters": "Filtre",
+ "label.first-click": "Første klik",
+ "label.first-seen": "Først set",
+ "label.funnel": "Tragt",
+ "label.funnel-description": "Forstå brugernes konverterings- og frafaldsrate.",
+ "label.funnels": "Tragte",
+ "label.goal": "Mål",
+ "label.goals": "Mål",
+ "label.goals-description": "Følg dine mål for sidevisninger og hændelser.",
+ "label.greater-than": "Større end",
+ "label.greater-than-equals": "Større end eller lig med",
+ "label.grouped": "Gruperet",
+ "label.hostname": "Værtsnavn",
+ "label.includes": "Inkluderer",
+ "label.insight": "Indsigt",
+ "label.insights": "Indsigter",
+ "label.insights-description": "Dyk dybere ned i dine data ved at bruge segmenter og filtre.",
+ "label.is": "Er",
+ "label.is-false": "Er falsk",
+ "label.is-not": "Er ikke",
+ "label.is-not-set": "Er ikke sat",
+ "label.is-set": "Er sat",
+ "label.is-true": "Er sandt",
+ "label.join": "Deltag",
+ "label.join-team": "Deltag i team",
+ "label.journey": "Rejse",
+ "label.journey-description": "Forstå hvordan brugere navigerer på din hjemmeside.",
+ "label.journeys": "Rejser",
"label.language": "Sprog",
"label.languages": "Sprog",
"label.laptop": "Laptop",
+ "label.last-click": "Sidste klik",
"label.last-days": "Sidste {x} dage",
"label.last-hours": "Sidste {x} timer",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "Sidste {x} måneder",
+ "label.last-seen": "Sidst set",
+ "label.leave": "Forlad",
+ "label.leave-team": "Forlad team",
+ "label.less-than": "Mindre end",
+ "label.less-than-equals": "Mindre end eller lig med",
+ "label.links": "Links",
"label.login": "Log ind",
"label.logout": "Log ud",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
+ "label.manage": "Administrer",
+ "label.manager": "Leder",
+ "label.max": "Maks",
+ "label.maximize": "Udvid",
+ "label.medium": "Medium",
+ "label.member": "Medlem",
+ "label.members": "Medlemmer",
"label.min": "Min",
"label.mobile": "Mobil",
+ "label.model": "Model",
"label.more": "Mere",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "Min konto",
+ "label.my-websites": "Mine hjemmesider",
"label.name": "Navn",
"label.new-password": "Ny adgangskode",
- "label.none": "None",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
+ "label.none": "Ingen",
+ "label.number-of-records": "{x} {x, plural, one {post} other {poster}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organisk søgning",
+ "label.organic-shopping": "Organisk shopping",
+ "label.organic-social": "Organisk social",
+ "label.organic-video": "Organisk video",
"label.os": "OS",
- "label.overview": "Overview",
+ "label.other": "Andet",
+ "label.overview": "Oversigt",
"label.owner": "Ejer",
- "label.page-of": "Page {current} of {total}",
+ "label.page": "Side",
+ "label.page-of": "Side {current} af {total}",
"label.page-views": "Sidevisninger",
- "label.pageTitle": "Page title",
+ "label.pageTitle": "Sidetitel",
"label.pages": "Sider",
+ "label.paid-ads": "Betalte annoncer",
+ "label.paid-search": "Betalt søgning",
+ "label.paid-shopping": "Betalt shopping",
+ "label.paid-social": "Betalt social",
+ "label.paid-video": "Betalt video",
"label.password": "Adgangskode",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Sti",
+ "label.paths": "Stier",
+ "label.pixels": "Pixels",
"label.powered-by": "Drevet af {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
"label.previous-year": "Previous year",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
- "label.query": "Query",
- "label.query-parameters": "Query parameters",
+ "label.properties": "Egenskaber",
+ "label.property": "Egenskab",
+ "label.queries": "Forespørgsler",
+ "label.query": "Forespørgsel",
+ "label.query-parameters": "Forespørgselsparametre",
"label.realtime": "Realtid",
- "label.referrer": "Referrer",
+ "label.referral": "Henvisning",
+ "label.referrer": "Henviser",
"label.referrers": "Henvisninger",
"label.refresh": "Opdater",
- "label.regenerate": "Regenerate",
+ "label.regenerate": "Gendan",
"label.region": "Region",
- "label.regions": "Regions",
- "label.remove": "Remove",
- "label.remove-member": "Remove member",
- "label.reports": "Reports",
+ "label.regions": "Regioner",
+ "label.remaining": "Tilbageværende",
+ "label.remove": "Fjern",
+ "label.remove-member": "Fjern medlem",
+ "label.reports": "Rapporter",
"label.required": "Påkrævet",
"label.reset": "Nulstil",
- "label.reset-website": "Nulstil statistikker",
- "label.retention": "Retention",
- "label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
- "label.role": "Role",
- "label.run-query": "Run query",
+ "label.reset-website": "Nulstil statistik",
+ "label.retention": "Fastholdelse",
+ "label.retention-description": "Mål hvor ofte brugere vender tilbage til din hjemmeside.",
+ "label.revenue": "Indtægt",
+ "label.revenue-description": "Se din indtægt over tid.",
+ "label.role": "Rolle",
+ "label.run-query": "Kør forespørgsel",
"label.save": "Gem",
- "label.screens": "Screens",
- "label.search": "Search",
- "label.select": "Select",
- "label.select-date": "Select date",
- "label.select-role": "Select role",
- "label.select-website": "Select website",
+ "label.screens": "Skærme",
+ "label.search": "Søg",
+ "label.select": "Vælg",
+ "label.select-date": "Vælg dato",
+ "label.select-filter": "Vælg filter",
+ "label.select-role": "Vælg rolle",
+ "label.select-website": "Vælg hjemmeside",
"label.session": "Session",
- "label.sessions": "Sessions",
+ "label.session-data": "Sessionsdata",
+ "label.sessions": "Sessioner",
"label.settings": "Indstillinger",
+ "label.share": "Del",
"label.share-url": "Del URL",
"label.single-day": "Enkelt dag",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
+ "label.sms": "SMS",
+ "label.sources": "Kilder",
+ "label.start-step": "Starttrin",
+ "label.steps": "Trin",
"label.sum": "Sum",
"label.tablet": "Tablet",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "Team",
"label.team-id": "Team ID",
- "label.team-manager": "Team manager",
- "label.team-member": "Team member",
- "label.team-name": "Team name",
- "label.team-owner": "Team owner",
- "label.team-view-only": "Team view only",
- "label.team-websites": "Team websites",
+ "label.team-manager": "Teamleder",
+ "label.team-member": "Teammedlem",
+ "label.team-name": "Teamnavn",
+ "label.team-owner": "Teamejer",
+ "label.team-settings": "Teamindstillinger",
+ "label.team-view-only": "Kun visning af team",
+ "label.team-websites": "Teamets hjemmesider",
"label.teams": "Teams",
+ "label.terms": "Vilkår",
"label.theme": "Tema",
"label.this-month": "Denne måned",
"label.this-week": "Denne uge",
@@ -213,10 +271,7 @@
"label.unknown": "Ukendt",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Brugernavn",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} nuværende {x, plural, one {bruger} other {brugere}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Er du sikker på at du vil slette {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Alle tilknyttede data slettes også.",
"message.error": "Noget gik galt.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Gå til betjeningspanel",
"message.incorrect-username-password": "Ugyldigt brugernavn/adgangskode.",
"message.invalid-domain": "Ugyldigt domæne",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "Du har ikke konfigureret nogen hjemmesider.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Side ikke fundet.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "Alle statistikker for denne hjemmeside ville blive slettet, men sporingskode ville forblive intakt.",
"message.saved": "Gemt!",
+ "message.sever-error": "Server error",
"message.share-url": "Dette er den offentlige delings-URL til {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Besøgende fra {country} bruger {browser} på {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Besøgende fra {country} bruger {browser} på {os} {device}"
}
diff --git a/src/lang/de-CH.json b/src/lang/de-CH.json
index e99ecaca..55734ebd 100644
--- a/src/lang/de-CH.json
+++ b/src/lang/de-CH.json
@@ -3,32 +3,47 @@
"label.actions": "Aktione",
"label.activity": "Aktivitätsverlauf",
"label.add": "hinzuefüege",
+ "label.add-board": "Board hinzuefüege",
"label.add-description": "Beschriibig hinzuefüege",
"label.add-member": "Mitglied hinzuefüege",
"label.add-step": "Schritt hinzuefüege",
"label.add-website": "Websiite hinzuefüege",
"label.admin": "Administrator",
+ "label.affiliate": "Partnerprogramm",
"label.after": "Nach",
"label.all": "Alli",
"label.all-time": "Gsamte Zitruum",
- "label.analytics": "Analytics",
+ "label.analytics": "Analytik",
+ "label.apply": "Aawände",
+ "label.attribution": "Zuordnig",
+ "label.attribution-description": "Lueg wie d'Benutzer mit dim Marketing interagiere und was zu Umwandlige führt.",
"label.average": "Durchschnitt",
"label.back": "Zrugg",
"label.before": "Vor",
+ "label.behavior": "Verhalte",
+ "label.boards": "Boards",
"label.bounce-rate": "Absprungsrate",
"label.breakdown": "Uufschlüsselig",
"label.browser": "Browser",
"label.browsers": "Browser",
+ "label.campaigns": "Kampagne",
"label.cancel": "Abbreche",
"label.change-password": "Passwort ändere",
+ "label.channels": "Kanäle",
"label.cities": "Städt",
"label.city": "Stadt",
"label.clear-all": "Alles lösche",
+ "label.cohort": "Gruppe",
"label.compare": "Vergliiche",
+ "label.compare-dates": "Datum vergleiche",
"label.confirm": "Bestätige",
"label.confirm-password": "Passwort widerhole",
"label.contains": "Enthaltet",
+ "label.content": "Inhalt",
"label.continue": "Wiiter",
+ "label.conversion": "Umwandlig",
+ "label.conversion-rate": "Umwandligsrate",
+ "label.conversion-step": "Umwandligsschritt",
"label.count": "Azahl",
"label.countries": "Länder",
"label.country": "Land",
@@ -37,7 +52,8 @@
"label.create-team": "Team erstelle",
"label.create-user": "Benutzer erstelle",
"label.created": "Erstellt",
- "label.created-by": "Created By",
+ "label.created-by": "Erstellt vo",
+ "label.currency": "Währung",
"label.current": "Aktuell",
"label.current-password": "Aktuells Passwort",
"label.custom-range": "Benutzerdefinierte Bereich",
@@ -57,19 +73,26 @@
"label.details": "Details",
"label.device": "Grät",
"label.devices": "Grät",
+ "label.direct": "Direkt",
"label.dismiss": "Verwärfe",
+ "label.distinct-id": "Eindeutigi ID",
"label.does-not-contain": "Enthaltet nid",
+ "label.does-not-include": "Isch nid debii",
+ "label.doest-not-exist": "Existiert nid",
"label.domain": "Domain",
"label.dropoff": "Absprung",
"label.edit": "Bearbeite",
"label.edit-dashboard": "Dashboard bearbeite",
"label.edit-member": "Mitglied bearbeite",
+ "label.email": "Email",
"label.enable-share-url": "Freigab-URL aktiviere",
"label.end-step": "Schlussschritt",
"label.entry": "Iigangs URL",
"label.event": "Ereigniss",
"label.event-data": "Ereigniss Date",
+ "label.event-name": "Ereignissname",
"label.events": "Ereigniss",
+ "label.exists": "Existiert",
"label.exit": "Uusgangs URL",
"label.false": "Falsch",
"label.field": "Fäld",
@@ -78,29 +101,37 @@
"label.filter-combined": "Kombiniert",
"label.filter-raw": "Rohdate",
"label.filters": "Filters",
+ "label.first-click": "Erste Klick",
"label.first-seen": "Erstmal gse",
"label.funnel": "Tunnel",
"label.funnel-description": "Verstönd Sie d Konversions- und Abspruungsrate vo Nutzer.",
+ "label.funnels": "Funnels",
"label.goal": "Ziel",
"label.goals": "Ziele",
"label.goals-description": "verfolged Sie Ihri Ziel für Siitenufrüef und Ereigniss.",
"label.greater-than": "Grösser als",
"label.greater-than-equals": "Grösser oder gliich",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Gruppiert",
+ "label.hostname": "Hostnam",
+ "label.includes": "Isch debii",
+ "label.insight": "Iiblick",
"label.insights": "Iiblick",
"label.insights-description": "Vertüfed Sie sich i Ihri Date, mit Hilf vo Segment und Filter.",
"label.is": "Isch",
+ "label.is-false": "Isch falsch",
"label.is-not": "Isch nid",
"label.is-not-set": "Isch ned gsetzt",
"label.is-set": "Isch gsetzt",
+ "label.is-true": "Isch wahr",
"label.join": "Biträte",
"label.join-team": "Team biträte",
"label.journey": "Reis",
"label.journey-description": "Verstönd Sie, wie Nutzer dur Ihri Website navigiered.",
+ "label.journeys": "Reise",
"label.language": "Sprach",
"label.languages": "Sprache",
"label.laptop": "Laptop",
+ "label.last-click": "Letzte Klick",
"label.last-days": "Letzti {x} Täg",
"label.last-hours": "Letzti {x} Stunde",
"label.last-months": "Letzti {x} Mönet",
@@ -109,15 +140,19 @@
"label.leave-team": "Team verlah",
"label.less-than": "Kliiner als",
"label.less-than-equals": "Kliiner oder gliich",
+ "label.links": "Links",
"label.login": "Aamälde",
"label.logout": "Abmälde",
"label.manage": "Verwalte",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Uusklappe",
+ "label.medium": "Medium",
"label.member": "Mitglied",
"label.members": "Mitglieder",
"label.min": "Min",
"label.mobile": "Händy",
+ "label.model": "Model",
"label.more": "Meh",
"label.my-account": "Min Account",
"label.my-websites": "Mini Websiite",
@@ -126,33 +161,48 @@
"label.none": "Keis",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organischi Suechi",
+ "label.organic-shopping": "Organischi Iikauf",
+ "label.organic-social": "Organischi Social Media",
+ "label.organic-video": "Organischi Video",
"label.os": "OS",
+ "label.other": "Anderi",
"label.overview": "Übersicht",
"label.owner": "Bsitzer",
+ "label.page": "Siite",
"label.page-of": "Siite {current} vo {total}",
"label.page-views": "Siitenufrüef",
"label.pageTitle": "Siitetitel",
"label.pages": "Siite",
+ "label.paid-ads": "Bezahlti Werbung",
+ "label.paid-search": "Bezahlti Suechi",
+ "label.paid-shopping": "Bezahlti Iikauf",
+ "label.paid-social": "Bezahlti Social Media",
+ "label.paid-video": "Bezahlti Video",
"label.password": "Passwort",
"label.path": "Pfad",
"label.paths": "Pfade",
+ "label.pixels": "Pixel",
"label.powered-by": "Betriibe dur {name}",
"label.previous": "Vorherig",
"label.previous-period": "Vorherigi Periode",
"label.previous-year": "Vorherigs Jahr",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Eigeschafte",
+ "label.property": "Eigeschafte",
"label.queries": "Abfrage",
"label.query": "Abfrag",
"label.query-parameters": "Abfragparameter",
"label.realtime": "Echtzit",
+ "label.referral": "Empfehlig",
"label.referrer": "Verwiiser",
"label.referrers": "Verwiisendi",
"label.refresh": "Aktualisiere",
"label.regenerate": "Erneuere",
"label.region": "Region",
"label.regions": "Regionä",
+ "label.remaining": "Verblibe",
"label.remove": "Entferne",
"label.remove-member": "Mitglied entferne",
"label.reports": "Brichte",
@@ -163,7 +213,6 @@
"label.retention-description": "Mässed Sie d Verwiilduur vo Ihrere Website, indem Sie verfolged wie oft ihri Nutzer zruggkehred.",
"label.revenue": "Umsatz",
"label.revenue-description": "Lueged Sie sich Ihre Umsatz im Lauf vor Ziit a.",
- "label.revenue-property": "Umsatzeigenschafte",
"label.role": "Rollä",
"label.run-query": "Abfrag starte",
"label.save": "Speichere",
@@ -171,26 +220,35 @@
"label.search": "Sueche",
"label.select": "Auswähle",
"label.select-date": "Datä uuswähle",
+ "label.select-filter": "Filter uuswähle",
"label.select-role": "Rollä uuswähle",
"label.select-website": "Websiite uuswähle",
"label.session": "Sitzig",
+ "label.session-data": "Sitzigsdate",
"label.sessions": "Sitzige",
"label.settings": "Istellige",
+ "label.share": "Teile",
"label.share-url": "Freigab-URL",
"label.single-day": "Ein Tag",
+ "label.sms": "SMS",
+ "label.sources": "Quälle",
"label.start-step": "Startschritt",
"label.steps": "Schritt",
"label.sum": "Summe",
"label.tablet": "Tablet",
+ "label.tag": "Tag",
+ "label.tags": "Stichwort",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team Manager",
"label.team-member": "Team Mitglied",
"label.team-name": "Team Name",
"label.team-owner": "Team Bsitzer",
+ "label.team-settings": "Team Istellige",
"label.team-view-only": "Nur für Teammitglieder sichtbar",
"label.team-websites": "Team Websiite",
"label.teams": "Teams",
+ "label.terms": "Bedingige",
"label.theme": "Thema",
"label.this-month": "Dä Monet",
"label.this-week": "Diä Wuuche",
@@ -213,10 +271,7 @@
"label.unknown": "Unbekannt",
"label.untitled": "Unbennant",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Benutzer",
- "label.user-property": "Benutzereigeschafte",
"label.username": "Benutzername",
"label.users": "Benutzer",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Gester",
"message.action-confirmation": "Typed Sie {confirmation} is Feld underhalb um z bestätige.",
"message.active-users": "{x} {x, plural, one {aktive Bsuecher} other {aktivi Bsuecher}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Gsammleti Date",
"message.confirm-delete": "Sind Sie sich sicher, {target} zlösche?",
"message.confirm-leave": "Sind Sie sich sicher, {target} zverlah?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Alli dezueghörige Date werded ebefalls glöscht.",
"message.error": "Es isch en Fehler ufträte.",
"message.event-log": "{event} uf {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Zu de Istellige",
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
"message.invalid-domain": "Ungültigi Domain",
@@ -259,10 +316,13 @@
"message.no-teams": "Bisher sind no kei Teams erstellt worde.",
"message.no-users": "Da gits kei Benutzer",
"message.no-websites-configured": "Es isch kei Websiite vorhande.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Siite ned gfunde.",
"message.reset-website": "Um die Websiite zruggzsetze, typed Sie {confirmation} is Feld unde dran.",
"message.reset-website-warning": "Alli Date für die Websiite werdet glöscht, nur de Tracking Code blibt bestah.",
"message.saved": "Erfolgrich gspeichert.",
+ "message.sever-error": "Server error",
"message.share-url": "Ihri Websiitestatistik isch under de folgende URL öffentlich zuegänglich:",
"message.team-already-member": "Sie sind bereits es Mitglied vo däm Team.",
"message.team-not-found": "Team nöd gfunde.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Wähled Sie s Team zum däm Websiite transferiert werde söll.",
"message.transfer-website": "Übertraged Sie d Websiite Eigetümerrecht uf Ihre Account oder uf es anders Team",
"message.triggered-event": "Usglösts Ereigniss",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Bnutzer glöscht.",
"message.viewed-page": "Siite agluegt",
- "message.visitor-log": "Bsuecher us {country} nutzt {browser} uf {os} {device}",
- "message.visitors-dropped-off": "Bsuercher verlore"
-}
\ No newline at end of file
+ "message.visitor-log": "Bsuecher us {country} nutzt {browser} uf {os} {device}"
+}
diff --git a/src/lang/de-DE.json b/src/lang/de-DE.json
index 36090a84..3436eb89 100644
--- a/src/lang/de-DE.json
+++ b/src/lang/de-DE.json
@@ -3,32 +3,47 @@
"label.actions": "Aktionen",
"label.activity": "Aktivitätsverlauf",
"label.add": "Hinzufügen",
+ "label.add-board": "Board hinzufügen",
"label.add-description": "Beschreibung hinzufügen",
"label.add-member": "Mitglied hinzufügen",
"label.add-step": "Schritt hinzufügen",
"label.add-website": "Website hinzufügen",
"label.admin": "Administrator",
+ "label.affiliate": "Partnerprogramm",
"label.after": "Nach",
"label.all": "Alle",
"label.all-time": "Gesamter Zeitraum",
"label.analytics": "Analysen",
+ "label.apply": "Anwenden",
+ "label.attribution": "Zuordnung",
+ "label.attribution-description": "Sehen Sie, wie Nutzer mit Ihrem Marketing interagieren und was zu Konversionen führt.",
"label.average": "Durchschnitt",
"label.back": "Zurück",
"label.before": "Vor",
+ "label.behavior": "Verhalten",
+ "label.boards": "Boards",
"label.bounce-rate": "Absprungrate",
"label.breakdown": "Aufschlüsselung",
"label.browser": "Browser",
"label.browsers": "Browser",
+ "label.campaigns": "Kampagnen",
"label.cancel": "Abbrechen",
"label.change-password": "Passwort ändern",
+ "label.channels": "Kanäle",
"label.cities": "Städte",
"label.city": "Stadt",
"label.clear-all": "Alles löschen",
+ "label.cohort": "Gruppe",
"label.compare": "Vergleichen",
+ "label.compare-dates": "Daten vergleichen",
"label.confirm": "Bestätigen",
"label.confirm-password": "Passwort wiederholen",
"label.contains": "Enthält",
+ "label.content": "Inhalt",
"label.continue": "Weiter",
+ "label.conversion": "Konversion",
+ "label.conversion-rate": "Konversionsrate",
+ "label.conversion-step": "Konversionsschritt",
"label.count": "Anzahl",
"label.countries": "Länder",
"label.country": "Land",
@@ -38,6 +53,7 @@
"label.create-user": "Benutzer erstellen",
"label.created": "Erstellt",
"label.created-by": "Erstellt von",
+ "label.currency": "Währung",
"label.current": "Aktuell",
"label.current-password": "Derzeitiges Passwort",
"label.custom-range": "Benutzerdefinierter Bereich",
@@ -57,19 +73,26 @@
"label.details": "Details",
"label.device": "Gerät",
"label.devices": "Geräte",
+ "label.direct": "Direkt",
"label.dismiss": "Verwerfen",
+ "label.distinct-id": "Eindeutige ID",
"label.does-not-contain": "Enthält nicht",
+ "label.does-not-include": "Nicht enthalten",
+ "label.doest-not-exist": "Existiert nicht",
"label.domain": "Domain",
"label.dropoff": "Absprung",
"label.edit": "Bearbeiten",
"label.edit-dashboard": "Dashboard bearbeiten",
"label.edit-member": "Mitglied bearbeiten",
+ "label.email": "Email",
"label.enable-share-url": "Freigabe-URL aktivieren",
"label.end-step": "Schlussschritt",
"label.entry": "Eingangs-URL",
"label.event": "Ereignis",
"label.event-data": "Ereignisdaten",
+ "label.event-name": "Ereignisname",
"label.events": "Ereignisse",
+ "label.exists": "Existiert",
"label.exit": "Ausgangs-URL",
"label.false": "Falsch",
"label.field": "Feld",
@@ -78,29 +101,37 @@
"label.filter-combined": "Kombiniert",
"label.filter-raw": "Rohdaten",
"label.filters": "Filter",
+ "label.first-click": "Erster Klick",
"label.first-seen": "Erstmalig gesehen",
"label.funnel": "Trichter",
"label.funnel-description": "Verstehen Sie die Konversions- und Absprungrate Ihrer Nutzer.",
+ "label.funnels": "Funnels",
"label.goal": "Ziel",
"label.goals": "Ziele",
"label.goals-description": "Verfolgen Sie Ihre Ziele für Seitenaufrufe und Ereignisse.",
"label.greater-than": "Größer als",
"label.greater-than-equals": "Größer oder gleich",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Gruppiert",
+ "label.hostname": "Hostname",
+ "label.includes": "Enthält",
+ "label.insight": "Einblick",
"label.insights": "Einblicke",
"label.insights-description": "Vertiefen Sie sich mit Hilfe von Segmenten und Filtern in Ihre Daten.",
"label.is": "Ist",
+ "label.is-false": "Ist falsch",
"label.is-not": "Ist nicht",
"label.is-not-set": "Ist nicht gesetzt",
"label.is-set": "Ist gesetzt",
+ "label.is-true": "Ist wahr",
"label.join": "Beitreten",
"label.join-team": "Team beitreten",
"label.journey": "Reise",
"label.journey-description": "Verstehen Sie, wie Nutzer auf Ihrer Website navigieren.",
+ "label.journeys": "Reisen",
"label.language": "Sprache",
"label.languages": "Sprachen",
"label.laptop": "Laptop",
+ "label.last-click": "Letzter Klick",
"label.last-days": "Letzten {x} Tage",
"label.last-hours": "Letzten {x} Stunden",
"label.last-months": "Letzten {x} Monate",
@@ -109,15 +140,19 @@
"label.leave-team": "Team verlassen",
"label.less-than": "Kleiner als",
"label.less-than-equals": "Kleiner oder gleich",
+ "label.links": "Links",
"label.login": "Anmelden",
"label.logout": "Abmelden",
"label.manage": "Verwalten",
"label.manager": "Verwaltung",
"label.max": "Max",
+ "label.maximize": "Erweitern",
+ "label.medium": "Medium",
"label.member": "Mitglied",
"label.members": "Mitglieder",
"label.min": "Min",
"label.mobile": "Handy",
+ "label.model": "Model",
"label.more": "Mehr",
"label.my-account": "Mein Account",
"label.my-websites": "Meine Websites",
@@ -126,33 +161,48 @@
"label.none": "Keine",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organische Suche",
+ "label.organic-shopping": "Organisches Shopping",
+ "label.organic-social": "Organisches Social Media",
+ "label.organic-video": "Organisches Video",
"label.os": "OS",
+ "label.other": "Andere",
"label.overview": "Übersicht",
"label.owner": "Besitzer",
+ "label.page": "Seite",
"label.page-of": "Seite {current} von {total}",
"label.page-views": "Seitenaufrufe",
"label.pageTitle": "Seitentitel",
"label.pages": "Seiten",
+ "label.paid-ads": "Bezahlte Anzeigen",
+ "label.paid-search": "Bezahlte Suche",
+ "label.paid-shopping": "Bezahltes Shopping",
+ "label.paid-social": "Bezahltes Social Media",
+ "label.paid-video": "Bezahltes Video",
"label.password": "Passwort",
"label.path": "Pfad",
"label.paths": "Pfade",
+ "label.pixels": "Pixel",
"label.powered-by": "Betrieben durch {name}",
"label.previous": "Vorherig",
"label.previous-period": "Vorherige Periode",
"label.previous-year": "Vorheriges Jahr",
"label.profile": "Profil",
"label.properties": "Eigenschaften",
- "label.property": "Eigentum",
+ "label.property": "Eigenschaft",
"label.queries": "Abfragen",
"label.query": "Abfrage",
"label.query-parameters": "Abfrageparameter",
"label.realtime": "Echtzeit",
+ "label.referral": "Empfehlung",
"label.referrer": "Übermittler",
"label.referrers": "Übermittler",
"label.refresh": "Aktualisieren",
"label.regenerate": "Erneuern",
"label.region": "Region",
"label.regions": "Regionen",
+ "label.remaining": "Verbleibend",
"label.remove": "Entfernen",
"label.remove-member": "Mitglied entfernen",
"label.reports": "Berichte",
@@ -163,7 +213,6 @@
"label.retention-description": "Messen Sie die Verweildauer auf Ihrer Website, indem Sie verfolgen, wie oft die Nutzer zurückkehren.",
"label.revenue": "Umsatz",
"label.revenue-description": "Haben Sie einen Blick auf Ihre Umsätze im Laufe der Zeit.",
- "label.revenue-property": "Umsatzeigenschaften",
"label.role": "Rolle",
"label.run-query": "Abfrage starten",
"label.save": "Speichern",
@@ -171,26 +220,35 @@
"label.search": "Suche",
"label.select": "Auswählen",
"label.select-date": "Datum auswählen",
+ "label.select-filter": "Filter auswählen",
"label.select-role": "Rolle auswählen",
"label.select-website": "Website auswählen",
"label.session": "Sitzung",
+ "label.session-data": "Sitzungsdaten",
"label.sessions": "Sitzungen",
"label.settings": "Einstellungen",
+ "label.share": "Teilen",
"label.share-url": "Freigabe-URL",
"label.single-day": "Ein Tag",
+ "label.sms": "SMS",
+ "label.sources": "Quellen",
"label.start-step": "Startschritt",
"label.steps": "Schritte",
"label.sum": "Summe",
"label.tablet": "Tablet",
+ "label.tag": "Tag",
+ "label.tags": "Stichworte",
"label.team": "Team",
"label.team-id": "Team-ID",
"label.team-manager": "Team-Manager",
"label.team-member": "Team-Mitglied",
"label.team-name": "Name des Teams",
"label.team-owner": "Team-Eigentümer",
+ "label.team-settings": "Team-Einstellungen",
"label.team-view-only": "Nur für Team-Mitglieder sichtbar",
"label.team-websites": "Team-Websites",
"label.teams": "Teams",
+ "label.terms": "Bedingungen",
"label.theme": "Thema",
"label.this-month": "Diesen Monat",
"label.this-week": "Diese Woche",
@@ -213,10 +271,7 @@
"label.unknown": "Unbekannt",
"label.untitled": "Unbenannt",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Benutzer",
- "label.user-property": "Benutzereigenschaften",
"label.username": "Benutzername",
"label.users": "Benutzer",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Gestern",
"message.action-confirmation": "Schreibe {confirmation} in die Box zur bestätigung.",
"message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Gesammelte Daten",
"message.confirm-delete": "Sind Sie sich sicher, {target} zu löschen?",
"message.confirm-leave": "Sind Sie sicher, dass die {target} verlassen möchten?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.",
"message.error": "Es ist ein Fehler aufgetreten.",
"message.event-log": "{event} auf {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Zu den Einstellungen",
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
"message.invalid-domain": "Ungültige Domain",
@@ -259,10 +316,13 @@
"message.no-teams": "Bisher wurden keine Teams erstellt.",
"message.no-users": "Hier gibt es keine Benutzer.",
"message.no-websites-configured": "Es ist keine Website vorhanden.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Seite nicht gefunden.",
"message.reset-website": "Um diese Website zurückzusetzen, geben Sie zur Bestätigung {confirmation} in das Feld unten ein.",
"message.reset-website-warning": "Alle Daten für diese Website werden gelöscht, jedoch bleibt der Tracking Code bestehen.",
"message.saved": "Erfolgreich gespeichert.",
+ "message.sever-error": "Server error",
"message.share-url": "Die Statistiken Ihrer Website sind unter folgender URL öffentlich zugänglich:",
"message.team-already-member": "Sie sind bereits Mitglied des Teams.",
"message.team-not-found": "Team nicht gefunden.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Wählen Sie ein Team aus, zu dem die Website transferiert werden soll.",
"message.transfer-website": "Übertragen Sie die Eigentümerrechte zu Ihrem Account oder einem anderen Team.",
"message.triggered-event": "Ereignis ausgelöst",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Benutzer gelöscht.",
"message.viewed-page": "Seite besucht",
- "message.visitor-log": "Besucher aus {country} benutzt {browser} auf {os} {device}",
- "message.visitors-dropped-off": "Besucherverlust"
+ "message.visitor-log": "Besucher aus {country} benutzt {browser} auf {os} {device}"
}
diff --git a/src/lang/el-GR.json b/src/lang/el-GR.json
index 263ea7e9..720ff5ea 100644
--- a/src/lang/el-GR.json
+++ b/src/lang/el-GR.json
@@ -3,32 +3,47 @@
"label.actions": "Ενέργειες",
"label.activity": "Activity log",
"label.add": "Add",
+ "label.add-board": "Add board",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "Προσθήκη ιστότοπου",
"label.admin": "Διαχειριστής",
+ "label.affiliate": "Affiliate",
"label.after": "After",
"label.all": "All",
"label.all-time": "All time",
"label.analytics": "Analytics",
+ "label.apply": "Apply",
+ "label.attribution": "Attribution",
+ "label.attribution-description": "See how users engage with your marketing and what drives conversions.",
"label.average": "Average",
"label.back": "Πίσω",
"label.before": "Before",
+ "label.boards": "Boards",
"label.bounce-rate": "Ποσοστό αναπήδησης",
"label.breakdown": "Breakdown",
+ "label.behavior": "Συμπεριφορά",
"label.browser": "Browser",
"label.browsers": "Προγράμματα περιήγησης",
+ "label.campaigns": "Campaigns",
"label.cancel": "Ακύρωση",
"label.change-password": "Αλλαγή κωδικού",
+ "label.channels": "Channels",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
+ "label.cohort": "Cohort",
"label.compare": "Compare",
+ "label.compare-dates": "Compare dates",
"label.confirm": "Confirm",
"label.confirm-password": "Επιβεβαίωση κωδικού",
"label.contains": "Contains",
+ "label.content": "Content",
"label.continue": "Continue",
+ "label.conversion": "Conversion",
+ "label.conversion-rate": "Conversion rate",
+ "label.conversion-step": "Conversion step",
"label.count": "Count",
"label.countries": "Χώρες",
"label.country": "Country",
@@ -38,6 +53,7 @@
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
+ "label.currency": "Currency",
"label.current": "Current",
"label.current-password": "Τωρινός κωδικός πρόσβασης",
"label.custom-range": "Προσαρμοσμένο εύρος",
@@ -57,19 +73,26 @@
"label.details": "Details",
"label.device": "Device",
"label.devices": "Συσκευές",
+ "label.direct": "Direct",
"label.dismiss": "Dismiss",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "Does not contain",
+ "label.does-not-include": "Does not include",
+ "label.doest-not-exist": "Does not exist",
"label.domain": "Τομέας",
"label.dropoff": "Dropoff",
"label.edit": "Επεξεργασία",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
+ "label.email": "Email",
"label.enable-share-url": "Ενεργοποίηση κοινής χρήσης URL",
"label.end-step": "End Step",
"label.entry": "Entry URL",
"label.event": "Event",
"label.event-data": "Event data",
+ "label.event-name": "Event name",
"label.events": "Γεγονότα",
+ "label.exists": "Exists",
"label.exit": "Exit URL",
"label.false": "False",
"label.field": "Field",
@@ -78,29 +101,37 @@
"label.filter-combined": "Σε συνδυασμό",
"label.filter-raw": "Ακατέργαστο",
"label.filters": "Filters",
+ "label.first-click": "First click",
"label.first-seen": "First seen",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
+ "label.funnels": "Funnels",
"label.goal": "Goal",
"label.goals": "Goals",
"label.goals-description": "Track your goals for pageviews and events.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grouped",
+ "label.hostname": "Hostname",
+ "label.includes": "Includes",
+ "label.insight": "Insight",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
+ "label.is-false": "Is false",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
+ "label.is-true": "Is true",
"label.join": "Join",
"label.join-team": "Join team",
"label.journey": "Journey",
"label.journey-description": "Understand how users navigate through your website.",
+ "label.journeys": "Journeys",
"label.language": "Language",
"label.languages": "Languages",
"label.laptop": "Λάπτοπ",
+ "label.last-click": "Last click",
"label.last-days": "Τελευταίες {x} ημέρες",
"label.last-hours": "Τελευταίες {x} ώρες",
"label.last-months": "Last {x} months",
@@ -109,15 +140,19 @@
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
+ "label.links": "Links",
"label.login": "Είσοδος",
"label.logout": "Αποσύνδεση",
"label.manage": "Manage",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Expand",
+ "label.medium": "Medium",
"label.member": "Member",
"label.members": "Members",
"label.min": "Min",
"label.mobile": "Κινητό",
+ "label.model": "Model",
"label.more": "Περισσότερα",
"label.my-account": "My account",
"label.my-websites": "My websites",
@@ -126,16 +161,29 @@
"label.none": "None",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organic search",
+ "label.organic-shopping": "Organic shopping",
+ "label.organic-social": "Organic social",
+ "label.organic-video": "Organic video",
"label.os": "OS",
+ "label.other": "Other",
"label.overview": "Overview",
"label.owner": "Owner",
+ "label.page": "Page",
"label.page-of": "Page {current} of {total}",
"label.page-views": "Προβολές σελίδας",
"label.pageTitle": "Page title",
"label.pages": "Σελίδες",
+ "label.paid-ads": "Paid ads",
+ "label.paid-search": "Paid search",
+ "label.paid-shopping": "Paid shopping",
+ "label.paid-social": "Paid social",
+ "label.paid-video": "Paid video",
"label.password": "Κωδικός",
"label.path": "Path",
"label.paths": "Paths",
+ "label.pixels": "Pixels",
"label.powered-by": "Με την υποστήριξη του {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
@@ -147,12 +195,14 @@
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.realtime": "Realtime",
+ "label.referral": "Referral",
"label.referrer": "Referrer",
"label.referrers": "Παραπομπές",
"label.refresh": "Ανανέωση",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Remaining",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -163,7 +213,6 @@
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "Αποθήκευση",
@@ -171,26 +220,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Select filter",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.session": "Session",
+ "label.session-data": "Session data",
"label.sessions": "Sessions",
"label.settings": "Ρυθμίσεις",
+ "label.share": "Share",
"label.share-url": "Κοινοποίηση διεύθυνσης URL",
"label.single-day": "Ημερήσια",
+ "label.sms": "SMS",
+ "label.sources": "Sources",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "Τάμπλετ",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Team settings",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Terms",
"label.theme": "Theme",
"label.this-month": "Αυτο το μήνα",
"label.this-week": "Αυτή την εβδομάδα",
@@ -213,10 +271,7 @@
"label.unknown": "Άγνωστο",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Όνομα χρήστη",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} ενεργοί {x, plural, one {επισκέπτης} other {επισκέπτες}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το {target};",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Όλα τα σχετικά δεδομένα θα διαγραφούν επίσης.",
"message.error": "Κάτι πήγε στραβά.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Μεταβείτε στις ρυθμίσεις",
"message.incorrect-username-password": "Εσφαλμένο όνομα χρήστη / κωδικός πρόσβασης.",
"message.invalid-domain": "Μη έγκυρος τομέας",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "Δεν έχετε ρυθμίσει κανένα ιστότοπο.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Η σελίδα δεν βρέθηκε.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.saved": "Αποθηκεύτηκε επιτυχώς.",
+ "message.sever-error": "Server error",
"message.share-url": "Αυτό είναι το κοινόχρηστο URL για το {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}"
}
diff --git a/src/lang/en-GB.json b/src/lang/en-GB.json
index 80bbbd7b..7803dd68 100644
--- a/src/lang/en-GB.json
+++ b/src/lang/en-GB.json
@@ -3,32 +3,47 @@
"label.actions": "Actions",
"label.activity": "Activity log",
"label.add": "Add",
+ "label.add-board": "Add board",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "Add website",
"label.admin": "Administrator",
+ "label.affiliate": "Affiliate",
"label.after": "After",
"label.all": "All",
"label.all-time": "All time",
"label.analytics": "Analytics",
+ "label.apply": "Apply",
+ "label.attribution": "Attribution",
+ "label.attribution-description": "See how users engage with your marketing and what drives conversions.",
"label.average": "Average",
"label.back": "Back",
"label.before": "Before",
+ "label.behavior": "Behavior",
+ "label.boards": "Boards",
"label.bounce-rate": "Bounce rate",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.browsers": "Browsers",
+ "label.campaigns": "Campaigns",
"label.cancel": "Cancel",
"label.change-password": "Change password",
+ "label.channels": "Channels",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
+ "label.cohort": "Cohort",
"label.compare": "Compare",
+ "label.compare-dates": "Compare dates",
"label.confirm": "Confirm",
"label.confirm-password": "Confirm password",
"label.contains": "Contains",
+ "label.content": "Content",
"label.continue": "Continue",
+ "label.conversion": "Conversion",
+ "label.conversion-rate": "Conversion rate",
+ "label.conversion-step": "Conversion step",
"label.count": "Count",
"label.countries": "Countries",
"label.country": "Country",
@@ -38,6 +53,7 @@
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
+ "label.currency": "Currency",
"label.current": "Current",
"label.current-password": "Current password",
"label.custom-range": "Custom range",
@@ -57,19 +73,26 @@
"label.details": "Details",
"label.device": "Device",
"label.devices": "Devices",
+ "label.direct": "Direct",
"label.dismiss": "Dismiss",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "Does not contain",
+ "label.does-not-include": "Does not include",
+ "label.doest-not-exist": "Does not exist",
"label.domain": "Domain",
"label.dropoff": "Dropoff",
"label.edit": "Edit",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
+ "label.email": "Email",
"label.enable-share-url": "Enable share URL",
"label.end-step": "End Step",
"label.entry": "Entry URL",
"label.event": "Event",
"label.event-data": "Event data",
+ "label.event-name": "Event name",
"label.events": "Events",
+ "label.exists": "Exists",
"label.exit": "Exit URL",
"label.false": "False",
"label.field": "Field",
@@ -78,29 +101,37 @@
"label.filter-combined": "Combined",
"label.filter-raw": "Raw",
"label.filters": "Filters",
+ "label.first-click": "First click",
"label.first-seen": "First seen",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
+ "label.funnels": "Funnels",
"label.goal": "Goal",
"label.goals": "Goals",
"label.goals-description": "Track your goals for pageviews and events.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grouped",
+ "label.hostname": "Hostname",
+ "label.includes": "Includes",
+ "label.insight": "Insight",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
+ "label.is-false": "Is false",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
+ "label.is-true": "Is true",
"label.join": "Join",
"label.join-team": "Join team",
"label.journey": "Journey",
"label.journey-description": "Understand how users navigate through your website.",
+ "label.journeys": "Journeys",
"label.language": "Language",
"label.languages": "Languages",
"label.laptop": "Laptop",
+ "label.last-click": "Last click",
"label.last-days": "Last {x} days",
"label.last-hours": "Last {x} hours",
"label.last-months": "Last {x} months",
@@ -109,15 +140,19 @@
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
+ "label.links": "Links",
"label.login": "Login",
"label.logout": "Logout",
"label.manage": "Manage",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Expand",
+ "label.medium": "Medium",
"label.member": "Member",
"label.members": "Members",
"label.min": "Min",
"label.mobile": "Mobile",
+ "label.model": "Model",
"label.more": "More",
"label.my-account": "My account",
"label.my-websites": "My websites",
@@ -126,16 +161,29 @@
"label.none": "None",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organic search",
+ "label.organic-shopping": "Organic shopping",
+ "label.organic-social": "Organic social",
+ "label.organic-video": "Organic video",
"label.os": "OS",
+ "label.other": "Other",
"label.overview": "Overview",
"label.owner": "Owner",
+ "label.page": "Page",
"label.page-of": "Page {current} of {total}",
"label.page-views": "Page views",
"label.pageTitle": "Page title",
"label.pages": "Pages",
+ "label.paid-ads": "Paid ads",
+ "label.paid-search": "Paid search",
+ "label.paid-shopping": "Paid shopping",
+ "label.paid-social": "Paid social",
+ "label.paid-video": "Paid video",
"label.password": "Password",
"label.path": "Path",
"label.paths": "Paths",
+ "label.pixels": "Pixels",
"label.powered-by": "Powered by {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
@@ -147,12 +195,14 @@
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.realtime": "Realtime",
+ "label.referral": "Referral",
"label.referrer": "Referrer",
"label.referrers": "Referrers",
"label.refresh": "Refresh",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Remaining",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -163,7 +213,6 @@
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "Save",
@@ -171,26 +220,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Select filter",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.session": "Session",
+ "label.session-data": "Session data",
"label.sessions": "Sessions",
"label.settings": "Settings",
+ "label.share": "Share",
"label.share-url": "Share URL",
"label.single-day": "Single day",
+ "label.sms": "SMS",
+ "label.sources": "Sources",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "Tablet",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Team settings",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Terms",
"label.theme": "Theme",
"label.this-month": "This month",
"label.this-week": "This week",
@@ -213,10 +271,7 @@
"label.unknown": "Unknown",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Username",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Are you sure you want to delete {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "All associated data will be deleted as well.",
"message.error": "Something went wrong.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Go to settings",
"message.incorrect-username-password": "Incorrect username/password.",
"message.invalid-domain": "Invalid domain",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "You don't have any websites configured.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Page not found.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.saved": "Saved successfully.",
+ "message.sever-error": "Server error",
"message.share-url": "This is the publicly shared URL for {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}"
}
diff --git a/src/lang/en-US.json b/src/lang/en-US.json
index 8fe33145..3e588f50 100644
--- a/src/lang/en-US.json
+++ b/src/lang/en-US.json
@@ -3,32 +3,46 @@
"label.actions": "Actions",
"label.activity": "Activity",
"label.add": "Add",
+ "label.add-board": "Add board",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "Add website",
- "label.admin": "Administrator",
+ "label.admin": "Admin",
+ "label.affiliate": "Affiliate",
"label.after": "After",
"label.all": "All",
"label.all-time": "All time",
"label.analytics": "Analytics",
+ "label.apply": "Apply",
+ "label.attribution": "Attribution",
+ "label.attribution-description": "See how users engage with your marketing and what drives conversions.",
"label.average": "Average",
"label.back": "Back",
"label.before": "Before",
+ "label.boards": "Boards",
"label.bounce-rate": "Bounce rate",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.browsers": "Browsers",
+ "label.campaigns": "Campaigns",
"label.cancel": "Cancel",
"label.change-password": "Change password",
+ "label.channels": "Channels",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
+ "label.cohort": "Cohort",
"label.compare": "Compare",
+ "label.compare-dates": "Compare dates",
"label.confirm": "Confirm",
"label.confirm-password": "Confirm password",
"label.contains": "Contains",
+ "label.content": "Content",
"label.continue": "Continue",
+ "label.conversion": "Conversion",
+ "label.conversion-rate": "Conversion rate",
+ "label.conversion-step": "Conversion step",
"label.count": "Count",
"label.countries": "Countries",
"label.country": "Country",
@@ -38,6 +52,7 @@
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
+ "label.currency": "Currency",
"label.current": "Current",
"label.current-password": "Current password",
"label.custom-range": "Custom range",
@@ -57,20 +72,27 @@
"label.details": "Details",
"label.device": "Device",
"label.devices": "Devices",
+ "label.direct": "Direct",
"label.dismiss": "Dismiss",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "Does not contain",
+ "label.does-not-include": "Does not include",
+ "label.doest-not-exist": "Does not exist",
"label.domain": "Domain",
"label.dropoff": "Dropoff",
"label.edit": "Edit",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
+ "label.email": "Email",
"label.enable-share-url": "Enable share URL",
"label.end-step": "End Step",
- "label.entry": "Entry path",
+ "label.entry": "Entry page",
"label.event": "Event",
"label.event-data": "Event data",
+ "label.event-name": "Event name",
"label.events": "Events",
- "label.exit": "Exit path",
+ "label.exists": "Exists",
+ "label.exit": "Exit page",
"label.false": "False",
"label.field": "Field",
"label.fields": "Fields",
@@ -78,29 +100,37 @@
"label.filter-combined": "Combined",
"label.filter-raw": "Raw",
"label.filters": "Filters",
+ "label.first-click": "First click",
"label.first-seen": "First seen",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
+ "label.funnels": "Funnels",
"label.goal": "Goal",
"label.goals": "Goals",
"label.goals-description": "Track your goals for pageviews and events.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grouped",
+ "label.hostname": "Hostname",
+ "label.includes": "Includes",
+ "label.insight": "Insight",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
+ "label.is-false": "Is false",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
+ "label.is-true": "Is true",
"label.join": "Join",
"label.join-team": "Join team",
"label.journey": "Journey",
"label.journey-description": "Understand how users navigate through your website.",
+ "label.journeys": "Journeys",
"label.language": "Language",
"label.languages": "Languages",
"label.laptop": "Laptop",
+ "label.last-click": "Last click",
"label.last-days": "Last {x} days",
"label.last-hours": "Last {x} hours",
"label.last-months": "Last {x} months",
@@ -109,15 +139,19 @@
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
+ "label.links": "Links",
"label.login": "Login",
"label.logout": "Logout",
"label.manage": "Manage",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Maximize",
+ "label.medium": "Medium",
"label.member": "Member",
"label.members": "Members",
"label.min": "Min",
"label.mobile": "Mobile",
+ "label.model": "Model",
"label.more": "More",
"label.my-account": "My account",
"label.my-websites": "My websites",
@@ -126,16 +160,29 @@
"label.none": "None",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organic search",
+ "label.organic-shopping": "Organic shopping",
+ "label.organic-social": "Organic social",
+ "label.organic-video": "Organic video",
"label.os": "OS",
+ "label.other": "Other",
"label.overview": "Overview",
"label.owner": "Owner",
+ "label.page": "Page",
"label.page-of": "Page {current} of {total}",
"label.page-views": "Page views",
"label.pageTitle": "Page title",
"label.pages": "Pages",
+ "label.paid-ads": "Paid ads",
+ "label.paid-search": "Paid search",
+ "label.paid-shopping": "Paid shopping",
+ "label.paid-social": "Paid social",
+ "label.paid-video": "Paid video",
"label.password": "Password",
"label.path": "Path",
"label.paths": "Paths",
+ "label.pixels": "Pixels",
"label.powered-by": "Powered by {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
@@ -147,12 +194,14 @@
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.realtime": "Realtime",
+ "label.referral": "Referral",
"label.referrer": "Referrer",
"label.referrers": "Referrers",
"label.refresh": "Refresh",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Remaining",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -162,8 +211,7 @@
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue-description": "Look into your revenue data and how users are spending.",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "Save",
@@ -171,26 +219,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Select filter",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.session": "Session",
+ "label.session-data": "Session data",
"label.sessions": "Sessions",
"label.settings": "Settings",
+ "label.share": "Share",
"label.share-url": "Share URL",
"label.single-day": "Single day",
+ "label.sms": "SMS",
+ "label.sources": "Sources",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "Tablet",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Team settings",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Terms",
"label.theme": "Theme",
"label.this-month": "This month",
"label.this-week": "This week",
@@ -213,10 +270,7 @@
"label.unknown": "Unknown",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Username",
"label.users": "Users",
"label.utm": "UTM",
@@ -235,8 +289,10 @@
"label.websites": "Websites",
"label.window": "Window",
"label.yesterday": "Yesterday",
+ "label.behavior": "Behavior",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Are you sure you want to delete {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "All website data will be deleted.",
"message.error": "Something went wrong.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Go to settings",
"message.incorrect-username-password": "Incorrect username and/or password.",
"message.invalid-domain": "Invalid domain. Do not include http/https.",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "You do not have any websites configured.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Page not found",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your settings will remain intact.",
"message.saved": "Saved.",
+ "message.sever-error": "Server error",
"message.share-url": "Your website stats are publicly available at the following URL:",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}"
}
diff --git a/src/lang/es-ES.json b/src/lang/es-ES.json
index 5f930be7..e3a4d38d 100644
--- a/src/lang/es-ES.json
+++ b/src/lang/es-ES.json
@@ -3,32 +3,46 @@
"label.actions": "Acciones",
"label.activity": "Registro de actividad",
"label.add": "Añadir",
+ "label.add-board": "Añadir tablero",
"label.add-description": "Añadir descripción",
"label.add-member": "Añadir miembro",
"label.add-step": "Añadir paso",
"label.add-website": "Nuevo sitio web",
"label.admin": "Administrador",
+ "label.affiliate": "Afiliado",
"label.after": "Después",
"label.all": "Todos",
"label.all-time": "Todos los tiempos",
"label.analytics": "Analíticas",
+ "label.apply": "Aplicar",
+ "label.attribution": "Atribución",
+ "label.attribution-description": "Vea cómo los usuarios interactúan con su marketing y qué impulsa las conversiones.",
"label.average": "Media",
"label.back": "Atrás",
"label.before": "Antes",
+ "label.boards": "Tableros",
"label.bounce-rate": "Porcentaje de rebote",
"label.breakdown": "Desglose",
"label.browser": "Navegador",
"label.browsers": "Navegadores",
+ "label.campaigns": "Campañas",
"label.cancel": "Cancelar",
"label.change-password": "Cambiar contraseña",
+ "label.channels": "Canales",
"label.cities": "Ciudades",
"label.city": "Ciudad",
"label.clear-all": "Limpiar todo",
+ "label.cohort": "Cohorte",
"label.compare": "Comparar",
+ "label.compare-dates": "Comparar fechas",
"label.confirm": "Confirmar",
"label.confirm-password": "Confirmar contraseña",
"label.contains": "Contiene",
+ "label.content": "Contenido",
"label.continue": "Continuar",
+ "label.conversion": "Conversión",
+ "label.conversion-rate": "Tasa de conversión",
+ "label.conversion-step": "Paso de conversión",
"label.count": "Contar",
"label.countries": "Países",
"label.country": "País",
@@ -38,6 +52,7 @@
"label.create-user": "Crear usuario",
"label.created": "Creado",
"label.created-by": "Creado por",
+ "label.currency": "Moneda",
"label.current": "Actual",
"label.current-password": "Contraseña actual",
"label.custom-range": "Intervalo personalizado",
@@ -57,19 +72,26 @@
"label.details": "Detalles",
"label.device": "Dispositivo",
"label.devices": "Dispositivos",
+ "label.direct": "Directo",
"label.dismiss": "Cerrar",
+ "label.distinct-id": "ID distinto",
"label.does-not-contain": "No contiene",
+ "label.does-not-include": "No incluye",
+ "label.doest-not-exist": "No existe",
"label.domain": "Dominio",
"label.dropoff": "Abandono",
"label.edit": "Editar",
"label.edit-dashboard": "Editar panel",
"label.edit-member": "Editar miembro",
+ "label.email": "Email",
"label.enable-share-url": "Habilitar compartir URL",
"label.end-step": "Paso final",
"label.entry": "URL de entrada",
"label.event": "Evento",
"label.event-data": "Datos de evento",
+ "label.event-name": "Nombre del evento",
"label.events": "Eventos",
+ "label.exists": "Existe",
"label.exit": "URL de salida",
"label.false": "Falso",
"label.field": "Campo",
@@ -78,81 +100,108 @@
"label.filter-combined": "Combinado",
"label.filter-raw": "En crudo",
"label.filters": "Filtros",
- "label.first-seen": "First seen",
+ "label.first-click": "Primer clic",
+ "label.first-seen": "Primera vez visto",
"label.funnel": "Embudo",
"label.funnel-description": "Comprender conversión y abandono de usuarios.",
+ "label.funnels": "Embudos",
"label.goal": "Objetivo",
"label.goals": "Objetivos",
"label.goals-description": "Realice un seguimiento de sus objetivos de páginas vistas y eventos.",
"label.greater-than": "Mayor que",
"label.greater-than-equals": "Mayor que o igual a",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
+ "label.grouped": "Agrupado",
+ "label.hostname": "Nombre de host",
+ "label.includes": "Incluye",
+ "label.insight": "Perspectiva",
+ "label.insights": "Perspectivas",
"label.insights-description": "Profundice en sus datos mediante el uso de segmentos y filtros.",
"label.is": "Es igual a",
+ "label.is-false": "Es falso",
"label.is-not": "No es igual a",
"label.is-not-set": "No está establecido",
"label.is-set": "Está establecido",
+ "label.is-true": "Es verdadero",
"label.join": "Unir",
"label.join-team": "Unirse al equipo",
"label.journey": "Viaje",
"label.journey-description": "Comprenda cómo los usuarios navegan por su sitio web.",
+ "label.journeys": "Viajes",
"label.language": "Idioma",
"label.languages": "Idiomas",
"label.laptop": "Portátil",
+ "label.last-click": "Último clic",
"label.last-days": "Últimos {x} días",
"label.last-hours": "Últimas {x} horas",
"label.last-months": "Últimos {x} meses",
- "label.last-seen": "Last seen",
+ "label.last-seen": "Visto por última vez",
"label.leave": "Abandonar",
"label.leave-team": "Abandonar equipo",
"label.less-than": "Menor que",
"label.less-than-equals": "Menor que o igual a",
+ "label.links": "Enlaces",
"label.login": "Iniciar sesión",
"label.logout": "Cerrar sesión",
"label.manage": "Administrar",
- "label.manager": "Manager",
- "label.max": "Max",
+ "label.manager": "Gerente",
+ "label.max": "Máximo",
+ "label.maximize": "Expandir",
+ "label.medium": "Medio",
"label.member": "Miembro",
"label.members": "Miembros",
- "label.min": "Min",
+ "label.min": "Mínimo",
"label.mobile": "Móvil",
+ "label.model": "Modelo",
"label.more": "Más",
"label.my-account": "Mi cuenta",
"label.my-websites": "Mis sitios web",
"label.name": "Nombre",
"label.new-password": "Nueva contraseña",
"label.none": "Ninguno",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
+ "label.number-of-records": "{x} {x, plural, one {registro} other {registros}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Búsqueda orgánica",
+ "label.organic-shopping": "Compras orgánicas",
+ "label.organic-social": "Social orgánico",
+ "label.organic-video": "Video orgánico",
"label.os": "Sistema",
+ "label.other": "Otro",
"label.overview": "Resumen",
"label.owner": "Propietario",
+ "label.page": "Página",
"label.page-of": "Página {current} de {total}",
"label.page-views": "Vistas",
"label.pageTitle": "Título de página",
"label.pages": "Páginas",
+ "label.paid-ads": "Anuncios pagados",
+ "label.paid-search": "Búsqueda pagada",
+ "label.paid-shopping": "Compras pagadas",
+ "label.paid-social": "Social pagado",
+ "label.paid-video": "Video pagado",
"label.password": "Contraseña",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Ruta",
+ "label.paths": "Rutas",
+ "label.pixels": "Píxeles",
"label.powered-by": "Analíticas de {name}",
"label.previous": "Anterior",
"label.previous-period": "Periodo anterior",
"label.previous-year": "Año anterior",
"label.profile": "Perfil",
- "label.properties": "Properties",
+ "label.properties": "Propiedades",
"label.property": "Propiedad",
"label.queries": "Consultas",
"label.query": "Consulta",
- "label.query-parameters": "Parámetros de petición",
+ "label.query-parameters": "Parámetros de consulta",
"label.realtime": "Tiempo real",
+ "label.referral": "Referencia",
"label.referrer": "Referido",
"label.referrers": "Referido desde",
"label.refresh": "Actualizar",
"label.regenerate": "Regenerar",
"label.region": "Región",
"label.regions": "Regiones",
+ "label.remaining": "Restante",
"label.remove": "Quitar",
"label.remove-member": "Eliminar miembro",
"label.reports": "Informes",
@@ -161,9 +210,9 @@
"label.reset-website": "Reiniciar analíticas",
"label.retention": "Retención",
"label.retention-description": "Medir la frecuencia con la que los usuarios vuelven a tu sitio web.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Ganancias",
+ "label.revenue-description": "Analice sus ganancias a lo largo del tiempo.",
+ "label.revenue-property": "Propiedad de ganancias",
"label.role": "Rol",
"label.run-query": "Ejecutar consulta",
"label.save": "Guardar",
@@ -171,26 +220,34 @@
"label.search": "Buscar",
"label.select": "Seleccionar",
"label.select-date": "Seleccionar fecha",
+ "label.select-filter": "Seleccionar filtro",
"label.select-role": "Seleccionar rol",
"label.select-website": "Seleccionar sitio web",
- "label.session": "Session",
+ "label.session": "Sesión",
"label.sessions": "Sesiones",
"label.settings": "Ajustes",
+ "label.share": "Compartir",
"label.share-url": "Compartir URL",
"label.single-day": "Un solo día",
- "label.start-step": "Paso inical",
+ "label.sms": "SMS",
+ "label.sources": "Fuentes",
+ "label.start-step": "Paso inicial",
"label.steps": "Pasos",
"label.sum": "Suma",
"label.tablet": "Tableta",
+ "label.tag": "Etiqueta",
+ "label.tags": "Etiquetas",
"label.team": "Equipo",
"label.team-id": "ID del equipo",
"label.team-manager": "Jefe de equipo",
"label.team-member": "Miembro del equipo",
"label.team-name": "Nombre del equipo",
"label.team-owner": "Admin. del equipo",
+ "label.team-settings": "Configuración del equipo",
"label.team-view-only": "Vista solo del equipo",
"label.team-websites": "Sitios web del equipo",
"label.teams": "Equipos",
+ "label.terms": "Términos",
"label.theme": "Tema",
"label.this-month": "Este mes",
"label.this-week": "Esta semana",
@@ -202,21 +259,19 @@
"label.total": "Total",
"label.total-records": "Total de registros",
"label.tracking-code": "Código de rastreo",
- "label.transactions": "Transactions",
+ "label.transactions": "Transacciones",
"label.transfer": "Transferir",
"label.transfer-website": "Transferir sitio web",
"label.true": "Verdadero",
"label.type": "Tipo",
"label.unique": "Único",
"label.unique-visitors": "Visitantes únicos",
- "label.uniqueCustomers": "Unique Customers",
+ "label.uniqueCustomers": "Clientes únicos",
"label.unknown": "Desconocida",
"label.untitled": "Sin título",
"label.update": "Actualizar",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Usuario",
- "label.user-property": "User Property",
+ "label.user-property": "Propiedad de usuario",
"label.username": "Nombre de usuario",
"label.users": "Usuarios",
"label.utm": "UTM",
@@ -235,8 +290,10 @@
"label.websites": "Sitios web",
"label.window": "Ventana",
"label.yesterday": "Ayer",
+ "label.behavior": "Comportamiento",
"message.action-confirmation": "Escriba {confirmation} en el cuadro a continuación para confirmar.",
"message.active-users": "{x} {x, plural, one {activo} other {activos}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Datos obtenidos",
"message.confirm-delete": "¿Seguro que quieres eliminar {target}?",
"message.confirm-leave": "¿Seguro que quieres abandonar {target}?",
@@ -246,6 +303,7 @@
"message.delete-website-warning": "Toda la información relacionada será eliminada.",
"message.error": "Algo falló.",
"message.event-log": "{event} en {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Ir a la configuración",
"message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.",
"message.invalid-domain": "Dominio inválido",
@@ -259,10 +317,13 @@
"message.no-teams": "No has creado ningún equipo.",
"message.no-users": "No hay usuarios.",
"message.no-websites-configured": "No tienes ningún sitio web configurado.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Página no encontrada",
"message.reset-website": "Para reiniciar este sitio web, escribe {confirmation} a continuación para confirmar.",
"message.reset-website-warning": "Todas las estadísticas de esta página serán eliminadas, pero el código de rastreo permanecerá intacto.",
"message.saved": "Guardado",
+ "message.sever-error": "Server error",
"message.share-url": "Esta es la URL pública para {target}.",
"message.team-already-member": "Ya eres miembro de este equipo.",
"message.team-not-found": "Equipo no encontrado.",
@@ -272,8 +333,8 @@
"message.transfer-user-website-to-team": "Seleccione el equipo al que transferir este sitio web.",
"message.transfer-website": "Seleccione el equipo al que transferir este sitio web.",
"message.triggered-event": "Evento lanzado",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Usuario eliminado.",
"message.viewed-page": "Página vista",
- "message.visitor-log": "Visitante desde {country} usando {browser} en {os} {device}",
- "message.visitors-dropped-off": "Los visitantes salieron"
+ "message.visitor-log": "Visitante desde {country} usando {browser} en {os} {device}"
}
diff --git a/src/lang/fa-IR.json b/src/lang/fa-IR.json
index 493ca487..96b3da9b 100644
--- a/src/lang/fa-IR.json
+++ b/src/lang/fa-IR.json
@@ -3,32 +3,47 @@
"label.actions": "اقدامات",
"label.activity": "فعالیت",
"label.add": "افزودن",
+ "label.add-board": "افزودن برد",
"label.add-description": "افزودن توضیحات",
"label.add-member": "افزودن عضو",
"label.add-step": "افزودن قدم",
"label.add-website": "افزودن وبسایت",
"label.admin": "مدیر",
+ "label.affiliate": "همکار فروش",
"label.after": "بعد",
"label.all": "همه",
"label.all-time": "تمامی زمانها",
"label.analytics": "تجزیه و تحلیل",
+ "label.apply": "اعمال",
+ "label.attribution": "انتساب",
+ "label.attribution-description": "ببینید کاربران چگونه با بازاریابی شما تعامل دارند و چه چیزی باعث تبدیل میشود.",
"label.average": "میانگین",
"label.back": "بازگشت",
"label.before": "قبل از",
+ "label.behavior": "رفتار",
+ "label.boards": "بردها",
"label.bounce-rate": "نرخ ریزش",
"label.breakdown": "تفکیک",
"label.browser": "مرورگر",
"label.browsers": "مرورگرها",
+ "label.campaigns": "کمپینها",
"label.cancel": "انصراف",
"label.change-password": "تغییر رمز",
+ "label.channels": "کانالها",
"label.cities": "شهرها",
"label.city": "شهر",
"label.clear-all": "پاک کردن همه",
+ "label.cohort": "گروه",
"label.compare": "مقایسه",
+ "label.compare-dates": "مقایسه تاریخها",
"label.confirm": "تأیید",
"label.confirm-password": "تأیید رمز",
"label.contains": "شامل",
+ "label.content": "محتوا",
"label.continue": "ادامه",
+ "label.conversion": "تبدیل",
+ "label.conversion-rate": "نرخ تبدیل",
+ "label.conversion-step": "گام تبدیل",
"label.count": "تعداد",
"label.countries": "کشورها",
"label.country": "کشور",
@@ -38,6 +53,7 @@
"label.create-user": "ایجاد کاربر",
"label.created": "ایجاد شد",
"label.created-by": "ایجاد شده توسط",
+ "label.currency": "واحد پول",
"label.current": "فعلی",
"label.current-password": "رمز فعلی",
"label.custom-range": "محدودهی دلخواه",
@@ -57,19 +73,26 @@
"label.details": "جزئیات",
"label.device": "دستگاه",
"label.devices": "دستگاهها",
+ "label.direct": "مستقیم",
"label.dismiss": "رد کردن",
+ "label.distinct-id": "شناسه یکتا",
"label.does-not-contain": "شامل نمیشود",
+ "label.does-not-include": "شامل نمیشود",
+ "label.doest-not-exist": "وجود ندارد",
"label.domain": "دامنه",
"label.dropoff": "رها کردن",
"label.edit": "ویرایش",
"label.edit-dashboard": "ویرایش داشبورد",
"label.edit-member": "ویرایش عضو",
+ "label.email": "ایمیل",
"label.enable-share-url": "فعال کردن اشتراک گذاری آدرس اینترنتی",
"label.end-step": "قدم پایانی",
"label.entry": "آدرس اینترنتی ورودی",
"label.event": "رویداد",
"label.event-data": "دادههای رویداد",
+ "label.event-name": "نام رویداد",
"label.events": "رویدادها",
+ "label.exists": "وجود دارد",
"label.exit": "آدرس اینترنتی خروجی",
"label.false": "نادرست",
"label.field": "فیلد",
@@ -78,29 +101,37 @@
"label.filter-combined": "ترکیب شده",
"label.filter-raw": "خام",
"label.filters": "فیلترها",
+ "label.first-click": "اولین کلیک",
"label.first-seen": "اولین بار دیده شده",
"label.funnel": "فانل",
"label.funnel-description": "نرخ تبدیل و رها کردن کاربران را درک کنید.",
+ "label.funnels": "قیفها",
"label.goal": "هدف",
"label.goals": "اهداف",
"label.goals-description": "اهداف خود را برای بازدید از صفحه و رویدادها دنبال کنید.",
"label.greater-than": "بزرگتر از",
"label.greater-than-equals": "بزرگتر یا مساوی",
- "label.host": "هاست",
- "label.hosts": "هاستها",
+ "label.grouped": "گروهبندی شده",
+ "label.hostname": "نام میزبان",
+ "label.includes": "شامل میشود",
+ "label.insight": "بینش",
"label.insights": "بینش",
"label.insights-description": "با استفاده از بخشها و فیلترها، در دادههای خود عمیقتر شوید.",
"label.is": "برابر است با",
+ "label.is-false": "نادرست است",
"label.is-not": "برابر نیست با",
"label.is-not-set": "تعیین نشده",
"label.is-set": "تعیین شده",
+ "label.is-true": "درست است",
"label.join": "پیوستن",
"label.join-team": "پیوستن به تیم",
"label.journey": "مسیر",
"label.journey-description": "درک کنید که کاربران چگونه در وبسایت شما حرکت می کنند.",
+ "label.journeys": "مسیرها",
"label.language": "زبان",
"label.languages": "زبانها",
"label.laptop": "لپتاپ",
+ "label.last-click": "آخرین کلیک",
"label.last-days": "{x} روز گذشته",
"label.last-hours": "{x} ساعت گذشته",
"label.last-months": "{x} ماه گذشته",
@@ -109,15 +140,19 @@
"label.leave-team": "ترک تیم",
"label.less-than": "کمتر از",
"label.less-than-equals": "کمتر یا مساوی",
+ "label.links": "لینکها",
"label.login": "ورود",
"label.logout": "خروج",
"label.manage": "مدیریت",
"label.manager": "مدیر",
"label.max": "حداکثر",
+ "label.maximize": "گسترش",
+ "label.medium": "متوسط",
"label.member": "عضو",
"label.members": "اعضا",
"label.min": "حداقل",
"label.mobile": "موبایل",
+ "label.model": "مدل",
"label.more": "بیشتر",
"label.my-account": "حساب کاربری من",
"label.my-websites": "وبسایتهای من",
@@ -126,16 +161,29 @@
"label.none": "هیچ",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "تایید",
+ "label.online": "Online",
+ "label.organic-search": "جستجوی ارگانیک",
+ "label.organic-shopping": "خرید ارگانیک",
+ "label.organic-social": "شبکه اجتماعی ارگانیک",
+ "label.organic-video": "ویدیوی ارگانیک",
"label.os": "سیستم عامل",
+ "label.other": "سایر",
"label.overview": "بررسی کلی",
"label.owner": "مالک",
+ "label.page": "صفحه",
"label.page-of": "صفحه {current} از {total}",
"label.page-views": "بازدید صفحه",
"label.pageTitle": "عنوان صفحه",
"label.pages": "صفحهها",
+ "label.paid-ads": "تبلیغات پولی",
+ "label.paid-search": "جستجوی پولی",
+ "label.paid-shopping": "خرید پولی",
+ "label.paid-social": "شبکه اجتماعی پولی",
+ "label.paid-video": "ویدیوی پولی",
"label.password": "رمز",
"label.path": "مسیر",
"label.paths": "مسیرها",
+ "label.pixels": "پیکسلها",
"label.powered-by": "قدرت گرفته توسط {name}",
"label.previous": "قبلی",
"label.previous-period": "دورهی قبل",
@@ -147,12 +195,14 @@
"label.query": "کوئری",
"label.query-parameters": "پارامترهای کوئری",
"label.realtime": "آمار زنده",
+ "label.referral": "ارجاع",
"label.referrer": "ارجاع دهنده",
"label.referrers": "ارجاع دهندگان",
"label.refresh": "بهروزرسانی",
"label.regenerate": "تولید مجدد",
"label.region": "منطقه",
"label.regions": "مناطق",
+ "label.remaining": "باقیمانده",
"label.remove": "حذف",
"label.remove-member": "حذف عضو",
"label.reports": "گزارشها",
@@ -163,7 +213,6 @@
"label.retention-description": "چسبندگی وبسایت خود را با دنبال کردن تعداد دفعات بازگشت کاربران اندازهگیری کنید.",
"label.revenue": "درآمد",
"label.revenue-description": "به درآمد خود در طول زمان نگاه کنید.",
- "label.revenue-property": "ویژگی درآمد",
"label.role": "نقش",
"label.run-query": "اجرای کوئری",
"label.save": "ذخیره",
@@ -171,26 +220,35 @@
"label.search": "جستجو",
"label.select": "انتخاب",
"label.select-date": "انتخاب تاریخ",
+ "label.select-filter": "انتخاب فیلتر",
"label.select-role": "انتخاب نقش",
"label.select-website": "انتخاب وبسایت",
"label.session": "نشست",
+ "label.session-data": "دادههای نشست",
"label.sessions": "نشستها",
"label.settings": "تنظیمات",
+ "label.share": "اشتراکگذاری",
"label.share-url": "به اشتراک گذاری آدرس اینترنتی",
"label.single-day": "یک روز",
+ "label.sms": "SMS",
+ "label.sources": "منابع",
"label.start-step": "قدم شروع",
"label.steps": "قدمها",
"label.sum": "جمع",
"label.tablet": "تبلت",
+ "label.tag": "برچسب",
+ "label.tags": "برچسبها",
"label.team": "تیم",
"label.team-id": "شناسه تیم",
"label.team-manager": "مدیر تیم",
"label.team-member": "عضو تیم",
"label.team-name": "نام تیم",
"label.team-owner": "مالک تیم",
+ "label.team-settings": "تنظیمات تیم",
"label.team-view-only": "فقط مشاهدهی تیم",
"label.team-websites": "وبسایتهای تیم",
"label.teams": "تیمها",
+ "label.terms": "شرایط",
"label.theme": "تم",
"label.this-month": "این ماه",
"label.this-week": "این هفته",
@@ -213,10 +271,7 @@
"label.unknown": "ناشناخته",
"label.untitled": "بدون عنوان",
"label.update": "بهروزرسانی",
- "label.url": "آدرس اینترنتی",
- "label.urls": "آدرسهای اینترنتی",
"label.user": "کاربر",
- "label.user-property": "ویژگی کاربر",
"label.username": "نام کاربری",
"label.users": "کاربران",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "دیروز",
"message.action-confirmation": "برای تأیید این عملیات، لطفاً {confirmation} را تایپ کنید.",
"message.active-users": "{x} فعلی {x, plural, one {یک} other {از میان}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "دادههای جمعآوری شده",
"message.confirm-delete": "آیا مطمئن هستید میخواهید {target} را حذف کنید؟",
"message.confirm-leave": "آیا مطمئن هستید میخواهید از {target} خارج شوید؟",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "همهی دادههای وبسایت هم حذف خواهد شد.",
"message.error": "مشکلی پیش آمده است.",
"message.event-log": "{event} در {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "رفتن به تنظیمات",
"message.incorrect-username-password": "نام کاربری / رمز نادرست است.",
"message.invalid-domain": "دامنه نامعتبر است.",
@@ -259,10 +316,13 @@
"message.no-teams": "شما هیچ تیمی را ایجاد نکردهاید.",
"message.no-users": "هیچ کاربری وجود ندارد.",
"message.no-websites-configured": "شما هیچ وبسایتی را پیکربندی نکردهاید.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "صفحه یافت نشد.",
"message.reset-website": "برای بازنشانی وبسایت، لطفاً {confirmation} را تایپ کنید.",
"message.reset-website-warning": "تمامی آمارهای این وبسایت حذف خواهد شد اما کدهای رهگیری بدون تغییر باقی میماند.",
"message.saved": "ذخیره شد.",
+ "message.sever-error": "Server error",
"message.share-url": "آمار وبسایت شما به صورت عمومی در آدرس زیر قابل مشاهده است.",
"message.team-already-member": "شما از قبل عضو این تیم هستید.",
"message.team-not-found": "تیم یافت نشد.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "تیم مورد نظر را برای انتقال وبسایت انتخاب کنید.",
"message.transfer-website": "مالکیت وبسایت را به حساب خودت یا یک تیم دیگر منتقل کنید.",
"message.triggered-event": "رویداد فعال شده",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "کاربر حذف شد.",
"message.viewed-page": "صفحه مشاهده شد",
- "message.visitor-log": "بازدیدکننده از کشور {country} با مروگر {browser} در {os} {device}",
- "message.visitors-dropped-off": "ریزش بازدیدکنندهها"
+ "message.visitor-log": "بازدیدکننده از کشور {country} با مروگر {browser} در {os} {device}"
}
diff --git a/src/lang/fi-FI.json b/src/lang/fi-FI.json
index a47df265..daaa62f0 100644
--- a/src/lang/fi-FI.json
+++ b/src/lang/fi-FI.json
@@ -1,158 +1,207 @@
{
- "label.access-code": "Access code",
+ "label.access-code": "Pääsykoodi",
"label.actions": "Toiminnat",
- "label.activity": "Activity log",
- "label.add": "Add",
- "label.add-description": "Add description",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.activity": "Toimintaloki",
+ "label.add": "Lisää",
+ "label.add-board": "Lisää taulu",
+ "label.add-description": "Lisää kuvaus",
+ "label.add-member": "Lisää jäsen",
+ "label.add-step": "Lisää vaihe",
"label.add-website": "Lisää verkkosivu",
"label.admin": "Järjestelmänvalvoja",
- "label.after": "After",
+ "label.affiliate": "Kumppani",
+ "label.after": "Jälkeen",
"label.all": "Kaikki",
"label.all-time": "Alusta lähtien",
- "label.analytics": "Analytics",
- "label.average": "Average",
+ "label.analytics": "Analytiikka",
+ "label.apply": "Käytä",
+ "label.attribution": "Attribuutio",
+ "label.attribution-description": "Katso, miten käyttäjät ovat vuorovaikutuksessa markkinointisi kanssa ja mikä johtaa konversioihin.",
+ "label.average": "Keskiarvo",
"label.back": "Takaisin",
- "label.before": "Before",
+ "label.before": "Ennen",
+ "label.boards": "Taulut",
"label.bounce-rate": "Välitön poistuminen",
- "label.breakdown": "Breakdown",
- "label.browser": "Browser",
+ "label.breakdown": "Erittele",
+ "label.browser": "Selain",
"label.browsers": "Selaimet",
+ "label.campaigns": "Kampanjat",
"label.cancel": "Peruuta",
"label.change-password": "Vaihda salasana",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
+ "label.channels": "Kanavat",
+ "label.cities": "Kaupungit",
+ "label.city": "Kaupunki",
+ "label.clear-all": "Tyhjennä kaikki",
+ "label.cohort": "Kohortti",
+ "label.compare": "Vertaa",
+ "label.compare-dates": "Vertaa päivämääriä",
+ "label.confirm": "Vahvista",
"label.confirm-password": "Vahvista salasana",
"label.contains": "Contains",
- "label.continue": "Continue",
- "label.count": "Count",
+ "label.content": "Sisältö",
+ "label.continue": "Jatka",
+ "label.conversion": "Konversio",
+ "label.conversion-rate": "Konversioprosentti",
+ "label.conversion-step": "Konversiovaihe",
+ "label.count": "Lukumäärä",
"label.countries": "Maat",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.country": "Maa",
+ "label.create": "Luo",
+ "label.create-report": "Luo raportti",
+ "label.create-team": "Luo tiimi",
+ "label.create-user": "Luo käyttäjä",
+ "label.created": "Luotu",
+ "label.created-by": "Luonut",
+ "label.currency": "Valuutta",
+ "label.current": "Nykyinen",
"label.current-password": "Nykyinen salasana",
"label.custom-range": "Mukautettu ajanjakso",
"label.dashboard": "Ohjauspaneeli",
"label.data": "Data",
- "label.date": "Date",
+ "label.date": "Päivämäärä",
"label.date-range": "Ajanjakso",
- "label.day": "Day",
+ "label.day": "Päivä",
"label.default-date-range": "Oletusajanjakso",
"label.delete": "Poista",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "Poista raportti",
+ "label.delete-team": "Poista tiimi",
+ "label.delete-user": "Poista käyttäjä",
"label.delete-website": "Poista verkkosivu",
- "label.description": "Description",
+ "label.description": "Kuvaus",
"label.desktop": "Pöytäkone",
- "label.details": "Details",
- "label.device": "Device",
+ "label.details": "Tiedot",
+ "label.device": "Laite",
"label.devices": "Laitteet",
+ "label.direct": "Suora",
"label.dismiss": "Hylkää",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "Yksilöllinen ID",
+ "label.does-not-contain": "Ei sisällä",
+ "label.does-not-include": "Ei sisällä",
+ "label.doest-not-exist": "Ei ole olemassa",
"label.domain": "Verkkotunnus",
- "label.dropoff": "Dropoff",
+ "label.dropoff": "Poistuminen",
"label.edit": "Muokkaa",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
+ "label.edit-dashboard": "Muokkaa ohjauspaneelia",
+ "label.edit-member": "Muokkaa jäsentä",
+ "label.email": "Sähköposti",
"label.enable-share-url": "Ota jakamisen URL-osoite käyttöön",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event data",
+ "label.end-step": "Loppuvaihe",
+ "label.entry": "Tulo-URL",
+ "label.event": "Tapahtuma",
+ "label.event-data": "Tapahtumatiedot",
+ "label.event-name": "Tapahtuman nimi",
"label.events": "Tapahtumat",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
+ "label.exists": "On olemassa",
+ "label.exit": "Poistumis-URL",
+ "label.false": "Epätosi",
+ "label.field": "Kenttä",
+ "label.fields": "Kentät",
"label.filter": "Filter",
"label.filter-combined": "Yhdistetty",
"label.filter-raw": "Käsittelemätön",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.filters": "Suodattimet",
+ "label.first-click": "Ensimmäinen klikkaus",
+ "label.first-seen": "Ensimmäinen havainto",
+ "label.funnel": "Suppilo",
+ "label.funnel-description": "Ymmärrä käyttäjien konversio- ja poistumisprosentti.",
+ "label.funnels": "Suppilot",
+ "label.goal": "Tavoite",
+ "label.goals": "Tavoitteet",
+ "label.goals-description": "Seuraa sivun katselujen ja tapahtumien tavoitteitasi.",
+ "label.greater-than": "Suurempi kuin",
+ "label.greater-than-equals": "Suurempi tai yhtä suuri kuin",
+ "label.grouped": "Ryhmitelty",
+ "label.hostname": "Isäntänimi",
+ "label.includes": "Sisältää",
+ "label.insight": "Oivallus",
+ "label.insights": "Oivallukset",
+ "label.insights-description": "Sukella syvemmälle tietoihisi käyttämällä segmenttejä ja suodattimia.",
+ "label.is": "On",
+ "label.is-false": "On epätosi",
+ "label.is-not": "Ei ole",
+ "label.is-not-set": "Ei asetettu",
+ "label.is-set": "Asetettu",
+ "label.is-true": "On tosi",
+ "label.join": "Liity",
+ "label.join-team": "Liity tiimiin",
+ "label.journey": "Polku",
+ "label.journey-description": "Ymmärrä, miten käyttäjät navigoivat sivustollasi.",
+ "label.journeys": "Polut",
"label.language": "Kieli",
"label.languages": "Kielet",
"label.laptop": "Kannettava tietokone",
+ "label.last-click": "Viimeinen klikkaus",
"label.last-days": "Viimeisimmät {x} päivää",
"label.last-hours": "Viimeisimmät {x} tuntia",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "Viimeiset {x} kuukautta",
+ "label.last-seen": "Viimeksi nähty",
+ "label.leave": "Poistu",
+ "label.leave-team": "Poistu tiimistä",
+ "label.less-than": "Vähemmän kuin",
+ "label.less-than-equals": "Vähemmän tai yhtä suuri kuin",
+ "label.links": "Linkit",
"label.login": "Kirjaudu sisään",
"label.logout": "Kirjaudu ulos",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
+ "label.manage": "Hallinnoi",
+ "label.manager": "Päällikkö",
+ "label.max": "Maksimi",
+ "label.maximize": "Laajenna",
+ "label.medium": "Keskitaso",
+ "label.member": "Jäsen",
+ "label.members": "Jäsenet",
+ "label.min": "Minimi",
"label.mobile": "Puhelin",
+ "label.model": "Model",
"label.more": "Lisää",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "Oma tili",
+ "label.my-websites": "Omat verkkosivut",
"label.name": "Nimi",
"label.new-password": "Uusi salasana",
- "label.none": "None",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
+ "label.none": "Ei mitään",
+ "label.number-of-records": "{x} {x, plural, one {tietue} other {tietuetta}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Orgaaninen haku",
+ "label.organic-shopping": "Orgaaninen ostaminen",
+ "label.organic-social": "Orgaaninen sosiaalinen",
+ "label.organic-video": "Orgaaninen video",
"label.os": "OS",
- "label.overview": "Overview",
+ "label.other": "Muu",
+ "label.overview": "Yleiskatsaus",
"label.owner": "Omistaja",
- "label.page-of": "Page {current} of {total}",
+ "label.page": "Sivu",
+ "label.page-of": "Sivu {current} / {total}",
"label.page-views": "Sivun näyttökerrat",
- "label.pageTitle": "Page title",
+ "label.pageTitle": "Sivun otsikko",
"label.pages": "Sivut",
+ "label.paid-ads": "Maksetut mainokset",
+ "label.paid-search": "Maksettu haku",
+ "label.paid-shopping": "Maksettu ostaminen",
+ "label.paid-social": "Maksettu sosiaalinen",
+ "label.paid-video": "Maksettu video",
"label.password": "Salasana",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Polku",
+ "label.paths": "Polut",
+ "label.pixels": "Pikselit",
"label.powered-by": "Voimanlähteenä {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
"label.previous-year": "Previous year",
"label.profile": "Profiili",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
- "label.query": "Query",
- "label.query-parameters": "Query parameters",
+ "label.properties": "Ominaisuudet",
+ "label.property": "Ominaisuus",
+ "label.queries": "Kyselyt",
+ "label.query": "Kysely",
+ "label.query-parameters": "Kyselyn parametrit",
"label.realtime": "Juuri nyt",
+ "label.referral": "Viittaus",
"label.referrer": "Referrer",
"label.referrers": "Viittaajat",
"label.refresh": "Päivitä",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Jäljellä",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -161,36 +210,44 @@
"label.reset-website": "Nollaa tilastot",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Tulot",
+ "label.revenue-description": "Katso tulosi ajan mittaan.",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "Tallenna",
- "label.screens": "Screens",
+ "label.screens": "Näytöt",
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Valitse suodatin",
"label.select-role": "Select role",
"label.select-website": "Select website",
- "label.session": "Session",
+ "label.session": "Istunto",
+ "label.session-data": "Istuntotiedot",
"label.sessions": "Sessions",
"label.settings": "Asetukset",
+ "label.share": "Jaa",
"label.share-url": "Jaa URL",
"label.single-day": "Yksi päivä",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
+ "label.sms": "SMS",
+ "label.sources": "Lähteet",
+ "label.start-step": "Aloitusvaihe",
+ "label.steps": "Vaiheet",
"label.sum": "Sum",
"label.tablet": "Tabletti",
+ "label.tag": "Tunniste",
+ "label.tags": "Tunnisteet",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Tiimin asetukset",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Ehdot",
"label.theme": "Teema",
"label.this-month": "Tämä kuukausi",
"label.this-week": "Tämä viikko",
@@ -213,10 +270,7 @@
"label.unknown": "Tuntematon",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Käyttäjänimi",
"label.users": "Users",
"label.utm": "UTM",
@@ -235,8 +289,10 @@
"label.websites": "Verkkosivut",
"label.window": "Window",
"label.yesterday": "Yesterday",
+ "label.behavior": "Behavior",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} {x, plural, one {vierailija} other {vierailijaa}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Haluatko varmasti poistaa sivuston {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Kaikki siihen liittyvät tiedot poistetaan.",
"message.error": "Jotain meni pieleen.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Mene asetuksiin",
"message.incorrect-username-password": "Väärä käyttäjänimi/salasana.",
"message.invalid-domain": "Virheellinen verkkotunnus",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "Sinulla ei ole määritettyjä verkkosivustoja.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Sivua ei löydetty.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "Kaikki sivuston tilastot poistetaan, mutta seurantakoodi pysyy muuttumattomana.",
"message.saved": "Tallennettu onnistuneesti.",
+ "message.sever-error": "Server error",
"message.share-url": "Tämä on julkisesti jaettu URL sivustolle {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Vierailija maasta {country} selaimella {browser} laitteella {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Vierailija maasta {country} selaimella {browser} laitteella {os} {device}"
}
diff --git a/src/lang/fo-FO.json b/src/lang/fo-FO.json
index 23175302..6fca4258 100644
--- a/src/lang/fo-FO.json
+++ b/src/lang/fo-FO.json
@@ -1,197 +1,255 @@
{
- "label.access-code": "Access code",
+ "label.access-code": "Aðgangskoda",
"label.actions": "Gerðir",
"label.activity": "Activity log",
- "label.add": "Add",
- "label.add-description": "Add description",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.add": "Legg afturat",
+ "label.add-board": "Legg borð afturat",
+ "label.add-description": "Legg lýsing afturat",
+ "label.add-member": "Legg lim afturat",
+ "label.add-step": "Legg stig afturat",
"label.add-website": "Legg heimasíðu afturat",
"label.admin": "Fyrisitari",
- "label.after": "After",
+ "label.affiliate": "Samband",
+ "label.after": "Eftir",
"label.all": "Alt",
- "label.all-time": "All time",
- "label.analytics": "Analytics",
- "label.average": "Average",
+ "label.all-time": "Allur tíðin",
+ "label.analytics": "Greining",
+ "label.apply": "Nýt",
+ "label.attribution": "Áseting",
+ "label.attribution-description": "Síggj hvussu brúkarar samskifta við marknaðarføringina og hvat førir til umvendingar.",
+ "label.average": "Miðal",
"label.back": "Aftur",
- "label.before": "Before",
+ "label.before": "Áðrenn",
+ "label.behavior": "Atferð",
+ "label.boards": "Borð",
"label.bounce-rate": "Bounce prosenttal",
- "label.breakdown": "Breakdown",
- "label.browser": "Browser",
+ "label.breakdown": "Sundurgreining",
+ "label.browser": "Kagi",
"label.browsers": "Kagar",
+ "label.campaigns": "Herferðir",
"label.cancel": "Strika",
"label.change-password": "Skift loyniorð",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
+ "label.channels": "Rásir",
+ "label.cities": "Býir",
+ "label.city": "Býur",
+ "label.clear-all": "Tøm alt",
+ "label.cohort": "Bólkur",
+ "label.compare": "Samanber",
+ "label.compare-dates": "Samanber dato",
+ "label.confirm": "Staðfest",
"label.confirm-password": "Vátta loyniorð",
- "label.contains": "Contains",
- "label.continue": "Continue",
- "label.count": "Count",
+ "label.contains": "Inniheldur",
+ "label.content": "Innihald",
+ "label.continue": "Halt fram",
+ "label.conversion": "Umvending",
+ "label.conversion-rate": "Umvendingarprosent",
+ "label.conversion-step": "Umvendingarstigur",
+ "label.count": "Tal",
"label.countries": "Lond",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.country": "Land",
+ "label.create": "Stovna",
+ "label.create-report": "Stovna frágreiðing",
+ "label.create-team": "Stovna lið",
+ "label.create-user": "Stovna brúkara",
+ "label.created": "Stovnaður",
+ "label.created-by": "Stovnaður av",
+ "label.currency": "Gjaldoyra",
+ "label.current": "Núverandi",
"label.current-password": "Núverandi loyniorð",
"label.custom-range": "Tillaga spenni",
"label.dashboard": "Yvirlitsskíggi",
- "label.data": "Data",
- "label.date": "Date",
+ "label.data": "Dáta",
+ "label.date": "Dato",
"label.date-range": "Vel dato",
- "label.day": "Day",
+ "label.day": "Dagur",
"label.default-date-range": "Forsett dato",
"label.delete": "Sletta",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "Strika frágreiðing",
+ "label.delete-team": "Strika lið",
+ "label.delete-user": "Strika brúkara",
"label.delete-website": "Sletta heimasíðu",
- "label.description": "Description",
+ "label.description": "Lýsing",
"label.desktop": "Borðtelda",
- "label.details": "Details",
- "label.device": "Device",
+ "label.details": "Nærri upplýsingar",
+ "label.device": "Tól",
"label.devices": "Tóleindir",
+ "label.direct": "Beinleiðis",
"label.dismiss": "Lat fara",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "Sermerkt ID",
+ "label.does-not-contain": "Inniheldur ikki",
+ "label.does-not-include": "Er ikki við",
+ "label.doest-not-exist": "Er ikki til",
"label.domain": "Økisnavn",
"label.dropoff": "Dropoff",
"label.edit": "Ger broyting",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
+ "label.edit-dashboard": "Ritstjórna yvirlitsskíggja",
+ "label.edit-member": "Ritstjórna lim",
+ "label.email": "Teldupostur",
"label.enable-share-url": "Virkja deili leinki",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event data",
+ "label.end-step": "Endastigur",
+ "label.entry": "Inngangs URL",
+ "label.event": "Tiltak",
+ "label.event-data": "Tiltaksdata",
+ "label.event-name": "Tiltaksnavn",
"label.events": "Hendingar/tiltøk",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
- "label.filter": "Filter",
+ "label.exists": "Er til",
+ "label.exit": "Útgangs URL",
+ "label.false": "Falskt",
+ "label.field": "Øki",
+ "label.fields": "Øki",
+ "label.filter": "Sía",
"label.filter-combined": "Samansett",
"label.filter-raw": "Óviðgjørt",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
- "label.language": "Language",
- "label.languages": "Languages",
+ "label.filters": "Síur",
+ "label.first-click": "Fyrsta trýst",
+ "label.first-seen": "Fyrst sæddur",
+ "label.funnel": "Traktari",
+ "label.funnel-description": "Fá yvirlit yvir umvendingar og fráfall hjá brúkarum.",
+ "label.funnels": "Traktarar",
+ "label.goal": "Mál",
+ "label.goals": "Mál",
+ "label.goals-description": "Fylg við málum fyri síðuvísingar og tiltøk.",
+ "label.greater-than": "Størri enn",
+ "label.greater-than-equals": "Størri ella javnt",
+ "label.grouped": "Bólkað",
+ "label.hostname": "Vertnavn",
+ "label.includes": "Inniheldur",
+ "label.insight": "Innlit",
+ "label.insights": "Innlit",
+ "label.insights-description": "Fá meira innlit í tínar dátur við at brúka bólkar og síur.",
+ "label.is": "Er",
+ "label.is-false": "Er falskt",
+ "label.is-not": "Er ikki",
+ "label.is-not-set": "Er ikki sett",
+ "label.is-set": "Er sett",
+ "label.is-true": "Er satt",
+ "label.join": "Luttak",
+ "label.join-team": "Luttak í liði",
+ "label.journey": "Ferð",
+ "label.journey-description": "Fá yvirlit yvir hvussu brúkarar ferðast á heimasíðuni.",
+ "label.journeys": "Ferðir",
+ "label.language": "Mál",
+ "label.languages": "Mál",
"label.laptop": "Fartelda",
+ "label.last-click": "Seinasta trýst",
"label.last-days": "Seinastu {x} dagarnar",
"label.last-hours": "Seinastu {x} tímarnar",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "Seinastu {x} mánaðirnar",
+ "label.last-seen": "Síðst sæddur",
+ "label.leave": "Far burtur",
+ "label.leave-team": "Far úr liði",
+ "label.less-than": "Minni enn",
+ "label.less-than-equals": "Minni ella javnt",
+ "label.links": "Leinkjur",
"label.login": "Rita inn",
"label.logout": "Rita út",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
+ "label.manage": "Stýra",
+ "label.manager": "Stjóri",
+ "label.max": "Mest",
+ "label.maximize": "Víðka",
+ "label.medium": "Miðal",
+ "label.member": "Limur",
+ "label.members": "Limir",
+ "label.min": "Minst",
"label.mobile": "Telefon",
+ "label.model": "Model",
"label.more": "Meira",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "Mín konto",
+ "label.my-websites": "Mínar heimasíður",
"label.name": "Navn",
"label.new-password": "Nýtt loyniorð",
- "label.none": "None",
+ "label.none": "Eingin",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organisk leiting",
+ "label.organic-shopping": "Organisk keyp",
+ "label.organic-social": "Organisk sosial miðla",
+ "label.organic-video": "Organisk video",
"label.os": "OS",
- "label.overview": "Overview",
- "label.owner": "Owner",
- "label.page-of": "Page {current} of {total}",
+ "label.other": "Annað",
+ "label.overview": "Yvirlit",
+ "label.owner": "Eigari",
+ "label.page": "Síða",
+ "label.page-of": "Síða {current} av {total}",
"label.page-views": "Opnaðar síðir",
- "label.pageTitle": "Page title",
+ "label.pageTitle": "Síðuheiti",
"label.pages": "Síðir",
+ "label.paid-ads": "Goldnar lýsingar",
+ "label.paid-search": "Goldin leiting",
+ "label.paid-shopping": "Goldið keyp",
+ "label.paid-social": "Goldin sosial miðla",
+ "label.paid-video": "Goldið video",
"label.password": "Loyniorð",
- "label.path": "Path",
- "label.paths": "Paths",
- "label.powered-by": "Powered by {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.path": "Leið",
+ "label.paths": "Leiðir",
+ "label.pixels": "Pikslur",
+ "label.powered-by": "Rikið av {name}",
+ "label.previous": "Fyrra",
+ "label.previous-period": "Fyrra tíðarskeið",
+ "label.previous-year": "Fyrra ár",
"label.profile": "Vangi",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
- "label.query": "Query",
- "label.query-parameters": "Query parameters",
+ "label.properties": "Eginleikar",
+ "label.property": "Eginleiki",
+ "label.queries": "Fyrispurningar",
+ "label.query": "Fyrispurningur",
+ "label.query-parameters": "Fyrispurningsparametrar",
"label.realtime": "Beinleiðis",
- "label.referrer": "Referrer",
+ "label.referral": "Ávísing",
+ "label.referrer": "Ávísari",
"label.referrers": "Framsendingar",
- "label.refresh": "Endurskapa",
- "label.regenerate": "Regenerate",
- "label.region": "Region",
- "label.regions": "Regions",
- "label.remove": "Remove",
- "label.remove-member": "Remove member",
- "label.reports": "Reports",
- "label.required": "Kravt",
+ "label.refresh": "Dagfør",
+ "label.regenerate": "Endurskapa",
+ "label.region": "Øki",
+ "label.regions": "Øki",
+ "label.remaining": "Eftir",
+ "label.remove": "Fjern",
+ "label.remove-member": "Fjern lim",
+ "label.reports": "Frágreiðingar",
+ "label.required": "Kravið",
"label.reset": "Nulstilla",
- "label.reset-website": "Reset statistics",
- "label.retention": "Retention",
- "label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
- "label.role": "Role",
- "label.run-query": "Run query",
+ "label.reset-website": "Nulstilla heimasíðu",
+ "label.retention": "Hald",
+ "label.retention-description": "Mát hvussu ofta brúkarar koma aftur á tína síðu.",
+ "label.revenue": "Inntøka",
+ "label.revenue-description": "Fá yvirlit yvir inntøku yvir tíð.",
+ "label.role": "Leiklutur",
+ "label.run-query": "Koyr fyrispurning",
"label.save": "Goym",
- "label.screens": "Screens",
- "label.search": "Search",
- "label.select": "Select",
- "label.select-date": "Select date",
- "label.select-role": "Select role",
- "label.select-website": "Select website",
- "label.session": "Session",
- "label.sessions": "Sessions",
+ "label.screens": "Skíggjar",
+ "label.search": "Leita",
+ "label.select": "Vel",
+ "label.select-date": "Vel dato",
+ "label.select-filter": "Vel síu",
+ "label.select-role": "Vel leiklut",
+ "label.select-website": "Vel heimasíðu",
+ "label.session": "Seta",
+ "label.session-data": "Setudáta",
+ "label.sessions": "Setur",
"label.settings": "Stillingar",
+ "label.share": "Deil",
"label.share-url": "Deil leinku",
"label.single-day": "Einkultur dagur",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
- "label.sum": "Sum",
+ "label.sms": "SMS",
+ "label.sources": "Keldur",
+ "label.start-step": "Byrjanarstigur",
+ "label.steps": "Stig",
+ "label.sum": "Samanlagt",
"label.tablet": "Teldil",
- "label.team": "Team",
- "label.team-id": "Team ID",
- "label.team-manager": "Team manager",
- "label.team-member": "Team member",
- "label.team-name": "Team name",
- "label.team-owner": "Team owner",
- "label.team-view-only": "Team view only",
- "label.team-websites": "Team websites",
- "label.teams": "Teams",
- "label.theme": "Theme",
+ "label.tag": "Merki",
+ "label.tags": "Merki",
+ "label.team": "Lið",
+ "label.team-id": "Lið ID",
+ "label.team-manager": "Liðleiðari",
+ "label.team-member": "Liðlimur",
+ "label.team-name": "Liðnavn",
+ "label.team-owner": "Liðeigari",
+ "label.team-settings": "Liðstillingar",
+ "label.team-view-only": "Bert til at síggja lið",
+ "label.team-websites": "Lið heimasíður",
+ "label.teams": "Lið",
+ "label.terms": "Treytir",
+ "label.theme": "Evni",
"label.this-month": "Hendan mánan",
"label.this-week": "Hesa vikuna",
"label.this-year": "Hetta árið",
@@ -213,10 +271,7 @@
"label.unknown": "Ókent",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Brúkaranavn",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} í løtuni {x, plural, one {vitjandi} other { vitjandi }}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Ert tú sikkur at tú ynskir at strika {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Øll data ið er knýtt at verður eisini strika.",
"message.error": "Okkurt bleiv gali.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Far til stillingar",
"message.incorrect-username-password": "Skeivt brúkaranavn/loyniorð.",
"message.invalid-domain": "Ógilt økisnavn",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "Tú hevur ongar heimasíður stillaða til.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Síðan bleiv ikki funnin.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.saved": "Goymt.",
+ "message.sever-error": "Server error",
"message.share-url": "Hettar er tann almenna leinkan av {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Vitjandi frá {country} brúkar {browser} á {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Vitjandi frá {country} brúkar {browser} á {os} {device}"
}
diff --git a/src/lang/fr-FR.json b/src/lang/fr-FR.json
index 55911dcc..cd6a96bc 100644
--- a/src/lang/fr-FR.json
+++ b/src/lang/fr-FR.json
@@ -3,6 +3,7 @@
"label.actions": "Actions",
"label.activity": "Journal d'activité",
"label.add": "Ajouter",
+ "label.add-board": "Ajouter un tableau",
"label.add-description": "Ajouter une description",
"label.add-member": "Ajouter un membre",
"label.add-step": "Ajouter une étape",
@@ -12,12 +13,14 @@
"label.after": "Après",
"label.all": "Tout",
"label.all-time": "Toutes les données",
- "label.analytics": "Analytics",
+ "label.analytics": "Analytique",
+ "label.apply": "Appliquer",
"label.attribution": "Attribution",
"label.attribution-description": "Découvrez comment les utilisateurs s'engagent avec votre marketing et ce qui génère des conversions.",
"label.average": "Moyenne",
"label.back": "Retour",
"label.before": "Avant",
+ "label.boards": "Tableaux",
"label.bounce-rate": "Taux de rebond",
"label.breakdown": "Répartition",
"label.browser": "Navigateur",
@@ -29,12 +32,16 @@
"label.cities": "Villes",
"label.city": "Ville",
"label.clear-all": "Réinitialiser",
+ "label.cohort": "Cohorte",
"label.compare": "Comparer",
+ "label.compare-dates": "Comparer les dates",
"label.confirm": "Confirmer",
"label.confirm-password": "Confirmation du mot de passe",
"label.contains": "Contient",
"label.content": "Contenu",
"label.continue": "Continuer",
+ "label.conversion": "Conversion",
+ "label.conversion-rate": "Taux de conversion",
"label.conversion-step": "Étape de conversion",
"label.count": "Compte",
"label.countries": "Pays",
@@ -67,7 +74,10 @@
"label.devices": "Appareils",
"label.direct": "Direct",
"label.dismiss": "Ignorer",
+ "label.distinct-id": "ID distinct",
"label.does-not-contain": "Ne contient pas",
+ "label.does-not-include": "N'inclut pas",
+ "label.doest-not-exist": "N'existe pas",
"label.domain": "Domaine",
"label.dropoff": "Abandons",
"label.edit": "Modifier",
@@ -79,7 +89,9 @@
"label.entry": "Chemin d'entrée",
"label.event": "Évènement",
"label.event-data": "Données d'évènements",
+ "label.event-name": "Nom de l'évènement",
"label.events": "Évènements",
+ "label.exists": "Existe",
"label.exit": "Chemin de sortie",
"label.false": "Faux",
"label.field": "Champ",
@@ -88,30 +100,37 @@
"label.filter-combined": "Combiné",
"label.filter-raw": "Brut",
"label.filters": "Filtres",
+ "label.first-click": "Premier clic",
"label.first-seen": "Vu pour la première fois",
"label.funnel": "Entonnoir",
"label.funnel-description": "Comprenez les taux de conversions et d'abandons des utilisateurs.",
+ "label.funnels": "Entonnoirs",
"label.goal": "Objectif",
"label.goals": "Objectifs",
"label.goals-description": "Suivez vos objectifs en matière de pages vues et d'événements.",
"label.greater-than": "Supérieur à",
"label.greater-than-equals": "Supérieur ou égal à",
"label.grouped": "Groupé",
- "label.host": "Hôte",
- "label.hosts": "Hôtes",
- "label.insights": "Insights",
+ "label.hostname": "Nom d'hôte",
+ "label.includes": "Inclut",
+ "label.insight": "Aperçu",
+ "label.insights": "Aperçus",
"label.insights-description": "Analysez précisément vos données en utilisant des segments et des filtres.",
"label.is": "Est",
+ "label.is-false": "Est faux",
"label.is-not": "N'est pas",
"label.is-not-set": "N'est pas défini",
"label.is-set": "Est défini",
+ "label.is-true": "Est vrai",
"label.join": "Rejoindre",
"label.join-team": "Rejoindre une équipe",
"label.journey": "Parcours",
"label.journey-description": "Comprennez comment les utilisateurs naviguent sur votre site.",
+ "label.journeys": "Parcours",
"label.language": "Langue",
"label.languages": "Langues",
"label.laptop": "Portable",
+ "label.last-click": "Dernier clic",
"label.last-days": "{x} derniers jours",
"label.last-hours": "{x} dernières heures",
"label.last-months": "{x} derniers mois",
@@ -120,12 +139,14 @@
"label.leave-team": "Quitter l'équipe",
"label.less-than": "Inférieur à",
"label.less-than-equals": "Inférieur ou égal à",
+ "label.links": "Liens",
"label.login": "Connexion",
"label.logout": "Déconnexion",
"label.manage": "Gérer",
- "label.manager": "Manager",
+ "label.manager": "Gestionnaire",
"label.max": "Max",
- "label.medium": "Support",
+ "label.maximize": "Développer",
+ "label.medium": "Moyen",
"label.member": "Membre",
"label.members": "Membres",
"label.min": "Min",
@@ -139,26 +160,29 @@
"label.none": "Aucun",
"label.number-of-records": "{x} {x, plural, one {enregistrement} other {enregistrements}}",
"label.ok": "OK",
+ "label.online": "Online",
"label.organic-search": "Recherche organique",
- "label.organic-shopping": "E-commerce organique",
+ "label.organic-shopping": "Achat organique",
"label.organic-social": "Réseau social organique",
"label.organic-video": "Vidéo organique",
"label.os": "OS",
"label.other": "Autre",
"label.overview": "Vue d'ensemble",
"label.owner": "Propriétaire",
+ "label.page": "Page",
"label.page-of": "Page {current} sur {total}",
"label.page-views": "Pages vues",
"label.pageTitle": "Titre de page",
"label.pages": "Pages",
"label.paid-ads": "Publicités payantes",
"label.paid-search": "Recherche payante",
- "label.paid-shopping": "E-commerce payant",
+ "label.paid-shopping": "Achat payant",
"label.paid-social": "Réseau social payant",
"label.paid-video": "Vidéo payante",
"label.password": "Mot de passe",
"label.path": "Chemin",
"label.paths": "Chemins",
+ "label.pixels": "Pixels",
"label.powered-by": "Propulsé par {name}",
"label.previous": "Précédent",
"label.previous-period": "Période précédente",
@@ -177,6 +201,7 @@
"label.regenerate": "Régénérer",
"label.region": "Région",
"label.regions": "Régions",
+ "label.remaining": "Restant",
"label.remove": "Retirer",
"label.remove-member": "Retirer le membre",
"label.reports": "Rapports",
@@ -185,21 +210,23 @@
"label.reset-website": "Réinitialiser les statistiques",
"label.retention": "Rétention",
"label.retention-description": "Mesurez l'attractivité de votre site en suivant la fréquence de retour des utilisateurs.",
- "label.revenue": "Recettes",
- "label.revenue-description": "Examinez vos recettes et comment dépensent vos utilisateurs.",
+ "label.revenue": "Revenus",
+ "label.revenue-description": "Consultez vos revenus au fil du temps.",
"label.role": "Rôle",
"label.run-query": "Exécuter la requête",
"label.save": "Enregistrer",
- "label.screens": "Résolutions d'écran",
+ "label.screens": "Écrans",
"label.search": "Rechercher",
"label.select": "Sélectionner",
"label.select-date": "Choisir une période",
+ "label.select-filter": "Sélectionner un filtre",
"label.select-role": "Choisir un rôle",
"label.select-website": "Choisir un site",
"label.session": "Session",
- "label.session-data": "Session data",
+ "label.session-data": "Données de session",
"label.sessions": "Sessions",
"label.settings": "Paramètres",
+ "label.share": "Partager",
"label.share-url": "URL de partage",
"label.single-day": "Journée",
"label.sms": "SMS",
@@ -208,14 +235,15 @@
"label.steps": "Étapes",
"label.sum": "Somme",
"label.tablet": "Tablette",
- "label.tag": "Tag",
- "label.tags": "Tags",
+ "label.tag": "Étiquette",
+ "label.tags": "Étiquettes",
"label.team": "Équipe",
"label.team-id": "ID d'équipe",
"label.team-manager": "Manager de l'équipe",
"label.team-member": "Membre de l'équipe",
"label.team-name": "Nom de l'équipe",
"label.team-owner": "Propriétaire de l'équipe",
+ "label.team-settings": "Team settings",
"label.team-view-only": "Vue d'équipe uniquement",
"label.team-websites": "Sites d'équipes",
"label.teams": "Équipes",
@@ -242,8 +270,6 @@
"label.unknown": "Inconnu",
"label.untitled": "Sans titre",
"label.update": "Modifier",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Utilisateur",
"label.username": "Nom d'utilisateur",
"label.users": "Utilisateurs",
@@ -263,8 +289,12 @@
"label.websites": "Sites",
"label.window": "Fenêtre",
"label.yesterday": "Hier",
+ "label.behavior": "Comportement",
+ "label.traffic": "Trafic",
+ "label.segments": "Segments",
"message.action-confirmation": "Taper {confirmation} ci-dessous pour confirmer.",
"message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement",
+ "message.bad-request": "Bad request",
"message.collected-data": "Donnée collectée",
"message.confirm-delete": "Êtes-vous sûr de vouloir supprimer {target} ?",
"message.confirm-leave": "Êtes-vous sûr de vouloir quitter {target} ?",
@@ -274,6 +304,7 @@
"message.delete-website-warning": "Toutes les données associées seront supprimées.",
"message.error": "Un problème est survenu.",
"message.event-log": "{event} sur {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Aller aux paramètres",
"message.incorrect-username-password": "Nom d'utilisateur/Mot de passe incorrect.",
"message.invalid-domain": "Domaine invalide",
@@ -287,10 +318,13 @@
"message.no-teams": "Vous n'avez pas créé d'équipe.",
"message.no-users": "Aucun utilisateur.",
"message.no-websites-configured": "Vous n'avez pas configuré de site.",
+ "message.not-found": "Non trouvé!",
+ "message.nothing-selected": "Rien n'est sélectionné.",
"message.page-not-found": "Page non trouvée.",
"message.reset-website": "Pour réinitialiser ce site, taper {confirmation} ci-dessous pour confirmer.",
"message.reset-website-warning": "Toutes les statistiques pour ce site seront supprimées, mais votre code de suivi restera intact.",
"message.saved": "Enregistré.",
+ "message.sever-error": "Erreur serveur",
"message.share-url": "Les statistiques de votre site sont accessibles publiquement sur cette URL :",
"message.team-already-member": "Vous êtes déjà membre de cette équipe.",
"message.team-not-found": "Équipe non trouvée.",
@@ -300,8 +334,8 @@
"message.transfer-user-website-to-team": "Choisir l'équipe à laquelle transférer ce site.",
"message.transfer-website": "Transférer la propriété du site sur votre compte ou à une autre équipe.",
"message.triggered-event": "Évènement déclenché",
+ "message.unauthorized": "Non authorisé!",
"message.user-deleted": "Utilisateur supprimé.",
"message.viewed-page": "Page vue",
- "message.visitor-log": "Visiteur de {country} utilisant {browser} sur {os} {device}",
- "message.visitors-dropped-off": "Visiteurs ont abandonné"
+ "message.visitor-log": "Visiteur de {country} utilisant {browser} sur {os} {device}"
}
diff --git a/src/lang/ga-ES.json b/src/lang/ga-ES.json
index 3af959f7..20826005 100644
--- a/src/lang/ga-ES.json
+++ b/src/lang/ga-ES.json
@@ -3,41 +3,56 @@
"label.actions": "Accións",
"label.activity": "Rexistro de actividade",
"label.add": "Engadir",
+ "label.add-board": "Engadir taboleiro",
"label.add-description": "Engadir descrición",
"label.add-member": "Engadir membro",
"label.add-step": "Engadir paso",
"label.add-website": "Engadir sitio web",
"label.admin": "Administrador/a",
- "label.after": "After",
+ "label.affiliate": "Afiliado",
+ "label.after": "Despois",
"label.all": "Todo",
"label.all-time": "Sempre",
"label.analytics": "Analíticas",
+ "label.apply": "Aplicar",
+ "label.attribution": "Atribución",
+ "label.attribution-description": "Vexa como os usuarios interactúan co seu márketing e que impulsa as conversións.",
"label.average": "Media",
"label.back": "Atrás",
"label.before": "Antes",
+ "label.boards": "Taboleiros",
"label.bounce-rate": "Proporción de rebote",
"label.breakdown": "Desglose",
"label.browser": "Navegador",
"label.browsers": "Navegadores",
+ "label.campaigns": "Campañas",
"label.cancel": "Cancelar",
"label.change-password": "Mudar contrasinal",
+ "label.channels": "Canles",
"label.cities": "Cidades",
"label.city": "Cidade",
- "label.clear-all": "Clear all",
+ "label.clear-all": "Limpar todo",
+ "label.cohort": "Cohorte",
"label.compare": "Comparar",
+ "label.compare-dates": "Comparar datas",
"label.confirm": "Confirmar",
"label.confirm-password": "Confirmar contrasinal",
"label.contains": "Contén",
+ "label.content": "Contido",
"label.continue": "Continuar",
+ "label.conversion": "Conversión",
+ "label.conversion-rate": "Taxa de conversión",
+ "label.conversion-step": "Paso de conversión",
"label.count": "Reconto",
"label.countries": "Países",
"label.country": "País",
"label.create": "Crear",
- "label.create-report": "Crear report",
- "label.create-team": "Crear team",
- "label.create-user": "Crear user",
+ "label.create-report": "Crear informe",
+ "label.create-team": "Crear equipo",
+ "label.create-user": "Crear usuario",
"label.created": "Creado",
"label.created-by": "Creado por",
+ "label.currency": "Moeda",
"label.current": "Actual",
"label.current-password": "Contrasinal actual",
"label.custom-range": "Rango personalizado",
@@ -57,19 +72,26 @@
"label.details": "Detalles",
"label.device": "Dispositivo",
"label.devices": "Dispositivos",
+ "label.direct": "Directo",
"label.dismiss": "Desbotar",
+ "label.distinct-id": "ID distinto",
"label.does-not-contain": "Non contén",
+ "label.does-not-include": "Non inclúe",
+ "label.doest-not-exist": "Non existe",
"label.domain": "Dominio",
"label.dropoff": "Disminución",
"label.edit": "Editar",
"label.edit-dashboard": "Editar taboleiro",
"label.edit-member": "Editar membro",
+ "label.email": "Correo electrónico",
"label.enable-share-url": "Activar URL de compartición",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
+ "label.end-step": "Paso final",
+ "label.entry": "URL de entrada",
"label.event": "Evento",
"label.event-data": "Datos do evento",
+ "label.event-name": "Nome do evento",
"label.events": "Eventos",
+ "label.exists": "Existe",
"label.exit": "URL de saída",
"label.false": "Falso",
"label.field": "Campo",
@@ -78,29 +100,37 @@
"label.filter-combined": "Combinado",
"label.filter-raw": "Crú",
"label.filters": "Filtros",
+ "label.first-click": "Primeiro clic",
"label.first-seen": "Primeira visita",
- "label.funnel": "Funnel",
+ "label.funnel": "Funil",
"label.funnel-description": "Entende a taxa de conversión e de abandono dos usuarios.",
+ "label.funnels": "Funís",
"label.goal": "Obxectivo",
"label.goals": "Obxectivos",
"label.goals-description": "Segue os teus obxectivos de visualizacións de páxinas e eventos.",
"label.greater-than": "Maior que",
"label.greater-than-equals": "Maior ou igual que",
- "label.host": "Dominio",
- "label.hosts": "Dominios",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
+ "label.grouped": "Agrupado",
+ "label.hostname": "Nome do host",
+ "label.includes": "Inclúe",
+ "label.insight": "Información",
+ "label.insights": "Informacións",
+ "label.insights-description": "Afonda nos teus datos usando segmentos e filtros.",
"label.is": "É",
+ "label.is-false": "É falso",
"label.is-not": "Non é",
"label.is-not-set": "Non está establecido",
"label.is-set": "Está establecido",
+ "label.is-true": "É verdadeiro",
"label.join": "Unirse",
"label.join-team": "Unirse ao equipo",
"label.journey": "Traxectoria",
"label.journey-description": "Entende como os usuarios navegan polo teu sitio web.",
+ "label.journeys": "Traxectorias",
"label.language": "Idioma",
"label.languages": "Idiomas",
"label.laptop": "Portátil",
+ "label.last-click": "Último clic",
"label.last-days": "Últimos {x} días",
"label.last-hours": "Últimas {x} horas",
"label.last-months": "Últimos {x} meses",
@@ -109,33 +139,50 @@
"label.leave-team": "Deixar o equipo",
"label.less-than": "Menor que",
"label.less-than-equals": "Menor ou igual que",
+ "label.links": "Ligazóns",
"label.login": "Acceder",
"label.logout": "Pechar sesión",
"label.manage": "Xestionar",
"label.manager": "Xestor",
"label.max": "Max",
+ "label.maximize": "Expandir",
+ "label.medium": "Medio",
"label.member": "Membro",
"label.members": "Membros",
"label.min": "Min",
"label.mobile": "Móbil",
+ "label.model": "Modelo",
"label.more": "Máis",
"label.my-account": "A miña conta",
"label.my-websites": "Os meus sitios web",
"label.name": "Nome",
"label.new-password": "Novo contrasinal",
- "label.none": "None",
+ "label.none": "Ningún",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Busca orgánica",
+ "label.organic-shopping": "Compra orgánica",
+ "label.organic-social": "Social orgánico",
+ "label.organic-video": "Vídeo orgánico",
"label.os": "Sistema operativo",
+ "label.other": "Outro",
"label.overview": "Resumo",
"label.owner": "Propietario/a",
+ "label.page": "Páxina",
"label.page-of": "Páxina {current} de {total}",
"label.page-views": "Vistas de páxinas",
"label.pageTitle": "Título da páxina",
"label.pages": "Páxinas",
+ "label.paid-ads": "Anuncios de pago",
+ "label.paid-search": "Busca de pago",
+ "label.paid-shopping": "Compra de pago",
+ "label.paid-social": "Social de pago",
+ "label.paid-video": "Vídeo de pago",
"label.password": "Contrasinal",
"label.path": "Ruta",
"label.paths": "Rutas",
+ "label.pixels": "Píxeles",
"label.powered-by": "Funciona grazas a {name}",
"label.previous": "Anterior",
"label.previous-period": "Periodo anterior",
@@ -147,12 +194,14 @@
"label.query": "Petición",
"label.query-parameters": "Parámetros da petición",
"label.realtime": "Agora mesmo",
+ "label.referral": "Referencia",
"label.referrer": "Orixe",
"label.referrers": "Orixes",
"label.refresh": "Actualizar",
"label.regenerate": "Rexenerar",
"label.region": "Rexión",
"label.regions": "Rexións",
+ "label.remaining": "Restante",
"label.remove": "Eliminar",
"label.remove-member": "Eliminar membro",
"label.reports": "Reportes",
@@ -163,7 +212,6 @@
"label.retention-description": "Mide a fidelidade dos usuarios ao teu sitio web seguindo a frecuencia coa que volven.",
"label.revenue": "Ingresos",
"label.revenue-description": "Consulta os teus ingresos ao longo do tempo.",
- "label.revenue-property": "Revenue Property",
"label.role": "Rol",
"label.run-query": "Executar petición",
"label.save": "Gardar",
@@ -171,26 +219,35 @@
"label.search": "Buscar",
"label.select": "Seleccionar",
"label.select-date": "Seleccionar data",
+ "label.select-filter": "Seleccionar filtro",
"label.select-role": "Seleccionar rol",
"label.select-website": "Seleccionar sitio web",
"label.session": "Sesión",
+ "label.session-data": "Datos da sesión",
"label.sessions": "Sesións",
"label.settings": "Axustes",
+ "label.share": "Compartir",
"label.share-url": "Compartir URL",
"label.single-day": "Un só día",
+ "label.sms": "SMS",
+ "label.sources": "Fontes",
"label.start-step": "Start Step",
"label.steps": "Pasos",
"label.sum": "Suma",
"label.tablet": "Tableta",
+ "label.tag": "Etiqueta",
+ "label.tags": "Etiquetas",
"label.team": "Equipo",
"label.team-id": "ID do equipo",
"label.team-manager": "Xestor do equipo",
"label.team-member": "Membro do equipo",
"label.team-name": "Nome do equipo",
"label.team-owner": "Propietario do equipo",
+ "label.team-settings": "Axustes do equipo",
"label.team-view-only": "Equipo de só lectura",
"label.team-websites": "Sitios web do equipo",
"label.teams": "Equipos",
+ "label.terms": "Termos",
"label.theme": "Decorado",
"label.this-month": "Este mes",
"label.this-week": "Esta semana",
@@ -213,10 +270,7 @@
"label.unknown": "Descoñecido",
"label.untitled": "Sen título",
"label.update": "Actualizar",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Usuario",
- "label.user-property": "Propiedade do usuario",
"label.username": "Identificador",
"label.users": "Usuarios",
"label.utm": "UTM",
@@ -235,8 +289,10 @@
"label.websites": "Sitios web",
"label.window": "Ventá",
"label.yesterday": "Onte",
+ "label.behavior": "Comportamento",
"message.action-confirmation": "Escribe {confirmation} na caixa de embaixo para confirmar.",
"message.active-users": "{x} actual {x, plural, one {visitante} other {visitantes}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Datos recopilados",
"message.confirm-delete": "Estás seguro/a de que queres eliminar {target}?",
"message.confirm-leave": "Estás seguro/a de que queres deixar {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Tamén serán borrados tódolos datos asociados.",
"message.error": "Houbo un fallo.",
"message.event-log": "{event} en {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Ir aos axustes",
"message.incorrect-username-password": "Credenciais incorrectas.",
"message.invalid-domain": "Dominio non válido",
@@ -259,10 +316,13 @@
"message.no-teams": "Non creaches ningún equipo.",
"message.no-users": "Non hai usuarios.",
"message.no-websites-configured": "Non tes sitios web configurados.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Páxina non atopada.",
"message.reset-website": "Para restablecer este sitio web, escriba {confirmation} na caixa de embaixo para confirmar.",
"message.reset-website-warning": "Vanse eliminar tódalas estatísticas deste sitio web, pero o código de seguimento permanecerá sen cambios.",
"message.saved": "Gardouse correctamente.",
+ "message.sever-error": "Server error",
"message.share-url": "Este é o URL da compartición pública de {target}.",
"message.team-already-member": "Xa es membro do equipo.",
"message.team-not-found": "Equipo non atopado.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Selecciona o equipo ao que transferir este sitio web.",
"message.transfer-website": "Transferir propiedade do sitio web á túa conta ou a outro equipo.",
"message.triggered-event": "Activou o evento",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Usuario eliminado.",
"message.viewed-page": "Páxina vista",
- "message.visitor-log": "Visitante desde {country} usando {browser} en {os} {device}",
- "message.visitors-dropped-off": "Visitantes abandonados"
+ "message.visitor-log": "Visitante desde {country} usando {browser} en {os} {device}"
}
diff --git a/src/lang/he-IL.json b/src/lang/he-IL.json
index fd25cd14..2d115c8d 100644
--- a/src/lang/he-IL.json
+++ b/src/lang/he-IL.json
@@ -1,158 +1,207 @@
{
- "label.access-code": "Access code",
+ "label.access-code": "קוד גישה",
"label.actions": "פעולות",
- "label.activity": "Activity log",
- "label.add": "Add",
- "label.add-description": "Add description",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.activity": "יומן פעילות",
+ "label.add": "הוסף",
+ "label.add-board": "הוסף לוח",
+ "label.add-description": "הוסף תיאור",
+ "label.add-member": "הוסף חבר",
+ "label.add-step": "הוסף שלב",
"label.add-website": "הוספת אתר",
"label.admin": "מנהל",
- "label.after": "After",
+ "label.affiliate": "שותף",
+ "label.after": "אחרי",
"label.all": "הכל",
- "label.all-time": "All time",
- "label.analytics": "Analytics",
- "label.average": "Average",
+ "label.all-time": "כל הזמנים",
+ "label.analytics": "אנליטיקה",
+ "label.apply": "החל",
+ "label.attribution": "שיוך",
+ "label.attribution-description": "צפה כיצד משתמשים מתקשרים עם השיווק שלך ומה מניע המרות.",
+ "label.average": "ממוצע",
"label.back": "חזרה",
- "label.before": "Before",
- "label.bounce-rate": "Bounce rate",
- "label.breakdown": "Breakdown",
- "label.browser": "Browser",
+ "label.before": "לפני",
+ "label.boards": "לוחות",
+ "label.bounce-rate": "שיעור נטישה",
+ "label.breakdown": "פירוט",
+ "label.browser": "דפדפן",
"label.browsers": "דפדפנים",
+ "label.campaigns": "קמפיינים",
"label.cancel": "ביטול",
"label.change-password": "שינוי סיסמה",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
+ "label.channels": "ערוצים",
+ "label.cities": "ערים",
+ "label.city": "עיר",
+ "label.clear-all": "נקה הכל",
+ "label.cohort": "קבוצה",
+ "label.compare": "השווה",
+ "label.compare-dates": "השווה תאריכים",
+ "label.confirm": "אשר",
"label.confirm-password": "אישור סיסמה",
"label.contains": "Contains",
- "label.continue": "Continue",
- "label.count": "Count",
+ "label.content": "תוכן",
+ "label.continue": "המשך",
+ "label.conversion": "המרה",
+ "label.conversion-rate": "שיעור המרה",
+ "label.conversion-step": "שלב המרה",
+ "label.count": "ספירה",
"label.countries": "מדינות",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.country": "מדינה",
+ "label.create": "צור",
+ "label.create-report": "צור דוח",
+ "label.create-team": "צור צוות",
+ "label.create-user": "צור משתמש",
+ "label.created": "נוצר",
+ "label.created-by": "נוצר על ידי",
+ "label.currency": "מטבע",
+ "label.current": "נוכחי",
"label.current-password": "סיסמה נוכחית",
"label.custom-range": "טווח מותאם",
"label.dashboard": "דשבורד",
- "label.data": "Data",
- "label.date": "Date",
+ "label.data": "נתונים",
+ "label.date": "תאריך",
"label.date-range": "טווח תאריכים",
- "label.day": "Day",
+ "label.day": "יום",
"label.default-date-range": "טווח תאריכים בברירת מחדל",
"label.delete": "הסרה",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "מחק דוח",
+ "label.delete-team": "מחק צוות",
+ "label.delete-user": "מחק משתמש",
"label.delete-website": "הסרת אתר",
- "label.description": "Description",
- "label.desktop": "דסקטופ",
- "label.details": "Details",
- "label.device": "Device",
+ "label.description": "תיאור",
+ "label.desktop": "מחשב שולחני",
+ "label.details": "פרטים",
+ "label.device": "מכשיר",
"label.devices": "מכשירים",
+ "label.direct": "ישיר",
"label.dismiss": "שיחרור",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "מזהה ייחודי",
+ "label.does-not-contain": "לא מכיל",
+ "label.does-not-include": "לא כולל",
+ "label.doest-not-exist": "לא קיים",
"label.domain": "דומיין",
- "label.dropoff": "Dropoff",
+ "label.dropoff": "עזיבה",
"label.edit": "עריכה",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
+ "label.edit-dashboard": "ערוך לוח מחוונים",
+ "label.edit-member": "ערוך חבר",
+ "label.email": "אימייל",
"label.enable-share-url": "הפעלת URL שיתוף",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event data",
+ "label.end-step": "שלב סיום",
+ "label.entry": "כתובת כניסה",
+ "label.event": "אירוע",
+ "label.event-data": "נתוני אירוע",
+ "label.event-name": "שם האירוע",
"label.events": "אירועים",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
+ "label.exists": "קיים",
+ "label.exit": "כתובת יציאה",
+ "label.false": "שקר",
+ "label.field": "שדה",
+ "label.fields": "שדות",
"label.filter": "Filter",
"label.filter-combined": "משותף",
"label.filter-raw": "גולמי",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.filters": "מסננים",
+ "label.first-click": "קליק ראשון",
+ "label.first-seen": "נראה לראשונה",
+ "label.funnel": "משפך",
+ "label.funnel-description": "הבן את שיעור ההמרה והעזיבה של המשתמשים.",
+ "label.funnels": "משפכים",
+ "label.goal": "מטרה",
+ "label.goals": "מטרות",
+ "label.goals-description": "עקוב אחרי המטרות שלך לצפיות בדף ואירועים.",
+ "label.greater-than": "גדול מ-",
+ "label.greater-than-equals": "גדול או שווה ל-",
+ "label.grouped": "מקובץ",
+ "label.hostname": "שם מארח",
+ "label.includes": "כולל",
+ "label.insight": "תובנה",
+ "label.insights": "תובנות",
+ "label.insights-description": "צלול עמוק יותר לנתונים שלך באמצעות פילוחים ומסננים.",
+ "label.is": "הוא",
+ "label.is-false": "הוא שקר",
+ "label.is-not": "אינו",
+ "label.is-not-set": "לא הוגדר",
+ "label.is-set": "הוגדר",
+ "label.is-true": "הוא אמת",
+ "label.join": "הצטרף",
+ "label.join-team": "הצטרף לצוות",
+ "label.journey": "מסע",
+ "label.journey-description": "הבן כיצד משתמשים מנווטים באתר שלך.",
+ "label.journeys": "מסעות",
"label.language": "Language",
"label.languages": "Languages",
"label.laptop": "לפטופ",
+ "label.last-click": "קליק אחרון",
"label.last-days": "{x} ימים אחרונים",
"label.last-hours": "{x} שעות אחרונות",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "{x} חודשים אחרונים",
+ "label.last-seen": "נראה לאחרונה",
+ "label.leave": "עזוב",
+ "label.leave-team": "עזוב צוות",
+ "label.less-than": "פחות מ-",
+ "label.less-than-equals": "פחות או שווה ל-",
+ "label.links": "קישורים",
"label.login": "התחברות",
"label.logout": "התנתקות",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
+ "label.manage": "נהל",
+ "label.manager": "מנהל",
+ "label.max": "מקסימום",
+ "label.maximize": "הרחב",
+ "label.medium": "בינוני",
+ "label.member": "חבר",
+ "label.members": "חברים",
+ "label.min": "מינימום",
"label.mobile": "מובייל",
+ "label.model": "Model",
"label.more": "עוד",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "החשבון שלי",
+ "label.my-websites": "האתרים שלי",
"label.name": "שם",
"label.new-password": "סיסמה חדשה",
- "label.none": "None",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
+ "label.none": "ללא",
+ "label.number-of-records": "{x} {x, plural, one {רשומה} other {רשומות}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "חיפוש אורגני",
+ "label.organic-shopping": "קניות אורגניות",
+ "label.organic-social": "רשת חברתית אורגנית",
+ "label.organic-video": "וידאו אורגני",
"label.os": "OS",
- "label.overview": "Overview",
- "label.owner": "Owner",
- "label.page-of": "Page {current} of {total}",
+ "label.other": "אחר",
+ "label.overview": "סקירה כללית",
+ "label.owner": "בעלים",
+ "label.page": "דף",
+ "label.page-of": "דף {current} מתוך {total}",
"label.page-views": "צפיות בדפים",
"label.pageTitle": "Page title",
"label.pages": "דפים",
+ "label.paid-ads": "מודעות בתשלום",
+ "label.paid-search": "חיפוש בתשלום",
+ "label.paid-shopping": "קניות בתשלום",
+ "label.paid-social": "רשת חברתית בתשלום",
+ "label.paid-video": "וידאו בתשלום",
"label.password": "סיסמה",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "נתיב",
+ "label.paths": "נתיבים",
+ "label.pixels": "פיקסלים",
"label.powered-by": "Powered by {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
"label.previous-year": "Previous year",
"label.profile": "פרופיל",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
- "label.query": "Query",
- "label.query-parameters": "Query parameters",
+ "label.properties": "מאפיינים",
+ "label.property": "מאפיין",
+ "label.queries": "שאילתות",
+ "label.query": "שאילתה",
+ "label.query-parameters": "פרמטרי שאילתה",
"label.realtime": "זמן אמת",
+ "label.referral": "הפניה",
"label.referrer": "Referrer",
"label.referrers": "מפנים",
"label.refresh": "רענון",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "נותר",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -161,36 +210,44 @@
"label.reset-website": "Reset statistics",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "הכנסה",
+ "label.revenue-description": "בדוק את ההכנסות שלך לאורך זמן.",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "שמירה",
- "label.screens": "Screens",
+ "label.screens": "מסכים",
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "בחר מסנן",
"label.select-role": "Select role",
"label.select-website": "Select website",
- "label.session": "Session",
+ "label.session": "סשן",
+ "label.session-data": "נתוני סשן",
"label.sessions": "Sessions",
"label.settings": "הגדרות",
+ "label.share": "שתף",
"label.share-url": "שיתוף URL",
"label.single-day": "יום בודד",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
+ "label.sms": "SMS",
+ "label.sources": "מקורות",
+ "label.start-step": "שלב התחלה",
+ "label.steps": "שלבים",
"label.sum": "Sum",
"label.tablet": "טאבלט",
+ "label.tag": "תגית",
+ "label.tags": "תגיות",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "הגדרות צוות",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "תנאים",
"label.theme": "Theme",
"label.this-month": "החודש",
"label.this-week": "השבוע",
@@ -213,10 +270,7 @@
"label.unknown": "לא ידוע",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "שם משתמש",
"label.users": "Users",
"label.utm": "UTM",
@@ -224,6 +278,7 @@
"label.value": "Value",
"label.view": "View",
"label.view-details": "פרטים נוספים",
+ "label.behavior": "התנהגות",
"label.view-only": "View only",
"label.views": "צפיות",
"label.views-per-visit": "Views per visit",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} נוכחיים {x, plural, one {מבקר} other {מבקרים}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "האם באמת למחוק את {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "כל המידע המקושר יימחק",
"message.error": "משהו השתבש",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "להדרותג",
"message.incorrect-username-password": "שם משתמש או סיסמה לא נכונים",
"message.invalid-domain": "דומיין לא תקין",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "לא מוגדרים אתרים",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "דף לא נמצא",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.saved": "נשמר בהצלחה",
+ "message.sever-error": "Server error",
"message.share-url": "זהו URL ציבורי עבור {target}",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "מבקר ממדינת {country} משתמבש בדפדפן {browser} ב-{os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "מבקר ממדינת {country} משתמבש בדפדפן {browser} ב-{os} {device}"
}
diff --git a/src/lang/hi-IN.json b/src/lang/hi-IN.json
index 65a3622e..54cac301 100644
--- a/src/lang/hi-IN.json
+++ b/src/lang/hi-IN.json
@@ -3,195 +3,253 @@
"label.actions": "कार्य",
"label.activity": "गतिविधि लॉग",
"label.add": "जोडो",
+ "label.add-board": "बोर्ड जोड़ें",
"label.add-description": "विवरण लिखें",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.add-member": "सदस्य जोड़ें",
+ "label.add-step": "चरण जोड़ें",
"label.add-website": "वेबसाइट",
"label.admin": "प्रशासक",
- "label.after": "After",
+ "label.affiliate": "संबद्ध",
+ "label.after": "बाद में",
"label.all": "सब",
- "label.all-time": "All time",
- "label.analytics": "Analytics",
- "label.average": "Average",
+ "label.all-time": "सभी समय",
+ "label.analytics": "विश्लेषण",
+ "label.apply": "लागू करें",
+ "label.attribution": "अर्पण",
+ "label.attribution-description": "देखें कि उपयोगकर्ता आपके विपणन के साथ कैसे जुड़ते हैं और क्या रूपांतरण को प्रेरित करता है।",
+ "label.average": "औसत",
"label.back": "पीछे",
- "label.before": "Before",
+ "label.before": "पहले",
+ "label.behavior": "व्यवहार",
+ "label.boards": "बोर्ड्स",
"label.bounce-rate": "उछाल दर",
- "label.breakdown": "Breakdown",
- "label.browser": "Browser",
+ "label.breakdown": "विभाजन",
+ "label.browser": "ब्राउज़र",
"label.browsers": "वेब ब्राउज़र",
+ "label.campaigns": "अभियान",
"label.cancel": "रद्द करें",
"label.change-password": "पासवर्ड बदलें",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
+ "label.channels": "चैनल",
+ "label.cities": "शहर",
+ "label.city": "शहर",
+ "label.clear-all": "सभी साफ करें",
+ "label.cohort": "समूह",
+ "label.compare": "तुलना करें",
+ "label.compare-dates": "तिथियों की तुलना करें",
+ "label.confirm": "पुष्टि करें",
"label.confirm-password": "पासवर्ड की पुष्टि कीजिये",
- "label.contains": "Contains",
- "label.continue": "Continue",
- "label.count": "Count",
+ "label.contains": "शामिल है",
+ "label.content": "सामग्री",
+ "label.continue": "जारी रखें",
+ "label.conversion": "रूपांतरण",
+ "label.conversion-rate": "रूपांतरण दर",
+ "label.conversion-step": "रूपांतरण चरण",
+ "label.count": "गिनती",
"label.countries": "देश",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.country": "देश",
+ "label.create": "बनाएँ",
+ "label.create-report": "रिपोर्ट बनाएं",
+ "label.create-team": "टीम बनाएं",
+ "label.create-user": "उपयोगकर्ता बनाएं",
+ "label.created": "बनाया गया",
+ "label.created-by": "द्वारा बनाया गया",
+ "label.currency": "मुद्रा",
+ "label.current": "वर्तमान",
"label.current-password": "वर्तमान पासवर्ड",
"label.custom-range": "कस्टम रेंज",
"label.dashboard": "नियंत्रण-पट्ट",
- "label.data": "Data",
- "label.date": "Date",
+ "label.data": "डेटा",
+ "label.date": "तिथि",
"label.date-range": "तिथि सीमा",
- "label.day": "Day",
+ "label.day": "दिन",
"label.default-date-range": "डिफ़ॉल्ट तिथि सीमा",
"label.delete": "खाता हटाएं",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "रिपोर्ट हटाएं",
+ "label.delete-team": "टीम हटाएं",
+ "label.delete-user": "उपयोगकर्ता हटाएं",
"label.delete-website": "वेबसाइट हटाएं",
- "label.description": "Description",
+ "label.description": "विवरण",
"label.desktop": "डेस्कटॉप",
- "label.details": "Details",
- "label.device": "Device",
+ "label.details": "विवरण",
+ "label.device": "डिवाइस",
"label.devices": "उपकरण",
+ "label.direct": "प्रत्यक्ष",
"label.dismiss": "खारिज कीजिये",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "अद्वितीय आईडी",
+ "label.does-not-contain": "शामिल नहीं है",
+ "label.does-not-include": "शामिल नहीं है",
+ "label.doest-not-exist": "मौजूद नहीं है",
"label.domain": "डोमेन",
"label.dropoff": "Dropoff",
"label.edit": "संपादित करें",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
+ "label.edit-dashboard": "डैशबोर्ड संपादित करें",
+ "label.edit-member": "सदस्य संपादित करें",
+ "label.email": "ईमेल",
"label.enable-share-url": "शेयर URL सक्षम करें",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event data",
+ "label.end-step": "अंतिम चरण",
+ "label.entry": "प्रवेश URL",
+ "label.event": "घटना",
+ "label.event-data": "घटना डेटा",
+ "label.event-name": "घटना नाम",
"label.events": "स्पर्धाएँ",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
- "label.filter": "Filter",
+ "label.exists": "मौजूद है",
+ "label.exit": "निकास URL",
+ "label.false": "गलत",
+ "label.field": "फ़ील्ड",
+ "label.fields": "फ़ील्ड्स",
+ "label.filter": "फ़िल्टर",
"label.filter-combined": "संयुक्त",
"label.filter-raw": "रॉ",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
- "label.language": "Language",
- "label.languages": "Languages",
+ "label.filters": "फ़िल्टर",
+ "label.first-click": "पहला क्लिक",
+ "label.first-seen": "पहली बार देखा गया",
+ "label.funnel": "फनल",
+ "label.funnel-description": "उपयोगकर्ताओं की रूपांतरण और ड्रॉप-ऑफ दर को समझें।",
+ "label.funnels": "फनल्स",
+ "label.goal": "लक्ष्य",
+ "label.goals": "लक्ष्य",
+ "label.goals-description": "पृष्ठदृश्यों और घटनाओं के लिए अपने लक्ष्यों को ट्रैक करें।",
+ "label.greater-than": "से अधिक",
+ "label.greater-than-equals": "से अधिक या बराबर",
+ "label.grouped": "समूहित",
+ "label.hostname": "होस्टनाम",
+ "label.includes": "शामिल है",
+ "label.insight": "अंतर्दृष्टि",
+ "label.insights": "अंतर्दृष्टियाँ",
+ "label.insights-description": "सेगमेंट और फ़िल्टर का उपयोग करके अपने डेटा में गहराई से जाएं।",
+ "label.is": "है",
+ "label.is-false": "गलत है",
+ "label.is-not": "नहीं है",
+ "label.is-not-set": "सेट नहीं है",
+ "label.is-set": "सेट है",
+ "label.is-true": "सही है",
+ "label.join": "शामिल हों",
+ "label.join-team": "टीम में शामिल हों",
+ "label.journey": "यात्रा",
+ "label.journey-description": "समझें कि उपयोगकर्ता आपकी वेबसाइट पर कैसे नेविगेट करते हैं।",
+ "label.journeys": "यात्राएँ",
+ "label.language": "भाषा",
+ "label.languages": "भाषाएँ",
"label.laptop": "लैपटॉप",
+ "label.last-click": "अंतिम क्लिक",
"label.last-days": "पिछले {x} दिन",
"label.last-hours": "पिछले {x} घंटे",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "पिछले {x} महीने",
+ "label.last-seen": "अंतिम बार देखा गया",
+ "label.leave": "छोड़ें",
+ "label.leave-team": "टीम छोड़ें",
+ "label.less-than": "से कम",
+ "label.less-than-equals": "से कम या बराबर",
+ "label.links": "लिंक",
"label.login": "लॉग इन",
"label.logout": "लॉग आउट",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
+ "label.manage": "प्रबंधित करें",
+ "label.manager": "प्रबंधक",
+ "label.max": "अधिकतम",
+ "label.maximize": "विस्तार करें",
+ "label.medium": "मध्यम",
+ "label.member": "सदस्य",
+ "label.members": "सदस्यगण",
+ "label.min": "न्यूनतम",
"label.mobile": "मोबाइल फोन",
+ "label.model": "मॉडल",
"label.more": "और",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "मेरा खाता",
+ "label.my-websites": "मेरी वेबसाइट्स",
"label.name": "नाम",
"label.new-password": "नया पासवर्ड",
- "label.none": "None",
+ "label.none": "कोई नहीं",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "ऑर्गेनिक खोज",
+ "label.organic-shopping": "ऑर्गेनिक खरीदारी",
+ "label.organic-social": "ऑर्गेनिक सोशल",
+ "label.organic-video": "ऑर्गेनिक वीडियो",
"label.os": "OS",
- "label.overview": "Overview",
- "label.owner": "Owner",
- "label.page-of": "Page {current} of {total}",
+ "label.other": "अन्य",
+ "label.overview": "सारांश",
+ "label.owner": "मालिक",
+ "label.page": "पृष्ठ",
+ "label.page-of": "पृष्ठ {current} का {total}",
"label.page-views": "पृष्ठ दृश्य",
- "label.pageTitle": "Page title",
+ "label.pageTitle": "पृष्ठ शीर्षक",
"label.pages": "पृष्ठों",
+ "label.paid-ads": "पेड विज्ञापन",
+ "label.paid-search": "पेड खोज",
+ "label.paid-shopping": "पेड खरीदारी",
+ "label.paid-social": "पेड सोशल",
+ "label.paid-video": "पेड वीडियो",
"label.password": "पासवर्ड",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "पथ",
+ "label.paths": "पथ",
+ "label.pixels": "पिक्सेल",
"label.powered-by": "{name} द्वारा संचालित",
"label.previous": "Previous",
"label.previous-period": "Previous period",
"label.previous-year": "Previous year",
"label.profile": "प्रोफ़ाइल",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
- "label.query": "Query",
- "label.query-parameters": "Query parameters",
+ "label.properties": "गुण",
+ "label.property": "गुण",
+ "label.queries": "प्रश्न",
+ "label.query": "प्रश्न",
+ "label.query-parameters": "प्रश्न पैरामीटर",
"label.realtime": "वास्तव काल",
- "label.referrer": "Referrer",
+ "label.referral": "संदर्भ",
+ "label.referrer": "संदर्भकर्ता",
"label.referrers": "सन्दर्भदाता",
"label.refresh": "रिफ्रेश",
- "label.regenerate": "Regenerate",
- "label.region": "Region",
- "label.regions": "Regions",
- "label.remove": "Remove",
- "label.remove-member": "Remove member",
- "label.reports": "प्रतिवेदन",
+ "label.regenerate": "पुनः उत्पन्न करें",
+ "label.region": "क्षेत्र",
+ "label.regions": "क्षेत्र",
+ "label.remaining": "शेष",
+ "label.remove": "हटाएं",
+ "label.remove-member": "सदस्य हटाएं",
+ "label.reports": "रिपोर्ट्स",
"label.required": "अपेक्षित",
"label.reset": "रीसेट",
- "label.reset-website": "Reset statistics",
- "label.retention": "Retention",
- "label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
- "label.role": "Role",
- "label.run-query": "Run query",
+ "label.reset-website": "आँकड़े रीसेट करें",
+ "label.retention": "पुनः आगमन",
+ "label.retention-description": "यह मापें कि उपयोगकर्ता कितनी बार आपकी वेबसाइट पर लौटते हैं।",
+ "label.revenue": "राजस्व",
+ "label.revenue-description": "समय के साथ अपने राजस्व को देखें।",
+ "label.role": "भूमिका",
+ "label.run-query": "प्रश्न चलाएँ",
"label.save": "सहेजें",
- "label.screens": "Screens",
- "label.search": "Search",
- "label.select": "Select",
- "label.select-date": "Select date",
- "label.select-role": "Select role",
- "label.select-website": "Select website",
- "label.session": "Session",
- "label.sessions": "Sessions",
+ "label.screens": "स्क्रीन",
+ "label.search": "खोजें",
+ "label.select": "चुनें",
+ "label.select-date": "तिथि चुनें",
+ "label.select-filter": "फ़िल्टर चुनें",
+ "label.select-role": "भूमिका चुनें",
+ "label.select-website": "वेबसाइट चुनें",
+ "label.session": "सत्र",
+ "label.session-data": "सत्र डेटा",
+ "label.sessions": "सत्र",
"label.settings": "समायोजन",
+ "label.share": "साझा करें",
"label.share-url": "यूआरएल साझा करें",
"label.single-day": "एक दिन",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
- "label.sum": "Sum",
+ "label.sms": "SMS",
+ "label.sources": "स्रोत",
+ "label.start-step": "प्रारंभिक चरण",
+ "label.steps": "चरण",
+ "label.sum": "योग",
"label.tablet": "टैबलेट",
- "label.team": "Team",
- "label.team-id": "Team ID",
- "label.team-manager": "Team manager",
- "label.team-member": "Team member",
- "label.team-name": "Team name",
- "label.team-owner": "Team owner",
- "label.team-view-only": "Team view only",
- "label.team-websites": "Team websites",
- "label.teams": "Teams",
- "label.theme": "Theme",
+ "label.tag": "टैग",
+ "label.tags": "टैग्स",
+ "label.team": "टीम",
+ "label.team-id": "टीम आईडी",
+ "label.team-manager": "टीम प्रबंधक",
+ "label.team-member": "टीम सदस्य",
+ "label.team-name": "टीम नाम",
+ "label.team-owner": "टीम मालिक",
+ "label.team-settings": "टीम सेटिंग्स",
+ "label.team-view-only": "केवल टीम देखें",
+ "label.team-websites": "टीम वेबसाइट्स",
+ "label.teams": "टीमें",
+ "label.terms": "शर्तें",
+ "label.theme": "थीम",
"label.this-month": "इस महीने",
"label.this-week": "इस सप्ताह",
"label.this-year": "इस साल",
@@ -213,10 +271,7 @@
"label.unknown": "अज्ञात",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "उपयोगकर्ता नाम",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} मौजूद {x, plural, one {आगंतुक} other {आगंतुकों}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "क्या आप वाकई में {target} हटाना चाहते हैं?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "सभी संबद्ध डेटा को भी हटा दिया जाएगा।",
"message.error": "कुछ गलत हो गया।",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "समायोजन में जाइए",
"message.incorrect-username-password": "ग़लत उपयोगकर्ता नाम / पासवर्ड।",
"message.invalid-domain": "अमान्य डोमेन",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "आपके पास कोई वेबसाइट कॉन्फ़िगर नहीं है।",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "पृष्ठ नहीं मिला।",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.saved": "सफलतापूर्वक संचित कर लिया गया है।",
+ "message.sever-error": "Server error",
"message.share-url": "यह {target} के लिए सार्वजनिक रूप से साझा किया गया URL है।",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "{country} का आगंतुक, जो {browser} का उपयोग करता है, {os} यन्त्र पर",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "{country} का आगंतुक, जो {browser} का उपयोग करता है, {os} यन्त्र पर"
}
diff --git a/src/lang/hr-HR.json b/src/lang/hr-HR.json
index 6bf12237..141ad3fd 100644
--- a/src/lang/hr-HR.json
+++ b/src/lang/hr-HR.json
@@ -1,158 +1,208 @@
{
- "label.access-code": "Access code",
- "label.actions": "Actions",
- "label.activity": "Activity log",
- "label.add": "Add",
- "label.add-description": "Add description",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.access-code": "Pristupni kod",
+ "label.actions": "Akcije",
+ "label.activity": "Dnevnik aktivnosti",
+ "label.add": "Dodaj",
+ "label.add-board": "Dodaj ploču",
+ "label.add-description": "Dodaj opis",
+ "label.add-member": "Dodaj člana",
+ "label.add-step": "Dodaj korak",
"label.add-website": "Dodaj web stranicu",
"label.admin": "Administrator",
- "label.after": "After",
+ "label.affiliate": "Partner",
+ "label.after": "Nakon",
"label.all": "Sve",
"label.all-time": "Svo vrijeme",
- "label.analytics": "Analytics",
- "label.average": "Average",
+ "label.analytics": "Analitika",
+ "label.apply": "Primijeni",
+ "label.attribution": "Atribucija",
+ "label.attribution-description": "Pogledajte kako korisnici komuniciraju s vašim marketingom i što dovodi do konverzija.",
+ "label.average": "Prosjek",
"label.back": "Natrag ",
- "label.before": "Before",
- "label.bounce-rate": "Bounce rate",
- "label.breakdown": "Breakdown",
- "label.browser": "Browser",
- "label.browsers": "Browsers",
+ "label.before": "Prije",
+ "label.behavior": "Ponašanje",
+ "label.boards": "Ploče",
+ "label.bounce-rate": "Stopa napuštanja",
+ "label.breakdown": "Raspad",
+ "label.browser": "Preglednik",
+ "label.browsers": "Preglednici",
+ "label.campaigns": "Kampanje",
"label.cancel": "Odustani",
"label.change-password": "Promijeni lozinku",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
+ "label.channels": "Kanali",
+ "label.cities": "Gradovi",
+ "label.city": "Grad",
+ "label.clear-all": "Očisti sve",
+ "label.cohort": "Kohorta",
+ "label.compare": "Usporedi",
+ "label.compare-dates": "Usporedi datume",
+ "label.confirm": "Potvrdi",
"label.confirm-password": "Potvrdi lozinku",
"label.contains": "Contains",
- "label.continue": "Continue",
- "label.count": "Count",
+ "label.content": "Sadržaj",
+ "label.continue": "Nastavi",
+ "label.conversion": "Konverzija",
+ "label.conversion-rate": "Stopa konverzije",
+ "label.conversion-step": "Korak konverzije",
+ "label.count": "Broj",
"label.countries": "Countries",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.country": "Država",
+ "label.create": "Kreiraj",
+ "label.create-report": "Kreiraj izvještaj",
+ "label.create-team": "Kreiraj tim",
+ "label.create-user": "Kreiraj korisnika",
+ "label.created": "Kreirano",
+ "label.created-by": "Kreirao",
+ "label.currency": "Valuta",
+ "label.current": "Trenutno",
"label.current-password": "Trenutna lozinka",
"label.custom-range": "Prilagođeni raspon",
"label.dashboard": "Nadzorna ploča",
- "label.data": "Data",
- "label.date": "Date",
+ "label.data": "Podaci",
+ "label.date": "Datum",
"label.date-range": "Raspon datuma",
- "label.day": "Day",
+ "label.day": "Dan",
"label.default-date-range": "Zadani datumski raspon",
"label.delete": "Obriši",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "Obriši izvještaj",
+ "label.delete-team": "Obriši tim",
+ "label.delete-user": "Obriši korisnika",
"label.delete-website": "Obriši web stranicu",
- "label.description": "Description",
- "label.desktop": "Desktop",
- "label.details": "Details",
- "label.device": "Device",
- "label.devices": "Devices",
+ "label.description": "Opis",
+ "label.desktop": "Stolno računalo",
+ "label.details": "Detalji",
+ "label.device": "Uređaj",
+ "label.devices": "Uređaji",
+ "label.direct": "Direktno",
"label.dismiss": "Odbaci",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "Jedinstveni ID",
+ "label.does-not-contain": "Ne sadrži",
+ "label.does-not-include": "Ne uključuje",
+ "label.doest-not-exist": "Ne postoji",
"label.domain": "Domena",
- "label.dropoff": "Dropoff",
+ "label.dropoff": "Odlazak",
"label.edit": "Uredi",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
+ "label.edit-dashboard": "Uredi nadzornu ploču",
+ "label.edit-member": "Uredi člana",
+ "label.email": "E-mail",
"label.enable-share-url": "Omogući dijeljenje poveznice",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
+ "label.end-step": "Završni korak",
+ "label.entry": "Ulazni URL",
+ "label.event": "Događaj",
"label.event-data": "Podaci događaja",
+ "label.event-name": "Naziv događaja",
"label.events": "Events",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
+ "label.exists": "Postoji",
+ "label.exit": "Izlazni URL",
+ "label.false": "Netočno",
+ "label.field": "Polje",
+ "label.fields": "Polja",
"label.filter": "Filter",
"label.filter-combined": "Combined",
"label.filter-raw": "Raw",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.filters": "Filteri",
+ "label.first-click": "Prvi klik",
+ "label.first-seen": "Prvi put viđeno",
+ "label.funnel": "Lijevak",
+ "label.funnel-description": "Razumite stopu konverzije i odlaska korisnika.",
+ "label.funnels": "Ljevci",
+ "label.goal": "Cilj",
+ "label.goals": "Ciljevi",
+ "label.goals-description": "Pratite svoje ciljeve za prikaze stranica i događaje.",
+ "label.greater-than": "Veće od",
+ "label.greater-than-equals": "Veće ili jednako",
+ "label.grouped": "Grupirano",
+ "label.hostname": "Naziv hosta",
+ "label.includes": "Uključuje",
+ "label.insight": "Uvid",
+ "label.insights": "Uvidi",
+ "label.insights-description": "Dublje analizirajte svoje podatke pomoću segmenata i filtera.",
+ "label.is": "Je",
+ "label.is-false": "Je netočno",
+ "label.is-not": "Nije",
+ "label.is-not-set": "Nije postavljeno",
+ "label.is-set": "Postavljeno",
+ "label.is-true": "Je točno",
+ "label.join": "Pridruži se",
+ "label.join-team": "Pridruži se timu",
+ "label.journey": "Putovanje",
+ "label.journey-description": "Razumite kako korisnici navigiraju vašom web stranicom.",
+ "label.journeys": "Putovanja",
"label.language": "Jezik",
"label.languages": "Languages",
"label.laptop": "Laptop",
+ "label.last-click": "Zadnji klik",
"label.last-days": "Zadnjih {x} dana",
"label.last-hours": "Zadnjih {x} sati",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "Zadnjih {x} mjeseci",
+ "label.last-seen": "Zadnji put viđeno",
+ "label.leave": "Napusti",
+ "label.leave-team": "Napusti tim",
+ "label.less-than": "Manje od",
+ "label.less-than-equals": "Manje ili jednako",
+ "label.links": "Poveznice",
"label.login": "Prijava",
"label.logout": "Odjava",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
+ "label.manage": "Upravljaj",
+ "label.manager": "Upravitelj",
+ "label.max": "Maksimum",
+ "label.maximize": "Proširi",
+ "label.medium": "Srednje",
+ "label.member": "Član",
+ "label.members": "Članovi",
+ "label.min": "Minimum",
"label.mobile": "Mobile",
+ "label.model": "Model",
"label.more": "Više",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "Moj račun",
+ "label.my-websites": "Moje web stranice",
"label.name": "Ime",
"label.new-password": "Nova lozinka",
- "label.none": "Ništa",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
+ "label.none": "Nijedan",
+ "label.number-of-records": "{x} {x, plural, one {zapis} other {zapisa}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organsko pretraživanje",
+ "label.organic-shopping": "Organska kupovina",
+ "label.organic-social": "Organska društvena mreža",
+ "label.organic-video": "Organski videozapis",
"label.os": "OS",
- "label.overview": "Overview",
+ "label.other": "Ostalo",
+ "label.overview": "Pregled",
"label.owner": "Vlasnik",
- "label.page-of": "Page {current} of {total}",
+ "label.page": "Stranica",
+ "label.page-of": "Stranica {current} od {total}",
"label.page-views": "Page views",
"label.pageTitle": "Page title",
"label.pages": "Pages",
+ "label.paid-ads": "Plaćeni oglasi",
+ "label.paid-search": "Plaćeno pretraživanje",
+ "label.paid-shopping": "Plaćena kupovina",
+ "label.paid-social": "Plaćena društvena mreža",
+ "label.paid-video": "Plaćeni videozapis",
"label.password": "Lozinka",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Putanja",
+ "label.paths": "Putanje",
+ "label.pixels": "Pikseli",
"label.powered-by": "Powered by {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
"label.previous-year": "Previous year",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
- "label.query": "Query",
- "label.query-parameters": "Query parameters",
+ "label.properties": "Svojstva",
+ "label.property": "Svojstvo",
+ "label.queries": "Upiti",
+ "label.query": "Upit",
+ "label.query-parameters": "Parametri upita",
"label.realtime": "Stvarno vrijeme",
+ "label.referral": "Preporuka",
"label.referrer": "Referrer",
"label.referrers": "Referrers",
"label.refresh": "Osvježi",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Preostalo",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -161,36 +211,44 @@
"label.reset-website": "Resetirati web stranicu",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Prihod",
+ "label.revenue-description": "Pogledajte svoj prihod tijekom vremena.",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "Spremi",
- "label.screens": "Screens",
+ "label.screens": "Ekrani",
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Odaberi filter",
"label.select-role": "Select role",
"label.select-website": "Select website",
- "label.session": "Session",
+ "label.session": "Sesija",
+ "label.session-data": "Podaci sesije",
"label.sessions": "Sessions",
"label.settings": "Postavke",
+ "label.share": "Podijeli",
"label.share-url": "Podijeli poveznicu",
"label.single-day": "Jedan dan",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
+ "label.sms": "SMS",
+ "label.sources": "Izvori",
+ "label.start-step": "Početni korak",
+ "label.steps": "Koraci",
"label.sum": "Sum",
"label.tablet": "Tablet",
+ "label.tag": "Oznaka",
+ "label.tags": "Oznake",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Postavke tima",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Pojmovi",
"label.theme": "Tema",
"label.this-month": "Ovaj mjesec",
"label.this-week": "Ovaj tjedan",
@@ -213,10 +271,7 @@
"label.unknown": "Nepoznato",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Korisničko ime",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Jučer",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} Trenutno {x, plural, one {posjetitelj} other {posjetitelja}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Jeste li sigurni da želite obrisati {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "All website data will be deleted.",
"message.error": "Something went wrong.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Idi u postavke",
"message.incorrect-username-password": "Neispravno korisničke ime/lozinka.",
"message.invalid-domain": "Invalid domain. Do not include http/https.",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "You do not have any websites configured.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Stranica nije pronađena.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your settings will remain intact.",
"message.saved": "Saved.",
+ "message.sever-error": "Server error",
"message.share-url": "Ovo je javno dijeljena poveznica za {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}"
}
diff --git a/src/lang/hu-HU.json b/src/lang/hu-HU.json
index 8593b37e..1666b7a1 100644
--- a/src/lang/hu-HU.json
+++ b/src/lang/hu-HU.json
@@ -1,158 +1,208 @@
{
- "label.access-code": "Access code",
+ "label.access-code": "Hozzáférési kód",
"label.actions": "Műveletek",
- "label.activity": "Activity log",
- "label.add": "Add",
- "label.add-description": "Add description",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.activity": "Tevékenységnapló",
+ "label.add": "Hozzáadás",
+ "label.add-board": "Tábla hozzáadása",
+ "label.add-description": "Leírás hozzáadása",
+ "label.add-member": "Tag hozzáadása",
+ "label.add-step": "Lépés hozzáadása",
"label.add-website": "Weboldal hozzáadása",
"label.admin": "Adminisztrátor",
- "label.after": "After",
+ "label.affiliate": "Partner",
+ "label.after": "Után",
"label.all": "Összes",
- "label.all-time": "All time",
- "label.analytics": "Analytics",
- "label.average": "Average",
+ "label.all-time": "Minden időszak",
+ "label.analytics": "Analitika",
+ "label.apply": "Alkalmaz",
+ "label.attribution": "Attribúció",
+ "label.attribution-description": "Nézze meg, hogyan lépnek kapcsolatba a felhasználók a marketingjével, és mi vezet konverzióhoz.",
+ "label.average": "Átlag",
"label.back": "Vissza",
- "label.before": "Before",
+ "label.before": "Előtt",
+ "label.behavior": "Viselkedés",
+ "label.boards": "Táblák",
"label.bounce-rate": "Visszafordulási arány",
- "label.breakdown": "Breakdown",
- "label.browser": "Browser",
+ "label.breakdown": "Bontás",
+ "label.browser": "Böngésző",
"label.browsers": "Böngészők",
+ "label.campaigns": "Kampányok",
"label.cancel": "Mégsem",
"label.change-password": "Jelszó módosítása",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
+ "label.channels": "Csatornák",
+ "label.cities": "Városok",
+ "label.city": "Város",
+ "label.clear-all": "Összes törlése",
+ "label.cohort": "Kohorsz",
+ "label.compare": "Összehasonlít",
+ "label.compare-dates": "Dátumok összehasonlítása",
+ "label.confirm": "Megerősít",
"label.confirm-password": "Jelszó megerősítése",
"label.contains": "Contains",
- "label.continue": "Continue",
- "label.count": "Count",
+ "label.content": "Tartalom",
+ "label.continue": "Folytatás",
+ "label.conversion": "Konverzió",
+ "label.conversion-rate": "Konverziós arány",
+ "label.conversion-step": "Konverziós lépés",
+ "label.count": "Darabszám",
"label.countries": "Országok",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.country": "Ország",
+ "label.create": "Létrehozás",
+ "label.create-report": "Jelentés létrehozása",
+ "label.create-team": "Csapat létrehozása",
+ "label.create-user": "Felhasználó létrehozása",
+ "label.created": "Létrehozva",
+ "label.created-by": "Létrehozta",
+ "label.currency": "Pénznem",
+ "label.current": "Jelenlegi",
"label.current-password": "Jelenlegi jelszó",
"label.custom-range": "Egyedi tartomány",
"label.dashboard": "Áttekintés",
- "label.data": "Data",
- "label.date": "Date",
+ "label.data": "Adat",
+ "label.date": "Dátum",
"label.date-range": "Időintervallum",
- "label.day": "Day",
+ "label.day": "Nap",
"label.default-date-range": "Alapértelmezett időintervallum",
"label.delete": "Eltávolítás",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "Jelentés törlése",
+ "label.delete-team": "Csapat törlése",
+ "label.delete-user": "Felhasználó törlése",
"label.delete-website": "Weboldal eltávolítása",
- "label.description": "Description",
+ "label.description": "Leírás",
"label.desktop": "Asztali számítógép",
- "label.details": "Details",
- "label.device": "Device",
+ "label.details": "Részletek",
+ "label.device": "Eszköz",
"label.devices": "Eszközök",
+ "label.direct": "Közvetlen",
"label.dismiss": "Mellőzés",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "Egyedi azonosító",
+ "label.does-not-contain": "Nem tartalmazza",
+ "label.does-not-include": "Nem tartalmazza",
+ "label.doest-not-exist": "Nem létezik",
"label.domain": "Domain",
- "label.dropoff": "Dropoff",
+ "label.dropoff": "Lemorzsolódás",
"label.edit": "Módosítás",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
+ "label.edit-dashboard": "Irányítópult szerkesztése",
+ "label.edit-member": "Tag szerkesztése",
+ "label.email": "E-mail",
"label.enable-share-url": "URL-megosztás engedélyezése",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event data",
+ "label.end-step": "Befejező lépés",
+ "label.entry": "Belépési URL",
+ "label.event": "Esemény",
+ "label.event-data": "Eseményadatok",
+ "label.event-name": "Esemény neve",
"label.events": "Események",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
+ "label.exists": "Létezik",
+ "label.exit": "Kilépési URL",
+ "label.false": "Hamis",
+ "label.field": "Mező",
+ "label.fields": "Mezők",
"label.filter": "Filter",
"label.filter-combined": "Összevont",
"label.filter-raw": "Nyers",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.filters": "Szűrők",
+ "label.first-click": "Első kattintás",
+ "label.first-seen": "Első megtekintés",
+ "label.funnel": "Tölcsér",
+ "label.funnel-description": "Értse meg a felhasználók konverziós és lemorzsolódási arányát.",
+ "label.funnels": "Tölcsérek",
+ "label.goal": "Cél",
+ "label.goals": "Célok",
+ "label.goals-description": "Kövesse nyomon a céljait oldalmegtekintések és események alapján.",
+ "label.greater-than": "Nagyobb mint",
+ "label.greater-than-equals": "Nagyobb vagy egyenlő",
+ "label.grouped": "Csoportosítva",
+ "label.hostname": "Hosztnév",
+ "label.includes": "Tartalmazza",
+ "label.insight": "Betekintés",
+ "label.insights": "Betekintések",
+ "label.insights-description": "Merüljön el mélyebben az adataiban szegmensek és szűrők használatával.",
+ "label.is": "Az",
+ "label.is-false": "Hamis",
+ "label.is-not": "Nem az",
+ "label.is-not-set": "Nincs beállítva",
+ "label.is-set": "Beállítva",
+ "label.is-true": "Igaz",
+ "label.join": "Csatlakozás",
+ "label.join-team": "Csatlakozás a csapathoz",
+ "label.journey": "Út",
+ "label.journey-description": "Értse meg, hogyan navigálnak a felhasználók a weboldalán.",
+ "label.journeys": "Utak",
"label.language": "Language",
"label.languages": "Languages",
"label.laptop": "Laptop",
+ "label.last-click": "Utolsó kattintás",
"label.last-days": "Legutóbbi {x} nap",
"label.last-hours": "Legutóbbi {x} óra",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "Utolsó {x} hónap",
+ "label.last-seen": "Utoljára látva",
+ "label.leave": "Kilépés",
+ "label.leave-team": "Csapat elhagyása",
+ "label.less-than": "Kevesebb mint",
+ "label.less-than-equals": "Kevesebb vagy egyenlő",
+ "label.links": "Linkek",
"label.login": "Bejelentkezés",
"label.logout": "Kijelentkezés",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
+ "label.manage": "Kezelés",
+ "label.manager": "Menedzser",
+ "label.max": "Maximum",
+ "label.maximize": "Kibontás",
+ "label.medium": "Közepes",
+ "label.member": "Tag",
+ "label.members": "Tagok",
+ "label.min": "Minimum",
"label.mobile": "Telefon",
+ "label.model": "Model",
"label.more": "Bővebben",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "Saját fiók",
+ "label.my-websites": "Saját weboldalak",
"label.name": "Név",
"label.new-password": "Új jelszó",
- "label.none": "None",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
+ "label.none": "Nincs",
+ "label.number-of-records": "{x} {x, plural, one {rekord} other {rekord}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organikus keresés",
+ "label.organic-shopping": "Organikus vásárlás",
+ "label.organic-social": "Organikus közösségi",
+ "label.organic-video": "Organikus videó",
"label.os": "OS",
- "label.overview": "Overview",
- "label.owner": "Owner",
- "label.page-of": "Page {current} of {total}",
+ "label.other": "Egyéb",
+ "label.overview": "Áttekintés",
+ "label.owner": "Tulajdonos",
+ "label.page": "Oldal",
+ "label.page-of": "Oldal {current} / {total}",
"label.page-views": "Oldalmegtekintések",
"label.pageTitle": "Page title",
"label.pages": "Oldalak",
+ "label.paid-ads": "Fizetett hirdetések",
+ "label.paid-search": "Fizetett keresés",
+ "label.paid-shopping": "Fizetett vásárlás",
+ "label.paid-social": "Fizetett közösségi",
+ "label.paid-video": "Fizetett videó",
"label.password": "Jelszó",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Útvonal",
+ "label.paths": "Útvonalak",
+ "label.pixels": "Pixelek",
"label.powered-by": "Működteti az {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
"label.previous-year": "Previous year",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
- "label.query": "Query",
- "label.query-parameters": "Query parameters",
+ "label.properties": "Tulajdonságok",
+ "label.property": "Tulajdonság",
+ "label.queries": "Lekérdezések",
+ "label.query": "Lekérdezés",
+ "label.query-parameters": "Lekérdezési paraméterek",
"label.realtime": "Valós idejű",
+ "label.referral": "Hivatkozás",
"label.referrer": "Referrer",
"label.referrers": "Hivatkozók",
"label.refresh": "Frissítés",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Hátralévő",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -161,36 +211,44 @@
"label.reset-website": "Reset statistics",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Bevétel",
+ "label.revenue-description": "Tekintse meg bevételeit az idő múlásával.",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "Mentés",
- "label.screens": "Screens",
+ "label.screens": "Képernyők",
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Szűrő kiválasztása",
"label.select-role": "Select role",
"label.select-website": "Select website",
- "label.session": "Session",
+ "label.session": "Munkamenet",
+ "label.session-data": "Munkamenet adatai",
"label.sessions": "Sessions",
"label.settings": "Beállítások",
+ "label.share": "Megosztás",
"label.share-url": "URL megosztása",
"label.single-day": "Egy nap",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
+ "label.sms": "SMS",
+ "label.sources": "Források",
+ "label.start-step": "Kezdő lépés",
+ "label.steps": "Lépések",
"label.sum": "Sum",
"label.tablet": "Táblagép",
+ "label.tag": "Címke",
+ "label.tags": "Címkék",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Csapat beállításai",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Kifejezések",
"label.theme": "Theme",
"label.this-month": "Ezen hónap",
"label.this-week": "Ezen hét",
@@ -213,10 +271,7 @@
"label.unknown": "Ismeretlen",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Felhasználónév",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} {x, plural, one {látogató} other {latógató}} jelenleg",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Biztos, hogy törölni szeretnéd {target} elemet?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Minden társított adat törlésre kerül.",
"message.error": "Valami baj történt.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Tovább a beállításokhoz",
"message.incorrect-username-password": "Érvénytelen felhasználónév/jelszó.",
"message.invalid-domain": "Érvénytelen domain",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "Még nem állítottál be egyetlen weboldalt sem.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Oldal nem található.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.saved": "Sikeres mentés.",
+ "message.sever-error": "Server error",
"message.share-url": "{target} nyilvánosan megosztott URL címe.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Látógató {country} területéről, {os} {device} eszközön, {browser} böngészőből.",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Látógató {country} területéről, {os} {device} eszközön, {browser} böngészőből."
}
diff --git a/src/lang/id-ID.json b/src/lang/id-ID.json
index b6eb83ce..30a64b6c 100644
--- a/src/lang/id-ID.json
+++ b/src/lang/id-ID.json
@@ -3,32 +3,47 @@
"label.actions": "Aksi",
"label.activity": "Catatan aktivitas",
"label.add": "Tambah",
+ "label.add-board": "Tambah papan",
"label.add-description": "Tambah deskripsi",
"label.add-member": "Tambah anggota",
"label.add-step": "Tambah langkah",
"label.add-website": "Tambah situs web",
"label.admin": "Pengelola",
+ "label.affiliate": "Afiliasi",
"label.after": "Setelah",
"label.all": "Semua",
"label.all-time": "Semua waktu",
"label.analytics": "Analitik",
+ "label.apply": "Terapkan",
+ "label.attribution": "Atribusi",
+ "label.attribution-description": "Lihat bagaimana pengguna berinteraksi dengan pemasaran Anda dan apa yang mendorong konversi.",
"label.average": "Rata-rata",
"label.back": "Kembali",
"label.before": "Sebelum",
+ "label.behavior": "Perilaku",
+ "label.boards": "Papan",
"label.bounce-rate": "Rasio pentalan",
"label.breakdown": "Rincian",
"label.browser": "Peramban",
"label.browsers": "Peramban",
+ "label.campaigns": "Kampanye",
"label.cancel": "Batal",
"label.change-password": "Ganti kata sandi",
+ "label.channels": "Saluran",
"label.cities": "Kota",
"label.city": "Kota",
"label.clear-all": "Hapus semua",
+ "label.cohort": "Kelompok",
"label.compare": "Bandingkan",
+ "label.compare-dates": "Bandingkan tanggal",
"label.confirm": "Konfirmasi",
"label.confirm-password": "Konfirmasi kata sandi",
"label.contains": "Mengandung",
+ "label.content": "Konten",
"label.continue": "Lanjutkan",
+ "label.conversion": "Konversi",
+ "label.conversion-rate": "Tingkat konversi",
+ "label.conversion-step": "Langkah konversi",
"label.count": "Jumlah",
"label.countries": "Negara",
"label.country": "Negara",
@@ -38,6 +53,7 @@
"label.create-user": "Buat pengguna",
"label.created": "Dibuat",
"label.created-by": "Dibuat oleh",
+ "label.currency": "Mata uang",
"label.current": "Saat ini",
"label.current-password": "Kata sandi sekarang",
"label.custom-range": "Rentang khusus",
@@ -57,19 +73,26 @@
"label.details": "Detail",
"label.device": "Perangkat",
"label.devices": "Perangkat",
+ "label.direct": "Langsung",
"label.dismiss": "Tutup",
+ "label.distinct-id": "ID unik",
"label.does-not-contain": "Tidak mengandung",
+ "label.does-not-include": "Tidak termasuk",
+ "label.doest-not-exist": "Tidak ada",
"label.domain": "Domain",
"label.dropoff": "Penurunan",
"label.edit": "Sunting",
"label.edit-dashboard": "Sunting dasbor",
"label.edit-member": "Sunting anggota",
+ "label.email": "Email",
"label.enable-share-url": "Aktifkan URL berbagi",
"label.end-step": "Langkah akhir",
"label.entry": "URL masuk",
"label.event": "Peristiwa",
"label.event-data": "Data peristiwa",
+ "label.event-name": "Nama peristiwa",
"label.events": "Peristiwa",
+ "label.exists": "Ada",
"label.exit": "Exit URL",
"label.false": "Salah",
"label.field": "Kolom",
@@ -78,29 +101,37 @@
"label.filter-combined": "Gabungan",
"label.filter-raw": "Mentah",
"label.filters": "Filters",
+ "label.first-click": "Klik pertama",
"label.first-seen": "Pertama kali dilihat",
"label.funnel": "Funnel",
"label.funnel-description": "Pahami tingkat konversi dan penurunan pengguna.",
+ "label.funnels": "Corong",
"label.goal": "Tujuan",
"label.goals": "Tujuan",
"label.goals-description": "Lacak tujuan Anda untuk tampilan halaman dan peristiwa.",
"label.greater-than": "Lebih dari",
"label.greater-than-equals": "Lebih dari atau sama dengan",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Dikelompokkan",
+ "label.hostname": "Nama host",
+ "label.includes": "Termasuk",
+ "label.insight": "Wawasan",
"label.insights": "Wawasan",
"label.insights-description": "Jelajahi data Anda lebih dalam dengan menggunakan segmen dan filter.",
"label.is": "Adalah",
+ "label.is-false": "Salah",
"label.is-not": "Bukan",
"label.is-not-set": "Tidak diatur",
"label.is-set": "Diatur",
+ "label.is-true": "Benar",
"label.join": "Gabung",
"label.join-team": "Gabung tim",
"label.journey": "Perjalanan",
"label.journey-description": "Pahami bagaimana pengguna menavigasi situs web Anda.",
+ "label.journeys": "Perjalanan",
"label.language": "Bahasa",
"label.languages": "Bahasa",
"label.laptop": "Laptop",
+ "label.last-click": "Klik terakhir",
"label.last-days": "{x} hari terakhir",
"label.last-hours": "{x} jam terakhir",
"label.last-months": "{x} bulan terakhir",
@@ -109,50 +140,69 @@
"label.leave-team": "Keluar dari tim",
"label.less-than": "Kurang dari",
"label.less-than-equals": "Kurang dari atau sama dengan",
+ "label.links": "Tautan",
"label.login": "Masuk",
"label.logout": "Keluar",
"label.manage": "Kelola",
- "label.manager": "Pengelola",
- "label.max": "Maks",
+ "label.manager": "Manajer",
+ "label.max": "Maksimum",
+ "label.maximize": "Perluas",
+ "label.medium": "Sedang",
"label.member": "Anggota",
"label.members": "Anggota",
- "label.min": "Min",
+ "label.min": "Minimum",
"label.mobile": "Ponsel",
+ "label.model": "Model",
"label.more": "Lebih banyak",
"label.my-account": "Akun saya",
"label.my-websites": "Situs web saya",
"label.name": "Nama",
"label.new-password": "Kata sandi baru",
"label.none": "Tidak ada",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
+ "label.number-of-records": "{x} {x, plural, one {catatan} other {catatan}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Pencarian organik",
+ "label.organic-shopping": "Belanja organik",
+ "label.organic-social": "Sosial organik",
+ "label.organic-video": "Video organik",
"label.os": "OS",
+ "label.other": "Lainnya",
"label.overview": "Tinjauan umum",
"label.owner": "Pemilik",
+ "label.page": "Halaman",
"label.page-of": "Halaman {current} dari {total}",
"label.page-views": "Tampilan halaman",
"label.pageTitle": "Judul halaman",
"label.pages": "Halaman",
+ "label.paid-ads": "Iklan berbayar",
+ "label.paid-search": "Pencarian berbayar",
+ "label.paid-shopping": "Belanja berbayar",
+ "label.paid-social": "Sosial berbayar",
+ "label.paid-video": "Video berbayar",
"label.password": "Kata sandi",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Jalur",
+ "label.paths": "Jalur",
+ "label.pixels": "Piksel",
"label.powered-by": "Didukung oleh {name}",
"label.previous": "Sebelumnya",
"label.previous-period": "Periode sebelumnya",
"label.previous-year": "Tahun lalu",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
- "label.query": "Query",
- "label.query-parameters": "Query parameters",
+ "label.properties": "Properti",
+ "label.property": "Properti",
+ "label.queries": "Kueri",
+ "label.query": "Kueri",
+ "label.query-parameters": "Parameter kueri",
"label.realtime": "Waktu nyata",
+ "label.referral": "Rujukan",
"label.referrer": "Perujuk",
"label.referrers": "Perujuk",
"label.refresh": "Segarkan",
"label.regenerate": "Buat ulang",
"label.region": "Wilayah",
"label.regions": "Wilayah",
+ "label.remaining": "Tersisa",
"label.remove": "Hapus",
"label.remove-member": "Hapus anggota",
"label.reports": "Laporan",
@@ -163,7 +213,6 @@
"label.retention-description": "Ukur daya tarik situs web Anda dengan melacak seberapa sering pengguna kembali.",
"label.revenue": "Pendapatan",
"label.revenue-description": "Lihat pendapatan Anda seiring waktu.",
- "label.revenue-property": "Properti pendapatan",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "Simpan",
@@ -171,26 +220,35 @@
"label.search": "Cari",
"label.select": "Pilih",
"label.select-date": "Pilih tanggal",
+ "label.select-filter": "Pilih filter",
"label.select-role": "Pilih role",
"label.select-website": "Pilih situs web",
"label.session": "Sesi",
+ "label.session-data": "Data sesi",
"label.sessions": "Sesi",
"label.settings": "Pengaturan",
+ "label.share": "Bagikan",
"label.share-url": "Bagikan URL",
"label.single-day": "Sehari",
+ "label.sms": "SMS",
+ "label.sources": "Sumber",
"label.start-step": "Langkah awal",
"label.steps": "Langkah",
"label.sum": "Sum",
"label.tablet": "Tablet",
+ "label.tag": "Tag",
+ "label.tags": "Tag",
"label.team": "Tim",
"label.team-id": "ID tim",
"label.team-manager": "Pengelola tim",
"label.team-member": "Anggota tim",
"label.team-name": "Nama tim",
"label.team-owner": "Pemilik tim",
+ "label.team-settings": "Pengaturan tim",
"label.team-view-only": "Team view only",
"label.team-websites": "Situs web tim",
"label.teams": "Tim",
+ "label.terms": "Ketentuan",
"label.theme": "Tema",
"label.this-month": "Bulan ini",
"label.this-week": "Minggu ini",
@@ -213,10 +271,7 @@
"label.unknown": "Tidak diketahui",
"label.untitled": "Tanpa judul",
"label.update": "Perbarui",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Pengguna",
- "label.user-property": "User Property",
"label.username": "Nama pengguna",
"label.users": "Pengguna",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Kemarin",
"message.action-confirmation": "Ketik {confirmation} pada kotak di bawah untuk mengonfirmasi.",
"message.active-users": "{x} pengunjung saat ini",
+ "message.bad-request": "Bad request",
"message.collected-data": "Data dikumpulkan",
"message.confirm-delete": "Apakah kamu yakin ingin menghapus {target}?",
"message.confirm-leave": "Apakah Anda yakin ingin meninggalkan {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Semua data terkait juga akan dihapus.",
"message.error": "Ada yang salah.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Pergi ke pengaturan",
"message.incorrect-username-password": "Nama pengguna/kata sandi salah.",
"message.invalid-domain": "Domain tidak valid",
@@ -259,10 +316,13 @@
"message.no-teams": "Anda belum membuat tim.",
"message.no-users": "Tidak ada pengguna.",
"message.no-websites-configured": "Anda tidak memiliki situs web yang dikonfigurasi.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Halaman tidak ditemukan.",
"message.reset-website": "Untuk mengatur ulang situs web ini, ketik {confirmation} pada kotak di bawah untuk mengonfirmasi.",
"message.reset-website-warning": "Semua statistik pada situs web ini akan dihapus, tetapi kode lacak akan tetap terpasang",
"message.saved": "Berhasil disimpan.",
+ "message.sever-error": "Server error",
"message.share-url": "Ini adalah URL yang dibagikan secara publik untuk {target}.",
"message.team-already-member": "Anda sudah menjadi anggota tim ini.",
"message.team-not-found": "Tim tidak ditemukan.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Pilih tim tujuan untuk mentransfer situs web ini.",
"message.transfer-website": "Transfer kepemilikan situs web ke akun Anda atau tim lain",
"message.triggered-event": "Peristiwa terjadi",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Pengguna telah dihapus.",
"message.viewed-page": "Halaman dilihat",
- "message.visitor-log": "Pengunjung dari {country} dengan {browser} di {device} {os}",
- "message.visitors-dropped-off": "Pengunjung yang meninggalkan situs web"
+ "message.visitor-log": "Pengunjung dari {country} dengan {browser} di {device} {os}"
}
diff --git a/src/lang/it-IT.json b/src/lang/it-IT.json
index 4df7599a..40cb5ecd 100644
--- a/src/lang/it-IT.json
+++ b/src/lang/it-IT.json
@@ -1,158 +1,208 @@
{
- "label.access-code": "Access code",
+ "label.access-code": "Codice di accesso",
"label.actions": "Azioni",
- "label.activity": "Activity log",
- "label.add": "Add",
- "label.add-description": "Add description",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.activity": "Registro attività",
+ "label.add": "Aggiungi",
+ "label.add-board": "Aggiungi bacheca",
+ "label.add-description": "Aggiungi descrizione",
+ "label.add-member": "Aggiungi membro",
+ "label.add-step": "Aggiungi passaggio",
"label.add-website": "Aggiungi sito",
"label.admin": "Amministratore",
- "label.after": "After",
+ "label.affiliate": "Affiliato",
+ "label.after": "Dopo",
"label.all": "Tutto",
"label.all-time": "Sempre",
- "label.analytics": "Analytics",
- "label.average": "Average",
+ "label.analytics": "Analitica",
+ "label.apply": "Applica",
+ "label.attribution": "Attribuzione",
+ "label.attribution-description": "Scopri come gli utenti interagiscono con il tuo marketing e cosa genera conversioni.",
+ "label.average": "Media",
"label.back": "Indietro",
- "label.before": "Before",
+ "label.before": "Prima",
+ "label.behavior": "Comportamento",
+ "label.boards": "Bacheche",
"label.bounce-rate": "Frequenza di rimbalzo",
- "label.breakdown": "Breakdown",
+ "label.breakdown": "Dettaglio",
"label.browser": "Browser",
"label.browsers": "Browser",
+ "label.campaigns": "Campagne",
"label.cancel": "Annulla",
"label.change-password": "Modifica password",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
+ "label.channels": "Canali",
+ "label.cities": "Città",
+ "label.city": "Città",
+ "label.clear-all": "Cancella tutto",
+ "label.cohort": "Coorte",
+ "label.compare": "Confronta",
+ "label.compare-dates": "Confronta date",
+ "label.confirm": "Conferma",
"label.confirm-password": "Conferma password",
"label.contains": "Contains",
- "label.continue": "Continue",
- "label.count": "Count",
+ "label.content": "Contenuto",
+ "label.continue": "Continua",
+ "label.conversion": "Conversione",
+ "label.conversion-rate": "Tasso di conversione",
+ "label.conversion-step": "Passaggio di conversione",
+ "label.count": "Conteggio",
"label.countries": "Nazioni",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.country": "Paese",
+ "label.create": "Crea",
+ "label.create-report": "Crea rapporto",
+ "label.create-team": "Crea team",
+ "label.create-user": "Crea utente",
+ "label.created": "Creato",
+ "label.created-by": "Creato da",
+ "label.currency": "Valuta",
+ "label.current": "Attuale",
"label.current-password": "Password attuale",
"label.custom-range": "Personalizzato",
"label.dashboard": "Pannello di Controllo",
- "label.data": "Data",
- "label.date": "Date",
+ "label.data": "Dati",
+ "label.date": "Data",
"label.date-range": "Periodo",
- "label.day": "Day",
+ "label.day": "Giorno",
"label.default-date-range": "Periodo standard",
"label.delete": "Elimina",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "Elimina rapporto",
+ "label.delete-team": "Elimina team",
+ "label.delete-user": "Elimina utente",
"label.delete-website": "Elimina sito",
- "label.description": "Description",
+ "label.description": "Descrizione",
"label.desktop": "Desktop",
- "label.details": "Details",
- "label.device": "Device",
+ "label.details": "Dettagli",
+ "label.device": "Dispositivo",
"label.devices": "Dispositivi",
+ "label.direct": "Diretto",
"label.dismiss": "Scarta",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "ID distinto",
+ "label.does-not-contain": "Non contiene",
+ "label.does-not-include": "Non include",
+ "label.doest-not-exist": "Non esiste",
"label.domain": "Dominio",
- "label.dropoff": "Dropoff",
+ "label.dropoff": "Abbandono",
"label.edit": "Modifica",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
+ "label.edit-dashboard": "Modifica pannello di controllo",
+ "label.edit-member": "Modifica membro",
+ "label.email": "Email",
"label.enable-share-url": "Abilita URL di condivisione",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event data",
+ "label.end-step": "Passaggio finale",
+ "label.entry": "URL di ingresso",
+ "label.event": "Evento",
+ "label.event-data": "Dati evento",
+ "label.event-name": "Nome evento",
"label.events": "Eventi",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
+ "label.exists": "Esiste",
+ "label.exit": "URL di uscita",
+ "label.false": "Falso",
+ "label.field": "Campo",
+ "label.fields": "Campi",
"label.filter": "Filter",
"label.filter-combined": "Aggregati",
"label.filter-raw": "Raw",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
+ "label.filters": "Filtri",
+ "label.first-click": "Primo clic",
+ "label.first-seen": "Prima visualizzazione",
"label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.funnel-description": "Comprendi il tasso di conversione e di abbandono degli utenti.",
+ "label.funnels": "Funnel",
+ "label.goal": "Obiettivo",
+ "label.goals": "Obiettivi",
+ "label.goals-description": "Tieni traccia dei tuoi obiettivi per visualizzazioni di pagina ed eventi.",
+ "label.greater-than": "Maggiore di",
+ "label.greater-than-equals": "Maggiore o uguale a",
+ "label.grouped": "Raggruppato",
+ "label.hostname": "Nome host",
+ "label.includes": "Include",
+ "label.insight": "Approfondimento",
+ "label.insights": "Approfondimenti",
+ "label.insights-description": "Analizza più a fondo i tuoi dati utilizzando segmenti e filtri.",
+ "label.is": "È",
+ "label.is-false": "È falso",
+ "label.is-not": "Non è",
+ "label.is-not-set": "Non impostato",
+ "label.is-set": "Impostato",
+ "label.is-true": "È vero",
+ "label.join": "Unisciti",
+ "label.join-team": "Unisciti al team",
+ "label.journey": "Percorso",
+ "label.journey-description": "Comprendi come gli utenti navigano nel tuo sito web.",
+ "label.journeys": "Percorsi",
"label.language": "Lingua",
"label.languages": "Lingue",
"label.laptop": "Portatile",
+ "label.last-click": "Ultimo clic",
"label.last-days": "Ultimi {x} giorni",
"label.last-hours": "Ultime {x} ore",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "Ultimi {x} mesi",
+ "label.last-seen": "Ultima visualizzazione",
+ "label.leave": "Lascia",
+ "label.leave-team": "Lascia il team",
+ "label.less-than": "Meno di",
+ "label.less-than-equals": "Meno o uguale a",
+ "label.links": "Link",
"label.login": "Accedi",
"label.logout": "Esci",
- "label.manage": "Manage",
+ "label.manage": "Gestisci",
"label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
+ "label.max": "Massimo",
+ "label.maximize": "Espandi",
+ "label.medium": "Medio",
+ "label.member": "Membro",
+ "label.members": "Membri",
+ "label.min": "Minimo",
"label.mobile": "Cellulare",
+ "label.model": "Model",
"label.more": "Dettagli",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "Il mio account",
+ "label.my-websites": "I miei siti",
"label.name": "Nome",
"label.new-password": "Nuova password",
- "label.none": "None",
+ "label.none": "Nessuno",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Ricerca organica",
+ "label.organic-shopping": "Acquisto organico",
+ "label.organic-social": "Social organico",
+ "label.organic-video": "Video organico",
"label.os": "OS",
+ "label.other": "Altro",
"label.overview": "Overview",
"label.owner": "Proprietario",
+ "label.page": "Pagina",
"label.page-of": "Page {current} of {total}",
"label.page-views": "Visualizzazioni di pagina",
"label.pageTitle": "Page title",
"label.pages": "Pagine",
+ "label.paid-ads": "Annunci a pagamento",
+ "label.paid-search": "Ricerca a pagamento",
+ "label.paid-shopping": "Acquisto a pagamento",
+ "label.paid-social": "Social a pagamento",
+ "label.paid-video": "Video a pagamento",
"label.password": "Password",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Percorso",
+ "label.paths": "Percorsi",
+ "label.pixels": "Pixel",
"label.powered-by": "Powered by {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
"label.previous-year": "Previous year",
"label.profile": "Profilo",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
+ "label.properties": "Proprietà",
+ "label.property": "Proprietà",
+ "label.queries": "Query",
"label.query": "Query",
- "label.query-parameters": "Query parameters",
+ "label.query-parameters": "Parametri query",
"label.realtime": "Tempo reale",
+ "label.referral": "Referente",
"label.referrer": "Referrer",
"label.referrers": "Referrers",
"label.refresh": "Ricarica",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Rimanente",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -161,36 +211,44 @@
"label.reset-website": "Resetta le statistiche",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Ricavi",
+ "label.revenue-description": "Consulta i tuoi ricavi nel tempo.",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "Salva",
- "label.screens": "Screens",
+ "label.screens": "Schermi",
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Seleziona filtro",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.session": "Session",
+ "label.session-data": "Dati sessione",
"label.sessions": "Sessions",
"label.settings": "Impostazioni",
+ "label.share": "Condividi",
"label.share-url": "Condividi link",
"label.single-day": "Singolo giorno",
+ "label.sms": "SMS",
+ "label.sources": "Fonti",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "Tablet",
+ "label.tag": "Etichetta",
+ "label.tags": "Etichette",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Impostazioni team",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Termini",
"label.theme": "Tema",
"label.this-month": "Questo mese",
"label.this-week": "Questa settimana",
@@ -213,10 +271,7 @@
"label.unknown": "Sconosciuto",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Nome utente",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Ieri",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} {x, plural, one {visitatore} other {visitatori}} online",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Sei sicuro di voler eliminare {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Saranno eliminati anche tutti i dati associati.",
"message.error": "Si è verificato un errore.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Vai alle impostazioni",
"message.incorrect-username-password": "Username o password non corretti.",
"message.invalid-domain": "Dominio non valido",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "Non hai ancora configurato alcun sito.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Pagina non trovata",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "Tutte le statistiche verranno cancellate per questo sito, ma il tuo codice di tracciamento rimarrà invariato.",
"message.saved": "Salvato!",
+ "message.sever-error": "Server error",
"message.share-url": "Questo è l'URL di condivisione per {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Utenti da {country} tramite {browser} su {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Utenti da {country} tramite {browser} su {os} {device}"
}
diff --git a/src/lang/ja-JP.json b/src/lang/ja-JP.json
index ce4001f4..7d2bf403 100644
--- a/src/lang/ja-JP.json
+++ b/src/lang/ja-JP.json
@@ -3,32 +3,47 @@
"label.actions": "アクション",
"label.activity": "アクティビティログ",
"label.add": "追加",
+ "label.add-board": "ボードを追加",
"label.add-description": "説明を追加",
"label.add-member": "メンバーの追加",
"label.add-step": "ステップを追加",
"label.add-website": "Webサイトの追加",
"label.admin": "管理者",
+ "label.affiliate": "アフィリエイト",
"label.after": "直後",
"label.all": "すべて",
"label.all-time": "すべての時間帯",
"label.analytics": "アナリティクス",
+ "label.apply": "適用",
+ "label.attribution": "アトリビューション",
+ "label.attribution-description": "ユーザーがあなたのマーケティングにどのように関与し、何がコンバージョンを促進するかを確認します。",
"label.average": "平均",
"label.back": "戻る",
"label.before": "直前",
+ "label.behavior": "行動",
+ "label.boards": "ボード",
"label.bounce-rate": "直帰率",
"label.breakdown": "故障",
"label.browser": "ブラウザ",
"label.browsers": "ブラウザ",
+ "label.campaigns": "キャンペーン",
"label.cancel": "キャンセル",
"label.change-password": "パスワードの変更",
+ "label.channels": "チャンネル",
"label.cities": "都市",
"label.city": "都市",
"label.clear-all": "すべてクリア",
+ "label.cohort": "コホート",
"label.compare": "比較",
+ "label.compare-dates": "日付を比較",
"label.confirm": "確認",
"label.confirm-password": "パスワード(確認)",
"label.contains": "コンテンツ",
+ "label.content": "コンテンツ",
"label.continue": "続ける",
+ "label.conversion": "コンバージョン",
+ "label.conversion-rate": "コンバージョン率",
+ "label.conversion-step": "コンバージョンステップ",
"label.count": "回数",
"label.countries": "国名",
"label.country": "国",
@@ -38,6 +53,7 @@
"label.create-user": "ユーザーの作成",
"label.created": "作成されました",
"label.created-by": "作成者",
+ "label.currency": "通貨",
"label.current": "現在",
"label.current-password": "現在のパスワード",
"label.custom-range": "範囲指定",
@@ -57,19 +73,26 @@
"label.details": "詳細情報",
"label.device": "デバイス",
"label.devices": "デバイス",
+ "label.direct": "ダイレクト",
"label.dismiss": "却下",
+ "label.distinct-id": "識別ID",
"label.does-not-contain": "を含まない",
+ "label.does-not-include": "含まない",
+ "label.doest-not-exist": "存在しない",
"label.domain": "ドメイン",
"label.dropoff": "切り捨て",
"label.edit": "編集",
"label.edit-dashboard": "ダッシュボードの編集",
"label.edit-member": "メンバーの編集",
+ "label.email": "メール",
"label.enable-share-url": "共有URLを有効にする",
"label.end-step": "最終ステップ",
"label.entry": "訪問時のURL",
"label.event": "イベント",
"label.event-data": "イベントデータ",
+ "label.event-name": "イベント名",
"label.events": "イベント",
+ "label.exists": "存在する",
"label.exit": "退出時のURL",
"label.false": "偽",
"label.field": "フィールド",
@@ -78,29 +101,37 @@
"label.filter-combined": "結合",
"label.filter-raw": "RAW",
"label.filters": "フィルター",
+ "label.first-click": "最初のクリック",
"label.first-seen": "初回ログイン",
"label.funnel": "ファネル",
"label.funnel-description": "ユーザーのコンバージョン率と離脱率を分析します。",
+ "label.funnels": "ファネル",
"label.goal": "目標",
"label.goals": "目標",
"label.goals-description": "ページビューとイベントの目標を追跡します。",
"label.greater-than": "超過",
"label.greater-than-equals": "以上",
- "label.host": "ホスト",
- "label.hosts": "ホスト",
+ "label.grouped": "グループ化",
+ "label.hostname": "ホスト名",
+ "label.includes": "含む",
+ "label.insight": "インサイト",
"label.insights": "インサイト",
"label.insights-description": "セグメントとフィルタを使用して、データをさらに詳しく分析します。",
"label.is": "に等しい",
+ "label.is-false": "偽である",
"label.is-not": "に等しくない",
"label.is-not-set": "未設定",
"label.is-set": "設定済み",
+ "label.is-true": "真である",
"label.join": "参加",
"label.join-team": "チームに参加",
"label.journey": "ジャーニー",
"label.journey-description": "ユーザーがWebサイト内をどのように移動するかを把握します。",
+ "label.journeys": "ジャーニー",
"label.language": "言語",
"label.languages": "言語",
"label.laptop": "ノートPC",
+ "label.last-click": "最後のクリック",
"label.last-days": "過去{x}日間",
"label.last-hours": "過去{x}時間",
"label.last-months": "過去{x}月間",
@@ -109,15 +140,19 @@
"label.leave-team": "チームを離脱",
"label.less-than": "未満",
"label.less-than-equals": "以下",
+ "label.links": "リンク",
"label.login": "ログイン",
"label.logout": "ログアウト",
"label.manage": "管理",
"label.manager": "管理者",
"label.max": "最大",
+ "label.maximize": "展開",
+ "label.medium": "メディア",
"label.member": "メンバー",
"label.members": "メンバー",
"label.min": "最小",
"label.mobile": "携帯電話",
+ "label.model": "モデル",
"label.more": "もっと見る",
"label.my-account": "マイアカウント",
"label.my-websites": "マイWebサイト",
@@ -126,16 +161,29 @@
"label.none": "なし",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "オーガニック検索",
+ "label.organic-shopping": "オーガニックショッピング",
+ "label.organic-social": "オーガニックソーシャル",
+ "label.organic-video": "オーガニックビデオ",
"label.os": "OS",
+ "label.other": "その他",
"label.overview": "概要",
"label.owner": "所有者",
+ "label.page": "ページ",
"label.page-of": "ページ {current}/{total}",
"label.page-views": "閲覧数",
"label.pageTitle": "ページタイトル",
"label.pages": "ページ",
+ "label.paid-ads": "有料広告",
+ "label.paid-search": "有料検索",
+ "label.paid-shopping": "有料ショッピング",
+ "label.paid-social": "有料ソーシャル",
+ "label.paid-video": "有料ビデオ",
"label.password": "パスワード",
"label.path": "パス",
"label.paths": "パス",
+ "label.pixels": "ピクセル",
"label.powered-by": "Powered by {name}",
"label.previous": "以前",
"label.previous-period": "前期",
@@ -147,12 +195,14 @@
"label.query": "クエリ",
"label.query-parameters": "クエリパラメーター",
"label.realtime": "リアルタイム",
+ "label.referral": "Referral",
"label.referrer": "リファラー",
"label.referrers": "リファラー",
"label.refresh": "更新",
"label.regenerate": "再生成",
"label.region": "地域",
"label.regions": "地域",
+ "label.remaining": "残り",
"label.remove": "削除",
"label.remove-member": "メンバーの削除",
"label.reports": "レポート",
@@ -163,7 +213,6 @@
"label.retention-description": "ユーザーの再訪問回数を記録して、Webサイトのリテンション率を計測します。",
"label.revenue": "レベニュー",
"label.revenue-description": "時間あたりの売上高を確認します。",
- "label.revenue-property": "レベニュープロパティ",
"label.role": "ロール",
"label.run-query": "クエリ実行",
"label.save": "保存",
@@ -171,26 +220,35 @@
"label.search": "検索",
"label.select": "選択",
"label.select-date": "日付を選択",
+ "label.select-filter": "フィルターを選択",
"label.select-role": "ロールを選択",
"label.select-website": "Webサイトを選択",
"label.session": "セッション",
+ "label.session-data": "セッションデータ",
"label.sessions": "セッション",
"label.settings": "設定",
+ "label.share": "共有",
"label.share-url": "共有URL",
"label.single-day": "一日",
+ "label.sms": "SMS",
+ "label.sources": "ソース",
"label.start-step": "最初のステップ",
"label.steps": "ステップ",
"label.sum": "合計",
"label.tablet": "タブレット",
+ "label.tag": "タグ",
+ "label.tags": "タグ",
"label.team": "チーム",
"label.team-id": "チームID",
"label.team-manager": "チーム管理者",
"label.team-member": "チームメンバー",
"label.team-name": "チーム名",
"label.team-owner": "チームオーナー",
+ "label.team-settings": "チーム設定",
"label.team-view-only": "チーム表示のみ",
"label.team-websites": "チームのWebサイト",
"label.teams": "チーム",
+ "label.terms": "利用規約",
"label.theme": "テーマ",
"label.this-month": "今月",
"label.this-week": "今週",
@@ -213,10 +271,7 @@
"label.unknown": "不明",
"label.untitled": "無題",
"label.update": "更新",
- "label.url": "URL",
- "label.urls": "URL",
"label.user": "ユーザー",
- "label.user-property": "ユーザープロパティ",
"label.username": "ユーザー名",
"label.users": "ユーザー",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "昨日",
"message.action-confirmation": "承認する場合は、下のフォームに「{confirmation}」と入力してください。",
"message.active-users": "{x} {x, plural, one {アクティブな訪問者} other {アクティブな訪問者}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "収集されたデータ",
"message.confirm-delete": "{target}を削除してもよろしいですか?",
"message.confirm-leave": "{target}から離脱してもよろしいですか?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Webサイトのデータがすべて削除されます。",
"message.error": "未知のエラーが発生しました。",
"message.event-log": "{url}の{event}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "設定に移動する",
"message.incorrect-username-password": "ユーザー名またはパスワードが間違っています。",
"message.invalid-domain": "無効なドメインです。http/httpsを含めないでください。",
@@ -259,10 +316,13 @@
"message.no-teams": "チームを作成していません。",
"message.no-users": "ユーザーが存在しません。",
"message.no-websites-configured": "Webサイトが設定されていません。",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "ページが見つかりません",
"message.reset-website": "このWebサイトをリセットするには、下のフォームに「{confirmation}」と入力してください。",
"message.reset-website-warning": "このWebサイトの統計情報はすべて削除されますが、設定はそのまま残ります。",
"message.saved": "保存されました。",
+ "message.sever-error": "Server error",
"message.share-url": "あなたのWebサイトの統計情報は次のURLで公開されています:",
"message.team-already-member": "あなたはすでにチームのメンバーです。",
"message.team-not-found": "チームが見つかりません。",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "このWebサイトを移管するチームを選択してください。",
"message.transfer-website": "Webサイトの所有権を自分のアカウントまたは別のチームへ移管します。",
"message.triggered-event": "トリガーされたイベント",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "ユーザーが削除されました。",
"message.viewed-page": "閲覧されたページ",
- "message.visitor-log": "{os}({device})で{browser}を使用している{country}からの訪問者",
- "message.visitors-dropped-off": "訪問者の離脱率"
+ "message.visitor-log": "{os}({device})で{browser}を使用している{country}からの訪問者"
}
diff --git a/src/lang/km-KH.json b/src/lang/km-KH.json
index 8a87834a..087e24dc 100644
--- a/src/lang/km-KH.json
+++ b/src/lang/km-KH.json
@@ -1,34 +1,49 @@
{
- "label.access-code": "Access code",
+ "label.access-code": "កូដចូលប្រើ",
"label.actions": "សកម្មភាព",
"label.activity": "កំណត់ហេតុសកម្មភាព",
"label.add": "បង្កើតបន្ថែម",
+ "label.add-board": "បន្ថែមក្តារ",
"label.add-description": "បន្ថែមពិពណ៌នា",
"label.add-member": "បន្ថែមសមាជិក",
"label.add-step": "បន្ថែមជំហាន",
"label.add-website": "បន្ថែមគេហទំព័រ",
"label.admin": "អ្នកគ្រប់គ្រង",
+ "label.affiliate": "ដៃគូ",
"label.after": "បន្ទាប់",
"label.all": "ទាំងអស់",
"label.all-time": "គ្រប់ពេល",
- "label.analytics": "Analytics",
+ "label.analytics": "វិភាគ",
+ "label.apply": "អនុវត្ត",
+ "label.attribution": "ការបញ្ជាក់",
+ "label.attribution-description": "មើលថាប្រើប្រាស់របស់អ្នកធ្វើអ្វីជាមួយទីផ្សាររបស់អ្នក និងអ្វីជាហេតុបណ្តាលឲ្យមានការបម្លែង។",
"label.average": "ជាមធ្យម",
"label.back": "ថយក្រោយ",
"label.before": "មុន",
+ "label.behavior": "អាកប្បកិរិយា",
+ "label.boards": "ក្តារ",
"label.bounce-rate": "ចំនួនវិលត្រឡប់",
- "label.breakdown": "Breakdown",
- "label.browser": "Browser",
+ "label.breakdown": "បំបែកលម្អិត",
+ "label.browser": "កម្មវិធីរុករក",
"label.browsers": "កម្មវិធី",
+ "label.campaigns": "យុទ្ធនាការ",
"label.cancel": "បោះបង់",
"label.change-password": "ផ្លាស់ប្តូរពាក្យសម្ងាត់",
+ "label.channels": "ឆានែល",
"label.cities": "ទីក្រុង",
"label.city": "ទីក្រុង",
- "label.clear-all": "លុបទាំងអស់",
+ "label.clear-all": "លុបចេញទាំងអស់",
+ "label.cohort": "ក្រុម",
"label.compare": "ប្រៀបធៀប",
+ "label.compare-dates": "ប្រៀបធៀបទិន្នន័យថ្ងៃខែ",
"label.confirm": "បញ្ជាក់",
"label.confirm-password": "បញ្ជាក់ពាក្យសម្ងាត់",
"label.contains": "មាន",
+ "label.content": "មាតិកា",
"label.continue": "បន្ត",
+ "label.conversion": "ការបម្លែង",
+ "label.conversion-rate": "អត្រាបម្លែង",
+ "label.conversion-step": "ជំហានបម្លែង",
"label.count": "ចំនួន",
"label.countries": "ប្រទេស",
"label.country": "ប្រទេស",
@@ -38,6 +53,7 @@
"label.create-user": "បង្កើតអ្នកប្រើប្រាស់",
"label.created": "បង្កើតនៅ",
"label.created-by": "បង្កើតដោយ",
+ "label.currency": "រូបិយប័ណ្ណ",
"label.current": "បច្ចុប្បន្ន",
"label.current-password": "ពាក្យសម្ងាត់បច្ចុប្បន្ន",
"label.custom-range": "កំណត់ដោយខ្លួនឯង",
@@ -57,19 +73,26 @@
"label.details": "ព័ត៌មានលម្អិត",
"label.device": "ឧបករណ៍",
"label.devices": "ឧបករណ៍",
+ "label.direct": "ផ្ទាល់",
"label.dismiss": "រំសាយ",
+ "label.distinct-id": "លេខសម្គាល់ពិសេស",
"label.does-not-contain": "មិនមាន",
+ "label.does-not-include": "មិនរួមបញ្ចូល",
+ "label.doest-not-exist": "មិនមានទេ",
"label.domain": "Domain",
"label.dropoff": "Dropoff",
"label.edit": "កែប្រែ",
"label.edit-dashboard": "កែផ្ទាំងគ្រប់គ្រង",
"label.edit-member": "កែព័ត៌មានសមាជិក",
+ "label.email": "Email",
"label.enable-share-url": "បើកការចែករំលែក URL",
"label.end-step": "បញ្ចប់ជំហាន",
"label.entry": "URL ចូល",
"label.event": "ព្រឹត្តិការណ៍",
"label.event-data": "ទិន្នន័យព្រឹត្តិការណ៍",
+ "label.event-name": "ឈ្មោះព្រឹត្តិការណ៍",
"label.events": "ព្រឹត្តិការណ៍",
+ "label.exists": "មាន",
"label.exit": "URL ចេញ",
"label.false": "មិនពិត",
"label.field": "Field",
@@ -78,29 +101,37 @@
"label.filter-combined": "រួមបញ្ចូលគ្នា",
"label.filter-raw": "ដើម",
"label.filters": "ចម្រោះ",
+ "label.first-click": "ចុចដំបូង",
"label.first-seen": "First seen",
- "label.funnel": "Funnel",
+ "label.funnel": "ផ្លូវបង្ហាញ",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
+ "label.funnels": "ផ្លូវបង្ហាញ",
"label.goal": "គោលដៅ",
"label.goals": "គោលដៅ",
"label.goals-description": "តាមដានគោលដៅរបស់អ្នកសម្រាប់ pageviews និង events។",
"label.greater-than": "ធំជាង",
"label.greater-than-equals": "ធំជាងឬស្មើ",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "បានដាក់ជាក្រុម",
+ "label.hostname": "ឈ្មោះម៉ាស៊ីន",
+ "label.includes": "រួមបញ្ចូល",
+ "label.insight": "ការយល់ដឹង",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "គឺ",
+ "label.is-false": "មិនពិត",
"label.is-not": "មិនមែន",
"label.is-not-set": "មិនបានកំណត់",
"label.is-set": "បានកំណត់",
+ "label.is-true": "ពិត",
"label.join": "ចូលរួម",
"label.join-team": "ចូលក្រុម",
"label.journey": "ដំណើរ",
"label.journey-description": "ស្វែងយល់ពីការប្រើប្រាស់គេហទំព័ររបស់អតិថិជនអ្នក។",
+ "label.journeys": "ដំណើរ",
"label.language": "ភាសា",
"label.languages": "ភាសា",
"label.laptop": "កុំព្យូទ័រយួរដៃ",
+ "label.last-click": "ចុចចុងក្រោយ",
"label.last-days": "{x} ថ្ងៃចុងក្រោយ",
"label.last-hours": "{x} ម៉ោងចុងក្រោយ",
"label.last-months": "{x} ខែចុងក្រោយ",
@@ -109,61 +140,79 @@
"label.leave-team": "ចេញពីក្រុម",
"label.less-than": "តិចជាង",
"label.less-than-equals": "តិចជាង ឬស្មើ",
+ "label.links": "តំណភ្ជាប់",
"label.login": "Login",
"label.logout": "Logout",
"label.manage": "គ្រប់គ្រង",
"label.manager": "អ្នកគ្រប់គ្រង",
"label.max": "Max",
+ "label.maximize": "ពង្រីក",
+ "label.medium": "មធ្យម",
"label.member": "សមាជិក",
"label.members": "សមាជិក",
"label.min": "Min",
"label.mobile": "ទូរស័ព្ទចល័ត",
+ "label.model": "ម៉ូដែល",
"label.more": "បន្ថែម",
"label.my-account": "គណនីរបស់ខ្ញុំ",
"label.my-websites": "គេហទំព័ររបស់ខ្ញុំ",
"label.name": "ឈ្មោះ",
"label.new-password": "ពាក្យសម្ងាត់ថ្មី",
- "label.none": "មិនមាន",
+ "label.none": "គ្មាន",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "ស្វែងរកធម្មជាតិ",
+ "label.organic-shopping": "ការទិញធម្មជាតិ",
+ "label.organic-social": "សង្គមធម្មជាតិ",
+ "label.organic-video": "វីដេអូធម្មជាតិ",
"label.os": "OS",
+ "label.other": "ផ្សេងទៀត",
"label.overview": "ទិដ្ឋភាពរួម",
"label.owner": "ម្ចាស់",
+ "label.page": "ទំព័រ",
"label.page-of": "ទំព័រទី {current} នៃ {total}",
"label.page-views": "អ្នកមើលទំព័រ",
"label.pageTitle": "ចំណងជើងទំព័រ",
"label.pages": "ទំព័រ",
+ "label.paid-ads": "ផ្សាយពាណិជ្ជកម្មបង់ប្រាក់",
+ "label.paid-search": "ស្វែងរកបង់ប្រាក់",
+ "label.paid-shopping": "ទិញបង់ប្រាក់",
+ "label.paid-social": "សង្គមបង់ប្រាក់",
+ "label.paid-video": "វីដេអូបង់ប្រាក់",
"label.password": "ពាក្យសម្ងាត់",
"label.path": "Path",
"label.paths": "Paths",
+ "label.pixels": "ភីកសែល",
"label.powered-by": "ដំណើរការដោយ {name}",
"label.previous": "មុន",
"label.previous-period": "មួយរយៈពេលមុន",
"label.previous-year": "ឆ្នាំមុន",
"label.profile": "គណនី",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "លក្ខណៈពិសេស",
+ "label.property": "លក្ខណៈពិសេស",
"label.queries": "Queries",
"label.query": "Query",
"label.query-parameters": "ប៉ារ៉ាម៉ែត្រ Query",
"label.realtime": "ឥលូវនេះ",
+ "label.referral": "ការបញ្ជូន",
"label.referrer": "អ្នកណែនាំ",
"label.referrers": "អ្នកណែនាំ",
"label.refresh": "ផ្ទុកឡើងវិញ",
"label.regenerate": "Regenerate",
"label.region": "តំបន់",
"label.regions": "តំបន់",
+ "label.remaining": "នៅសល់",
"label.remove": "លុប",
"label.remove-member": "លុបសមាជិកក្រុម",
"label.reports": "របាយការណ៍",
"label.required": "ទាមទារ",
"label.reset": "កែសម្រួល",
- "label.reset-website": "កំណត់ស្ថិតិឡើងវិញ",
+ "label.reset-website": "ដើម្បីកែគេហទំព័រនេះឡើងវិញ សូមសរសេរ {confirmation} នៅក្នុងប្រអប់ខាងក្រោមដើម្បីបញ្ជាក់។",
"label.retention": "ការរក្សាទុក",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
"label.role": "មុខងារ",
"label.run-query": "Run query",
"label.save": "រក្សាទុក",
@@ -171,26 +220,35 @@
"label.search": "ស្វែងរក",
"label.select": "ជ្រើសរើស",
"label.select-date": "ជ្រើសរើសកាលបរិច្ឆេទ",
+ "label.select-filter": "ជ្រើសរើសតម្រង",
"label.select-role": "ជ្រើសរើសមុខងារ",
"label.select-website": "ជ្រើសរើសគេហទំព័រ",
"label.session": "Session",
+ "label.session-data": "ទិន្នន័យសម័យ",
"label.sessions": "Sessions",
"label.settings": "ការកំណត់",
+ "label.share": "ចែករំលែក",
"label.share-url": "ចែករំលែក URL",
"label.single-day": "ថ្ងៃតែមួយ",
+ "label.sms": "SMS",
+ "label.sources": "ប្រភព",
"label.start-step": "ជំហានចាប់ផ្តើម",
"label.steps": "ជំហាន",
"label.sum": "Sum",
"label.tablet": "ថេប្លេត",
+ "label.tag": "ស្លាក",
+ "label.tags": "ស្លាក",
"label.team": "ក្រុម",
"label.team-id": "ID ក្រុម",
"label.team-manager": "អ្នកគ្រប់គ្រងក្រុម",
"label.team-member": "សមាជិកក្រុម",
"label.team-name": "ឈ្មោះក្រុម",
"label.team-owner": "ម្ចាស់ក្រុម",
+ "label.team-settings": "ការកំណត់ក្រុម",
"label.team-view-only": "Team view only",
"label.team-websites": "គេហទំព័ររបស់ក្រុម",
"label.teams": "ក្រុម",
+ "label.terms": "លក្ខខណ្ឌ",
"label.theme": "រូបរាង",
"label.this-month": "ខែនេះ",
"label.this-week": "សប្តាហ៍នេះ",
@@ -213,10 +271,7 @@
"label.unknown": "មិនស្គាល់",
"label.untitled": "គ្មានចំណងជើង",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "អ្នកប្រើប្រាស់",
- "label.user-property": "User Property",
"label.username": "ឈ្មោះអ្នកប្រើប្រាស់",
"label.users": "អ្នកប្រើប្រាស់",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "ម្សិលមិញ",
"message.action-confirmation": "សសេរ {confirmation} នៅក្នុងប្រអប់ខាងក្រោមដើម្បីបញ្ជាក់។",
"message.active-users": "មានអ្នកមើល {x} នាក់ ឥលូវនេះ",
+ "message.bad-request": "Bad request",
"message.collected-data": "ទិន្នន័យដែលបានប្រមូលទុក",
"message.confirm-delete": "តើអ្នកប្រាកដថាចង់លុប {target} ទេ?",
"message.confirm-leave": "តើអ្នកប្រាកដថាចង់ចាកចេញ {target} ទេ?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "ទិន្នន័យរបស់គេហទំព័រទាំងអស់នឹងត្រូវលុបចោល។",
"message.error": "មានអ្វីមួយមិនប្រក្រតី។",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "ការកំណត់",
"message.incorrect-username-password": "ឈ្មោះអ្នកប្រើឬពាក្យសម្ងាត់មិនត្រឹមត្រូវ។",
"message.invalid-domain": "Domain មិនត្រឹមត្រូវ",
@@ -259,10 +316,13 @@
"message.no-teams": "អ្នកមិនទាន់បានបង្កើតក្រុមណាមួយទេ។",
"message.no-users": "មិនមានអ្នកប្រើប្រាស់ទេ។",
"message.no-websites-configured": "អ្នកមិនទាន់បានដាក់គេហទំព័រណាមួយចូលទេ។",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "រកមិនឃើញទំព័រ។",
"message.reset-website": "ដើម្បីកែគេហទំព័រនេះឡើងវិញ សូមសរសេរ {confirmation} នៅក្នុងប្រអប់ខាងក្រោមដើម្បីបញ្ជាក់។",
"message.reset-website-warning": "ស្ថិតិទាំងអស់សម្រាប់គេហទំព័រនេះនឹងត្រូវបានលុប ប៉ុន្តែកូដតាមដានរបស់អ្នកនឹងនៅដដែល។",
"message.saved": "រក្សាទុកដោយជោគជ័យ។",
+ "message.sever-error": "Server error",
"message.share-url": "នេះគឺជា URL ដែលអាចចែករំលែកជាសាធារណៈបានសម្រាប់ {target}។",
"message.team-already-member": "អ្នកគឺជាសមាជិកនៃក្រុមរួចហើយ។",
"message.team-not-found": "រកក្រុមមិនឃើញទេ។",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "ជ្រើសក្រុមដែរត្រូវផ្ទេរគេហទំព័រនេះទៅ។",
"message.transfer-website": "ផ្ទេរកម្មសិទ្ធិគេហទំព័រទៅគណនីរបស់អ្នក ឬក្រុមផ្សេងទៀត។",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "អ្នកប្រើប្រាស់ត្រូវបានលុបចោល។",
"message.viewed-page": "ទំព័រដែលបានមើល",
- "message.visitor-log": "អ្នកមើលពីប្រទេស {country} ប្រើប្រាស់កម្មវិធី {browser} លើឧបករណ៍ {os} {device}",
- "message.visitors-dropped-off": "ចំនួនអ្នកទស្សនាធ្លាក់ចុះ"
+ "message.visitor-log": "អ្នកមើលពីប្រទេស {country} ប្រើប្រាស់កម្មវិធី {browser} លើឧបករណ៍ {os} {device}"
}
diff --git a/src/lang/ko-KR.json b/src/lang/ko-KR.json
index 933e2883..977eea4e 100644
--- a/src/lang/ko-KR.json
+++ b/src/lang/ko-KR.json
@@ -3,32 +3,47 @@
"label.actions": "동작",
"label.activity": "활동",
"label.add": "추가",
+ "label.add-board": "보드 추가",
"label.add-description": "설명 추가",
"label.add-member": "멤버 추가",
"label.add-step": "단계 추가",
"label.add-website": "웹사이트 추가",
"label.admin": "관리자",
+ "label.affiliate": "제휴사",
"label.after": "이후",
"label.all": "전체",
"label.all-time": "전체 시간",
"label.analytics": "분석",
+ "label.apply": "적용",
+ "label.attribution": "기여도",
+ "label.attribution-description": "사용자가 마케팅에 어떻게 반응하고 전환을 유도하는지 확인하세요.",
"label.average": "평균",
"label.back": "뒤로",
"label.before": "이전",
+ "label.behavior": "행동",
+ "label.boards": "보드",
"label.bounce-rate": "이탈률",
"label.breakdown": "세부 사항",
"label.browser": "브라우저",
"label.browsers": "브라우저",
+ "label.campaigns": "캠페인",
"label.cancel": "취소",
"label.change-password": "비밀번호 변경",
+ "label.channels": "채널",
"label.cities": "도시",
"label.city": "도시",
"label.clear-all": "모두 지우기",
+ "label.cohort": "코호트",
"label.compare": "비교",
+ "label.compare-dates": "날짜 비교",
"label.confirm": "확인",
"label.confirm-password": "비밀번호 확인",
"label.contains": "포함",
+ "label.content": "콘텐츠",
"label.continue": "계속",
+ "label.conversion": "전환",
+ "label.conversion-rate": "전환율",
+ "label.conversion-step": "전환 단계",
"label.count": "수",
"label.countries": "국가",
"label.country": "국가",
@@ -38,6 +53,7 @@
"label.create-user": "사용자 만들기",
"label.created": "생성됨",
"label.created-by": "작성자",
+ "label.currency": "통화",
"label.current": "현재",
"label.current-password": "현재 비밀번호",
"label.custom-range": "범위 지정",
@@ -57,19 +73,26 @@
"label.details": "세부 정보",
"label.device": "기기",
"label.devices": "기기",
+ "label.direct": "직접",
"label.dismiss": "무시하기",
+ "label.distinct-id": "고유 ID",
"label.does-not-contain": "포함하지 않음",
+ "label.does-not-include": "포함하지 않음",
+ "label.doest-not-exist": "존재하지 않음",
"label.domain": "도메인",
"label.dropoff": "이탈",
"label.edit": "편집",
"label.edit-dashboard": "대시보드 편집",
"label.edit-member": "멤버 편집",
+ "label.email": "이메일",
"label.enable-share-url": "URL 공유 활성화",
"label.end-step": "마지막 단계",
"label.entry": "입장 URL",
"label.event": "이벤트",
"label.event-data": "이벤트 데이터",
+ "label.event-name": "이벤트 이름",
"label.events": "이벤트",
+ "label.exists": "존재함",
"label.exit": "퇴장 URL",
"label.false": "거짓",
"label.field": "필드",
@@ -78,29 +101,37 @@
"label.filter-combined": "합쳐 보기",
"label.filter-raw": "전체 보기",
"label.filters": "필터",
+ "label.first-click": "첫 클릭",
"label.first-seen": "첫 접속",
"label.funnel": "퍼널",
"label.funnel-description": "사용자 전환율 및 이탈률을 살펴보세요.",
+ "label.funnels": "퍼널",
"label.goal": "목표",
"label.goals": "목표",
"label.goals-description": "페이지 조회 및 이벤트 목표를 추적합니다.",
"label.greater-than": "이상",
"label.greater-than-equals": "이상",
- "label.host": "호스트",
- "label.hosts": "호스트",
+ "label.grouped": "그룹화됨",
+ "label.hostname": "호스트명",
+ "label.includes": "포함",
+ "label.insight": "인사이트",
"label.insights": "인사이트",
"label.insights-description": "세그먼트 및 필터를 사용하여 데이터를 더 자세히 살펴보세요.",
"label.is": "해당",
+ "label.is-false": "거짓임",
"label.is-not": "해당하지 않음",
"label.is-not-set": "설정되지 않음",
"label.is-set": "설정됨",
+ "label.is-true": "참임",
"label.join": "가입하기",
"label.join-team": "팀 가입하기",
"label.journey": "여정",
"label.journey-description": "사용자가 웹사이트를 탐색하는 경로를 살펴보세요.",
+ "label.journeys": "여정",
"label.language": "언어",
"label.languages": "언어",
"label.laptop": "노트북",
+ "label.last-click": "마지막 클릭",
"label.last-days": "지난 {x}일",
"label.last-hours": "지난 {x}시간",
"label.last-months": "지난 {x}개월",
@@ -109,15 +140,19 @@
"label.leave-team": "팀 떠나기",
"label.less-than": "미만",
"label.less-than-equals": "이하",
+ "label.links": "링크",
"label.login": "로그인",
"label.logout": "로그아웃",
"label.manage": "관리",
"label.manager": "관리자",
"label.max": "최대",
+ "label.maximize": "확장",
+ "label.medium": "미디엄",
"label.member": "멤버",
"label.members": "멤버",
"label.min": "최소",
"label.mobile": "모바일",
+ "label.model": "모델",
"label.more": "더 보기",
"label.my-account": "내 계정",
"label.my-websites": "내 웹사이트",
@@ -126,16 +161,29 @@
"label.none": "없음",
"label.number-of-records": "{x}개 레코드",
"label.ok": "확인",
+ "label.online": "Online",
+ "label.organic-search": "자연 검색",
+ "label.organic-shopping": "자연 쇼핑",
+ "label.organic-social": "자연 소셜",
+ "label.organic-video": "자연 비디오",
"label.os": "운영 체제",
+ "label.other": "기타",
"label.overview": "개요",
"label.owner": "소유자",
+ "label.page": "페이지",
"label.page-of": "페이지 {current}/{total}",
"label.page-views": "페이지 조회",
"label.pageTitle": "페이지 제목",
"label.pages": "페이지",
+ "label.paid-ads": "유료 광고",
+ "label.paid-search": "유료 검색",
+ "label.paid-shopping": "유료 쇼핑",
+ "label.paid-social": "유료 소셜",
+ "label.paid-video": "유료 비디오",
"label.password": "비밀번호",
"label.path": "패스",
"label.paths": "패스",
+ "label.pixels": "픽셀",
"label.powered-by": "Powered by {name}",
"label.previous": "이전",
"label.previous-period": "이전 기간",
@@ -147,12 +195,14 @@
"label.query": "쿼리",
"label.query-parameters": "쿼리 매개 변수",
"label.realtime": "실시간",
+ "label.referral": "Referral",
"label.referrer": "리퍼러",
"label.referrers": "리퍼러",
"label.refresh": "새로 고침",
"label.regenerate": "다시 생성",
"label.region": "지역",
"label.regions": "지역",
+ "label.remaining": "남음",
"label.remove": "제거",
"label.remove-member": "멤버 제거",
"label.reports": "보고서",
@@ -163,7 +213,6 @@
"label.retention-description": "사용자가 얼마나 자주 돌아오는지를 추적하여 웹사이트의 리텐션을 측정하세요.",
"label.revenue": "수익",
"label.revenue-description": "시간대별 수익을 살펴보세요.",
- "label.revenue-property": "수익 속성",
"label.role": "역할",
"label.run-query": "쿼리 실행",
"label.save": "저장",
@@ -171,26 +220,35 @@
"label.search": "검색",
"label.select": "선택",
"label.select-date": "날짜 선택",
+ "label.select-filter": "필터 선택",
"label.select-role": "역할 선택",
"label.select-website": "웹사이트 선택",
"label.session": "세션",
+ "label.session-data": "세션 데이터",
"label.sessions": "세션",
"label.settings": "설정",
+ "label.share": "공유",
"label.share-url": "공유 URL",
"label.single-day": "하루",
+ "label.sms": "SMS",
+ "label.sources": "소스",
"label.start-step": "시작 단계",
"label.steps": "단계",
"label.sum": "합계",
"label.tablet": "태블릿",
+ "label.tag": "태그",
+ "label.tags": "태그",
"label.team": "팀",
"label.team-id": "팀 ID",
"label.team-manager": "팀 관리자",
"label.team-member": "팀 멤버",
"label.team-name": "팀 이름",
"label.team-owner": "팀 소유자",
+ "label.team-settings": "팀 설정",
"label.team-view-only": "팀 보기 전용",
"label.team-websites": "팀 웹사이트",
"label.teams": "팀",
+ "label.terms": "약관",
"label.theme": "테마",
"label.this-month": "이번 달",
"label.this-week": "이번 주",
@@ -213,10 +271,7 @@
"label.unknown": "알 수 없음",
"label.untitled": "제목 없음",
"label.update": "업데이트",
- "label.url": "URL",
- "label.urls": "URL",
"label.user": "사용자",
- "label.user-property": "사용자 속성",
"label.username": "사용자 이름",
"label.users": "사용자",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "어제",
"message.action-confirmation": "확인을 위해 아래 상자에 {confirmation}을(를) 입력하세요.",
"message.active-users": "현재 방문자 {x}명",
+ "message.bad-request": "Bad request",
"message.collected-data": "수집된 데이터",
"message.confirm-delete": "{target}을(를) 삭제하시겠습니까?",
"message.confirm-leave": "{target}을(를) 떠나시겠습니까?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "관련된 모든 데이터가 삭제됩니다.",
"message.error": "문제가 발생했습니다.",
"message.event-log": "{event} - {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "설정으로 이동",
"message.incorrect-username-password": "사용자 이름 또는 비밀번호를 잘못 입력했습니다.",
"message.invalid-domain": "잘못된 도메인입니다. http/https를 포함하지 마세요.",
@@ -259,10 +316,13 @@
"message.no-teams": "만든 팀이 없습니다.",
"message.no-users": "사용자가 없습니다.",
"message.no-websites-configured": "설정된 웹사이트가 없습니다.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "페이지를 찾을 수 없음",
"message.reset-website": "이 웹사이트를 초기화하려면 아래 상자에 {confirmation}을(를) 입력하세요.",
"message.reset-website-warning": "이 웹사이트의 모든 통계가 삭제되지만 설정은 그대로 유지됩니다.",
"message.saved": "저장했습니다.",
+ "message.sever-error": "Server error",
"message.share-url": "아래 링크를 통해 웹사이트의 통계를 누구나 볼 수 있습니다.",
"message.team-already-member": "이미 팀 멤버입니다.",
"message.team-not-found": "팀을 찾을 수 없습니다.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "이 웹사이트를 전송받을 팀을 선택하세요.",
"message.transfer-website": "웹사이트 소유권을 계정이나 다른 팀으로 전송합니다.",
"message.triggered-event": "트리거된 이벤트",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "사용자를 삭제했습니다.",
"message.viewed-page": "조회한 페이지",
- "message.visitor-log": "{os} {device}에서 {browser}을(를) 사용하는 {country}의 방문자",
- "message.visitors-dropped-off": "방문자 이탈함"
+ "message.visitor-log": "{os} {device}에서 {browser}을(를) 사용하는 {country}의 방문자"
}
diff --git a/src/lang/lt-LT.json b/src/lang/lt-LT.json
index 2e7776a3..772fa34e 100644
--- a/src/lang/lt-LT.json
+++ b/src/lang/lt-LT.json
@@ -3,42 +3,58 @@
"label.actions": "Veiksmai",
"label.activity": "Veiklos žurnalas",
"label.add": "Pridėti",
+ "label.add-board": "Pridėti lentą",
"label.add-description": "Pridėti aprašymą",
"label.add-member": "Pridėti narį",
- "label.add-step": "Add step",
+ "label.add-step": "Pridėti žingsnį",
"label.add-website": "Pridėti svetainę",
"label.admin": "Administrator",
+ "label.affiliate": "Partneris",
"label.after": "Po",
"label.all": "Visi",
"label.all-time": "Visas laikotarpis",
- "label.analytics": "Analytics",
+ "label.analytics": "Analitika",
+ "label.apply": "Taikyti",
+ "label.attribution": "Priskyrimas",
+ "label.attribution-description": "Žiūrėkite, kaip naudotojai įsitraukia į jūsų rinkodarą ir kas lemia konversijas.",
"label.average": "Vidurkis",
"label.back": "Atgal",
"label.before": "Prieš",
+ "label.behavior": "Elgsena",
+ "label.boards": "Lentos",
"label.bounce-rate": "Atmetimo rodiklis",
- "label.breakdown": "Breakdown",
+ "label.breakdown": "Išskaidymas",
"label.browser": "Naršyklė",
"label.browsers": "Naršyklės",
+ "label.campaigns": "Kampanijos",
"label.cancel": "Atšaukti",
"label.change-password": "Pakeisti slaptažodį",
+ "label.channels": "Kanalai",
"label.cities": "Miestai",
"label.city": "Miestas",
"label.clear-all": "Išvalyti visus",
- "label.compare": "Compare",
+ "label.cohort": "Kohorta",
+ "label.compare": "Palyginti",
+ "label.compare-dates": "Palyginti datas",
"label.confirm": "Patvirtinti",
"label.confirm-password": "Patvirtinti slaptažodį",
- "label.contains": "Contains",
- "label.continue": "Continue",
- "label.count": "Count",
+ "label.contains": "Turi",
+ "label.content": "Turinys",
+ "label.continue": "Tęsti",
+ "label.conversion": "Konversija",
+ "label.conversion-rate": "Konversijos rodiklis",
+ "label.conversion-step": "Konversijos žingsnis",
+ "label.count": "Skaičius",
"label.countries": "Šalys",
"label.country": "Šalis",
"label.create": "Sukurti",
"label.create-report": "Kurti ataskaitą",
"label.create-team": "Sukurti komandą",
"label.create-user": "Sukurti vartotoją",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.created": "Sukurta",
+ "label.created-by": "Sukūrė",
+ "label.currency": "Valiuta",
+ "label.current": "Dabartinis",
"label.current-password": "Dabartinis slaptažodis",
"label.custom-range": "Pasirinktinis intervalas",
"label.dashboard": "Švieslentė",
@@ -53,144 +69,186 @@
"label.delete-user": "Ištrinti vartotoją",
"label.delete-website": "Ištrinti svetainę",
"label.description": "Aprašymas",
- "label.desktop": "Desktop",
+ "label.desktop": "Stalinis kompiuteris",
"label.details": "Detalės",
"label.device": "Įrenginys",
"label.devices": "Įrenginiai",
+ "label.direct": "Tiesioginis",
"label.dismiss": "Gerai",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "Unikalus ID",
+ "label.does-not-contain": "Neturi",
+ "label.does-not-include": "Neįtraukia",
+ "label.doest-not-exist": "Neegzistuoja",
"label.domain": "Domenas",
- "label.dropoff": "Dropoff",
+ "label.dropoff": "Atsitraukimas",
"label.edit": "Redaguoti",
"label.edit-dashboard": "Redaguoti švieslentę",
"label.edit-member": "Redaguoti narį",
+ "label.email": "El. paštas",
"label.enable-share-url": "Įjungti bendrinimą su nuoroda",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
+ "label.end-step": "Paskutinis žingsnis",
+ "label.entry": "Įėjimo URL",
"label.event": "Įvykis",
"label.event-data": "Įvykių duomenys",
+ "label.event-name": "Įvykio pavadinimas",
"label.events": "Įvykiai",
- "label.exit": "Exit URL",
- "label.false": "False",
+ "label.exists": "Egzistuoja",
+ "label.exit": "Išėjimo URL",
+ "label.false": "Netiesa",
"label.field": "Laukelis",
"label.fields": "Laukeliai",
"label.filter": "Filtruoti",
"label.filter-combined": "Kombinuoti",
"label.filter-raw": "Neapdoroti",
"label.filters": "Filtrai",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.first-click": "Pirmas paspaudimas",
+ "label.first-seen": "Pirmą kartą matyta",
+ "label.funnel": "Piltuvas",
+ "label.funnel-description": "Supraskite naudotojų konversijos ir atsitraukimo rodiklius.",
+ "label.funnels": "Piltuvai",
+ "label.goal": "Tikslas",
+ "label.goals": "Tikslai",
+ "label.goals-description": "Sekite savo tikslus puslapių peržiūroms ir įvykiams.",
+ "label.greater-than": "Daugiau nei",
+ "label.greater-than-equals": "Daugiau arba lygu",
+ "label.grouped": "Grupuota",
+ "label.hostname": "Pagrindinis kompiuteris",
+ "label.includes": "Įtraukia",
+ "label.insight": "Įžvalga",
"label.insights": "Įžvalgos",
"label.insights-description": "Pasinerkite giliau į savo duomenis naudodami segmentus ir filtrus.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
+ "label.is": "Yra",
+ "label.is-false": "Yra netiesa",
+ "label.is-not": "Nėra",
+ "label.is-not-set": "Nenurodyta",
+ "label.is-set": "Nustatyta",
+ "label.is-true": "Yra tiesa",
"label.join": "Prisijungti",
"label.join-team": "Prisijungti į komandą",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.journey": "Kelionė",
+ "label.journey-description": "Sužinokite, kaip naudotojai naršo jūsų svetainėje.",
+ "label.journeys": "Kelionės",
"label.language": "Kalba",
"label.languages": "Kalbos",
- "label.laptop": "Laptop",
+ "label.laptop": "Nešiojamas kompiuteris",
+ "label.last-click": "Paskutinis paspaudimas",
"label.last-days": "{x, plural, =0 {Paskutinės # dienų} zero {Paskutinės # dienų} one {Paskutinė diena} other {Paskutinės # dienos}}",
"label.last-hours": "{x, plural, =0 {Paskutinės # valandų} zero {Paskutinės # valandų} one {Paskutinė # valanda} other {Paskutinės # valandos}}",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
+ "label.last-months": "Paskutiniai {x} mėnesiai",
+ "label.last-seen": "Paskutinį kartą matyta",
"label.leave": "Išeiti",
"label.leave-team": "Išeiti iš komandos",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.less-than": "Mažiau nei",
+ "label.less-than-equals": "Mažiau arba lygu",
+ "label.links": "Nuorodos",
"label.login": "Prisijungti",
"label.logout": "Atsijungti",
"label.manage": "Tvarkyti",
- "label.manager": "Manager",
- "label.max": "Max",
+ "label.manager": "Vadovas",
+ "label.max": "Maksimumas",
+ "label.maximize": "Išplėsti",
+ "label.medium": "Vidutinis",
"label.member": "Narys",
"label.members": "Nariai",
- "label.min": "Min",
+ "label.min": "Minimumas",
"label.mobile": "Mobilusis",
+ "label.model": "Modelis",
"label.more": "Daugiau",
"label.my-account": "Mano paskyra",
"label.my-websites": "Mano svetainės",
"label.name": "Pavadinimas",
"label.new-password": "Naujas slaptažodis",
- "label.none": "None",
+ "label.none": "Nėra",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organinė paieška",
+ "label.organic-shopping": "Organinis apsipirkimas",
+ "label.organic-social": "Organinis socialinis",
+ "label.organic-video": "Organinis vaizdo įrašas",
"label.os": "Operacinės sistemos",
+ "label.other": "Kita",
"label.overview": "Apžvalga",
"label.owner": "Savininkas",
+ "label.page": "Puslapis",
"label.page-of": "Puslapis {current} iš {total}",
"label.page-views": "Puslapių peržiūros",
"label.pageTitle": "Puslapio pavadinimas",
"label.pages": "Puslapiai",
+ "label.paid-ads": "Mokama reklama",
+ "label.paid-search": "Mokama paieška",
+ "label.paid-shopping": "Mokamas apsipirkimas",
+ "label.paid-social": "Mokamas socialinis",
+ "label.paid-video": "Mokamas vaizdo įrašas",
"label.password": "Slaptažodis",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Kelias",
+ "label.paths": "Keliai",
+ "label.pixels": "Pikseliai",
"label.powered-by": "Powered by {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "Ankstesnis",
+ "label.previous-period": "Ankstesnis laikotarpis",
+ "label.previous-year": "Ankstesni metai",
"label.profile": "Profilis",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Savybės",
+ "label.property": "Savybė",
"label.queries": "Užklausos",
"label.query": "Užklausa",
"label.query-parameters": "Užklausų parametrai",
"label.realtime": "Realiuoju laiku",
+ "label.referral": "Persiuntimas",
"label.referrer": "Persiuntėjas",
"label.referrers": "Persiuntėjai",
"label.refresh": "Atnaujinti",
- "label.regenerate": "Regenerate",
+ "label.regenerate": "Sugeneruoti iš naujo",
"label.region": "Regionas",
"label.regions": "Regionai",
+ "label.remaining": "Likę",
"label.remove": "Pašalinti",
"label.remove-member": "Pašalinti narį",
"label.reports": "Ataskaitos",
"label.required": "Reikalinga",
"label.reset": "Atstatyti",
"label.reset-website": "Atstatyti statistikos duomenis",
- "label.retention": "Retention",
- "label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
- "label.role": "Rolė",
- "label.run-query": "Run query",
+ "label.retention": "Išlaikymas",
+ "label.retention-description": "Išmatuokite, kaip dažnai naudotojai grįžta į jūsų svetainę.",
+ "label.revenue": "Pajamos",
+ "label.revenue-description": "Peržiūrėkite savo pajamas laikui bėgant.",
+ "label.role": "Vaidmuo",
+ "label.run-query": "Vykdyti užklausą",
"label.save": "Išsaugoti",
"label.screens": "Ekranai",
"label.search": "Ieškoti",
- "label.select": "Select",
+ "label.select": "Pasirinkti",
"label.select-date": "Pasirinkti laikotarpį",
+ "label.select-filter": "Pasirinkti filtrą",
"label.select-role": "Pasirinkti rolę",
"label.select-website": "Pasirinkti svetainę",
- "label.session": "Session",
+ "label.session": "Sesija",
+ "label.session-data": "Sesijos duomenys",
"label.sessions": "Sesijos",
"label.settings": "Nustatymai",
+ "label.share": "Dalintis",
"label.share-url": "Pasidalinti nuoroda",
"label.single-day": "Viena diena",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
+ "label.sms": "SMS",
+ "label.sources": "Šaltiniai",
+ "label.start-step": "Pradžios žingsnis",
+ "label.steps": "Žingsniai",
"label.sum": "Suma",
"label.tablet": "Planšetė",
+ "label.tag": "Žyma",
+ "label.tags": "Žymos",
"label.team": "Komanda",
"label.team-id": "Komandos ID",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Komandos vadovas",
"label.team-member": "Komandos narys",
"label.team-name": "Komandos pavadinimas",
"label.team-owner": "Komandos savininkas",
- "label.team-view-only": "Team view only",
- "label.team-websites": "Team websites",
+ "label.team-settings": "Komandos nustatymai",
+ "label.team-view-only": "Tik peržiūra",
+ "label.team-websites": "Komandos svetainės",
"label.teams": "Komandos",
+ "label.terms": "Sąlygos",
"label.theme": "Spalvų tema",
"label.this-month": "Šis mėnuo",
"label.this-week": "Ši savaitė",
@@ -213,10 +271,7 @@
"label.unknown": "Nežinoma",
"label.untitled": "Be pavadinimo",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Vartotojas",
- "label.user-property": "User Property",
"label.username": "Vartotojo vardas",
"label.users": "Vartotojai",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Vakar",
"message.action-confirmation": "Įrašykite {confirmation} žemiau, kad patvirtintumėte.",
"message.active-users": "{x, plural, =0 {# aktyvių vartotojų} zero {# aktyvių vartotojų} one {# aktyvus vartotojas} other {# aktyvūs vartotojai}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Ar esate tikri, jog norite ištrinti svetainę {target}?",
"message.confirm-leave": "Ar esate tikri, jog norite palikti {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Visi susiję duomenys taip pat bus ištrinti.",
"message.error": "Kažkas įvyko ne taip.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Eiti į nustatymus",
"message.incorrect-username-password": "Neteisingas vartotojo vardas/slaptažodis.",
"message.invalid-domain": "Klaidingas domenas",
@@ -259,10 +316,13 @@
"message.no-teams": "Jūs nesate sukūrę jokių komandų.",
"message.no-users": "Nėra jokių vartotojų.",
"message.no-websites-configured": "Jūs nesate susikonfiguravę jokių svetainių.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Puslapis nerastas.",
"message.reset-website": "Kad atstatyti šią svetainę, įrašykite {confirmation} žemiau, kad patvirtintumėte.",
"message.reset-website-warning": "Visi šios svetainės statistikos duomenys bus ištrinti, bet sekimo kodas išliks nepaliestas.",
"message.saved": "Sėkmingai išsaugota.",
+ "message.sever-error": "Server error",
"message.share-url": "Tai yra viešai prieinama {target} nuoroda (URL).",
"message.team-already-member": "Jūs jau esate šios komandos narys.",
"message.team-not-found": "Komanda nerasta.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Pasirinkite komandą, kuriai norite perduoti šią svetainę.",
"message.transfer-website": "Perduoti svetainės nuosavybę į savo paskyrą arba kitą komandą.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Vartotojas ištrintas.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Lankytojas iš {country}, naudojantis {browser} sistemoje {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Lankytojas iš {country}, naudojantis {browser} sistemoje {os} {device}"
}
diff --git a/src/lang/mn-MN.json b/src/lang/mn-MN.json
index 60797aef..e9c649db 100644
--- a/src/lang/mn-MN.json
+++ b/src/lang/mn-MN.json
@@ -3,32 +3,47 @@
"label.actions": "Үйлдлүүд",
"label.activity": "Үйл ажиллагааны бүртгэл",
"label.add": "Нэмэх",
+ "label.add-board": "Самбар нэмэх",
"label.add-description": "Тайлбар нэмэх",
"label.add-member": "Гишүүн нэмэх",
"label.add-step": "Алхам нэмэх",
"label.add-website": "Веб нэмэх",
"label.admin": "Админ",
+ "label.affiliate": "Харьяа",
"label.after": "Хойно",
"label.all": "Бүх",
"label.all-time": "Бүх цаг үеийн",
"label.analytics": "Аналитик",
+ "label.apply": "Хэрэглэх",
+ "label.attribution": "Холбогдол",
+ "label.attribution-description": "Хэрэглэгчид таны маркетингт хэрхэн оролцож, ямар зүйлс хөрвүүлэлтэд нөлөөлж байгааг хараарай.",
"label.average": "Дундаж",
"label.back": "Буцах",
"label.before": "Өмнө",
+ "label.behavior": "Зан төлөв",
+ "label.boards": "Самбарууд",
"label.bounce-rate": "Нэг хуудас үзээд гарсан",
"label.breakdown": "Задаргаа",
"label.browser": "Хөтөч",
"label.browsers": "Хөтөч",
+ "label.campaigns": "Аянууд",
"label.cancel": "Цуцлах",
"label.change-password": "Нууц үг солих",
+ "label.channels": "Суваг",
"label.cities": "Хотууд",
"label.city": "Хот",
"label.clear-all": "Бүгдийг арилгах",
+ "label.cohort": "Бүлэг",
"label.compare": "Харьцуулах",
+ "label.compare-dates": "Огноо харьцуулах",
"label.confirm": "Батлах",
"label.confirm-password": "Шинэ нууц үгээ давтах",
"label.contains": "Агуулах",
+ "label.content": "Агуулга",
"label.continue": "Үргэлжлүүлэх",
+ "label.conversion": "Хөрвүүлэлт",
+ "label.conversion-rate": "Хөрвүүлэлтийн хувь",
+ "label.conversion-step": "Хөрвүүлэлтийн алхам",
"label.count": "Тоо",
"label.countries": "Улс",
"label.country": "Улс",
@@ -38,6 +53,7 @@
"label.create-user": "Хэрэглэгч үүсгэх",
"label.created": "Үүсгэсэн",
"label.created-by": "Үүсгэсэн",
+ "label.currency": "Валют",
"label.current": "Одоогийн",
"label.current-password": "Ашиглаж буй нууц үг",
"label.custom-range": "Дурын хугацаа",
@@ -57,19 +73,26 @@
"label.details": "Мэдээлэл",
"label.device": "Төхөөрөмж",
"label.devices": "Төхөөрөмж",
+ "label.direct": "Шууд",
"label.dismiss": "Үл хэрэгсэх",
+ "label.distinct-id": "Ялгаатай ID",
"label.does-not-contain": "Агуулахгүй",
+ "label.does-not-include": "Агуулаагүй",
+ "label.doest-not-exist": "Байхгүй",
"label.domain": "Домэйн",
"label.dropoff": "Уналт",
"label.edit": "Засах",
"label.edit-dashboard": "Хянах самбар засах",
"label.edit-member": "Гишүүн засах",
+ "label.email": "Имэйл",
"label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
"label.end-step": "Төгсгөлийн алхам",
"label.entry": "Орох зам",
"label.event": "Үйлдэл",
"label.event-data": "Үйлдлийн өгөгдөл",
+ "label.event-name": "Үйлдлийн нэр",
"label.events": "Үйлдэл",
+ "label.exists": "Байгаа",
"label.exit": "Гарах зам",
"label.false": "Худал",
"label.field": "Талбар",
@@ -78,29 +101,37 @@
"label.filter-combined": "Нэгтгэсэн",
"label.filter-raw": "Түүхий",
"label.filters": "Шүүлтүүр",
+ "label.first-click": "Эхний даралт",
"label.first-seen": "Анх харсан",
"label.funnel": "Цутгал",
"label.funnel-description": "Хэрэглэгчдийн шилжилт, уналтын хэмжээг шинжлэх.",
+ "label.funnels": "Цутгалууд",
"label.goal": "Зорилго",
"label.goals": "Зорилго",
"label.goals-description": "Хуудас үзсэн болон үйлдлийн зорилгыг мөрдөх.",
"label.greater-than": "Их",
"label.greater-than-equals": "Их буюу тэнцүү",
- "label.host": "Хост",
- "label.hosts": "Хост",
+ "label.grouped": "Бүлэглэсэн",
+ "label.hostname": "Хост нэр",
+ "label.includes": "Агуулсан",
+ "label.insight": "Ойлголт",
"label.insights": "Шинжлэх",
"label.insights-description": "Өгөгдлөө хэсэгчлэн хуваах, шүүх байдлаар задлан шинжлэх.",
"label.is": "Бол",
+ "label.is-false": "Худал байна",
"label.is-not": "Биш",
"label.is-not-set": "Утга оноогоогүй",
"label.is-set": "Утга оноосон",
+ "label.is-true": "Үнэн байна",
"label.join": "Нэгдэх",
"label.join-team": "Багт нэгдэх",
"label.journey": "Аялал",
"label.journey-description": "Хэрэглэгчид таны цахим хуудсаар хэрхэн шилжиж явсныг шинжлэх.",
+ "label.journeys": "Аялалууд",
"label.language": "Хэл",
"label.languages": "Хэл",
"label.laptop": "Зөөврийн компьютер",
+ "label.last-click": "Сүүлийн даралт",
"label.last-days": "Сүүлийн {x} хоног",
"label.last-hours": "Сүүлийн {x} цаг",
"label.last-months": "Сүүлийн {x} сар",
@@ -109,15 +140,19 @@
"label.leave-team": "Багаас гарах",
"label.less-than": "Бага",
"label.less-than-equals": "Бага буюу тэнцүү",
+ "label.links": "Холбоосууд",
"label.login": "Нэвтрэх",
"label.logout": "Гарах",
"label.manage": "Удирдах",
"label.manager": "Удирдагч",
"label.max": "Max",
+ "label.maximize": "Өргөтгөх",
+ "label.medium": "Дунд",
"label.member": "Гишүүн",
"label.members": "Гишүүд",
"label.min": "Min",
"label.mobile": "Утас",
+ "label.model": "Загвар",
"label.more": "Цааш",
"label.my-account": "Миний бүртгэл",
"label.my-websites": "Миний вебүүд",
@@ -126,16 +161,29 @@
"label.none": "Байхгүй",
"label.number-of-records": "{x} {x, plural, one {бичлэг} other {бичлэг}}",
"label.ok": "ЗА",
+ "label.online": "Online",
+ "label.organic-search": "Байгалийн хайлт",
+ "label.organic-shopping": "Байгалийн дэлгүүр",
+ "label.organic-social": "Байгалийн сошиал",
+ "label.organic-video": "Байгалийн видео",
"label.os": "OS",
+ "label.other": "Бусад",
"label.overview": "Тойм",
"label.owner": "Эзэмшигч",
+ "label.page": "Хуудас",
"label.page-of": "Хуудас {total}-с {current}",
"label.page-views": "Хуудас үзсэн",
"label.pageTitle": "Хуудасны гарчиг",
"label.pages": "Хуудас",
+ "label.paid-ads": "Төлбөртэй зар",
+ "label.paid-search": "Төлбөртэй хайлт",
+ "label.paid-shopping": "Төлбөртэй дэлгүүр",
+ "label.paid-social": "Төлбөртэй сошиал",
+ "label.paid-video": "Төлбөртэй видео",
"label.password": "Нууц үг",
"label.path": "Зам",
"label.paths": "Зам",
+ "label.pixels": "Пиксел",
"label.powered-by": "{name} дээр суурилсан",
"label.previous": "Өмнөх",
"label.previous-period": "Өмнөх үе",
@@ -147,12 +195,14 @@
"label.query": "Query",
"label.query-parameters": "Query параметр",
"label.realtime": "Яг одоо",
+ "label.referral": "Referral",
"label.referrer": "Чиглүүлэгч",
"label.referrers": "Чиглүүлэгч",
"label.refresh": "Сэргээх",
"label.regenerate": "Дахин үүсгэх",
"label.region": "Бүс",
"label.regions": "Бүсүүд",
+ "label.remaining": "Үлдсэн",
"label.remove": "Устгах",
"label.remove-member": "Гишүүн хасах",
"label.reports": "Тайлан",
@@ -163,7 +213,6 @@
"label.retention-description": "Хэрэглэгчид таны веб рүү дахин хандах буюу хэрэглэгчдээ хэр тогтоож буйг хэмжих.",
"label.revenue": "Орлого",
"label.revenue-description": "Цаг хугацааны туршид орлогын өөрчлөлтийг харах.",
- "label.revenue-property": "Орлогын шинж чанар",
"label.role": "Эрх",
"label.run-query": "Query ажиллуулах",
"label.save": "Хадгалах",
@@ -171,26 +220,35 @@
"label.search": "Хайх",
"label.select": "Сонгох",
"label.select-date": "Огноо сонгох",
+ "label.select-filter": "Шүүлтүүр сонгох",
"label.select-role": "Select role",
"label.select-website": "Веб сонгох",
"label.session": "Session",
+ "label.session-data": "Сессийн өгөгдөл",
"label.sessions": "Sessions",
"label.settings": "Тохиргоо",
+ "label.share": "Хуваалцах",
"label.share-url": "Хуваалцах холбоос",
"label.single-day": "Нэг өдөр",
+ "label.sms": "SMS",
+ "label.sources": "Эх сурвалжууд",
"label.start-step": "Эхлэх алхам",
"label.steps": "Алхам",
"label.sum": "Нийлбэр",
"label.tablet": "Таблет",
+ "label.tag": "Таг",
+ "label.tags": "Тагууд",
"label.team": "Баг",
"label.team-id": "Багийн ID",
"label.team-manager": "Багийн удирдагч",
"label.team-member": "Багийн гишүүн",
"label.team-name": "Багийн нэр",
"label.team-owner": "Багийн эзэмшигч",
+ "label.team-settings": "Багийн тохиргоо",
"label.team-view-only": "Team view only",
"label.team-websites": "Багийн вебүүд",
"label.teams": "Багууд",
+ "label.terms": "Нөхцөл",
"label.theme": "Загвар",
"label.this-month": "Энэ сар",
"label.this-week": "Энэ долоо хоног",
@@ -213,10 +271,7 @@
"label.unknown": "Тодорхойгүй",
"label.untitled": "Гарчиггүй",
"label.update": "Шинэчлэх",
- "label.url": "URL",
- "label.urls": "URL-ууд",
"label.user": "Хэрэглэгч",
- "label.user-property": "Хэрэглэгчийн шинж",
"label.username": "Хэрэглэгчийн нэр",
"label.users": "Хэрэглэгчид",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Өчигдөр",
"message.action-confirmation": "Доорх хэсэгт {confirmation} гэж бичин баталгаажуулна уу.",
"message.active-users": "одоо {x} {x, plural, one {зочин} other {зочин}} байна",
+ "message.bad-request": "Bad request",
"message.collected-data": "Цуглуулсан өгөгдөл",
"message.confirm-delete": "Та {target}-г устгахдаа итгэлтэй байна уу?",
"message.confirm-leave": "Та {target}-с гарахдаа итгэлтэй байна уу?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Энэ вебтэй холбоотой бүх өгөгдөл устах болно.",
"message.error": "Ямар нэг зүйл буруу боллоо.",
"message.event-log": "{url}-д {event}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Тохиргоо руу очих",
"message.incorrect-username-password": "Буруу хэрэглэгчийн нэр/нууц үг.",
"message.invalid-domain": "Буруу домэйн",
@@ -259,10 +316,13 @@
"message.no-teams": "Та ямар ч баг үүсгээгүй байна.",
"message.no-users": "Хэрэглэгч байхгүй байна.",
"message.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Хуудас олдсонгүй.",
"message.reset-website": "Тоон үзүүлэлтийг дахин эхлүүлэхийн тулд доорх хэсэгт {confirmation} гэж бичиж, баталгаажуулна уу.",
"message.reset-website-warning": "Энэ вебийн бүх тоон үзүүлэлтүүдийг устгах болно. Гэхдээ мөрдөх код хэвээрээ үлдэнэ.",
"message.saved": "Хадгалсан.",
+ "message.sever-error": "Server error",
"message.share-url": "Таны вебийн тоон үзүүлэлтүүд доорх URL дээр нийтэд харагдах болно:",
"message.team-already-member": "Та аль хэдийн энэ багийн гишүүн болсон байна.",
"message.team-not-found": "Баг олдсонгүй.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Энэ вебийг шилжүүлж авах багийг сонгоно уу.",
"message.transfer-website": "Энэ вебийг өөрийн бүртгэл рүү эсвэл багт шилжүүлж авах.",
"message.triggered-event": "Өдөөсөн үйлдэл",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Хэрэглэгч устсан.",
"message.viewed-page": "Үзсэн хуудас",
- "message.visitor-log": "{country} улсаас {os} {device} дээр {browser} хөтөч ашиглан орсон",
- "message.visitors-dropped-off": "Зочдын уналт"
+ "message.visitor-log": "{country} улсаас {os} {device} дээр {browser} хөтөч ашиглан орсон"
}
diff --git a/src/lang/ms-MY.json b/src/lang/ms-MY.json
index a8686573..32abd085 100644
--- a/src/lang/ms-MY.json
+++ b/src/lang/ms-MY.json
@@ -3,32 +3,47 @@
"label.actions": "Aksi",
"label.activity": "Activity log",
"label.add": "Add",
+ "label.add-board": "Add board",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "Tambah laman web",
"label.admin": "Pentadbir",
+ "label.affiliate": "Affiliate",
"label.after": "After",
"label.all": "Semua",
"label.all-time": "All time",
"label.analytics": "Analytics",
+ "label.apply": "Apply",
+ "label.attribution": "Attribution",
+ "label.attribution-description": "See how users engage with your marketing and what drives conversions.",
"label.average": "Average",
"label.back": "Kembali",
"label.before": "Before",
+ "label.behavior": "Behavior",
+ "label.boards": "Boards",
"label.bounce-rate": "Kadar lantunan",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.browsers": "Pelayar web",
+ "label.campaigns": "Campaigns",
"label.cancel": "Batal",
"label.change-password": "Tukar kata laluan",
+ "label.channels": "Channels",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
+ "label.cohort": "Cohort",
"label.compare": "Compare",
+ "label.compare-dates": "Compare dates",
"label.confirm": "Confirm",
"label.confirm-password": "Sahkan kata laluan",
"label.contains": "Contains",
+ "label.content": "Content",
"label.continue": "Continue",
+ "label.conversion": "Conversion",
+ "label.conversion-rate": "Conversion rate",
+ "label.conversion-step": "Conversion step",
"label.count": "Count",
"label.countries": "Negara",
"label.country": "Country",
@@ -38,6 +53,7 @@
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
+ "label.currency": "Currency",
"label.current": "Current",
"label.current-password": "Kata laluan semasa",
"label.custom-range": "Julat khas",
@@ -57,19 +73,26 @@
"label.details": "Details",
"label.device": "Device",
"label.devices": "Peranti",
+ "label.direct": "Direct",
"label.dismiss": "Ketepikan",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "Does not contain",
+ "label.does-not-include": "Does not include",
+ "label.doest-not-exist": "Does not exist",
"label.domain": "Domain",
"label.dropoff": "Dropoff",
"label.edit": "Edit",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
+ "label.email": "Email",
"label.enable-share-url": "Aktifkan url berkongsi",
"label.end-step": "End Step",
"label.entry": "Entry URL",
"label.event": "Event",
"label.event-data": "Event data",
+ "label.event-name": "Event name",
"label.events": "Peristiwa",
+ "label.exists": "Exists",
"label.exit": "Exit URL",
"label.false": "False",
"label.field": "Field",
@@ -78,29 +101,37 @@
"label.filter-combined": "Digabungkan",
"label.filter-raw": "Mentah",
"label.filters": "Filters",
+ "label.first-click": "First click",
"label.first-seen": "First seen",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
+ "label.funnels": "Funnels",
"label.goal": "Goal",
"label.goals": "Goals",
"label.goals-description": "Track your goals for pageviews and events.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grouped",
+ "label.hostname": "Hostname",
+ "label.includes": "Includes",
+ "label.insight": "Insight",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
+ "label.is-false": "Is false",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
+ "label.is-true": "Is true",
"label.join": "Join",
"label.join-team": "Join team",
"label.journey": "Journey",
"label.journey-description": "Understand how users navigate through your website.",
+ "label.journeys": "Journeys",
"label.language": "Language",
"label.languages": "Languages",
"label.laptop": "Laptop",
+ "label.last-click": "Last click",
"label.last-days": "{x} hari lepas",
"label.last-hours": "{x} jam lepas",
"label.last-months": "Last {x} months",
@@ -109,15 +140,19 @@
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
+ "label.links": "Links",
"label.login": "Log masuk",
"label.logout": "Log keluar",
"label.manage": "Manage",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Expand",
+ "label.medium": "Medium",
"label.member": "Member",
"label.members": "Members",
"label.min": "Min",
"label.mobile": "Telefon bimbit",
+ "label.model": "Model",
"label.more": "Lebih banyak lagi",
"label.my-account": "My account",
"label.my-websites": "My websites",
@@ -126,16 +161,29 @@
"label.none": "None",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organic search",
+ "label.organic-shopping": "Organic shopping",
+ "label.organic-social": "Organic social",
+ "label.organic-video": "Organic video",
"label.os": "OS",
+ "label.other": "Other",
"label.overview": "Overview",
"label.owner": "Owner",
+ "label.page": "Page",
"label.page-of": "Page {current} of {total}",
"label.page-views": "Paparan halaman",
"label.pageTitle": "Page title",
"label.pages": "Halaman",
+ "label.paid-ads": "Paid ads",
+ "label.paid-search": "Paid search",
+ "label.paid-shopping": "Paid shopping",
+ "label.paid-social": "Paid social",
+ "label.paid-video": "Paid video",
"label.password": "Kata laluan",
"label.path": "Path",
"label.paths": "Paths",
+ "label.pixels": "Pixels",
"label.powered-by": "Disediakan oleh {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
@@ -147,12 +195,14 @@
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.realtime": "Siaran langsung",
+ "label.referral": "Referral",
"label.referrer": "Referrer",
"label.referrers": "Perujuk",
"label.refresh": "Muat semula",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Remaining",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -163,7 +213,6 @@
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "Simpan",
@@ -171,26 +220,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Select filter",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.session": "Session",
+ "label.session-data": "Session data",
"label.sessions": "Sessions",
"label.settings": "Tetapan",
+ "label.share": "Share",
"label.share-url": "Kongsikan URL",
"label.single-day": "Satu hari",
+ "label.sms": "SMS",
+ "label.sources": "Sources",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "Tablet",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Team settings",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Terms",
"label.theme": "Theme",
"label.this-month": "Bulan ini",
"label.this-week": "Minggu ini",
@@ -213,10 +271,7 @@
"label.unknown": "Tidak diketahui",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Nama pengguna",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} semasa {x, plural, one {pelawat} other {pelawat}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Pastikah anda ingin memadam {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Semua data yang berkaitan juga akan dihapuskan.",
"message.error": "Ada yang tidak kena.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Pergi ke tetapan",
"message.incorrect-username-password": "Pengguna/kata laluan tidak betul.",
"message.invalid-domain": "Domain tidak sah",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "Anda tidak ada sebarang laman web yang telah dikonfigurasikan.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Halaman tidak dijumpai.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.saved": "Berjaya disimpan.",
+ "message.sever-error": "Server error",
"message.share-url": "Ini adalah URL berkongsi untuk {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Pelawat dari {country} mengguna {browser} pada {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Pelawat dari {country} mengguna {browser} pada {os} {device}"
}
diff --git a/src/lang/my-MM.json b/src/lang/my-MM.json
index 3ab75f88..156b0c26 100644
--- a/src/lang/my-MM.json
+++ b/src/lang/my-MM.json
@@ -3,32 +3,47 @@
"label.actions": "လုပ်ဆောင်ချက်များ",
"label.activity": "လုပ်ဆောင်ချက်စာရင်း",
"label.add": "ထပ်ထည့်မည်",
+ "label.add-board": "Add board",
"label.add-description": "အကြောင်းအရာဖော်ပြချက် ထည့်မည်",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "ဝက်ဘ်ဆိုဒ်ထည့်မည်",
"label.admin": "အက်ဒမင်",
+ "label.affiliate": "Affiliate",
"label.after": "ပြီးနောက်",
"label.all": "အားလုံး",
"label.all-time": "အချိန်အစမှအခုထိ",
"label.analytics": "အန်နလစ်တစ်",
+ "label.apply": "Apply",
+ "label.attribution": "Attribution",
+ "label.attribution-description": "See how users engage with your marketing and what drives conversions.",
"label.average": "ပျမ်းမျှ",
"label.back": "နောက်သို့",
"label.before": "မတိုင်မီ",
+ "label.behavior": "အပြုအမူ",
+ "label.boards": "Boards",
"label.bounce-rate": "Bounce နှုန်း",
"label.breakdown": "ခွဲခြမ်းစိတ်ဖြာမှု",
"label.browser": "Browser",
"label.browsers": "ဝက်ဘ်ဘရောင်ဇာများ",
+ "label.campaigns": "Campaigns",
"label.cancel": "မလုပ်တော့ပါ",
"label.change-password": "စကားဝှက် ပြောင်းမည်",
+ "label.channels": "Channels",
"label.cities": "မြို့များ",
"label.city": "City",
"label.clear-all": "အားလုံးကိုဖျက်မည်",
+ "label.cohort": "Cohort",
"label.compare": "Compare",
+ "label.compare-dates": "Compare dates",
"label.confirm": "အတည်ပြုသည်",
"label.confirm-password": "စကားဝှက်အတည်ပြုသည်",
"label.contains": "ပါဝင်သည်",
+ "label.content": "Content",
"label.continue": "ဆက်သွားမည်",
+ "label.conversion": "Conversion",
+ "label.conversion-rate": "Conversion rate",
+ "label.conversion-step": "Conversion step",
"label.count": "Count",
"label.countries": "နိုင်ငံများ",
"label.country": "Country",
@@ -38,6 +53,7 @@
"label.create-user": "အသုံးပြုသူထည့်မည်",
"label.created": "ပြုလုပ်ပြီးသော",
"label.created-by": "Created By",
+ "label.currency": "Currency",
"label.current": "Current",
"label.current-password": "လက်ရှိစကားဝှက်",
"label.custom-range": "အချိန်အပိုင်းအခြားရွေးရန်",
@@ -57,19 +73,26 @@
"label.details": "အသေးစိတ်",
"label.device": "Device",
"label.devices": "အသုံးပြုသည့် ကိရိယာများ",
+ "label.direct": "Direct",
"label.dismiss": "ပိတ်ပါ",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "မပါဝင်ပါ",
+ "label.does-not-include": "Does not include",
+ "label.doest-not-exist": "Does not exist",
"label.domain": "ဒိုမိန်း",
"label.dropoff": "Dropoff",
"label.edit": "ပြုပြင်မည်",
"label.edit-dashboard": "ဒက်ရှ်ဘုတ်ကို ပြုပြင်မည်",
"label.edit-member": "Edit member",
+ "label.email": "Email",
"label.enable-share-url": "ဝေငှခြင်းကိုလင့်ကို ဖွင့်မည်",
"label.end-step": "End Step",
"label.entry": "Entry URL",
"label.event": "အဖြစ်အပျက်",
"label.event-data": "အဖြစ်အပျက် ဒေတာ",
+ "label.event-name": "Event name",
"label.events": "အဖြစ်အပျက်များ",
+ "label.exists": "Exists",
"label.exit": "Exit URL",
"label.false": "မှားသည်",
"label.field": "Field အမည်",
@@ -78,29 +101,37 @@
"label.filter-combined": "ပေါင်းစပ်ပြီး",
"label.filter-raw": "အရှိအတိုင်း",
"label.filters": "Filter များ",
+ "label.first-click": "First click",
"label.first-seen": "First seen",
"label.funnel": "ဖန်နယ်",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
+ "label.funnels": "Funnels",
"label.goal": "Goal",
"label.goals": "Goals",
"label.goals-description": "Track your goals for pageviews and events.",
"label.greater-than": "ထက်ပို၍ကြီးသည်",
"label.greater-than-equals": "ထက်ပို၍ကြီးသည်သို့မဟုတ်တူသည်",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grouped",
+ "label.hostname": "Hostname",
+ "label.includes": "Includes",
+ "label.insight": "Insight",
"label.insights": "အသေးစိတ်သိမြင်နိုင်ရန်",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
+ "label.is-false": "Is false",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
+ "label.is-true": "Is true",
"label.join": "ဝင်မည်",
"label.join-team": "အသင်းဝင်မည်",
"label.journey": "Journey",
"label.journey-description": "Understand how users navigate through your website.",
+ "label.journeys": "Journeys",
"label.language": "ဘာသာစကား",
"label.languages": "ဘာသာစကားများ",
"label.laptop": "လက်တော့ပ်",
+ "label.last-click": "Last click",
"label.last-days": "လွန်ခဲ့သော {x} ရက်က",
"label.last-hours": "လွန်ခဲ့သော {x} နာရီက",
"label.last-months": "Last {x} months",
@@ -109,15 +140,19 @@
"label.leave-team": "အသင်းမှထွက်မည်",
"label.less-than": "ထက်ပို၍ငယ်သည်",
"label.less-than-equals": "ထက်ပို၍ငယ်သည်သို့မဟုတ်တူသည်",
+ "label.links": "Links",
"label.login": "လော့ဂ်အင်",
"label.logout": "လော့ဂ်အောက်လုပ်မည်",
"label.manage": "Manage",
"label.manager": "Manager",
"label.max": "အများဆုံး",
+ "label.maximize": "Expand",
+ "label.medium": "Medium",
"label.member": "Member",
"label.members": "အဖွဲ့ဝင်များ",
"label.min": "အနည်းဆုံး",
"label.mobile": "မိုဘိုင်း",
+ "label.model": "Model",
"label.more": "နောက်ထပ်",
"label.my-account": "My account",
"label.my-websites": "My websites",
@@ -126,16 +161,29 @@
"label.none": "မရှိပါ",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organic search",
+ "label.organic-shopping": "Organic shopping",
+ "label.organic-social": "Organic social",
+ "label.organic-video": "Organic video",
"label.os": "ကွန်ပျူတာလည်ပတ်မှုစနစ်",
+ "label.other": "Other",
"label.overview": "အပေါ်ယံမြင်ကွင်း",
"label.owner": "ပိုင်ဆိုင်သူ",
+ "label.page": "Page",
"label.page-of": "Page {current} of {total}",
"label.page-views": "ဝင်ရောက်ကြည့်ရှုသူ",
"label.pageTitle": "Page title",
"label.pages": "စာမျက်နှာများ",
+ "label.paid-ads": "Paid ads",
+ "label.paid-search": "Paid search",
+ "label.paid-shopping": "Paid shopping",
+ "label.paid-social": "Paid social",
+ "label.paid-video": "Paid video",
"label.password": "စကားဝှက်",
"label.path": "Path",
"label.paths": "Paths",
+ "label.pixels": "Pixels",
"label.powered-by": "{name} ထောက်ပံ့သည်",
"label.previous": "Previous",
"label.previous-period": "Previous period",
@@ -147,12 +195,14 @@
"label.query": "Query (ကွာရီ)",
"label.query-parameters": "Query parameters (ကွာရီပါရာမီတာများ)",
"label.realtime": "အချိန်နှင့်တပြေးညီ",
+ "label.referral": "Referral",
"label.referrer": "Referrer",
"label.referrers": "ရည်ညွှန်းမှုများ",
"label.refresh": "Refresh လုပ်မည်",
"label.regenerate": "ပြန်ထုတ်မည်",
"label.region": "Region",
"label.regions": "ဒေသများ",
+ "label.remaining": "Remaining",
"label.remove": "ဖျက်မည်",
"label.remove-member": "Remove member",
"label.reports": "တင်ပြမှုများ",
@@ -163,7 +213,6 @@
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
"label.role": "အခန်းကဏ္ဍ",
"label.run-query": "Query ကိုလုပ်ဆောင်မည်",
"label.save": "သိမ်းဆည်းမည်",
@@ -171,26 +220,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "ရက်ရွေးပါ",
+ "label.select-filter": "Select filter",
"label.select-role": "Select role",
"label.select-website": "ဝဘက်ဘ်ဆိုဒ်ရွေးပါ",
"label.session": "Session",
+ "label.session-data": "Session data",
"label.sessions": "ဆက်ရှင်များ",
"label.settings": "ဆက်တင်များ",
+ "label.share": "Share",
"label.share-url": "URL ကိုရှဲမည်",
"label.single-day": "တစ်ရက်အတွင်း",
+ "label.sms": "SMS",
+ "label.sources": "Sources",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "ပေါင်းလဒ်",
"label.tablet": "တက်ဘလက်",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "အသင်း",
"label.team-id": "အသင်း အိုင်ဒီ",
"label.team-manager": "Team manager",
"label.team-member": "အသင်းဝင်",
"label.team-name": "Team name",
"label.team-owner": "အသင်းကိုပိုင်ဆိုင်သူ",
+ "label.team-settings": "Team settings",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "အသင်းများ",
+ "label.terms": "Terms",
"label.theme": "Theme (အပြင်အဆင်)",
"label.this-month": "ယခုလ",
"label.this-week": "ယခုအပတ်",
@@ -213,10 +271,7 @@
"label.unknown": "မသိသော",
"label.untitled": "ခေါင်းစဉ်မရှိ",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URL များ",
"label.user": "အသုံးပြုသူ",
- "label.user-property": "User Property",
"label.username": "အသုံးပြုသူအမည်",
"label.users": "အသုံးပြုသူများ",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "မနေ့က",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} လက်ရှိအသုံးပြုနေသူ {x, plural, one {ယောက်} other {ယောက်}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "{target} ကို ဖျက်ရန် သေချာပါသလား?",
"message.confirm-leave": "{target} ကို ထွက်ရန် သေချာပါသလား?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "ဝက်ဘ်ဆိုဒ် ဒေတာအကုန် ဖျက်မည်",
"message.error": "မှားယွင်းမှုတစ်ခု ရှိသွားပါသည်",
"message.event-log": "{url} တွင် {event}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "ဆက်တင်သို့ သွားရန်",
"message.incorrect-username-password": "အသုံးပြုသူအမည် သို့မဟုတ် စကားဝှက် မှားနေသည်",
"message.invalid-domain": "ဒိုမိန်း မမှန်ပါ http/https. မပါရပါ",
@@ -259,10 +316,13 @@
"message.no-teams": "အသင်း မပြုလုပ်ရသေးပါ",
"message.no-users": "အသုံးပြုသူ မရှိသေးပါ",
"message.no-websites-configured": "ဝက်ဘ်ဆိုဒ်တစ်ခုမှ မထည့်ရသေးပါ",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "ဤစာမျက်နှာသည် မရှိပါ",
"message.reset-website": "ဤ ဝက်ဘ်ဆိုဒ်ဒေတာကိုဖျက်၍ ပြန်စလုပ်ရန် အောက်တွင် {confirmation} ကို ရိုက်ထည့်ပေးပါ",
"message.reset-website-warning": "ဤဝက်ဘ်ဆိုဒ်က စာရင်းအချက်အလက်များကို ဖျက်မည်၊ ဆက်တင်ဒေတာများ မပါပါ",
"message.saved": "မှတ်သားပြီး",
+ "message.sever-error": "Server error",
"message.share-url": "သင့်ဝက်ဆိုဒ်ဘ်၏ စာရင်းအချက်အလက်များကို အောက်ပါ URL တွင် ဝင်ရောက်ကြည့်ရှုနိုင်သည်",
"message.team-already-member": "ဤအသင်းတွင် ဝင်ပြီးသားဖြစ်နေသည်",
"message.team-not-found": "အသင်း မရှိပါ",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "အသုံးပြုသူ ဖျက်ပြီးပါပြီ",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "{country} မှ {browser} ဖြင့် {os} {device} တွင် ဝင်ရောက်ကြည့်ရှုသူ",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "{country} မှ {browser} ဖြင့် {os} {device} တွင် ဝင်ရောက်ကြည့်ရှုသူ"
}
diff --git a/src/lang/nb-NO.json b/src/lang/nb-NO.json
index 7c691002..adb4468e 100644
--- a/src/lang/nb-NO.json
+++ b/src/lang/nb-NO.json
@@ -3,32 +3,47 @@
"label.actions": "Handlinger",
"label.activity": "Aktivitetslogg",
"label.add": "Legg til",
+ "label.add-board": "Legg til tavle",
"label.add-description": "Legg til beskrivelse",
"label.add-member": "Legg til bruker",
"label.add-step": "Legg til steg",
"label.add-website": "Legg til nettsted",
"label.admin": "Administrator",
+ "label.affiliate": "Tilknyttet",
"label.after": "Etter",
"label.all": "Alle",
"label.all-time": "Noensinne",
- "label.analytics": "Analytics",
+ "label.analytics": "Analyse",
+ "label.apply": "Bruk",
+ "label.attribution": "Attribusjon",
+ "label.attribution-description": "Se hvordan brukere engasjerer seg i markedsføringen din og hva som driver konverteringer.",
"label.average": "Gjennomsnnitt",
"label.back": "Tilbake",
"label.before": "Før",
+ "label.behavior": "Atferd",
+ "label.boards": "Tavler",
"label.bounce-rate": "Avvisningsfrekvens",
"label.breakdown": "Nedbrytning",
"label.browser": "Nettleser",
"label.browsers": "Nettlesere",
+ "label.campaigns": "Kampanjer",
"label.cancel": "Avvis",
"label.change-password": "Bytt passord",
+ "label.channels": "Kanaler",
"label.cities": "Byer",
"label.city": "By",
"label.clear-all": "Tøm alle",
+ "label.cohort": "Kohort",
"label.compare": "Sammenlign",
+ "label.compare-dates": "Sammenlign datoer",
"label.confirm": "Bekreft",
"label.confirm-password": "Godkjenn passord",
"label.contains": "Inneholder",
+ "label.content": "Innhold",
"label.continue": "Fortsett",
+ "label.conversion": "Konvertering",
+ "label.conversion-rate": "Konverteringsrate",
+ "label.conversion-step": "Konverteringssteg",
"label.count": "Antall",
"label.countries": "Land",
"label.country": "Land",
@@ -38,6 +53,7 @@
"label.create-user": "Opprett bruker",
"label.created": "Opprettet",
"label.created-by": "Opprettet av",
+ "label.currency": "Valuta",
"label.current": "Nåværende",
"label.current-password": "Nåværende passord",
"label.custom-range": "Egendefinert utvalg",
@@ -48,28 +64,35 @@
"label.day": "Dag",
"label.default-date-range": "Standard datoperiode",
"label.delete": "Slett",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "Slett rapport",
+ "label.delete-team": "Slett team",
+ "label.delete-user": "Slett bruker",
"label.delete-website": "Slett nettstedet",
- "label.description": "Description",
+ "label.description": "Beskrivelse",
"label.desktop": "Stasjonær",
"label.details": "Detaljer",
"label.device": "Enhet",
"label.devices": "Enheter",
+ "label.direct": "Direkte",
"label.dismiss": "Avbryt",
+ "label.distinct-id": "Unik ID",
"label.does-not-contain": "Innholder ikke",
+ "label.does-not-include": "Inkluderer ikke",
+ "label.doest-not-exist": "Eksisterer ikke",
"label.domain": "Domene",
"label.dropoff": "Dropoff",
"label.edit": "Rediger",
"label.edit-dashboard": "Rediger dashboard",
"label.edit-member": "Rediger bruker",
+ "label.email": "Email",
"label.enable-share-url": "Aktiver delings-URL",
"label.end-step": "Avslutt steg",
"label.entry": "Inngangs-URL",
"label.event": "Hendelse",
"label.event-data": "Hendelsesdata",
+ "label.event-name": "Hendelsesnavn",
"label.events": "Hendelser",
+ "label.exists": "Eksisterer",
"label.exit": "Utgangs-URL",
"label.false": "Usant",
"label.field": "Felt",
@@ -78,29 +101,37 @@
"label.filter-combined": "Kombinert",
"label.filter-raw": "Rå",
"label.filters": "Filter",
+ "label.first-click": "Første klikk",
"label.first-seen": "Først sett",
"label.funnel": "Trakt",
"label.funnel-description": "Forstå konverteringen og drop-off frafallsfrekvens av brukere.",
+ "label.funnels": "Trakter",
"label.goal": "Mål",
"label.goals": "Mål",
"label.goals-description": "Spor dine mål for sidevisninger og hendelser.",
"label.greater-than": "Mer enn",
"label.greater-than-equals": "Mer enn eller lik",
- "label.host": "Vert",
- "label.hosts": "Verter",
+ "label.grouped": "Gruppert",
+ "label.hostname": "Vertsnavn",
+ "label.includes": "Inkluderer",
+ "label.insight": "Innsikt",
"label.insights": "Innsikt",
"label.insights-description": "Dykk dypere i din data ved bruk av segmentering og filtre.",
"label.is": "Er",
+ "label.is-false": "Er usant",
"label.is-not": "Er ikke",
"label.is-not-set": "Er ikke satt",
"label.is-set": "Er satt",
+ "label.is-true": "Er sant",
"label.join": "Bli med",
"label.join-team": "Bli med i teamet",
"label.journey": "Reise",
"label.journey-description": "Forstå hvordan brukerene navigerer gjennom din side.",
+ "label.journeys": "Reiser",
"label.language": "Språk",
"label.languages": "Språk",
"label.laptop": "Bærbar",
+ "label.last-click": "Siste klikk",
"label.last-days": "Siste {x} dager",
"label.last-hours": "Siste {x} timer",
"label.last-months": "Last {x} months",
@@ -109,15 +140,19 @@
"label.leave-team": "Forlat team",
"label.less-than": "Mindre enn",
"label.less-than-equals": "Mindre enn eller lik",
+ "label.links": "Lenker",
"label.login": "Logg inn",
"label.logout": "Logg ut",
"label.manage": "Administrer",
"label.manager": "Administrator",
"label.max": "Maks",
+ "label.maximize": "Utvid",
+ "label.medium": "Medium",
"label.member": "Bruker",
"label.members": "Brukere",
"label.min": "Min",
"label.mobile": "Mobiltelefon",
+ "label.model": "Modell",
"label.more": "Mer",
"label.my-account": "Min konto",
"label.my-websites": "Mine nettsider",
@@ -126,16 +161,29 @@
"label.none": "Ingen",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organisk søk",
+ "label.organic-shopping": "Organisk handel",
+ "label.organic-social": "Organisk sosial",
+ "label.organic-video": "Organisk video",
"label.os": "OS",
+ "label.other": "Annet",
"label.overview": "Oversikt",
"label.owner": "Eier",
+ "label.page": "Side",
"label.page-of": "Side {current} av {total}",
"label.page-views": "Sidevisninger",
"label.pageTitle": "Sidetittel",
"label.pages": "Sider",
+ "label.paid-ads": "Betalte annonser",
+ "label.paid-search": "Betalt søk",
+ "label.paid-shopping": "Betalt handel",
+ "label.paid-social": "Betalt sosial",
+ "label.paid-video": "Betalt video",
"label.password": "Passord",
"label.path": "Sti",
"label.paths": "Stier",
+ "label.pixels": "Piksler",
"label.powered-by": "Drevet av {name}",
"label.previous": "Forrige",
"label.previous-period": "Forrige periode",
@@ -147,12 +195,14 @@
"label.query": "Forespørsel",
"label.query-parameters": "Forespørsel parametere",
"label.realtime": "Sanntid",
+ "label.referral": "Referral",
"label.referrer": "Henviser",
"label.referrers": "Henvisere",
"label.refresh": "Oppdater",
"label.regenerate": "Regenerer",
"label.region": "Region",
"label.regions": "Regioner",
+ "label.remaining": "Gjenstår",
"label.remove": "Fjern",
"label.remove-member": "Fjern bruker",
"label.reports": "Rapporter",
@@ -163,7 +213,6 @@
"label.retention-description": "Mål nettstedets klebrighet ved å spore hvor ofte brukere kommer tilbake.",
"label.revenue": "Inntenker",
"label.revenue-description": "Se på inntektene dine over tid.",
- "label.revenue-property": "Inntektegenskaper",
"label.role": "Rolle",
"label.run-query": "Kjør spørring",
"label.save": "Lagre",
@@ -171,26 +220,35 @@
"label.search": "Søk",
"label.select": "Velg",
"label.select-date": "Velg dato",
+ "label.select-filter": "Velg filter",
"label.select-role": "Velg rolle",
"label.select-website": "Velg nettsted",
"label.session": "Økt",
+ "label.session-data": "Øktdata",
"label.sessions": "Økter",
"label.settings": "Innstillinger",
+ "label.share": "Del",
"label.share-url": "Del URL",
"label.single-day": "Enkeltdag",
+ "label.sms": "SMS",
+ "label.sources": "Kilder",
"label.start-step": "Starttrinn",
"label.steps": "Trinn",
"label.sum": "Sum",
"label.tablet": "Nettbrett",
+ "label.tag": "Tagg",
+ "label.tags": "Tagger",
"label.team": "Team",
"label.team-id": "Team-ID",
"label.team-manager": "Teamadministrator",
"label.team-member": "Teammedlem",
"label.team-name": "Teamnavn",
"label.team-owner": "Teameier",
+ "label.team-settings": "Teaminnstillinger",
"label.team-view-only": "Team (kun visning)",
"label.team-websites": "Team-nettsteder",
"label.teams": "Team",
+ "label.terms": "Vilkår",
"label.theme": "Tema",
"label.this-month": "Denne måneden",
"label.this-week": "Denne uka",
@@ -213,10 +271,7 @@
"label.unknown": "Ukjent",
"label.untitled": "Uten tittel",
"label.update": "Oppdater",
- "label.url": "URL",
- "label.urls": "URL-er",
"label.user": "Bruker",
- "label.user-property": "Brukeregenskap",
"label.username": "Brukernavn",
"label.users": "Brukere",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "I går",
"message.action-confirmation": "Skriv {confirmation} i feltet nedenfor for å bekrefte.",
"message.active-users": "{x} {x, plural, one {besøkende} other {besøkende}} nå",
+ "message.bad-request": "Bad request",
"message.collected-data": "Innsamlede data",
"message.confirm-delete": "Er du sikker på at du vil slette {target}?",
"message.confirm-leave": "Er du sikker på at du vil forlate {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Alle tilknyttede data vil også bli slettet.",
"message.error": "Noe gikk galt.",
"message.event-log": "{event} på {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Gå til innstillinger",
"message.incorrect-username-password": "Ugyldig brukernavn/passord.",
"message.invalid-domain": "Ugyldig domene",
@@ -259,10 +316,13 @@
"message.no-teams": "Du har ikke opprettet noen team.",
"message.no-users": "Ingen brukere.",
"message.no-websites-configured": "Du har ikke satt opp noen nettsteder.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Siden ble ikke funnet.",
"message.reset-website": "For å nullstille dette nettstedet, skriv {confirmation} i feltet nedenfor for å bekrefte.",
"message.reset-website-warning": "All statistikk for dette nettstedet vil bli slettet, men sporingskoden forblir uberørt.",
"message.saved": "Lagret!",
+ "message.sever-error": "Server error",
"message.share-url": "Dette er den offentlige delings-URL-en for {target}.",
"message.team-already-member": "Du er allerede medlem av teamet.",
"message.team-not-found": "Teamet ble ikke funnet.",
@@ -272,9 +332,8 @@
"message.transfer-user-website-to-team": "Velg teamet du vil overføre dette nettstedet til.",
"message.transfer-website": "Overfør eierskapet til nettstedet til din konto eller et annet team.",
"message.triggered-event": "Utløst hendelse",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Bruker slettet.",
"message.viewed-page": "Vist side",
- "message.visitor-log": "Besøkende fra {country} med {browser} på {os} {device}",
- "message.visitors-dropped-off": "Besøkende falt fra"
-
+ "message.visitor-log": "Besøkende fra {country} med {browser} på {os} {device}"
}
diff --git a/src/lang/nl-NL.json b/src/lang/nl-NL.json
index 78b41df7..1ec5c020 100644
--- a/src/lang/nl-NL.json
+++ b/src/lang/nl-NL.json
@@ -3,33 +3,48 @@
"label.actions": "Acties",
"label.activity": "Activiteiten logboek",
"label.add": "Toevoegen",
+ "label.add-board": "Bord toevoegen",
"label.add-description": "Omschrijving toevoegen",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.add-member": "Lid toevoegen",
+ "label.add-step": "Stap toevoegen",
"label.add-website": "Website koppelen",
"label.admin": "Administrator",
+ "label.affiliate": "Partner",
"label.after": "Na",
"label.all": "Alles",
"label.all-time": "Onbeperkt",
- "label.analytics": "Analytics",
+ "label.analytics": "Analyse",
+ "label.apply": "Toepassen",
+ "label.attribution": "Toewijzing",
+ "label.attribution-description": "Bekijk hoe gebruikers omgaan met je marketing en wat conversies stimuleert.",
"label.average": "Gemiddelde",
"label.back": "Terug",
"label.before": "Voor",
+ "label.behavior": "Gedrag",
+ "label.boards": "Borden",
"label.bounce-rate": "Bouncepercentage",
"label.breakdown": "Opsplitsen",
"label.browser": "Browser",
"label.browsers": "Browsers",
+ "label.campaigns": "Campagnes",
"label.cancel": "Annuleren",
"label.change-password": "Wachtwoord wijzigen",
+ "label.channels": "Kanalen",
"label.cities": "Steden",
"label.city": "Stad",
"label.clear-all": "Filters wissen",
- "label.compare": "Compare",
+ "label.cohort": "Cohort",
+ "label.compare": "Vergelijken",
+ "label.compare-dates": "Datums vergelijken",
"label.confirm": "Bevestigen",
"label.confirm-password": "Wachtwoord bevestigen",
"label.contains": "Bevat",
+ "label.content": "Inhoud",
"label.continue": "Doorgaan",
- "label.count": "Count",
+ "label.conversion": "Conversie",
+ "label.conversion-rate": "Conversieratio",
+ "label.conversion-step": "Conversiestap",
+ "label.count": "Aantal",
"label.countries": "Landen",
"label.country": "Land",
"label.create": "Aanmaken",
@@ -38,7 +53,8 @@
"label.create-user": "Gebruiker maken",
"label.created": "Gemaakt",
"label.created-by": "Gemaakt Door",
- "label.current": "Current",
+ "label.currency": "Valuta",
+ "label.current": "Huidig",
"label.current-password": "Huidig wachtwoord",
"label.custom-range": "Aangepast bereik",
"label.dashboard": "Overzicht",
@@ -50,26 +66,33 @@
"label.delete": "Verwijderen",
"label.delete-report": "Rapport verwijderen",
"label.delete-team": "Team verwijderen",
- "label.delete-user": "Verwijder gebruiker",
+ "label.delete-user": "Gebruiker verwijderen",
"label.delete-website": "Website verwijderen",
- "label.description": "Omschrijving",
+ "label.description": "Beschrijving",
"label.desktop": "Computer",
"label.details": "Informatie",
"label.device": "Apparaat",
"label.devices": "Apparaten",
+ "label.direct": "Direct",
"label.dismiss": "Negeren",
+ "label.distinct-id": "Uniek ID",
"label.does-not-contain": "Bevat geen",
+ "label.does-not-include": "Bevat niet",
+ "label.doest-not-exist": "Bestaat niet",
"label.domain": "Domein",
"label.dropoff": "Uitval",
"label.edit": "Bewerken",
"label.edit-dashboard": "Dashboard aanpassen",
"label.edit-member": "Gebruiker aanpassen",
+ "label.email": "Email",
"label.enable-share-url": "Sta delen via openbare URL toe",
"label.end-step": "End Step",
"label.entry": "Entry URL",
"label.event": "Gebeurtenis",
"label.event-data": "Datum gebeurtenis",
+ "label.event-name": "Gebeurtenisnaam",
"label.events": "Gebeurtenissen",
+ "label.exists": "Bestaat",
"label.exit": "Exit URL",
"label.false": "Onwaar",
"label.field": "Veld",
@@ -78,46 +101,58 @@
"label.filter-combined": "Gecombineerd",
"label.filter-raw": "Ruw",
"label.filters": "Filters",
+ "label.first-click": "Eerste klik",
"label.first-seen": "First seen",
"label.funnel": "Funnel",
"label.funnel-description": "Ontdek de conversie- en uitvalpercentages van gebruikers.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
+ "label.funnels": "Trechters",
+ "label.goal": "Doel",
+ "label.goals": "Doelen",
+ "label.goals-description": "Volg je doelen voor paginaweergaven en gebeurtenissen.",
"label.greater-than": "Groter dan",
"label.greater-than-equals": "Groter of gelijk aan",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Gegroepeerd",
+ "label.hostname": "Hostnaam",
+ "label.includes": "Bevat",
+ "label.insight": "Inzicht",
"label.insights": "Inzichten",
"label.insights-description": "Verken je gegevens verder door segmenten en filters te gebruiken.",
"label.is": "Is",
+ "label.is-false": "Is onwaar",
"label.is-not": "Is niet",
"label.is-not-set": "Is niet ingesteld",
"label.is-set": "Is ingesteld",
+ "label.is-true": "Is waar",
"label.join": "Lid worden",
"label.join-team": "Word lid van een team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.journey": "Reis",
+ "label.journey-description": "Begrijp hoe gebruikers door je website navigeren.",
+ "label.journeys": "Reizen",
"label.language": "Taal",
"label.languages": "Talen",
"label.laptop": "Laptop",
+ "label.last-click": "Laatste klik",
"label.last-days": "Laatste {x} dagen",
"label.last-hours": "Laatste {x} uur",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
+ "label.last-months": "Laatste {x} maanden",
+ "label.last-seen": "Laatst gezien",
"label.leave": "Verlaten",
"label.leave-team": "Verlaat team",
"label.less-than": "Minder dan",
"label.less-than-equals": "Minder of gelijk aan",
+ "label.links": "Koppelingen",
"label.login": "Inloggen",
"label.logout": "Uitloggen",
"label.manage": "Beheren",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Uitvouwen",
+ "label.medium": "Medium",
"label.member": "Gebruiker",
"label.members": "Gebruikers",
"label.min": "Min",
"label.mobile": "Mobiel",
+ "label.model": "Model",
"label.more": "Toon meer",
"label.my-account": "Mijn profiel",
"label.my-websites": "Mijn websites",
@@ -126,33 +161,48 @@
"label.none": "Geen",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organisch zoeken",
+ "label.organic-shopping": "Organisch winkelen",
+ "label.organic-social": "Organisch sociaal",
+ "label.organic-video": "Organische video",
"label.os": "OS",
+ "label.other": "Overig",
"label.overview": "Overzicht",
"label.owner": "Eigenaar",
+ "label.page": "Pagina",
"label.page-of": "Pagina {current} van {total}",
"label.page-views": "Paginaweergaven",
"label.pageTitle": "Pagina titel",
"label.pages": "Pagina's",
+ "label.paid-ads": "Betaalde advertenties",
+ "label.paid-search": "Betaald zoeken",
+ "label.paid-shopping": "Betaald winkelen",
+ "label.paid-social": "Betaald sociaal",
+ "label.paid-video": "Betaalde video",
"label.password": "Wachtwoord",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Pad",
+ "label.paths": "Paden",
+ "label.pixels": "Pixels",
"label.powered-by": "mogelijk gemaakt door {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "Vorige",
+ "label.previous-period": "Vorige periode",
+ "label.previous-year": "Vorig jaar",
"label.profile": "Profiel",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Eigenschappen",
+ "label.property": "Eigenschap",
"label.queries": "Parameters",
"label.query": "Query",
"label.query-parameters": "URL-parameters",
"label.realtime": "Actueel",
+ "label.referral": "Verwijzing",
"label.referrer": "Referrer",
"label.referrers": "Verwijzers",
"label.refresh": "Vernieuwen",
"label.regenerate": "Opnieuw genereren",
"label.region": "Regio",
"label.regions": "Regio's",
+ "label.remaining": "Resterend",
"label.remove": "Verwijderen",
"label.remove-member": "Gebruiker verwijderen",
"label.reports": "Rapporten",
@@ -161,9 +211,8 @@
"label.reset-website": "Statistieken opnieuw instellen",
"label.retention": "Retentie",
"label.retention-description": "Meet de retentie van je website door door bij te houden hoe vaak gebruikers terugkeren.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Omzet",
+ "label.revenue-description": "Bekijk je omzet in de loop van de tijd.",
"label.role": "Gebruikersrol",
"label.run-query": "Query uitvoeren",
"label.save": "Opslaan",
@@ -171,26 +220,35 @@
"label.search": "Zoeken",
"label.select": "Selecteer",
"label.select-date": "Datum selecteren",
+ "label.select-filter": "Filter selecteren",
"label.select-role": "Rol selecteren",
"label.select-website": "Website selecteren",
- "label.session": "Session",
+ "label.session": "Sessie",
+ "label.session-data": "Sessiegegevens",
"label.sessions": "Sessies",
"label.settings": "Instellingen",
+ "label.share": "Delen",
"label.share-url": "URL delen",
"label.single-day": "Enkele dag",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
+ "label.sms": "SMS",
+ "label.sources": "Bronnen",
+ "label.start-step": "Startstap",
+ "label.steps": "Stappen",
"label.sum": "Som",
"label.tablet": "Tablet",
+ "label.tag": "Label",
+ "label.tags": "Labels",
"label.team": "Team",
"label.team-id": "Team ID",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Teamleider",
"label.team-member": "Teamlid",
"label.team-name": "Teamnaam",
"label.team-owner": "Teameigenaar",
+ "label.team-settings": "Teaminstellingen",
"label.team-view-only": "Team alleen lezen",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Voorwaarden",
"label.theme": "Thema",
"label.this-month": "Deze maand",
"label.this-week": "Deze week",
@@ -213,10 +271,7 @@
"label.unknown": "Onbekend",
"label.untitled": "Ongetiteld",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URL's",
"label.user": "Gebruiker",
- "label.user-property": "User Property",
"label.username": "Gebruikersnaam",
"label.users": "Gebruikers",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Gisteren",
"message.action-confirmation": "Typ {confirmation} in het veld hieronder om te bevestigen.",
"message.active-users": "{x} actieve {x, plural, one {bezoeker} other {bezoekers}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Weet je zeker dat je {target} wilt verwijderen?",
"message.confirm-leave": "Weet je zeker dat je {target} wilt verlaten?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Alle verwante gegevens zullen ook verwijderd worden.",
"message.error": "Er is iets misgegaan.",
"message.event-log": "{event} op {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Naar instellingen",
"message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.",
"message.invalid-domain": "Ongeldig domein",
@@ -259,10 +316,13 @@
"message.no-teams": "Er zijn nog geen teams aangemaakt.",
"message.no-users": "Er zijn geen gebruikers.",
"message.no-websites-configured": "Je hebt geen websites ingesteld.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Pagina niet gevonden.",
"message.reset-website": "Typ {confirmation} in het veld hieronder om te bevestigen dat je de website wilt resetten.",
"message.reset-website-warning": "Alle bijhorende statistieken van deze website worden verwijderd, maar jouw volgcode blijft gelden.",
"message.saved": "Opslaan succesvol.",
+ "message.sever-error": "Server error",
"message.share-url": "Met deze URL kan {target} openbaar gedeeld worden.",
"message.team-already-member": "Je bent al lid van het team.",
"message.team-not-found": "Team niet gevonden.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Selecteer het team om deze website aan toe te voegen.",
"message.transfer-website": "Draag het eigenaarschap van de website over naar jouw account, of een ander team.",
"message.triggered-event": "Getriggerde gebeurtenis",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Gebruiker verwijderd",
"message.viewed-page": "Bekeken pagina",
- "message.visitor-log": "Bezoeker uit {country} met {browser} op een {os} {device}",
- "message.visitors-dropped-off": "Afgehaakte bezoekers"
+ "message.visitor-log": "Bezoeker uit {country} met {browser} op een {os} {device}"
}
diff --git a/src/lang/pl-PL.json b/src/lang/pl-PL.json
index 697beeb3..0c8b0004 100644
--- a/src/lang/pl-PL.json
+++ b/src/lang/pl-PL.json
@@ -3,32 +3,47 @@
"label.actions": "Działania",
"label.activity": "Dziennik aktywności",
"label.add": "Dodaj",
+ "label.add-board": "Dodaj tablicę",
"label.add-description": "Dodaj opis",
"label.add-member": "Dodaj członka",
"label.add-step": "Dodaj krok",
"label.add-website": "Dodaj witrynę",
"label.admin": "Administrator",
+ "label.affiliate": "Partner",
"label.after": "Po",
"label.all": "Wszystkie",
"label.all-time": "Cały czas",
"label.analytics": "Analityka",
+ "label.apply": "Zastosuj",
+ "label.attribution": "Atrybucja",
+ "label.attribution-description": "Zobacz, jak użytkownicy angażują się w Twoją reklamę i co napędza konwersje.",
"label.average": "Średnia",
"label.back": "Powrót",
"label.before": "Przed",
+ "label.behavior": "Zachowanie",
+ "label.boards": "Tablice",
"label.bounce-rate": "Współczynnik odrzuceń",
"label.breakdown": "Rozbicie",
"label.browser": "Przeglądarka",
"label.browsers": "Przeglądarki",
+ "label.campaigns": "Kampanie",
"label.cancel": "Anuluj",
"label.change-password": "Zmień hasło",
+ "label.channels": "Kanały",
"label.cities": "Miasta",
"label.city": "Miasto",
"label.clear-all": "Wyczyść wszystko",
+ "label.cohort": "Kohorta",
"label.compare": "Porównaj",
+ "label.compare-dates": "Porównaj daty",
"label.confirm": "Potwierdź",
"label.confirm-password": "Potwierdź hasło",
"label.contains": "Zawiera",
+ "label.content": "Treść",
"label.continue": "Kontynuuj",
+ "label.conversion": "Konwersja",
+ "label.conversion-rate": "Wskaźnik konwersji",
+ "label.conversion-step": "Etap konwersji",
"label.count": "Liczba",
"label.countries": "Kraje",
"label.country": "Państwo",
@@ -38,6 +53,7 @@
"label.create-user": "Utwórz użytkownika",
"label.created": "Utworzony",
"label.created-by": "Utworzony przez",
+ "label.currency": "Waluta",
"label.current": "Aktualny",
"label.current-password": "Aktualne hasło",
"label.custom-range": "Zakres niestandardowy",
@@ -57,19 +73,26 @@
"label.details": "Szczegóły",
"label.device": "Urządzenie",
"label.devices": "Urządzenia",
+ "label.direct": "Bezpośredni",
"label.dismiss": "Odrzuć",
+ "label.distinct-id": "Unikalny ID",
"label.does-not-contain": "Nie zawiera",
+ "label.does-not-include": "Nie zawiera",
+ "label.doest-not-exist": "Nie istnieje",
"label.domain": "Domena",
"label.dropoff": "Odpływ",
"label.edit": "Edytuj",
"label.edit-dashboard": "Edytuj panel",
"label.edit-member": "Edytuj członka",
+ "label.email": "Email",
"label.enable-share-url": "Włącz udostępnianie adresu URL",
"label.end-step": "Krok końcowy",
"label.entry": "Entry URL",
"label.event": "Zdarzenie",
"label.event-data": "Dane zdarzenia",
+ "label.event-name": "Nazwa zdarzenia",
"label.events": "Zdarzenia",
+ "label.exists": "Istnieje",
"label.exit": "URL wyjściowy",
"label.false": "Fałsz",
"label.field": "Pole",
@@ -78,46 +101,58 @@
"label.filter-combined": "Połączone",
"label.filter-raw": "Surowe dane",
"label.filters": "Filtry",
+ "label.first-click": "Pierwsze kliknięcie",
"label.first-seen": "First seen",
"label.funnel": "Lejek",
"label.funnel-description": "Zrozum wskaźniki konwersji i odpływu użytkowników.",
+ "label.funnels": "Lejki",
"label.goal": "Cel",
"label.goals": "Cele",
"label.goals-description": "Track your goals for pageviews and events.",
"label.greater-than": "Większe niż",
"label.greater-than-equals": "Większe niż lub równe",
- "label.host": "Host",
- "label.hosts": "Hosty",
+ "label.grouped": "Grupowane",
+ "label.hostname": "Nazwa hosta",
+ "label.includes": "Zawiera",
+ "label.insight": "Wgląd",
"label.insights": "Analiza",
"label.insights-description": "Poznaj lepiej swoje dane, korzystając z segmentów i filtrów.",
"label.is": "Równe",
+ "label.is-false": "Jest fałszem",
"label.is-not": "Nie jest równe",
"label.is-not-set": "Nieustawione",
"label.is-set": "Ustawione",
+ "label.is-true": "Jest prawdą",
"label.join": "Dołącz",
"label.join-team": "Dołącz do zespołu",
"label.journey": "Droga",
"label.journey-description": "Zrozum, w jaki sposób użytkownicy poruszają się po Twojej witrynie.",
+ "label.journeys": "Drogi",
"label.language": "Język",
"label.languages": "Języki",
"label.laptop": "Laptop",
+ "label.last-click": "Ostatnie kliknięcie",
"label.last-days": "Ostatnie {x} dni",
"label.last-hours": "Ostatnie {x} godzin",
- "label.last-months": "Osatnie {x} miesięcy",
- "label.last-seen": "Last seen",
+ "label.last-months": "Ostatnie {x} miesięcy",
+ "label.last-seen": "Ostatnio widziany",
"label.leave": "Opuść",
"label.leave-team": "Opuść zespół",
"label.less-than": "Mniejsze niż",
"label.less-than-equals": "Mniejsze niż lub równe",
+ "label.links": "Linki",
"label.login": "Zaloguj się",
"label.logout": "Wyloguj",
"label.manage": "Manage",
"label.manager": "Manager",
"label.max": "Maks",
+ "label.maximize": "Rozwiń",
+ "label.medium": "Medium",
"label.member": "Członek",
"label.members": "Członkowie",
"label.min": "Min",
"label.mobile": "Smartfon",
+ "label.model": "Model",
"label.more": "Więcej",
"label.my-account": "Moje konto",
"label.my-websites": "Moje witryny",
@@ -126,33 +161,48 @@
"label.none": "Brak",
"label.number-of-records": "{x} {x, plural, one {rekord} other {rekordy}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Wyszukiwanie organiczne",
+ "label.organic-shopping": "Zakupy organiczne",
+ "label.organic-social": "Organiczne social media",
+ "label.organic-video": "Organiczne wideo",
"label.os": "OS",
+ "label.other": "Inne",
"label.overview": "Przegląd",
"label.owner": "Właściciel",
+ "label.page": "Strona",
"label.page-of": "Strona {current} z {total}",
"label.page-views": "Wyświetlenia strony",
"label.pageTitle": "Tytuł strony",
"label.pages": "Strony",
+ "label.paid-ads": "Reklamy płatne",
+ "label.paid-search": "Płatne wyszukiwanie",
+ "label.paid-shopping": "Płatne zakupy",
+ "label.paid-social": "Płatne social media",
+ "label.paid-video": "Płatne wideo",
"label.password": "Hasło",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Ścieżka",
+ "label.paths": "Ścieżki",
+ "label.pixels": "Piksele",
"label.powered-by": "Obsługiwane przez {name}",
"label.previous": "Poprzedni",
"label.previous-period": "Poprzedni okres",
"label.previous-year": "Poprzedni rok",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Właściwości",
+ "label.property": "Właściwość",
"label.queries": "Zapytania",
"label.query": "Zapytanie",
"label.query-parameters": "Parametry zapytania",
"label.realtime": "Czas rzeczywisty",
+ "label.referral": "Polecenie",
"label.referrer": "Źródło odsyłające",
"label.referrers": "Źródła odsyłające",
"label.refresh": "Odśwież",
"label.regenerate": "Wygeneruj ponownie",
"label.region": "Region",
"label.regions": "Regiony",
+ "label.remaining": "Pozostało",
"label.remove": "Usuń",
"label.remove-member": "Usuń członka",
"label.reports": "Raporty",
@@ -161,9 +211,8 @@
"label.reset-website": "Zresetuj statystyki",
"label.retention": "Retencja",
"label.retention-description": "Mierz przyciągającą siłę swojej strony internetowej, śledząc, jak często użytkownicy powracają.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Przychód",
+ "label.revenue-description": "Sprawdź swoje przychody w czasie.",
"label.role": "Rola",
"label.run-query": "Uruchom zapytanie",
"label.save": "Zapisz",
@@ -171,26 +220,35 @@
"label.search": "Szukaj",
"label.select": "Wybierz",
"label.select-date": "Wybierz datę",
+ "label.select-filter": "Wybierz filtr",
"label.select-role": "Wybierz rolę",
"label.select-website": "Wybierz witrynę",
- "label.session": "Session",
+ "label.session": "Sesja",
+ "label.session-data": "Dane sesji",
"label.sessions": "Sesje",
"label.settings": "Ustawienia",
+ "label.share": "Udostępnij",
"label.share-url": "Udostępnij adres URL",
"label.single-day": "W tym dniu",
+ "label.sms": "SMS",
+ "label.sources": "Źródła",
"label.start-step": "Krok startowy",
"label.steps": "Kroki",
"label.sum": "Suma",
"label.tablet": "Tablet",
+ "label.tag": "Tag",
+ "label.tags": "Tagi",
"label.team": "Zespół",
"label.team-id": "ID zespołu",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Menedżer zespołu",
"label.team-member": "Członek zespołu",
"label.team-name": "Nazwa zespołu",
"label.team-owner": "Właściciel zespołu",
+ "label.team-settings": "Ustawienia zespołu",
"label.team-view-only": "Tylko do odczytu dla zespołu",
"label.team-websites": "Witryny zespołu",
"label.teams": "Zespoły",
+ "label.terms": "Warunki",
"label.theme": "Motyw",
"label.this-month": "W tym miesiącu",
"label.this-week": "W tym tygodniu",
@@ -213,10 +271,7 @@
"label.unknown": "Nieznany",
"label.untitled": "Bez tytułu",
"label.update": "Aktualizuj",
- "label.url": "Link",
- "label.urls": "Linki",
"label.user": "Użytkownik",
- "label.user-property": "User Property",
"label.username": "Nazwa użytkownika",
"label.users": "Użytkownicy",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Wczoraj",
"message.action-confirmation": "Wpisz {confirmation}, aby potwierdzić.",
"message.active-users": "{x} aktualnie {x, plural, one {odwiedzający} other {odwiedzających}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Zebrane dane",
"message.confirm-delete": "Czy na pewno chcesz usunąć {target}?",
"message.confirm-leave": "Czy na pewno chcesz opuścić {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Wszystkie powiązane dane również zostaną usunięte.",
"message.error": "Coś poszło nie tak.",
"message.event-log": "{event} na {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Przejdź do ustawień",
"message.incorrect-username-password": "Nieprawidłowa nazwa użytkownika lub hasło.",
"message.invalid-domain": "Nieprawidłowa witryna",
@@ -259,10 +316,13 @@
"message.no-teams": "Nie stworzyłeś żadnych zespołów.",
"message.no-users": "Nie ma żadnych użytkowników.",
"message.no-websites-configured": "Nie masz skonfigurowanych żadnych witryn internetowych.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Strona nie znaleziona.",
"message.reset-website": "Aby zresetować tę witrynę, wpisz {confirmation} w polu poniżej, aby potwierdzić.",
"message.reset-website-warning": "Wszystkie statystyki tej witryny zostaną usunięte, ale kod śledzenia pozostanie nienaruszony.",
"message.saved": "Zapisano pomyślnie.",
+ "message.sever-error": "Server error",
"message.share-url": "To jest publicznie udostępniany adres URL dla {target}.",
"message.team-already-member": "Jesteś już członkiem zespołu.",
"message.team-not-found": "Nie znaleziono zespołu.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Wybierz zespół, do którego chcesz przenieść tę witrynę.",
"message.transfer-website": "Przenieś własność witryny na swoje konto lub do innego zespołu.",
"message.triggered-event": "Zdarzenie wyzwalające",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Użytkownik usunięty.",
"message.viewed-page": "Obejrzana strona",
- "message.visitor-log": "Odwiedzający z {country} używa {browser} na {os} {device}",
- "message.visitors-dropped-off": "Odpływ użytkowników"
+ "message.visitor-log": "Odwiedzający z {country} używa {browser} na {os} {device}"
}
diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json
index 6e5b095e..c34c9ab0 100644
--- a/src/lang/pt-BR.json
+++ b/src/lang/pt-BR.json
@@ -3,33 +3,48 @@
"label.actions": "Ações do usuário",
"label.activity": "Registro de atividades",
"label.add": "Adicionar",
+ "label.add-board": "Adicionar quadro",
"label.add-description": "Adicionar descrição",
"label.add-member": "Adicionar membro",
"label.add-step": "Adicionar etapa",
"label.add-website": "Adicionar site",
"label.admin": "Administrador",
+ "label.affiliate": "Afiliado",
"label.after": "Depois",
"label.all": "Todos",
"label.all-time": "Todos os períodos",
"label.analytics": "Análise",
+ "label.apply": "Aplicar",
+ "label.attribution": "Atribuição",
+ "label.attribution-description": "Veja como os usuários interagem com seu marketing e o que impulsiona conversões.",
"label.average": "Média",
"label.back": "Voltar",
"label.before": "Antes",
+ "label.behavior": "Comportamento",
+ "label.boards": "Quadros",
"label.bounce-rate": "Taxa de rejeição",
"label.breakdown": "Detalhamento",
"label.browser": "Navegador",
"label.browsers": "Navegadores",
+ "label.campaigns": "Campanhas",
"label.cancel": "Cancelar",
"label.change-password": "Alterar senha",
+ "label.channels": "Canais",
"label.cities": "Cidades",
"label.city": "Cidade",
"label.clear-all": "Limpar tudo",
- "label.compare": "Compare",
+ "label.cohort": "Cohorte",
+ "label.compare": "Comparar",
+ "label.compare-dates": "Comparar datas",
"label.confirm": "Confirmar",
"label.confirm-password": "Confirmar senha",
"label.contains": "Contém",
+ "label.content": "Conteúdo",
"label.continue": "Continuar",
- "label.count": "Count",
+ "label.conversion": "Conversão",
+ "label.conversion-rate": "Taxa de conversão",
+ "label.conversion-step": "Etapa de conversão",
+ "label.count": "Contagem",
"label.countries": "Países",
"label.country": "País",
"label.create": "Criar",
@@ -38,7 +53,8 @@
"label.create-user": "Criar usuário",
"label.created": "Criado",
"label.created-by": "Criado por",
- "label.current": "Current",
+ "label.currency": "Moeda",
+ "label.current": "Atual",
"label.current-password": "Senha atual",
"label.custom-range": "Período personalizado",
"label.dashboard": "Painel",
@@ -53,23 +69,30 @@
"label.delete-user": "Excluir usuário",
"label.delete-website": "Excluir site",
"label.description": "Descrição",
- "label.desktop": "Desktop",
+ "label.desktop": "Computador",
"label.details": "Detalhes",
"label.device": "Dispositivo",
"label.devices": "Dispositivos",
+ "label.direct": "Direto",
"label.dismiss": "Fechar",
+ "label.distinct-id": "ID distinto",
"label.does-not-contain": "Não contém",
+ "label.does-not-include": "Não inclui",
+ "label.doest-not-exist": "Não existe",
"label.domain": "Domínio",
"label.dropoff": "Abandono",
"label.edit": "Editar",
"label.edit-dashboard": "Editar painel",
"label.edit-member": "Editar membro",
+ "label.email": "Email",
"label.enable-share-url": "Ativar link para compartilhar",
"label.end-step": "End Step",
"label.entry": "Entry URL",
"label.event": "Evento",
"label.event-data": "Dados do evento",
+ "label.event-name": "Nome do evento",
"label.events": "Tipos de eventos",
+ "label.exists": "Existe",
"label.exit": "Exit URL",
"label.false": "Não",
"label.field": "Campo",
@@ -78,46 +101,58 @@
"label.filter-combined": "Combinado",
"label.filter-raw": "Bruto",
"label.filters": "Filtros",
+ "label.first-click": "Primeiro clique",
"label.first-seen": "First seen",
"label.funnel": "Funil",
"label.funnel-description": "Entenda a taxa de conversão e abandono dos seus usuários.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
+ "label.funnels": "Funis",
+ "label.goal": "Meta",
+ "label.goals": "Metas",
+ "label.goals-description": "Acompanhe suas metas para visualizações de página e eventos.",
"label.greater-than": "Maior que",
"label.greater-than-equals": "Maior ou igual a",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Agrupado",
+ "label.hostname": "Nome do host",
+ "label.includes": "Inclui",
+ "label.insight": "Insight",
"label.insights": "Insights",
"label.insights-description": "Explore seus dados em mais detalhes usando filtros",
"label.is": "É igual a",
+ "label.is-false": "É falso",
"label.is-not": "Não é igual a",
"label.is-not-set": "Não definido",
"label.is-set": "Definido",
+ "label.is-true": "É verdadeiro",
"label.join": "Participar",
"label.join-team": "Participar da equipe",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.journey": "Jornada",
+ "label.journey-description": "Entenda como os usuários navegam pelo seu site.",
+ "label.journeys": "Jornadas",
"label.language": "Idioma",
"label.languages": "Idiomas",
"label.laptop": "Notebook",
+ "label.last-click": "Último clique",
"label.last-days": "Últimos {x} dias",
"label.last-hours": "Últimas {x} horas",
"label.last-months": "Últimos {x} meses",
- "label.last-seen": "Last seen",
+ "label.last-seen": "Última visualização",
"label.leave": "Sair",
"label.leave-team": "Sair da equipe",
"label.less-than": "Menor que",
"label.less-than-equals": "Menor ou igual a",
+ "label.links": "Links",
"label.login": "Entrar",
"label.logout": "Sair",
"label.manage": "Gerenciar",
"label.manager": "Manager",
"label.max": "Máximo",
+ "label.maximize": "Expandir",
+ "label.medium": "Médio",
"label.member": "Membro",
"label.members": "Membros",
"label.min": "Mínimo",
"label.mobile": "Celular",
+ "label.model": "Modelo",
"label.more": "Mais",
"label.my-account": "Minha conta",
"label.my-websites": "Meus sites",
@@ -126,33 +161,48 @@
"label.none": "Nenhum",
"label.number-of-records": "{x} {x, plural, one {registro} other {registros}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Busca orgânica",
+ "label.organic-shopping": "Compras orgânicas",
+ "label.organic-social": "Social orgânico",
+ "label.organic-video": "Vídeo orgânico",
"label.os": "Sistema operacional",
+ "label.other": "Outro",
"label.overview": "Visão geral",
"label.owner": "Proprietário",
+ "label.page": "Página",
"label.page-of": "Página {current} de {total}",
"label.page-views": "Visualizações de página",
"label.pageTitle": "Título",
"label.pages": "Páginas",
+ "label.paid-ads": "Anúncios pagos",
+ "label.paid-search": "Busca paga",
+ "label.paid-shopping": "Compras pagas",
+ "label.paid-social": "Social pago",
+ "label.paid-video": "Vídeo pago",
"label.password": "Senha",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Caminho",
+ "label.paths": "Caminhos",
+ "label.pixels": "Pixels",
"label.powered-by": "Desenvolvido por {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "Anterior",
+ "label.previous-period": "Período anterior",
+ "label.previous-year": "Ano anterior",
"label.profile": "Perfil",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Propriedades",
+ "label.property": "Propriedade",
"label.queries": "Consultas",
"label.query": "Consulta",
"label.query-parameters": "Parâmetros da consulta",
"label.realtime": "Tempo real",
+ "label.referral": "Referência",
"label.referrer": "Referência",
"label.referrers": "Referências",
"label.refresh": "Atualizar",
"label.regenerate": "Gerar novamente",
"label.region": "Estado",
"label.regions": "Estados",
+ "label.remaining": "Restante",
"label.remove": "Remover",
"label.remove-member": "Remover membro",
"label.reports": "Relatórios",
@@ -161,9 +211,8 @@
"label.reset-website": "Redefinir dados",
"label.retention": "Retenção",
"label.retention-description": "Avalie a fidelidade dos seus usuários medindo a frequência com que eles retornam.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Receita",
+ "label.revenue-description": "Veja sua receita ao longo do tempo.",
"label.role": "Função",
"label.run-query": "Executar consulta",
"label.save": "Salvar",
@@ -171,26 +220,35 @@
"label.search": "Pesquisar",
"label.select": "Selecionar",
"label.select-date": "Selecionar data",
+ "label.select-filter": "Selecionar filtro",
"label.select-role": "Selecionar função",
"label.select-website": "Selecionar site",
- "label.session": "Session",
+ "label.session": "Sessão",
+ "label.session-data": "Dados da sessão",
"label.sessions": "Sessões",
"label.settings": "Configurações",
+ "label.share": "Compartilhar",
"label.share-url": "Link para compartilhar",
"label.single-day": "Apenas um dia",
+ "label.sms": "SMS",
+ "label.sources": "Fontes",
"label.start-step": "Start Step",
"label.steps": "Etapas",
"label.sum": "Soma",
"label.tablet": "Tablet",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "Equipe",
"label.team-id": "ID da equipe",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Gerente da equipe",
"label.team-member": "Membro da equipe",
"label.team-name": "Nome da equipe",
"label.team-owner": "Proprietário da equipe",
+ "label.team-settings": "Configurações da equipe",
"label.team-view-only": "Apenas visualização da equipe",
"label.team-websites": "Sites da equipe",
"label.teams": "Equipes",
+ "label.terms": "Termos",
"label.theme": "Tema",
"label.this-month": "Este mês",
"label.this-week": "Esta semana",
@@ -213,10 +271,7 @@
"label.unknown": "Desconhecido",
"label.untitled": "Sem título",
"label.update": "Atualizar",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Usuário",
- "label.user-property": "User Property",
"label.username": "Nome de usuário",
"label.users": "Usuários",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Ontem",
"message.action-confirmation": "Digite {confirmation} na caixa abaixo para confirmar.",
"message.active-users": " Atualmente {x} usuários ativos",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Tem certeza de que deseja excluir {target}?",
"message.confirm-leave": "Tem certeza de que deseja sair de {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Todos os dados relacionados serão excluídos.",
"message.error": "Ocorreu um erro.",
"message.event-log": "{event} em {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Ir para as configurações",
"message.incorrect-username-password": "Nome de usuário ou senha incorretos.",
"message.invalid-domain": "Domínio inválido",
@@ -259,10 +316,13 @@
"message.no-teams": "Você ainda não criou nenhuma equipe.",
"message.no-users": "Não há usuários.",
"message.no-websites-configured": "Você ainda não configurou nenhum site.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Página não encontrada.",
"message.reset-website": "Se você tiver certeza de que deseja redefinir este site, digite {confirmation} na caixa de entrada abaixo para confirmar.",
"message.reset-website-warning": "Todos os dados estatísticos deste site serão excluídos, mas seu código de rastreamento permanecerá o mesmo.",
"message.saved": "Salvo com sucesso.",
+ "message.sever-error": "Server error",
"message.share-url": "Este é o link para compartilhar {target}.",
"message.team-already-member": "Você já é membro desta equipe.",
"message.team-not-found": "Equipe não encontrada.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Selecione para qual equipe deseja transferir este site.",
"message.transfer-website": "Transfira a propriedade do site para sua conta ou para outra equipe.",
"message.triggered-event": "Evento disparado",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Usuário excluído.",
"message.viewed-page": "Página visualizada",
- "message.visitor-log": "Visitante de {country} usando o navegador {browser} em um {device} com sistema operacional {os}.",
- "message.visitors-dropped-off": "Visitantes abandonados"
+ "message.visitor-log": "Visitante de {country} usando o navegador {browser} em um {device} com sistema operacional {os}."
}
diff --git a/src/lang/pt-PT.json b/src/lang/pt-PT.json
index ddc6b726..86734cb5 100644
--- a/src/lang/pt-PT.json
+++ b/src/lang/pt-PT.json
@@ -1,158 +1,208 @@
{
- "label.access-code": "Access code",
+ "label.access-code": "Código de acesso",
"label.actions": "Ações",
- "label.activity": "Activity log",
- "label.add": "Add",
- "label.add-description": "Add description",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.activity": "Registo de atividade",
+ "label.add": "Adicionar",
+ "label.add-board": "Adicionar quadro",
+ "label.add-description": "Adicionar descrição",
+ "label.add-member": "Adicionar membro",
+ "label.add-step": "Adicionar passo",
"label.add-website": "Adicionar website",
"label.admin": "Administrador",
- "label.after": "After",
+ "label.affiliate": "Afiliado",
+ "label.after": "Depois",
"label.all": "Todos",
"label.all-time": "Todo o tempo",
- "label.analytics": "Analytics",
- "label.average": "Average",
+ "label.analytics": "Análise",
+ "label.apply": "Aplicar",
+ "label.attribution": "Atribuição",
+ "label.attribution-description": "Veja como os utilizadores interagem com o seu marketing e o que impulsiona conversões.",
+ "label.average": "Média",
"label.back": "Voltar",
- "label.before": "Before",
+ "label.before": "Antes",
+ "label.behavior": "Comportamento",
+ "label.boards": "Quadros",
"label.bounce-rate": "Taxa de rejeição",
- "label.breakdown": "Breakdown",
- "label.browser": "Browser",
+ "label.breakdown": "Detalhamento",
+ "label.browser": "Navegador",
"label.browsers": "Navegadores",
+ "label.campaigns": "Campanhas",
"label.cancel": "Cancelar",
"label.change-password": "Alterar senha",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
+ "label.channels": "Canais",
+ "label.cities": "Cidades",
+ "label.city": "Cidade",
+ "label.clear-all": "Limpar tudo",
+ "label.cohort": "Cohorte",
+ "label.compare": "Comparar",
+ "label.compare-dates": "Comparar datas",
+ "label.confirm": "Confirmar",
"label.confirm-password": "Confirmar senha",
"label.contains": "Contains",
+ "label.content": "Conteúdo",
"label.continue": "Continue",
- "label.count": "Count",
+ "label.conversion": "Conversão",
+ "label.conversion-rate": "Taxa de conversão",
+ "label.conversion-step": "Passo de conversão",
+ "label.count": "Contagem",
"label.countries": "Países",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.country": "País",
+ "label.create": "Criar",
+ "label.create-report": "Criar relatório",
+ "label.create-team": "Criar equipa",
+ "label.create-user": "Criar utilizador",
+ "label.created": "Criado",
+ "label.created-by": "Criado por",
+ "label.currency": "Moeda",
+ "label.current": "Atual",
"label.current-password": "Senha atual",
"label.custom-range": "Intervalo personalizado",
"label.dashboard": "Painel",
"label.data": "Data",
"label.date": "Date",
"label.date-range": "Intervalo de datas",
- "label.day": "Day",
+ "label.day": "Dia",
"label.default-date-range": "Intervalo de datas predefinido",
"label.delete": "Eliminar",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "Eliminar relatório",
+ "label.delete-team": "Eliminar equipa",
+ "label.delete-user": "Eliminar utilizador",
"label.delete-website": "Eliminar website",
- "label.description": "Description",
- "label.desktop": "Desktop",
- "label.details": "Details",
- "label.device": "Device",
+ "label.description": "Descrição",
+ "label.desktop": "Computador",
+ "label.details": "Detalhes",
+ "label.device": "Dispositivo",
"label.devices": "Dispositivos",
+ "label.direct": "Direto",
"label.dismiss": "Ignorar",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "ID distinto",
+ "label.does-not-contain": "Não contém",
+ "label.does-not-include": "Não inclui",
+ "label.doest-not-exist": "Não existe",
"label.domain": "Domínio",
"label.dropoff": "Dropoff",
"label.edit": "Editar",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
+ "label.edit-dashboard": "Editar painel",
+ "label.edit-member": "Editar membro",
+ "label.email": "Email",
"label.enable-share-url": "Ativar link de partilha",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event data",
+ "label.end-step": "Passo final",
+ "label.entry": "URL de entrada",
+ "label.event": "Evento",
+ "label.event-data": "Dados do evento",
+ "label.event-name": "Nome do evento",
"label.events": "Eventos",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
- "label.filter": "Filter",
+ "label.exists": "Existe",
+ "label.exit": "URL de saída",
+ "label.false": "Falso",
+ "label.field": "Campo",
+ "label.fields": "Campos",
+ "label.filter": "Filtro",
"label.filter-combined": "Combinado",
"label.filter-raw": "Dados brutos",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.filters": "Filtros",
+ "label.first-click": "Primeiro clique",
+ "label.first-seen": "Primeira visualização",
+ "label.funnel": "Funil",
+ "label.funnel-description": "Compreenda a taxa de conversão e abandono dos utilizadores.",
+ "label.funnels": "Funis",
+ "label.goal": "Objetivo",
+ "label.goals": "Objetivos",
+ "label.goals-description": "Acompanhe os seus objetivos para visualizações de página e eventos.",
+ "label.greater-than": "Maior que",
+ "label.greater-than-equals": "Maior ou igual a",
+ "label.grouped": "Agrupado",
+ "label.hostname": "Nome do host",
+ "label.includes": "Inclui",
+ "label.insight": "Insight",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.is": "É",
+ "label.is-false": "É falso",
+ "label.is-not": "Não é",
+ "label.is-not-set": "Não definido",
+ "label.is-set": "Definido",
+ "label.is-true": "É verdadeiro",
+ "label.join": "Juntar-se",
+ "label.join-team": "Juntar-se à equipa",
+ "label.journey": "Jornada",
+ "label.journey-description": "Compreenda como os utilizadores navegam no seu website.",
+ "label.journeys": "Jornadas",
"label.language": "Língua",
"label.languages": "Línguas",
"label.laptop": "Portátil",
+ "label.last-click": "Último clique",
"label.last-days": "Últimos {x} dias",
"label.last-hours": "Últimas {x} horas",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "Últimos {x} meses",
+ "label.last-seen": "Última visualização",
+ "label.leave": "Sair",
+ "label.leave-team": "Sair da equipa",
+ "label.less-than": "Menor que",
+ "label.less-than-equals": "Menor ou igual a",
+ "label.links": "Ligações",
"label.login": "Iniciar sessão",
"label.logout": "Sair",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
+ "label.manage": "Gerir",
+ "label.manager": "Gestor",
+ "label.max": "Máximo",
+ "label.maximize": "Expandir",
+ "label.medium": "Médio",
+ "label.member": "Membro",
+ "label.members": "Membros",
+ "label.min": "Mínimo",
"label.mobile": "Telemóvel",
+ "label.model": "Modelo",
"label.more": "Mais",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "A minha conta",
+ "label.my-websites": "Os meus websites",
"label.name": "Nome",
"label.new-password": "Nova senha",
- "label.none": "None",
+ "label.none": "Nenhum",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Pesquisa orgânica",
+ "label.organic-shopping": "Compras orgânicas",
+ "label.organic-social": "Social orgânico",
+ "label.organic-video": "Vídeo orgânico",
"label.os": "OS",
+ "label.other": "Outro",
"label.overview": "Overview",
"label.owner": "Proprietário",
+ "label.page": "Página",
"label.page-of": "Page {current} of {total}",
"label.page-views": "Visualizações da página",
"label.pageTitle": "Page title",
"label.pages": "Páginas",
+ "label.paid-ads": "Anúncios pagos",
+ "label.paid-search": "Pesquisa paga",
+ "label.paid-shopping": "Compras pagas",
+ "label.paid-social": "Social pago",
+ "label.paid-video": "Vídeo pago",
"label.password": "Senha",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Caminho",
+ "label.paths": "Caminhos",
+ "label.pixels": "Píxeis",
"label.powered-by": "Distribuído por {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "Anterior",
+ "label.previous-period": "Período anterior",
+ "label.previous-year": "Ano anterior",
"label.profile": "Perfil",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Propriedades",
+ "label.property": "Propriedade",
"label.queries": "Queries",
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.realtime": "Tempo real",
+ "label.referral": "Referência",
"label.referrer": "Referrer",
"label.referrers": "Referenciadores",
"label.refresh": "Atualizar",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Restante",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -161,9 +211,8 @@
"label.reset-website": "Repor estatísticas",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Receita",
+ "label.revenue-description": "Veja a sua receita ao longo do tempo.",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "Guardar",
@@ -171,26 +220,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Selecionar filtro",
"label.select-role": "Select role",
"label.select-website": "Select website",
- "label.session": "Session",
+ "label.session": "Sessão",
+ "label.session-data": "Dados da sessão",
"label.sessions": "Sessions",
"label.settings": "Definições",
+ "label.share": "Partilhar",
"label.share-url": "Partilhar link",
"label.single-day": "Dia único",
+ "label.sms": "SMS",
+ "label.sources": "Fontes",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "Tablet",
+ "label.tag": "Etiqueta",
+ "label.tags": "Etiquetas",
"label.team": "Team",
"label.team-id": "Team ID",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Gestor de equipa",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Definições da equipa",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Termos",
"label.theme": "Tema",
"label.this-month": "Este mês",
"label.this-week": "Esta semana",
@@ -213,10 +271,7 @@
"label.unknown": "Desconhecido",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Nome de utilizador",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Tem a certeza que pretende eliminar {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Todos os dados associados também serão eliminados.",
"message.error": "Ocorreu um erro.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Ir para as definições",
"message.incorrect-username-password": "Nome de utilizador/senha incorretos.",
"message.invalid-domain": "Domínio inválido",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "Não tens nenhum website configurado.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Página não encontrada.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "Todas as estatísticas deste site serão eliminadas, mas o seu código de rastreamento permanecerá intacto.",
"message.saved": "Guardado com sucesso.",
+ "message.sever-error": "Server error",
"message.share-url": "Este é o link de partilha público para {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Visitante de {country} a usar {browser} no {device} {os}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Visitante de {country} a usar {browser} no {device} {os}"
}
diff --git a/src/lang/ro-RO.json b/src/lang/ro-RO.json
index feb3dd5b..78633304 100644
--- a/src/lang/ro-RO.json
+++ b/src/lang/ro-RO.json
@@ -3,32 +3,47 @@
"label.actions": "Acțiuni",
"label.activity": "Jurnal de activități",
"label.add": "Adaugă",
+ "label.add-board": "Adaugă panou",
"label.add-description": "Adaugă descriere",
"label.add-member": "Adaugă membru",
"label.add-step": "Adaugă pas",
"label.add-website": "Adaugă site web",
"label.admin": "Administrator",
+ "label.affiliate": "Afiliat",
"label.after": "După",
"label.all": "Toate",
"label.all-time": "Pentru tot timpul",
- "label.analytics": "Analytics",
+ "label.analytics": "Analiză",
+ "label.apply": "Aplică",
+ "label.attribution": "Atribuire",
+ "label.attribution-description": "Vezi cum utilizatorii interacționează cu marketingul tău și ce determină conversiile.",
"label.average": "Mediu",
"label.back": "Înapoi",
"label.before": "Înainte",
+ "label.behavior": "Comportament",
+ "label.boards": "Panouri",
"label.bounce-rate": "Rata de respingere",
"label.breakdown": "Detaliat",
"label.browser": "Browser",
"label.browsers": "Browsere",
+ "label.campaigns": "Campanii",
"label.cancel": "Anulează",
"label.change-password": "Schimbare parolă",
+ "label.channels": "Canale",
"label.cities": "Orașe",
"label.city": "Oraș",
"label.clear-all": "Șterge tot",
+ "label.cohort": "Cohortă",
"label.compare": "Compară",
+ "label.compare-dates": "Compară datele",
"label.confirm": "Confirm",
"label.confirm-password": "Confirmare parolă",
"label.contains": "Conține",
+ "label.content": "Conținut",
"label.continue": "Continuă",
+ "label.conversion": "Conversie",
+ "label.conversion-rate": "Rată de conversie",
+ "label.conversion-step": "Pas de conversie",
"label.count": "Număr",
"label.countries": "Țări",
"label.country": "Țară",
@@ -38,6 +53,7 @@
"label.create-user": "Crează utilizator",
"label.created": "Creat",
"label.created-by": "Creat de",
+ "label.currency": "Monedă",
"label.current": "Curent",
"label.current-password": "Parola curentă",
"label.custom-range": "Interval personalizat",
@@ -57,19 +73,26 @@
"label.details": "Detalii",
"label.device": "Dispozitiv",
"label.devices": "Dispozitive",
+ "label.direct": "Direct",
"label.dismiss": "Renunță",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "Nu conține",
+ "label.does-not-include": "Nu include",
+ "label.doest-not-exist": "Nu există",
"label.domain": "Domeniu",
"label.dropoff": "Rată de abandon",
"label.edit": "Editare",
"label.edit-dashboard": "Editare tablou de bord",
"label.edit-member": "Editare membru",
+ "label.email": "Email",
"label.enable-share-url": "Activare adresă URL de distribuire",
"label.end-step": "Pas final",
"label.entry": "URL de intrare",
"label.event": "Eveniment",
"label.event-data": "Date despre eveniment",
+ "label.event-name": "Nume eveniment",
"label.events": "Evenimente",
+ "label.exists": "Există",
"label.exit": "URL de ieșire",
"label.false": "Fals",
"label.field": "Câmp",
@@ -78,29 +101,37 @@
"label.filter-combined": "Combinat",
"label.filter-raw": "Brut",
"label.filters": "Filtre",
+ "label.first-click": "Primul click",
"label.first-seen": "Văzut pentru prima dată",
"label.funnel": "Parcursul utilizatorului",
"label.funnel-description": "Înțelege rata de conversie și rata de abandon a utilizatorilor.",
+ "label.funnels": "Parcursuri",
"label.goal": "Obiectiv",
"label.goals": "Obiective",
"label.goals-description": "Urmărește obiectivele de vizualizări și evenimente.",
"label.greater-than": "Mai mare decât",
"label.greater-than-equals": "Mai mare sau egal cu",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grupat",
+ "label.hostname": "Nume gazdă",
+ "label.includes": "Include",
+ "label.insight": "Perspectivă",
"label.insights": "Perspective",
"label.insights-description": "Aprofundează datele utilizând segmente și filtre.",
"label.is": "Este",
+ "label.is-false": "Este fals",
"label.is-not": "Nu este",
"label.is-not-set": "Nu este setat",
"label.is-set": "Este setat",
+ "label.is-true": "Este adevărat",
"label.join": "Alătură-te",
"label.join-team": "Alătură-te echipei",
"label.journey": "Traseu",
"label.journey-description": "Înțelege cum navighează vizitatorii prin website.",
+ "label.journeys": "Trasee",
"label.language": "Limbă",
"label.languages": "Limbi",
"label.laptop": "Laptop",
+ "label.last-click": "Ultimul click",
"label.last-days": "Ultimele {x} zile",
"label.last-hours": "Ultimele {x} ore",
"label.last-months": "Ultimele {x} luni",
@@ -109,15 +140,19 @@
"label.leave-team": "Părăsește echipa",
"label.less-than": "Mai puțin decât",
"label.less-than-equals": "Mai puțin sau egal cu",
+ "label.links": "Linkuri",
"label.login": "Autentificare",
"label.logout": "Ieșire din cont",
"label.manage": "Administrează",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Extinde",
+ "label.medium": "Mediu",
"label.member": "Membru",
"label.members": "Membri",
"label.min": "Min",
"label.mobile": "Mobil",
+ "label.model": "Model",
"label.more": "Mai mult",
"label.my-account": "Contul meu",
"label.my-websites": "Website-ul meu",
@@ -126,16 +161,29 @@
"label.none": "Niciunul",
"label.number-of-records": "{x} {x, plural, one {înregistrare} other {înregistrări}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Căutare organică",
+ "label.organic-shopping": "Cumpărături organice",
+ "label.organic-social": "Social organic",
+ "label.organic-video": "Video organic",
"label.os": "OS",
+ "label.other": "Altul",
"label.overview": "Vedere de ansamblu",
"label.owner": "Titular",
+ "label.page": "Pagină",
"label.page-of": "Pagina {current} din {total}",
"label.page-views": "Vizualizări de pagină",
"label.pageTitle": "Titlul paginii",
"label.pages": "Pagini",
+ "label.paid-ads": "Reclame plătite",
+ "label.paid-search": "Căutare plătită",
+ "label.paid-shopping": "Cumpărături plătite",
+ "label.paid-social": "Social plătit",
+ "label.paid-video": "Video plătit",
"label.password": "Parolă",
"label.path": "Rută",
"label.paths": "Rute",
+ "label.pixels": "Pixeli",
"label.powered-by": "Cu sprijinul {name}",
"label.previous": "Anterior",
"label.previous-period": "Perioda anterioară",
@@ -147,12 +195,14 @@
"label.query": "Interogare",
"label.query-parameters": "Parametri de interogare",
"label.realtime": "Timp real",
+ "label.referral": "Referral",
"label.referrer": "Proveniență",
"label.referrers": "Site-uri de proveniență",
"label.refresh": "Reîmprospătare",
"label.regenerate": "Regenerează",
"label.region": "Regiune",
"label.regions": "Regiuni",
+ "label.remaining": "Rămas",
"label.remove": "Îndepărtează",
"label.remove-member": "Îndepărtează membru",
"label.reports": "Rapoarte",
@@ -163,7 +213,6 @@
"label.retention-description": "Măsoară atractivitatea site-ului tău prin urmărirea frecvenței cu care utilizatorii se întorc.",
"label.revenue": "Venit",
"label.revenue-description": "Urmărește venitul în timp.",
- "label.revenue-property": "Revenue Property",
"label.role": "Rol",
"label.run-query": "Execută interogarea",
"label.save": "Salvează",
@@ -171,26 +220,35 @@
"label.search": "Căutare",
"label.select": "Selectează",
"label.select-date": "Selectează data",
+ "label.select-filter": "Selectează filtru",
"label.select-role": "Selectează rolul",
"label.select-website": "Selectează website",
"label.session": "Sesiune",
+ "label.session-data": "Date sesiune",
"label.sessions": "Sesiuni",
"label.settings": "Setări",
+ "label.share": "Partajează",
"label.share-url": "Partajare URL",
"label.single-day": "O singură zi",
+ "label.sms": "SMS",
+ "label.sources": "Surse",
"label.start-step": "Pas de început",
"label.steps": "Pași",
"label.sum": "Sumă",
"label.tablet": "Tabletă",
+ "label.tag": "Etichetă",
+ "label.tags": "Etichete",
"label.team": "Echipă",
"label.team-id": "ID Echipă",
"label.team-manager": "Manager echipă",
"label.team-member": "Membru echipă",
"label.team-name": "Nume echipă",
"label.team-owner": "Titular echipă",
+ "label.team-settings": "Setări echipă",
"label.team-view-only": "Doar vizualizare echipă",
"label.team-websites": "Website-uri echipă",
"label.teams": "Echipă",
+ "label.terms": "Termeni",
"label.theme": "Temă",
"label.this-month": "Această lună",
"label.this-week": "Această săptămână",
@@ -213,10 +271,7 @@
"label.unknown": "Necunoscut",
"label.untitled": "Fără titlu",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Utilizator",
- "label.user-property": "Proprietatea utilizatorului",
"label.username": "Nume utilizator",
"label.users": "Utilizatori",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Ieri",
"message.action-confirmation": "Scrie {confirmation} în câmpul de mai jos pentru a confirma.",
"message.active-users": "{x} {x, plural, one {vizitator activ} other {vizitatori activi}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Date colectate",
"message.confirm-delete": "Ești sigur că vrei să ștergi {target}?",
"message.confirm-leave": "Ești sigur că vrei să părăsești {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Toate datele asociate vor fi șterse, de asemenea.",
"message.error": "Ceva n-a mers bine.",
"message.event-log": "{event} la {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Mergi la Setări",
"message.incorrect-username-password": "Nume utilizator / parolă incorecte.",
"message.invalid-domain": "Domeniul nu este valid",
@@ -259,10 +316,13 @@
"message.no-teams": "Nu ai creat nicio echipă.",
"message.no-users": "Nu există utilizatori.",
"message.no-websites-configured": "Nu ai niciun site web configurat.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Pagina nu a fost găsită.",
"message.reset-website": "Pentru a reseta acest website, scrie {confirmation} În câmpul de mai jos pentru a confirma.",
"message.reset-website-warning": "Toate statisticile pentru acest site web vor fi șterse, dar codul de urmărire va rămâne intact.",
"message.saved": "Salvat cu succes.",
+ "message.sever-error": "Server error",
"message.share-url": "Aceasta este adresa URL de partajare pentru {target}.",
"message.team-already-member": "Deja ești membru al acestei echipe.",
"message.team-not-found": "Echipa nu a fost găsită.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Selectează echipa căreia vrei să îi transferi site-ul.",
"message.transfer-website": "Transferă titulatura site-ului către tine sau către o altă echipă.",
"message.triggered-event": "Eveniment declanșat",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Utilizator șters.",
"message.viewed-page": "Pagină vizualizată",
- "message.visitor-log": "Vizitator din {country} folosind {browser} pe {os} {device}",
- "message.visitors-dropped-off": "Vizitatori care au abandonat"
+ "message.visitor-log": "Vizitator din {country} folosind {browser} pe {os} {device}"
}
diff --git a/src/lang/ru-RU.json b/src/lang/ru-RU.json
index 558b411a..96d0538f 100644
--- a/src/lang/ru-RU.json
+++ b/src/lang/ru-RU.json
@@ -3,32 +3,47 @@
"label.actions": "Действия",
"label.activity": "Журнал активности",
"label.add": "Добавить",
+ "label.add-board": "Добавить доску",
"label.add-description": "Добавить описание",
"label.add-member": "Добавить участника",
"label.add-step": "Добавить шаг",
"label.add-website": "Добавить сайт",
"label.admin": "Администратор",
+ "label.affiliate": "Партнер",
"label.after": "После",
"label.all": "Все",
"label.all-time": "Все время",
"label.analytics": "Аналитика",
+ "label.apply": "Применить",
+ "label.attribution": "Атрибуция",
+ "label.attribution-description": "Посмотрите, как пользователи взаимодействуют с вашим маркетингом и что приводит к конверсиям.",
"label.average": "Средний",
"label.back": "Назад",
"label.before": "До",
+ "label.behavior": "Поведение",
+ "label.boards": "Доски",
"label.bounce-rate": "Отказы",
"label.breakdown": "Авария",
"label.browser": "Браузер",
"label.browsers": "Браузеры",
+ "label.campaigns": "Кампании",
"label.cancel": "Отменить",
"label.change-password": "Изменить пароль",
+ "label.channels": "Каналы",
"label.cities": "Города",
"label.city": "Город",
"label.clear-all": "Очистить все",
+ "label.cohort": "Когорта",
"label.compare": "Сравнить",
+ "label.compare-dates": "Сравнить даты",
"label.confirm": "Подтвердить",
"label.confirm-password": "Подтвердить пароль",
"label.contains": "Содержит",
+ "label.content": "Контент",
"label.continue": "Продолжить",
+ "label.conversion": "Конверсия",
+ "label.conversion-rate": "Коэффициент конверсии",
+ "label.conversion-step": "Шаг конверсии",
"label.count": "Считать",
"label.countries": "Страны",
"label.country": "Страна",
@@ -38,6 +53,7 @@
"label.create-user": "Создать пользователя",
"label.created": "Создано",
"label.created-by": "Создано",
+ "label.currency": "Валюта",
"label.current": "Текущий",
"label.current-password": "Текущий пароль",
"label.custom-range": "Другой период",
@@ -57,19 +73,26 @@
"label.details": "Подробности",
"label.device": "Устройство",
"label.devices": "Устройства",
+ "label.direct": "Direct",
"label.dismiss": "Отклонить",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "Не содержит",
+ "label.does-not-include": "Не включает",
+ "label.doest-not-exist": "Не существует",
"label.domain": "Домен",
"label.dropoff": "Высадка",
"label.edit": "Изменить",
"label.edit-dashboard": "Редактировать дашборд",
"label.edit-member": "Редактировать участника",
+ "label.email": "Email",
"label.enable-share-url": "Разрешить делиться ссылкой",
"label.end-step": "Конечный шаг",
"label.entry": "URL-адрес входа",
"label.event": "Событие",
"label.event-data": "Данные о событии",
+ "label.event-name": "Название события",
"label.events": "События",
+ "label.exists": "Существует",
"label.exit": "URL-адрес выхода",
"label.false": "Ложь",
"label.field": "Поле",
@@ -78,29 +101,37 @@
"label.filter-combined": "Объединенные",
"label.filter-raw": "Сырые данные",
"label.filters": "Фильтры",
+ "label.first-click": "Первый клик",
"label.first-seen": "Первый вход",
"label.funnel": "Воронка",
"label.funnel-description": "Изучите коэффициент конверсии и ухода пользователей.",
+ "label.funnels": "Воронки",
"label.goal": "Цель",
"label.goals": "Цели",
"label.goals-description": "Отслеживайте свои цели по просмотрам страниц и событиям.",
"label.greater-than": "Больше, чем",
"label.greater-than-equals": "Больше или равно",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Группировано",
+ "label.hostname": "Имя хоста",
+ "label.includes": "Включает",
+ "label.insight": "Инсайт",
"label.insights": "Информация",
"label.insights-description": "Погрузитесь глубже в свои данные с помощью сегментов и фильтров.",
"label.is": "Является",
+ "label.is-false": "Ложно",
"label.is-not": "Не установлен",
"label.is-not-set": "Не установлено",
"label.is-set": "Установлен",
+ "label.is-true": "Истинно",
"label.join": "Присоединиться",
"label.join-team": "Присоединиться к команде",
"label.journey": "Journey",
"label.journey-description": "Поймите, как пользователи перемещаются по вашему сайту.",
+ "label.journeys": "Пути",
"label.language": "Язык",
"label.languages": "Языки",
"label.laptop": "Ноутбук",
+ "label.last-click": "Последний клик",
"label.last-days": "Последние {x} дней",
"label.last-hours": "Последние {x} часа",
"label.last-months": "Последние {x} месяцев",
@@ -109,15 +140,19 @@
"label.leave-team": "Покинуть команду",
"label.less-than": "Меньше, чем",
"label.less-than-equals": "Меньше или равно",
+ "label.links": "Ссылки",
"label.login": "Войти",
"label.logout": "Выйти",
"label.manage": "Управление",
"label.manager": "Менеджер",
"label.max": "Максимум",
+ "label.maximize": "Развернуть",
+ "label.medium": "Средний",
"label.member": "Участник",
"label.members": "Участники",
"label.min": "Минимум",
"label.mobile": "Смартфон",
+ "label.model": "Модель",
"label.more": "Больше",
"label.my-account": "Мой профиль",
"label.my-websites": "Мои сайты",
@@ -126,16 +161,29 @@
"label.none": "Не указано",
"label.number-of-records": "{x} {x, plural, one {запись} other {записи}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Органический поиск",
+ "label.organic-shopping": "Органические покупки",
+ "label.organic-social": "Органические соцсети",
+ "label.organic-video": "Органическое видео",
"label.os": "OS",
+ "label.other": "Другое",
"label.overview": "Обзор",
"label.owner": "Владелец",
+ "label.page": "Страница",
"label.page-of": "Страница {current} из {total}",
"label.page-views": "Просмотры страниц",
"label.pageTitle": "Название страницы",
"label.pages": "Страницы",
+ "label.paid-ads": "Платная реклама",
+ "label.paid-search": "Платный поиск",
+ "label.paid-shopping": "Платные покупки",
+ "label.paid-social": "Платные соцсети",
+ "label.paid-video": "Платное видео",
"label.password": "Пароль",
"label.path": "Путь",
"label.paths": "Пути",
+ "label.pixels": "Пиксели",
"label.powered-by": "На движке {name}",
"label.previous": "Предыдущий",
"label.previous-period": "Предыдущий период",
@@ -147,12 +195,14 @@
"label.query": "Запрос",
"label.query-parameters": "Параметры запроса",
"label.realtime": "Реальное время",
+ "label.referral": "Referral",
"label.referrer": "Реферер",
"label.referrers": "Источники",
"label.refresh": "Обновить",
"label.regenerate": "Обновить",
"label.region": "Регион",
"label.regions": "Регионы",
+ "label.remaining": "Осталось",
"label.remove": "Удалить",
"label.remove-member": "Удалить участника",
"label.reports": "Отчеты",
@@ -163,7 +213,6 @@
"label.retention-description": "Измерьте «прилипаемость» вашего сайта, отслеживая, как часто пользователи возвращаются на него.",
"label.revenue": "Выручка",
"label.revenue-description": "Изучите свои доходы за определенное время.",
- "label.revenue-property": "Доходная недвижимость",
"label.role": "Роль",
"label.run-query": "Выполнить запрос",
"label.save": "Сохранить",
@@ -171,26 +220,35 @@
"label.search": "Поиск",
"label.select": "Выберите",
"label.select-date": "Выберите дату",
+ "label.select-filter": "Выберите фильтр",
"label.select-role": "Выберите роль",
"label.select-website": "Выбрать сайт",
"label.session": "Сессия",
+ "label.session-data": "Данные сессии",
"label.sessions": "Сессии",
"label.settings": "Настройки",
+ "label.share": "Поделиться",
"label.share-url": "Поделиться ссылкой",
"label.single-day": "Один день",
+ "label.sms": "SMS",
+ "label.sources": "Источники",
"label.start-step": "Начальный этап",
"label.steps": "Шаги",
"label.sum": "Сумма",
"label.tablet": "Планшет",
+ "label.tag": "Тег",
+ "label.tags": "Теги",
"label.team": "Команда",
"label.team-id": "ID команды",
"label.team-manager": "Менеджер команды",
"label.team-member": "Член команды",
"label.team-name": "Название команды",
"label.team-owner": "Владелец команды",
+ "label.team-settings": "Настройки команды",
"label.team-view-only": "Только командный просмотр",
"label.team-websites": "Веб-сайты команды",
"label.teams": "Команды",
+ "label.terms": "Условия",
"label.theme": "Тема",
"label.this-month": "Этот месяц",
"label.this-week": "Эта неделя",
@@ -213,10 +271,7 @@
"label.unknown": "Неизвестно",
"label.untitled": "Без названия",
"label.update": "Обновление",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Пользователь",
- "label.user-property": "Собственность пользователя",
"label.username": "Имя пользователя",
"label.users": "Пользователи",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Вчера",
"message.action-confirmation": "Введите {confirmation} в поле ниже, чтобы подтвердить.",
"message.active-users": "{x} текущих посетителей",
+ "message.bad-request": "Bad request",
"message.collected-data": "Собранные данные",
"message.confirm-delete": "Вы уверены, что хотите удалить {target}?",
"message.confirm-leave": "Вы уверены, что хотите уйти {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Все связанные данные будут также удалены.",
"message.error": "Что-то пошло не так.",
"message.event-log": "{event} на {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Перейти к настройкам",
"message.incorrect-username-password": "Неверное имя пользователя/пароль.",
"message.invalid-domain": "Некорректный домен",
@@ -259,10 +316,13 @@
"message.no-teams": "Вы не создали ни одной команды.",
"message.no-users": "Нет пользователей.",
"message.no-websites-configured": "У вас нет настроенных сайтов.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Страница не найдена.",
"message.reset-website": "Для сброса введите RESET",
"message.reset-website-warning": "Вся статистика для этого сайта будет удалена, но ваш код отслеживания останется нетронутым.",
"message.saved": "Успешно сохранено.",
+ "message.sever-error": "Server error",
"message.share-url": "Это публичная ссылка для {target}.",
"message.team-already-member": "Вы уже состоите в команде.",
"message.team-not-found": "Команда не найдена.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Выберите команду, которой нужно передать этот сайт.",
"message.transfer-website": "Передайте право владения сайтом своей учетной записи или другой команде.",
"message.triggered-event": "Запущенное событие",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Пользователь удален.",
"message.viewed-page": "Просмотренная страница",
- "message.visitor-log": "Посетитель из {country} используя {browser} на {os} {device}",
- "message.visitors-dropped-off": "Высадка посетителей"
+ "message.visitor-log": "Посетитель из {country} используя {browser} на {os} {device}"
}
diff --git a/src/lang/si-LK.json b/src/lang/si-LK.json
index 46dab6b3..3e6aff86 100644
--- a/src/lang/si-LK.json
+++ b/src/lang/si-LK.json
@@ -3,32 +3,47 @@
"label.actions": "Actions",
"label.activity": "Activity log",
"label.add": "Add",
+ "label.add-board": "Add board",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "වෙබ් අඩවිය එක් කරන්න",
"label.admin": "Administrator",
+ "label.affiliate": "Affiliate",
"label.after": "After",
"label.all": "සියල්ල",
"label.all-time": "හැම වෙලාවෙම",
"label.analytics": "Analytics",
+ "label.apply": "Apply",
+ "label.attribution": "Attribution",
+ "label.attribution-description": "See how users engage with your marketing and what drives conversions.",
"label.average": "Average",
"label.back": "ආපසු",
"label.before": "Before",
+ "label.behavior": "අචරණය",
+ "label.boards": "Boards",
"label.bounce-rate": "Bounce rate",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.browsers": "Browsers",
+ "label.campaigns": "Campaigns",
"label.cancel": "අවලංගු කරන්න",
"label.change-password": "මුරපදය වෙනස් කරන්න",
+ "label.channels": "Channels",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
+ "label.cohort": "Cohort",
"label.compare": "Compare",
+ "label.compare-dates": "Compare dates",
"label.confirm": "Confirm",
"label.confirm-password": "මුරපදය සත්යාපනය කරන්න",
"label.contains": "Contains",
+ "label.content": "Content",
"label.continue": "Continue",
+ "label.conversion": "Conversion",
+ "label.conversion-rate": "Conversion rate",
+ "label.conversion-step": "Conversion step",
"label.count": "Count",
"label.countries": "Countries",
"label.country": "Country",
@@ -38,6 +53,7 @@
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
+ "label.currency": "Currency",
"label.current": "Current",
"label.current-password": "වත්මන් මුරපදය",
"label.custom-range": "අභිරුචි පරාසය",
@@ -57,19 +73,26 @@
"label.details": "Details",
"label.device": "Device",
"label.devices": "Devices",
+ "label.direct": "Direct",
"label.dismiss": "මගහරින්න",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "Does not contain",
+ "label.does-not-include": "Does not include",
+ "label.doest-not-exist": "Does not exist",
"label.domain": "වසම",
"label.dropoff": "Dropoff",
"label.edit": "සංස්කරණය කරන්න",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
+ "label.email": "Email",
"label.enable-share-url": "බෙදාගැනීමේ URL සබල කරන්න",
"label.end-step": "End Step",
"label.entry": "Entry URL",
"label.event": "Event",
"label.event-data": "සිදුවීම් දත්ත",
+ "label.event-name": "Event name",
"label.events": "Events",
+ "label.exists": "Exists",
"label.exit": "Exit URL",
"label.false": "False",
"label.field": "Field",
@@ -78,29 +101,37 @@
"label.filter-combined": "Combined",
"label.filter-raw": "Raw",
"label.filters": "Filters",
+ "label.first-click": "First click",
"label.first-seen": "First seen",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
+ "label.funnels": "Funnels",
"label.goal": "Goal",
"label.goals": "Goals",
"label.goals-description": "Track your goals for pageviews and events.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grouped",
+ "label.hostname": "Hostname",
+ "label.includes": "Includes",
+ "label.insight": "Insight",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
+ "label.is-false": "Is false",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
+ "label.is-true": "Is true",
"label.join": "Join",
"label.join-team": "Join team",
"label.journey": "Journey",
"label.journey-description": "Understand how users navigate through your website.",
+ "label.journeys": "Journeys",
"label.language": "භාෂාව",
"label.languages": "Languages",
"label.laptop": "Laptop",
+ "label.last-click": "Last click",
"label.last-days": "අන්තිම {x} දින",
"label.last-hours": "අන්තිම {x} පැය",
"label.last-months": "Last {x} months",
@@ -109,15 +140,19 @@
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
+ "label.links": "Links",
"label.login": "ලොග් වෙන්න",
"label.logout": "පිටවීම",
"label.manage": "Manage",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Expand",
+ "label.medium": "Medium",
"label.member": "Member",
"label.members": "Members",
"label.min": "Min",
"label.mobile": "Mobile",
+ "label.model": "Model",
"label.more": "තවත්",
"label.my-account": "My account",
"label.my-websites": "My websites",
@@ -126,16 +161,29 @@
"label.none": "කිසිවක් නැත",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organic search",
+ "label.organic-shopping": "Organic shopping",
+ "label.organic-social": "Organic social",
+ "label.organic-video": "Organic video",
"label.os": "OS",
+ "label.other": "Other",
"label.overview": "Overview",
"label.owner": "හිමිකරු",
+ "label.page": "Page",
"label.page-of": "Page {current} of {total}",
"label.page-views": "Page views",
"label.pageTitle": "Page title",
"label.pages": "Pages",
+ "label.paid-ads": "Paid ads",
+ "label.paid-search": "Paid search",
+ "label.paid-shopping": "Paid shopping",
+ "label.paid-social": "Paid social",
+ "label.paid-video": "Paid video",
"label.password": "මුරපදය",
"label.path": "Path",
"label.paths": "Paths",
+ "label.pixels": "Pixels",
"label.powered-by": "Powered by {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
@@ -147,12 +195,14 @@
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.realtime": "තත්ය කාල",
+ "label.referral": "Referral",
"label.referrer": "Referrer",
"label.referrers": "Referrers",
"label.refresh": "නැවුම් කරන්න",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Remaining",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -163,7 +213,6 @@
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "සුරකින්න",
@@ -171,26 +220,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Select filter",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.session": "Session",
+ "label.session-data": "Session data",
"label.sessions": "Sessions",
"label.settings": "සැකසුම්",
+ "label.share": "Share",
"label.share-url": "බෙදාගැනීමේ URL",
"label.single-day": "තනි දවස",
+ "label.sms": "SMS",
+ "label.sources": "Sources",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "Tablet",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Team settings",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Terms",
"label.theme": "තේමාව",
"label.this-month": "මෙ මාසය",
"label.this-week": "මේ සතිය",
@@ -213,10 +271,7 @@
"label.unknown": "නොදනී",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "පරිශීලක නාමය",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "ඊයේ",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} දැන් {x, plural, one {අමුත්තා} other {අමුත්තන්}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "{target} මකා දැමීම ගැන විශ්වාසද?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "All website data will be deleted.",
"message.error": "Something went wrong.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "සැකසීම් වෙත යන්න",
"message.incorrect-username-password": "වැරදි පරිශීලක නාමය/මුරපදය.",
"message.invalid-domain": "Invalid domain. Do not include http/https.",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "You do not have any websites configured.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "පිටුව හමු නොවීය.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your settings will remain intact.",
"message.saved": "Saved.",
+ "message.sever-error": "Server error",
"message.share-url": "මේ {target} සඳහා ප්රසිද්ධියේ බෙදාගත් URL එකයි.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}"
}
diff --git a/src/lang/sk-SK.json b/src/lang/sk-SK.json
index 2a02b5c7..297d5e34 100644
--- a/src/lang/sk-SK.json
+++ b/src/lang/sk-SK.json
@@ -1,158 +1,208 @@
{
- "label.access-code": "Access code",
+ "label.access-code": "Prístupový kód",
"label.actions": "Akcie",
- "label.activity": "Activity log",
- "label.add": "Add",
- "label.add-description": "Add description",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.activity": "Denník aktivít",
+ "label.add": "Pridať",
+ "label.add-board": "Pridať tabuľu",
+ "label.add-description": "Pridať popis",
+ "label.add-member": "Pridať člena",
+ "label.add-step": "Pridať krok",
"label.add-website": "Pridať web",
"label.admin": "Administrátor",
- "label.after": "After",
+ "label.affiliate": "Partner",
+ "label.after": "Po",
"label.all": "Všetko",
- "label.all-time": "All time",
- "label.analytics": "Analytics",
- "label.average": "Average",
+ "label.all-time": "Celý čas",
+ "label.analytics": "Analytika",
+ "label.apply": "Použiť",
+ "label.attribution": "Priradenie",
+ "label.attribution-description": "Pozrite sa, ako používatelia interagujú s vaším marketingom a čo vedie ku konverziám.",
+ "label.average": "Priemer",
"label.back": "Späť",
- "label.before": "Before",
+ "label.before": "Pred",
+ "label.behavior": "Správanie",
+ "label.boards": "Tabule",
"label.bounce-rate": "Okamžité opustenie",
- "label.breakdown": "Breakdown",
- "label.browser": "Browser",
+ "label.breakdown": "Rozpis",
+ "label.browser": "Prehliadač",
"label.browsers": "Prehliadač",
+ "label.campaigns": "Kampane",
"label.cancel": "Zrušiť",
"label.change-password": "Zmeniť heslo",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
+ "label.channels": "Kanály",
+ "label.cities": "Mestá",
+ "label.city": "Mesto",
+ "label.clear-all": "Vymazať všetko",
+ "label.cohort": "Kohorta",
+ "label.compare": "Porovnať",
+ "label.compare-dates": "Porovnať dátumy",
+ "label.confirm": "Potvrdiť",
"label.confirm-password": "Potvrdiť heslo",
"label.contains": "Contains",
+ "label.content": "Obsah",
"label.continue": "Continue",
- "label.count": "Count",
+ "label.conversion": "Konverzia",
+ "label.conversion-rate": "Miera konverzie",
+ "label.conversion-step": "Krok konverzie",
+ "label.count": "Počet",
"label.countries": "Zem",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.country": "Krajina",
+ "label.create": "Vytvoriť",
+ "label.create-report": "Vytvoriť správu",
+ "label.create-team": "Vytvoriť tím",
+ "label.create-user": "Vytvoriť používateľa",
+ "label.created": "Vytvorené",
+ "label.created-by": "Vytvoril",
+ "label.currency": "Mena",
+ "label.current": "Aktuálny",
"label.current-password": "Aktuálne heslo",
"label.custom-range": "Vlastný rozsah",
"label.dashboard": "Prehlad",
"label.data": "Data",
"label.date": "Date",
"label.date-range": "Obdobie",
- "label.day": "Day",
+ "label.day": "Deň",
"label.default-date-range": "Predvolené obdobie",
"label.delete": "Zmazať",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete-report": "Zmazať správu",
+ "label.delete-team": "Zmazať tím",
+ "label.delete-user": "Zmazať používateľa",
"label.delete-website": "Zmazať web",
- "label.description": "Description",
+ "label.description": "Popis",
"label.desktop": "Stolný počítač",
"label.details": "Details",
- "label.device": "Device",
+ "label.device": "Zariadenie",
"label.devices": "Zariadenie",
+ "label.direct": "Priamy",
"label.dismiss": "Odísť",
- "label.does-not-contain": "Does not contain",
+ "label.distinct-id": "Jedinečné ID",
+ "label.does-not-contain": "Neobsahuje",
+ "label.does-not-include": "Nezahŕňa",
+ "label.doest-not-exist": "Neexistuje",
"label.domain": "Doména",
"label.dropoff": "Dropoff",
"label.edit": "Upraviť",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
+ "label.edit-dashboard": "Upraviť prehľad",
+ "label.edit-member": "Upraviť člena",
+ "label.email": "Email",
"label.enable-share-url": "Povoliť zdielanie URL",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event data",
+ "label.end-step": "Konečný krok",
+ "label.entry": "Vstupná URL",
+ "label.event": "Udalosť",
+ "label.event-data": "Dáta udalosti",
+ "label.event-name": "Názov udalosti",
"label.events": "Udalosti",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
+ "label.exists": "Existuje",
+ "label.exit": "Výstupná URL",
+ "label.false": "Nepravda",
+ "label.field": "Pole",
+ "label.fields": "Polia",
"label.filter": "Filter",
"label.filter-combined": "Kombinácie",
"label.filter-raw": "Nezpracované",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.filters": "Filtre",
+ "label.first-click": "Prvé kliknutie",
+ "label.first-seen": "Prvýkrát videné",
+ "label.funnel": "Lievik",
+ "label.funnel-description": "Pochopte mieru konverzie a odchodu používateľov.",
+ "label.funnels": "Lieviky",
+ "label.goal": "Cieľ",
+ "label.goals": "Ciele",
+ "label.goals-description": "Sledujte svoje ciele pre zobrazenia stránok a udalosti.",
+ "label.greater-than": "Väčšie ako",
+ "label.greater-than-equals": "Väčšie alebo rovné",
+ "label.grouped": "Zoskupené",
+ "label.hostname": "Názov hostiteľa",
+ "label.includes": "Zahŕňa",
+ "label.insight": "Prehľad",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
- "label.language": "Language",
- "label.languages": "Languages",
+ "label.is": "Je",
+ "label.is-false": "Je nepravda",
+ "label.is-not": "Nie je",
+ "label.is-not-set": "Nie je nastavené",
+ "label.is-set": "Nastavené",
+ "label.is-true": "Je pravda",
+ "label.join": "Pripojiť sa",
+ "label.join-team": "Pripojiť sa k tímu",
+ "label.journey": "Cesta",
+ "label.journey-description": "Pochopte, ako používatelia prechádzajú vaším webom.",
+ "label.journeys": "Cesty",
+ "label.language": "Jazyk",
+ "label.languages": "Jazyky",
"label.laptop": "Prenosný počítač",
+ "label.last-click": "Posledné kliknutie",
"label.last-days": "Posledných {x} dní",
"label.last-hours": "Posledných {x} hodín",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "Posledných {x} mesiacov",
+ "label.last-seen": "Naposledy videné",
+ "label.leave": "Odísť",
+ "label.leave-team": "Opustiť tím",
+ "label.less-than": "Menej ako",
+ "label.less-than-equals": "Menej alebo rovné",
+ "label.links": "Odkazy",
"label.login": "Prihlásiť",
"label.logout": "Odhlásiť",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
+ "label.manage": "Spravovať",
+ "label.manager": "Manažér",
+ "label.max": "Maximum",
+ "label.maximize": "Rozbaliť",
+ "label.medium": "Stredný",
+ "label.member": "Člen",
+ "label.members": "Členovia",
+ "label.min": "Minimum",
"label.mobile": "Mobilný telefon",
+ "label.model": "Model",
"label.more": "Viac",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "Môj účet",
+ "label.my-websites": "Moje weby",
"label.name": "Meno",
"label.new-password": "Nové heslo",
- "label.none": "None",
+ "label.none": "Žiadny",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organické vyhľadávanie",
+ "label.organic-shopping": "Organické nakupovanie",
+ "label.organic-social": "Organické sociálne siete",
+ "label.organic-video": "Organické video",
"label.os": "OS",
+ "label.other": "Iné",
"label.overview": "Overview",
"label.owner": "Owner",
+ "label.page": "Stránka",
"label.page-of": "Page {current} of {total}",
"label.page-views": "Zobrazenie stánok",
"label.pageTitle": "Page title",
"label.pages": "Stránky",
+ "label.paid-ads": "Platené reklamy",
+ "label.paid-search": "Platené vyhľadávanie",
+ "label.paid-shopping": "Platené nakupovanie",
+ "label.paid-social": "Platené sociálne siete",
+ "label.paid-video": "Platené video",
"label.password": "Heslo",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Cesta",
+ "label.paths": "Cesty",
+ "label.pixels": "Pixely",
"label.powered-by": "Powered by {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "Predchádzajúci",
+ "label.previous-period": "Predchádzajúce obdobie",
+ "label.previous-year": "Predchádzajúci rok",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Vlastnosti",
+ "label.property": "Vlastnosť",
"label.queries": "Queries",
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.realtime": "Aktuálne",
+ "label.referral": "Odporúčanie",
"label.referrer": "Referrer",
"label.referrers": "Odkazy",
"label.refresh": "Obnoviť",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Zostáva",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -161,9 +211,8 @@
"label.reset-website": "Reset statistics",
"label.retention": "Retention",
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Príjem",
+ "label.revenue-description": "Pozrite si svoj príjem v priebehu času.",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "Uložiť",
@@ -171,26 +220,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Vybrať filter",
"label.select-role": "Select role",
"label.select-website": "Select website",
- "label.session": "Session",
+ "label.session": "Sedenie",
+ "label.session-data": "Dáta sedenia",
"label.sessions": "Sessions",
"label.settings": "Nastavenia",
+ "label.share": "Zdieľať",
"label.share-url": "Zdielanie URL",
"label.single-day": "Jeden deň",
+ "label.sms": "SMS",
+ "label.sources": "Zdroje",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "Tablet",
+ "label.tag": "Značka",
+ "label.tags": "Značky",
"label.team": "Team",
"label.team-id": "Team ID",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Manažér tímu",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Nastavenia tímu",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Podmienky",
"label.theme": "Theme",
"label.this-month": "Tento mesiac",
"label.this-week": "Tento týždeň",
@@ -213,10 +271,7 @@
"label.unknown": "Neznámý",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "Užívateľské meno",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} aktuálne {x, plural, one {návštevník} other {návštěvníci}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Naozaj zmazať {target}?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Všetky príbuzné data budu tiež zmazané.",
"message.error": "Niečo sa pokazilo.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Ísť do nastavení",
"message.incorrect-username-password": "Nesprávné meno/heslo.",
"message.invalid-domain": "Neplatná doména",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "Nemáte nastavený žiadny web.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Stránka sa nenašla.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.saved": "Úspešne uložené.",
+ "message.sever-error": "Server error",
"message.share-url": "Toto je zdielané URL pre {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Návštevník z {country} s prehliadačom {browser} na {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Návštevník z {country} s prehliadačom {browser} na {os} {device}"
}
diff --git a/src/lang/sl-SI.json b/src/lang/sl-SI.json
index bd180bfb..3dd3226f 100644
--- a/src/lang/sl-SI.json
+++ b/src/lang/sl-SI.json
@@ -3,42 +3,52 @@
"label.actions": "Dejanja",
"label.activity": "Dnevnik dejavnosti",
"label.add": "Dodaj",
+ "label.add-board": "Dodaj tablo",
"label.add-description": "Dodaj opis",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.add-member": "Dodaj člana",
+ "label.add-step": "Dodaj korak",
"label.add-website": "Dodaj spletno mesto",
"label.admin": "Administrator",
+ "label.affiliate": "Partner",
"label.after": "Po",
"label.all": "Vsi",
"label.all-time": "Ves čas",
"label.analytics": "Analitika",
+ "label.apply": "Uporabi",
+ "label.attribution": "Pripis",
+ "label.attribution-description": "Oglejte si, kako uporabniki sodelujejo z vašim marketingom in kaj spodbuja konverzije.",
"label.average": "Povprečno",
"label.back": "Nazaj",
"label.before": "Pred",
+ "label.behavior": "Obnašanje",
+ "label.boards": "Table",
"label.bounce-rate": "Odbojna stopnja",
"label.breakdown": "Razčlenitev",
"label.browser": "Brskalnik",
"label.browsers": "Brskalniki",
+ "label.campaigns": "Kampanje",
"label.cancel": "Prekliči",
"label.change-password": "Zamenjaj geslo",
+ "label.channels": "Kanali",
"label.cities": "Mesta",
"label.city": "Mesto",
"label.clear-all": "Počisti vse",
- "label.compare": "Compare",
+ "label.compare": "Primerjaj",
"label.confirm": "Potrdi",
"label.confirm-password": "Potrdi geslo",
"label.contains": "Vsebuje",
+ "label.content": "Vsebina",
"label.continue": "Nadaljuj",
- "label.count": "Count",
+ "label.count": "Število",
"label.countries": "Države",
"label.country": "Država",
- "label.create": "Create",
+ "label.create": "Ustvari",
"label.create-report": "Ustvari poročilo",
"label.create-team": "Ustvari ekipo",
"label.create-user": "Ustvari uporabnika",
"label.created": "Ustvarjeno",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.created-by": "Ustvaril",
+ "label.current": "Trenutno",
"label.current-password": "Trenutno geslo",
"label.custom-range": "Obdobje po meri",
"label.dashboard": "Nadzorna plošča",
@@ -48,7 +58,7 @@
"label.day": "Dan",
"label.default-date-range": "Privzeto časovno obdobje",
"label.delete": "Izbriši",
- "label.delete-report": "Delete report",
+ "label.delete-report": "Izbriši poročilo",
"label.delete-team": "Izbriši ekipo",
"label.delete-user": "Izbriši uporabnika",
"label.delete-website": "Izbriši spletno mesto",
@@ -57,20 +67,25 @@
"label.details": "Podrobnosti",
"label.device": "Naprava",
"label.devices": "Naprave",
+ "label.direct": "Neposredno",
"label.dismiss": "Prezri",
+ "label.distinct-id": "Unikatni ID",
"label.does-not-contain": "Ne vsebuje",
+ "label.does-not-include": "Ne vključuje",
+ "label.doest-not-exist": "Ne obstaja",
"label.domain": "Domena",
"label.dropoff": "Zapustitev",
"label.edit": "Uredi",
"label.edit-dashboard": "Uredi nadzorno ploščo",
- "label.edit-member": "Edit member",
- "label.enable-share-url": "Uredi povezavo za deljenje",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
+ "label.edit-member": "Uredi člana",
+ "label.enable-share-url": "Omogoči povezavo za deljenje",
+ "label.end-step": "Končni korak",
+ "label.entry": "Vstopni URL",
"label.event": "Dogodek",
"label.event-data": "Podatki dogodka",
+ "label.event-name": "Ime dogodka",
"label.events": "Dogodki",
- "label.exit": "Exit URL",
+ "label.exit": "Izhodni URL",
"label.false": "Napačno",
"label.field": "Polje",
"label.fields": "Polja",
@@ -78,119 +93,142 @@
"label.filter-combined": "Skupaj",
"label.filter-raw": "Neobdelano",
"label.filters": "Filtri",
- "label.first-seen": "First seen",
+ "label.first-seen": "Prvič viden",
"label.funnel": "Prodajni lijak",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
+ "label.funnel-description": "Razumite stopnjo konverzije in osipa uporabnikov.",
+ "label.goal": "Cilj",
+ "label.goals": "Cilji",
+ "label.goals-description": "Spremljajte svoje cilje za oglede strani in dogodke.",
"label.greater-than": "Večje od",
"label.greater-than-equals": "Večje ali enako kot",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.host": "Gostitelj",
+ "label.hosts": "Gostitelji",
"label.insights": "Vpogled",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
+ "label.insights-description": "Poglobite se v podatke z uporabo segmentov in filtrov.",
"label.is": "Je",
+ "label.is-false": "Je napačno",
"label.is-not": "Ni",
"label.is-not-set": "Ni nastavljeno",
"label.is-set": "Je nastavljeno",
+ "label.is-true": "Je res",
"label.join": "Pridruži se",
"label.join-team": "Pridruži se ekipi",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.journey": "Uporabniška pot",
+ "label.journey-description": "Razumite, kako uporabniki krmarijo po vašem spletnem mestu.",
"label.language": "Jezik",
"label.languages": "Jeziki",
"label.laptop": "Prenosni računalnik",
+ "label.last-click": "Zadnji klik",
"label.last-days": "Zadnjih {x} dni",
"label.last-hours": "Zadnjih {x} ur",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
+ "label.last-months": "Zadnjih {x} mesecev",
+ "label.last-seen": "Nazadnje viden",
"label.leave": "Zapusti",
"label.leave-team": "Zapusti ekipo",
"label.less-than": "Manjše kot",
"label.less-than-equals": "Manjše ali enako kot",
+ "label.links": "Povezave",
"label.login": "Prijava",
"label.logout": "Odjava",
- "label.manage": "Manage",
- "label.manager": "Manager",
+ "label.manage": "Upravljaj",
+ "label.manager": "Upravitelj",
"label.max": "Največ",
- "label.member": "Member",
+ "label.member": "Član",
"label.members": "Člani",
"label.min": "Najmanj",
"label.mobile": "Mobilne naprave",
+ "label.model": "Model",
"label.more": "Več",
- "label.my-account": "My account",
+ "label.my-account": "Moj račun",
"label.my-websites": "Moja spletna mesta",
"label.name": "Ime",
"label.new-password": "Novo geslo",
- "label.none": "Brez",
+ "label.none": "Noben",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organsko iskanje",
+ "label.organic-shopping": "Organski nakupi",
+ "label.organic-social": "Organska družbena omrežja",
+ "label.organic-video": "Organski video",
"label.os": "OS",
+ "label.other": "Drugo",
"label.overview": "Pregled",
"label.owner": "Lastnik",
+ "label.page": "Stran",
"label.page-of": "Stran {current} od {total}",
"label.page-views": "Obiski strani",
"label.pageTitle": "Naslov strani",
"label.pages": "Strani",
+ "label.paid-ads": "Plačani oglasi",
+ "label.paid-search": "Plačano iskanje",
+ "label.paid-shopping": "Plačani nakupi",
+ "label.paid-social": "Plačana družbena omrežja",
+ "label.paid-video": "Plačani video",
"label.password": "Geslo",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Pot",
+ "label.paths": "Poti",
"label.powered-by": "Poganja {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "Prejšnji",
+ "label.previous-period": "Prejšnje obdobje",
+ "label.previous-year": "Prejšnje leto",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Lastnosti",
+ "label.property": "Lastnost",
"label.queries": "Poizvedbe",
"label.query": "Poizvedba",
"label.query-parameters": "Parametri poizvedbe",
"label.realtime": "V živo",
+ "label.referral": "Napoten",
"label.referrer": "Vir",
"label.referrers": "Viri",
"label.refresh": "Osveži",
"label.regenerate": "Ponovno generiraj",
"label.region": "Regija",
"label.regions": "Regije",
+ "label.remaining": "Preostalo",
"label.remove": "Odstrani",
- "label.remove-member": "Remove member",
+ "label.remove-member": "Odstrani člana",
"label.reports": "Poročila",
"label.required": "Zahtevano",
"label.reset": "Ponastavi",
"label.reset-website": "Ponastavi statistiko",
"label.retention": "Ohranjanje uporabnikov",
- "label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.retention-description": "Merite uporabnikovo zadržanost s sledenjem, kako pogosto se vračajo.",
+ "label.revenue": "Prihodki",
+ "label.revenue-description": "Preglejte svoje prihodke skozi čas.",
+ "label.revenue-property": "Lastnost prihodkov",
"label.role": "Vloga",
"label.run-query": "Izvedi poizvedbo",
"label.save": "Shrani",
"label.screens": "Zasloni",
- "label.search": "Search",
- "label.select": "Select",
+ "label.search": "Išči",
+ "label.select": "Izberi",
"label.select-date": "Izberi datum",
- "label.select-role": "Select role",
+ "label.select-role": "Izberi vlogo",
"label.select-website": "Izberi spletno mesto",
- "label.session": "Session",
+ "label.session": "Seja",
"label.sessions": "Seje",
"label.settings": "Nastavitve",
+ "label.share": "Deli",
"label.share-url": "Deli povezavo",
"label.single-day": "En dan",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
+ "label.start-step": "Začetni korak",
+ "label.steps": "Koraki",
"label.sum": "Seštevek",
"label.tablet": "Tablični računalnik",
+ "label.tag": "Oznaka",
+ "label.tags": "Oznake",
"label.team": "Ekipa",
"label.team-id": "ID ekipe",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Upravitelj ekipe",
"label.team-member": "Član ekipe",
"label.team-name": "Ime ekipe",
"label.team-owner": "Lastnik ekipe",
- "label.team-view-only": "Team view only",
+ "label.team-view-only": "Ekipa samo za ogled",
"label.team-websites": "Spletna mesta ekipe",
"label.teams": "Ekipe",
+ "label.terms": "Pogoji",
"label.theme": "Tema",
"label.this-month": "Ta mesec",
"label.this-week": "Ta teden",
@@ -213,10 +251,7 @@
"label.unknown": "Neznano",
"label.untitled": "Brez naslova",
"label.update": "Update",
- "label.url": "Povezava",
- "label.urls": "Povezave",
"label.user": "Uporabnik",
- "label.user-property": "User Property",
"label.username": "Uporabniško ime",
"label.users": "Uporabniki",
"label.utm": "UTM",
@@ -232,20 +267,21 @@
"label.visits": "Visits",
"label.website": "Spletno mesto",
"label.website-id": "ID spletnega mesta",
- "label.websites": "Spletnih mest",
+ "label.websites": "Spletna mesta",
"label.window": "Okno",
"label.yesterday": "Včeraj",
- "message.action-confirmation": "Type {confirmation} in the box below to confirm.",
+ "message.action-confirmation": "Za potrditev v spodnje polje vnesite {confirmation}.",
"message.active-users": "{x} trenutni {x, plural, one {obiskovalec} other {obiskovalcev}}",
- "message.collected-data": "Collected data",
+ "message.collected-data": "Zbrani podatki",
"message.confirm-delete": "Ste prepričani, da želite izbrisati {target}?",
"message.confirm-leave": "Ste prepričani, da želite zapustiti {target}?",
- "message.confirm-remove": "Are you sure you want to remove {target}?",
+ "message.confirm-remove": "Ali ste prepričani, da želite odstraniti {target}?",
"message.confirm-reset": "Ste prepričani, da želite ponastaviti statistiko {target}?",
- "message.delete-team-warning": "Deleting a team will also delete all team websites.",
+ "message.delete-team-warning": "Brisanje ekipe bo izbrisalo tudi vsa spletna mesta ekipe.",
"message.delete-website-warning": "Izbrisani bodo tudi vsi pripadajoči podatki.",
"message.error": "Nekaj je šlo narobe.",
"message.event-log": "{event} na {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Pojdi v nastavitve",
"message.incorrect-username-password": "Nepravilno uporabniško ime/geslo.",
"message.invalid-domain": "Neveljavna domena",
@@ -259,21 +295,24 @@
"message.no-teams": "Niste še ustvarili nobene ekipe.",
"message.no-users": "Ni uporabnikov.",
"message.no-websites-configured": "Nimate nastavljenih nobenih spletnih mest.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Stran ni bila najdena.",
"message.reset-website": "Za ponastavitev izbrisa tega spletnega mesta vnesite {confirmation} v spodnje polje.",
"message.reset-website-warning": "Vse statistike za to spletno mesto bodo izbrisane, koda za sledenje pa bo ostala nespremenjena.",
"message.saved": "Uspešno shranjeno.",
+ "message.sever-error": "Server error",
"message.share-url": "To je javno dostopna povezava za {target}.",
"message.team-already-member": "Ste že član ekipe.",
"message.team-not-found": "Ekipa ni bila najdena.",
"message.team-websites-info": "Spletne strani si lahko ogleda vsak član ekipe.",
"message.tracking-code": "Koda za sledenje",
- "message.transfer-team-website-to-user": "Transfer this website to your account?",
- "message.transfer-user-website-to-team": "Select the team to transfer this website to.",
- "message.transfer-website": "Transfer website ownership to your account or another team.",
- "message.triggered-event": "Triggered event",
+ "message.transfer-team-website-to-user": "Želite prenesti to spletno mesto v svoj račun?",
+ "message.transfer-user-website-to-team": "Izberite ekipo, na katero želite prenesti to spletno mesto.",
+ "message.transfer-website": "Prenesite lastništvo spletnega mesta na svoj račun ali drugo ekipo.",
+ "message.triggered-event": "Sprožen dogodek",
"message.user-deleted": "Uporabnik je izbrisan.",
- "message.viewed-page": "Viewed page",
+ "message.viewed-page": "Ogledana stran",
"message.visitor-log": "Obiskovalec iz {country} uporablja {browser} na {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitors-dropped-off": "Osip obiskovalcev"
}
diff --git a/src/lang/sv-SE.json b/src/lang/sv-SE.json
index cdaa676a..1f456b0e 100644
--- a/src/lang/sv-SE.json
+++ b/src/lang/sv-SE.json
@@ -3,33 +3,48 @@
"label.actions": "Händelser",
"label.activity": "Aktivitetslogg",
"label.add": "Lägg till",
+ "label.add-board": "Lägg till anslagstavla",
"label.add-description": "Lägg till beskrivning",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.add-member": "Lägg till medlem",
+ "label.add-step": "Lägg till steg",
"label.add-website": "Lägg till webbplats",
"label.admin": "Administratör",
+ "label.affiliate": "Partner",
"label.after": "Efter",
"label.all": "Alla",
"label.all-time": "Sedan början",
"label.analytics": "Webbplats Analys",
+ "label.apply": "Tillämpa",
+ "label.attribution": "Attribuering",
+ "label.attribution-description": "Se hur användare interagerar med din marknadsföring och vad som driver konverteringar.",
"label.average": "Genomsnitt",
"label.back": "Tillbaka",
"label.before": "Före",
+ "label.behavior": "Beteende",
+ "label.boards": "Anslagstavlor",
"label.bounce-rate": "Avvisningsfrekvens",
"label.breakdown": "Analys",
"label.browser": "Webbläsare",
"label.browsers": "Webbläsare",
+ "label.campaigns": "Kampanjer",
"label.cancel": "Avbryt",
"label.change-password": "Byt lösenord",
+ "label.channels": "Kanaler",
"label.cities": "Städer",
"label.city": "Stad",
"label.clear-all": "Rensa alla",
- "label.compare": "Compare",
+ "label.cohort": "Kohort",
+ "label.compare": "Jämför",
+ "label.compare-dates": "Jämför datum",
"label.confirm": "Bekräfta",
"label.confirm-password": "Bekräfta lösenord",
"label.contains": "Innehåller",
+ "label.content": "Innehåll",
"label.continue": "Fortsätt",
- "label.count": "Count",
+ "label.conversion": "Konvertering",
+ "label.conversion-rate": "Konverteringsfrekvens",
+ "label.conversion-step": "Konverteringssteg",
+ "label.count": "Antal",
"label.countries": "Länder",
"label.country": "Land",
"label.create": "Skapa",
@@ -37,8 +52,9 @@
"label.create-team": "Skapa team",
"label.create-user": "Skapa användare",
"label.created": "Skapad",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.created-by": "Skapad av",
+ "label.currency": "Valuta",
+ "label.current": "Nuvarande",
"label.current-password": "Nuvarande lösenord",
"label.custom-range": "Anpassat urval",
"label.dashboard": "Översikt",
@@ -48,7 +64,7 @@
"label.day": "Dag",
"label.default-date-range": "Standard datum-urval",
"label.delete": "Radera",
- "label.delete-report": "Delete report",
+ "label.delete-report": "Radera rapport",
"label.delete-team": "Radera team",
"label.delete-user": "Radera användare",
"label.delete-website": "Radera webbplats",
@@ -57,19 +73,26 @@
"label.details": "Detaljer",
"label.device": "Enhet",
"label.devices": "Enheter",
+ "label.direct": "Direkt",
"label.dismiss": "Avbryt",
+ "label.distinct-id": "Unikt ID",
"label.does-not-contain": "Innehåller inte",
+ "label.does-not-include": "Inkluderar inte",
+ "label.doest-not-exist": "Existerar inte",
"label.domain": "Domän",
"label.dropoff": "Bortfall",
"label.edit": "Redigera",
"label.edit-dashboard": "Redigera översikt",
- "label.edit-member": "Edit member",
+ "label.edit-member": "Redigera medlem",
+ "label.email": "Email",
"label.enable-share-url": "Aktivera delningslänk",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
+ "label.end-step": "Slutsteg",
+ "label.entry": "Ingångs-URL",
"label.event": "Händelse",
"label.event-data": "Händelsedata",
+ "label.event-name": "Händelsenamn",
"label.events": "Händelser",
+ "label.exists": "Existerar",
"label.exit": "Exit URL",
"label.false": "Falskt",
"label.field": "Fält",
@@ -78,81 +101,108 @@
"label.filter-combined": "Kombinerade",
"label.filter-raw": "Rådata",
"label.filters": "Filter",
+ "label.first-click": "Första klicket",
"label.first-seen": "First seen",
"label.funnel": "Funnel",
"label.funnel-description": "Förstå omvandlingen och bortfallsfrekvensen för användare.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
+ "label.funnels": "Trattar",
+ "label.goal": "Mål",
+ "label.goals": "Mål",
+ "label.goals-description": "Följ dina mål för sidvisningar och händelser.",
"label.greater-than": "Större än",
"label.greater-than-equals": "Större än eller lika med",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grupperad",
+ "label.hostname": "Värdnamn",
+ "label.includes": "Inkluderar",
+ "label.insight": "Insikt",
"label.insights": "Insikter",
"label.insights-description": "Dyk djupare in i din data genom att använda olika segment och filter.",
"label.is": "Är",
+ "label.is-false": "Är falskt",
"label.is-not": "Är inte",
"label.is-not-set": "Är inte inställd",
"label.is-set": "Är inställd",
+ "label.is-true": "Är sant",
"label.join": "Gå med",
"label.join-team": "Gå med i team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.journey": "Resa",
+ "label.journey-description": "Förstå hur användare navigerar på din webbplats.",
+ "label.journeys": "Resor",
"label.language": "Språk",
"label.languages": "Språk",
"label.laptop": "Bärbar",
+ "label.last-click": "Sista klicket",
"label.last-days": "Senaste {x} dagarna",
"label.last-hours": "Senaste {x} timmarna",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
+ "label.last-months": "Senaste {x} månaderna",
+ "label.last-seen": "Senast sedd",
"label.leave": "Lämna",
"label.leave-team": "Lämna team",
"label.less-than": "Mindre än",
"label.less-than-equals": "Mindre än eller lika med",
+ "label.links": "Länkar",
"label.login": "Logga in",
"label.logout": "Logga ut",
"label.manage": "Manage",
"label.manager": "Manager",
"label.max": "Max",
- "label.member": "Member",
+ "label.maximize": "Expandera",
+ "label.medium": "Medium",
+ "label.member": "Medlem",
"label.members": "Medlemmar",
"label.min": "Min",
"label.mobile": "Mobil",
+ "label.model": "Modell",
"label.more": "Mer",
- "label.my-account": "My account",
+ "label.my-account": "Mitt konto",
"label.my-websites": "Mina webbplatser",
"label.name": "Namn",
"label.new-password": "Nytt lösenord",
- "label.none": "Inga",
+ "label.none": "Ingen",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organisk sökning",
+ "label.organic-shopping": "Organisk shopping",
+ "label.organic-social": "Organisk social",
+ "label.organic-video": "Organisk video",
"label.os": "Operativsystem",
+ "label.other": "Annat",
"label.overview": "Översikt",
"label.owner": "Ägare",
+ "label.page": "Sida",
"label.page-of": "Sida {current} av {total}",
"label.page-views": "Sidvisningar",
"label.pageTitle": "Sidtitel",
"label.pages": "Sidor",
+ "label.paid-ads": "Betalda annonser",
+ "label.paid-search": "Betald sökning",
+ "label.paid-shopping": "Betald shopping",
+ "label.paid-social": "Betald social",
+ "label.paid-video": "Betald video",
"label.password": "Lösenord",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Sökväg",
+ "label.paths": "Sökvägar",
+ "label.pixels": "Pixlar",
"label.powered-by": "Drivs av {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "Föregående",
+ "label.previous-period": "Föregående period",
+ "label.previous-year": "Föregående år",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Egenskaper",
+ "label.property": "Egenskap",
"label.queries": "Frågor",
"label.query": "Fråga",
"label.query-parameters": "Frågeparametrar",
"label.realtime": "Realtid",
+ "label.referral": "Hänvisning",
"label.referrer": "Hänvisare",
"label.referrers": "Hänvisare",
"label.refresh": "Uppdatera",
"label.regenerate": "Förnya",
"label.region": "Region",
"label.regions": "Regioner",
+ "label.remaining": "Återstår",
"label.remove": "Ta bort",
"label.remove-member": "Remove member",
"label.reports": "Rapporter",
@@ -161,9 +211,8 @@
"label.reset-website": "Återställ webbplats",
"label.retention": "Retention",
"label.retention-description": "Mät din webbplats engagemang genom att följa hur ofta användare återvänder.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Intäkter",
+ "label.revenue-description": "Se dina intäkter över tid.",
"label.role": "Roll",
"label.run-query": "Kör sökning",
"label.save": "Spara",
@@ -171,26 +220,35 @@
"label.search": "Sök",
"label.select": "Select",
"label.select-date": "Välj datum",
+ "label.select-filter": "Välj filter",
"label.select-role": "Select role",
"label.select-website": "Välj webbplats",
"label.session": "Session",
+ "label.session-data": "Sessionsdata",
"label.sessions": "Sessioner",
"label.settings": "Inställningar",
+ "label.share": "Dela",
"label.share-url": "Delningslänk",
"label.single-day": "En dag",
+ "label.sms": "SMS",
+ "label.sources": "Källor",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Summa",
"label.tablet": "Surfplatta",
+ "label.tag": "Tagg",
+ "label.tags": "Taggar",
"label.team": "Team",
"label.team-id": "Team ID",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Teamledare",
"label.team-member": "Team-medlem",
"label.team-name": "Team namn",
"label.team-owner": "Team-ägare",
+ "label.team-settings": "Teaminställningar",
"label.team-view-only": "Team view only",
"label.team-websites": "Team webbplatser",
"label.teams": "Team",
+ "label.terms": "Villkor",
"label.theme": "Tema",
"label.this-month": "Denna månad",
"label.this-week": "Denna vecka",
@@ -213,10 +271,7 @@
"label.unknown": "Okänt",
"label.untitled": "Namnlös",
"label.update": "Update",
- "label.url": "Länk",
- "label.urls": "Länkar",
"label.user": "Användare",
- "label.user-property": "User Property",
"label.username": "Användarnamn",
"label.users": "Användare",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Igår",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} {x, plural, one {besökare} other {besökare}} just nu",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Är du säker på att du vill radera {target}?",
"message.confirm-leave": "Är du säker på att du vill lämna {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "All tillhörande data kommer också att raderas.",
"message.error": "Något gick fel.",
"message.event-log": "{event} på {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Gå till inställningar",
"message.incorrect-username-password": "Felaktigt användarnamn/lösenord.",
"message.invalid-domain": "Ogiltig domän",
@@ -259,10 +316,13 @@
"message.no-teams": "Du har inte skapat några team.",
"message.no-users": "Det finns inga användare.",
"message.no-websites-configured": "Du har inte konfigurerat några webbplatser.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Sidan kunde inte hittas.",
"message.reset-website": "För att återställa webbplatsen, skriv {confirmation} i rutan nedan.",
"message.reset-website-warning": "All statistik för webbplatsen tas bort, men spårningskoden förblir oförändrad.",
"message.saved": "Sparat!",
+ "message.sever-error": "Server error",
"message.share-url": "Det här är den offentliga delningslänken för {target}.",
"message.team-already-member": "Du är redan medlem i teamet.",
"message.team-not-found": "Teamet kunde inte hittas.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Användaren har raderats.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "Besökare från {country} med {browser} på {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "Besökare från {country} med {browser} på {os} {device}"
}
diff --git a/src/lang/ta-IN.json b/src/lang/ta-IN.json
index 1e4466b5..9e33d7b6 100644
--- a/src/lang/ta-IN.json
+++ b/src/lang/ta-IN.json
@@ -3,32 +3,47 @@
"label.actions": "செயல்கள்",
"label.activity": "Activity log",
"label.add": "Add",
+ "label.add-board": "Add board",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "வலைத்தளத்தைச் சேர்க்க",
"label.admin": "நிர்வாகியைச் சேர்க்க",
+ "label.affiliate": "Affiliate",
"label.after": "After",
"label.all": "எல்லாம்",
"label.all-time": "All time",
"label.analytics": "Analytics",
+ "label.apply": "Apply",
+ "label.attribution": "Attribution",
+ "label.attribution-description": "See how users engage with your marketing and what drives conversions.",
"label.average": "Average",
"label.back": "பின்னால்",
"label.before": "Before",
+ "label.boards": "Boards",
+ "label.behavior": "நடத்தை",
"label.bounce-rate": "துள்ளல் விகிதம்",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.browsers": "உலாவிகள்",
+ "label.campaigns": "Campaigns",
"label.cancel": "ரத்துசெய்",
"label.change-password": "கடவுச்சொல்லை மாற்று",
+ "label.channels": "Channels",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
+ "label.cohort": "Cohort",
"label.compare": "Compare",
+ "label.compare-dates": "Compare dates",
"label.confirm": "Confirm",
"label.confirm-password": "கடவுச்சொல்லை உறுதிப்படுத்தவும்",
"label.contains": "Contains",
+ "label.content": "Content",
"label.continue": "Continue",
+ "label.conversion": "Conversion",
+ "label.conversion-rate": "Conversion rate",
+ "label.conversion-step": "Conversion step",
"label.count": "Count",
"label.countries": "நாடுகள்",
"label.country": "Country",
@@ -38,6 +53,7 @@
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
+ "label.currency": "Currency",
"label.current": "Current",
"label.current-password": "தற்போதைய கடவுச்சொல்",
"label.custom-range": "தனிப்பயன் வேறுபாட்டெல்லை",
@@ -57,19 +73,26 @@
"label.details": "Details",
"label.device": "Device",
"label.devices": "சாதனங்கள்",
+ "label.direct": "Direct",
"label.dismiss": "நீக்கு",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "Does not contain",
+ "label.does-not-include": "Does not include",
+ "label.doest-not-exist": "Does not exist",
"label.domain": "கள முகவரி",
"label.dropoff": "Dropoff",
"label.edit": "திருத்துதல்",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
+ "label.email": "Email",
"label.enable-share-url": "கள முகவரியை பகிரலாம்",
"label.end-step": "End Step",
"label.entry": "Entry URL",
"label.event": "Event",
"label.event-data": "Event data",
+ "label.event-name": "Event name",
"label.events": "நிகழ்வுகள்",
+ "label.exists": "Exists",
"label.exit": "Exit URL",
"label.false": "False",
"label.field": "Field",
@@ -78,29 +101,37 @@
"label.filter-combined": "ஒருங்கிணைந்த",
"label.filter-raw": "மூல",
"label.filters": "Filters",
+ "label.first-click": "First click",
"label.first-seen": "First seen",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
+ "label.funnels": "Funnels",
"label.goal": "Goal",
"label.goals": "Goals",
"label.goals-description": "Track your goals for pageviews and events.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grouped",
+ "label.hostname": "Hostname",
+ "label.includes": "Includes",
+ "label.insight": "Insight",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
+ "label.is-false": "Is false",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
+ "label.is-true": "Is true",
"label.join": "Join",
"label.join-team": "Join team",
"label.journey": "Journey",
"label.journey-description": "Understand how users navigate through your website.",
+ "label.journeys": "Journeys",
"label.language": "Language",
"label.languages": "Languages",
"label.laptop": "மடிக்கணினி",
+ "label.last-click": "Last click",
"label.last-days": "முந்தைய {x} நாட்கள்",
"label.last-hours": "முந்தைய {x} மணி",
"label.last-months": "Last {x} months",
@@ -109,15 +140,19 @@
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
+ "label.links": "Links",
"label.login": "உள்நுழைய",
"label.logout": "வெளியேறு",
"label.manage": "Manage",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Expand",
+ "label.medium": "Medium",
"label.member": "Member",
"label.members": "Members",
"label.min": "Min",
"label.mobile": "கைபேசி",
+ "label.model": "Model",
"label.more": "மேலும்",
"label.my-account": "My account",
"label.my-websites": "My websites",
@@ -126,16 +161,29 @@
"label.none": "None",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organic search",
+ "label.organic-shopping": "Organic shopping",
+ "label.organic-social": "Organic social",
+ "label.organic-video": "Organic video",
"label.os": "OS",
+ "label.other": "Other",
"label.overview": "Overview",
"label.owner": "Owner",
+ "label.page": "Page",
"label.page-of": "Page {current} of {total}",
"label.page-views": "பக்க காட்சிகள்",
"label.pageTitle": "Page title",
"label.pages": "பக்கங்கள்",
+ "label.paid-ads": "Paid ads",
+ "label.paid-search": "Paid search",
+ "label.paid-shopping": "Paid shopping",
+ "label.paid-social": "Paid social",
+ "label.paid-video": "Paid video",
"label.password": "கடவுச்சொல்",
"label.path": "Path",
"label.paths": "Paths",
+ "label.pixels": "Pixels",
"label.powered-by": "{name} ஆல் இயக்கப்படுகிறது",
"label.previous": "Previous",
"label.previous-period": "Previous period",
@@ -147,12 +195,14 @@
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.realtime": "தற்போதைய",
+ "label.referral": "Referral",
"label.referrer": "Referrer",
"label.referrers": "குறிப்பிடுவோர்",
"label.refresh": "புதுப்பிப்பு",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Remaining",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -163,7 +213,6 @@
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "சேமி",
@@ -171,26 +220,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Select filter",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.session": "Session",
+ "label.session-data": "Session data",
"label.sessions": "Sessions",
"label.settings": "அமைப்புகள்",
+ "label.share": "Share",
"label.share-url": "வலைத்தள களத்தைப் பகிரவும்",
"label.single-day": "ஒரு நாள்",
+ "label.sms": "SMS",
+ "label.sources": "Sources",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "கையடக்க கணினி",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Team settings",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Terms",
"label.theme": "Theme",
"label.this-month": "இந்த மாதம்",
"label.this-week": "இந்த வாரம்",
@@ -213,10 +271,7 @@
"label.unknown": "தெரியாத",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "பயனர்பெயர்",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} தற்போதைய {x, plural, one {ஒன்று} other {மற்ற}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "நீங்கள் நிச்சயமாக {target} நீக்க விரும்புகிறீர்களா?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "தொடர்புடைய எல்லா தரவும் நீக்கப்படும்.",
"message.error": "ஏதோ தவறு நடந்துவிட்டது.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "அமைப்புகளுக்குச் செல்லவும்",
"message.incorrect-username-password": "தவறான பயனர்பெயர் / கடவுச்சொல்.",
"message.invalid-domain": "தவறான கள முகவரி",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "உங்களிடம் எந்த வலைத்தளங்களும் கட்டமைக்கப்படவில்லை.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "பக்கம் கிடைக்கவில்லை.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.saved": "வெற்றிகரமாக சேமிக்கப்பட்டது.",
+ "message.sever-error": "Server error",
"message.share-url": "{target} இது பொதுவில் பகிரும் வலைத்தள முகவரி.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "{country}வில் இருந்து பார்வையாளர் {browser} ஐ {os} {device}லில் பயன்படுத்துகிறார்",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "{country}வில் இருந்து பார்வையாளர் {browser} ஐ {os} {device}லில் பயன்படுத்துகிறார்"
}
diff --git a/src/lang/th-TH.json b/src/lang/th-TH.json
index 7cc35045..b94ca905 100644
--- a/src/lang/th-TH.json
+++ b/src/lang/th-TH.json
@@ -3,32 +3,47 @@
"label.actions": "การกระทำ",
"label.activity": "Activity log",
"label.add": "Add",
+ "label.add-board": "Add board",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "เพิ่มเว็บไซต์",
"label.admin": "ผู้ดูแลระบบ",
+ "label.affiliate": "Affiliate",
"label.after": "After",
"label.all": "ทั้งหมด",
"label.all-time": "ทุกช่วงเวลา",
"label.analytics": "Analytics",
+ "label.apply": "Apply",
+ "label.attribution": "Attribution",
+ "label.attribution-description": "See how users engage with your marketing and what drives conversions.",
"label.average": "Average",
"label.back": "ย้อนกลับ",
"label.before": "Before",
+ "label.behavior": "พฤติกรรม",
+ "label.boards": "Boards",
"label.bounce-rate": "อัตราตีกลับ",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.browsers": "เบราว์เซอร์",
+ "label.campaigns": "Campaigns",
"label.cancel": "ยกเลิก",
"label.change-password": "เปลี่ยนรหัสผ่าน",
+ "label.channels": "Channels",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
+ "label.cohort": "Cohort",
"label.compare": "Compare",
+ "label.compare-dates": "Compare dates",
"label.confirm": "Confirm",
"label.confirm-password": "ยืนยันรหัสผ่าน",
"label.contains": "Contains",
+ "label.content": "Content",
"label.continue": "Continue",
+ "label.conversion": "Conversion",
+ "label.conversion-rate": "Conversion rate",
+ "label.conversion-step": "Conversion step",
"label.count": "Count",
"label.countries": "ประเทศ",
"label.country": "Country",
@@ -38,6 +53,7 @@
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
+ "label.currency": "Currency",
"label.current": "Current",
"label.current-password": "รหัสผ่านปัจจุบัน",
"label.custom-range": "กำหนดช่วงเวลา",
@@ -57,19 +73,26 @@
"label.details": "Details",
"label.device": "Device",
"label.devices": "อุปกรณ์",
+ "label.direct": "Direct",
"label.dismiss": "ยกเลิก",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "Does not contain",
+ "label.does-not-include": "Does not include",
+ "label.doest-not-exist": "Does not exist",
"label.domain": "โดเมน",
"label.dropoff": "Dropoff",
"label.edit": "แก้ไข",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
+ "label.email": "Email",
"label.enable-share-url": "เปิดใช้งานการแชร์ลิงก์",
"label.end-step": "End Step",
"label.entry": "Entry URL",
"label.event": "Event",
"label.event-data": "Event data",
+ "label.event-name": "Event name",
"label.events": "เหตุการณ์",
+ "label.exists": "Exists",
"label.exit": "Exit URL",
"label.false": "False",
"label.field": "Field",
@@ -78,29 +101,37 @@
"label.filter-combined": "ข้อมูลรวม",
"label.filter-raw": "ข้อมูลดิบ",
"label.filters": "Filters",
+ "label.first-click": "First click",
"label.first-seen": "First seen",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
+ "label.funnels": "Funnels",
"label.goal": "Goal",
"label.goals": "Goals",
"label.goals-description": "Track your goals for pageviews and events.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grouped",
+ "label.hostname": "Hostname",
+ "label.includes": "Includes",
+ "label.insight": "Insight",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
+ "label.is-false": "Is false",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
+ "label.is-true": "Is true",
"label.join": "Join",
"label.join-team": "Join team",
"label.journey": "Journey",
"label.journey-description": "Understand how users navigate through your website.",
+ "label.journeys": "Journeys",
"label.language": "ภาษา",
"label.languages": "ภาษา",
"label.laptop": "แล็ปท็อป",
+ "label.last-click": "Last click",
"label.last-days": "{x} วันที่ผ่านมา",
"label.last-hours": "{x} ชั่วโมงที่ผ่านมา",
"label.last-months": "Last {x} months",
@@ -109,15 +140,19 @@
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
+ "label.links": "Links",
"label.login": "เข้าสู่ระบบ",
"label.logout": "ออกจากระบบ",
"label.manage": "Manage",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Expand",
+ "label.medium": "Medium",
"label.member": "Member",
"label.members": "Members",
"label.min": "Min",
"label.mobile": "โทรศัพท์มือถือ",
+ "label.model": "Model",
"label.more": "เพิ่มเติม",
"label.my-account": "My account",
"label.my-websites": "My websites",
@@ -126,16 +161,29 @@
"label.none": "ไม่ได้กำหนด",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organic search",
+ "label.organic-shopping": "Organic shopping",
+ "label.organic-social": "Organic social",
+ "label.organic-video": "Organic video",
"label.os": "OS",
+ "label.other": "Other",
"label.overview": "Overview",
"label.owner": "เจ้าของ",
+ "label.page": "Page",
"label.page-of": "Page {current} of {total}",
"label.page-views": "การเข้าชม",
"label.pageTitle": "Page title",
"label.pages": "หน้าเพจ",
+ "label.paid-ads": "Paid ads",
+ "label.paid-search": "Paid search",
+ "label.paid-shopping": "Paid shopping",
+ "label.paid-social": "Paid social",
+ "label.paid-video": "Paid video",
"label.password": "รหัสผ่าน",
"label.path": "Path",
"label.paths": "Paths",
+ "label.pixels": "Pixels",
"label.powered-by": "ขับเคลื่อนโดย {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
@@ -147,12 +195,14 @@
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.realtime": "เรียลไทม์",
+ "label.referral": "Referral",
"label.referrer": "Referrer",
"label.referrers": "แหล่งที่มา",
"label.refresh": "รีเฟรช",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Remaining",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -163,7 +213,6 @@
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "บันทึก",
@@ -171,26 +220,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Select filter",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.session": "Session",
+ "label.session-data": "Session data",
"label.sessions": "Sessions",
"label.settings": "ตั้งค่า",
+ "label.share": "Share",
"label.share-url": "แชร์ลิงก์",
"label.single-day": "วันที่",
+ "label.sms": "SMS",
+ "label.sources": "Sources",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "แท็บเล็ต",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Team settings",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Terms",
"label.theme": "ธีม",
"label.this-month": "เดือนปัจจุบัน",
"label.this-week": "สัปดาห์ปัจจุบัน",
@@ -213,10 +271,7 @@
"label.unknown": "ไม่รู้จัก",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "ชื่อผู้ใช้",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "มีผู้ใช้งาน {x} {x, plural, one {คนในขณะนี้} other {คนในขณะนี้}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "คุณแน่ใจหรือไม่ว่าต้องการลบ {target} ?",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "ข้อมูลที่เกี่ยวข้องทั้งหมดจะถูกลบ.",
"message.error": "เกิดข้อผิดพลาด.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "ไปที่การตั้งค่า",
"message.incorrect-username-password": "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง.",
"message.invalid-domain": "โดเมนไม่ถูกต้อง",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "คุณยังไม่ได้ตั้งค่าเว็บไซต์ใด ๆ ไว้.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "ไม่พบหน้านี้.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "สถิติทั้งหมดสำหรับเว็บไซต์นี้จะถูกลบออก แต่โค้ดสำหรับใช้ติดตามของคุณจะยังคงอยู่เหมือนเดิม.",
"message.saved": "บันทึกข้อมูลเรียบร้อย.",
+ "message.sever-error": "Server error",
"message.share-url": "นี่คือลิงก์ที่แชร์แบบสาธารณะสำหรับ {target}.",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "ผู้เข้าชมจาก {country} กำลังใช้งานผ่าน {browser} บน {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "ผู้เข้าชมจาก {country} กำลังใช้งานผ่าน {browser} บน {os} {device}"
}
diff --git a/src/lang/tr-TR.json b/src/lang/tr-TR.json
index 34a81f75..3a2dce4b 100644
--- a/src/lang/tr-TR.json
+++ b/src/lang/tr-TR.json
@@ -3,33 +3,48 @@
"label.actions": "Hareketler",
"label.activity": "Aktivite Kaydı",
"label.add": "Ekle",
+ "label.add-board": "Pano ekle",
"label.add-description": "Açıklama ekle",
"label.add-member": "Üye ekle",
"label.add-step": "Adım ekle",
"label.add-website": "Web sitesi ekle",
"label.admin": "Administrator",
+ "label.affiliate": "Ortak",
"label.after": "Sonra",
"label.all": "Tümü",
"label.all-time": "Tüm zamanlar",
"label.analytics": "Analitik",
+ "label.apply": "Uygula",
+ "label.attribution": "Atıf",
+ "label.attribution-description": "Kullanıcıların pazarlamanızla nasıl etkileşime girdiğini ve dönüşümleri neyin tetiklediğini görün.",
"label.average": "Ortalama",
"label.back": "Geri",
"label.before": "Önce",
+ "label.behavior": "Davranış",
+ "label.boards": "Panolar",
"label.bounce-rate": "Tek sayfa ziyaret oranı",
"label.breakdown": "Dağılım",
"label.browser": "Tarayıcı",
"label.browsers": "Tarayıcılar",
+ "label.campaigns": "Kampanyalar",
"label.cancel": "İptal",
"label.change-password": "Şifre değiştir",
+ "label.channels": "Kanallar",
"label.cities": "Şehirler",
"label.city": "Şehir",
"label.clear-all": "Hepsini temizle",
- "label.compare": "Compare",
+ "label.cohort": "Kohort",
+ "label.compare": "Karşılaştır",
+ "label.compare-dates": "Tarihleri karşılaştır",
"label.confirm": "Onayla",
"label.confirm-password": "Parolayı onayla",
"label.contains": "İçeriği",
+ "label.content": "İçerik",
"label.continue": "Devam et",
- "label.count": "Count",
+ "label.conversion": "Dönüşüm",
+ "label.conversion-rate": "Dönüşüm oranı",
+ "label.conversion-step": "Dönüşüm adımı",
+ "label.count": "Adet",
"label.countries": "Ülkeler",
"label.country": "Ülke",
"label.create": "Oluştur",
@@ -38,7 +53,8 @@
"label.create-user": "Kullanıcı oluştur",
"label.created": "Oluşturuldu",
"label.created-by": "Tarafından oluşturldu",
- "label.current": "Current",
+ "label.currency": "Para birimi",
+ "label.current": "Mevcut",
"label.current-password": "Mevcut parola",
"label.custom-range": "Özelleştirilmiş aralık",
"label.dashboard": "Kontrol Paneli",
@@ -57,19 +73,26 @@
"label.details": "Detaylar",
"label.device": "Cihaz",
"label.devices": "Cihazlar",
+ "label.direct": "Doğrudan",
"label.dismiss": "Reddet",
+ "label.distinct-id": "Benzersiz ID",
"label.does-not-contain": "İçermez",
+ "label.does-not-include": "İçermiyor",
+ "label.doest-not-exist": "Mevcut değil",
"label.domain": "Alan adı",
"label.dropoff": "Bırakma",
"label.edit": "Düzenle",
"label.edit-dashboard": "Kontrol panelini düzenle",
"label.edit-member": "Üyeyi düzenle",
+ "label.email": "Email",
"label.enable-share-url": "Anonim paylaşım URL'i aktif",
"label.end-step": "End Step",
"label.entry": "Entry URL",
"label.event": "Olay",
"label.event-data": "Olay verisi",
+ "label.event-name": "Olay adı",
"label.events": "Olaylar",
+ "label.exists": "Mevcut",
"label.exit": "Exit URL",
"label.false": "Yanlış",
"label.field": "Alan",
@@ -78,46 +101,58 @@
"label.filter-combined": "Birleşik filtre",
"label.filter-raw": "Ham filtre",
"label.filters": "Filtreler",
+ "label.first-click": "İlk tıklama",
"label.first-seen": "First seen",
"label.funnel": "Huni",
"label.funnel-description": "Kullanıcıların dönüşüm ve ayrılma oranlarını anlayın.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
+ "label.funnels": "Huniler",
+ "label.goal": "Hedef",
+ "label.goals": "Hedefler",
+ "label.goals-description": "Sayfa görüntüleme ve olaylar için hedeflerinizi takip edin.",
"label.greater-than": "Büyüktür",
"label.greater-than-equals": "Büyük veya eşittir",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Gruplandırılmış",
+ "label.hostname": "Sunucu adı",
+ "label.includes": "İçerir",
+ "label.insight": "İçgörü",
"label.insights": "Insights",
"label.insights-description": "Segmentleri ve filtreleri kullanarak verilerinizi derinlemesine inceleyin.",
"label.is": "Is",
+ "label.is-false": "Yanlış",
"label.is-not": "Değil",
"label.is-not-set": "Ayarlanmamış",
"label.is-set": "Ayarlandı",
+ "label.is-true": "Doğru",
"label.join": "Katıl",
"label.join-team": "Takıma katıl",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.journey": "Yolculuk",
+ "label.journey-description": "Kullanıcıların sitenizde nasıl gezindiğini anlayın.",
+ "label.journeys": "Yolculuklar",
"label.language": "Dil",
"label.languages": "Diller",
"label.laptop": "Dizüstü",
+ "label.last-click": "Son tıklama",
"label.last-days": "Son {x} gün",
"label.last-hours": "Son {x} saat",
"label.last-months": "Son {x} ay",
- "label.last-seen": "Last seen",
+ "label.last-seen": "Son görüldü",
"label.leave": "Ayrıl",
"label.leave-team": "Takımdan Ayrıl",
"label.less-than": "Küçüktür",
"label.less-than-equals": "Küçük veya eşittir",
+ "label.links": "Bağlantılar",
"label.login": "Giriş Yap",
"label.logout": "Çıkış Yap",
"label.manage": "Yönet",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Genişlet",
+ "label.medium": "Orta",
"label.member": "Üye",
"label.members": "Üyeler",
"label.min": "Min",
"label.mobile": "Mobil Cihaz",
+ "label.model": "Model",
"label.more": "Detaylı göster",
"label.my-account": "Hesabım",
"label.my-websites": "Web sitelerim",
@@ -126,33 +161,48 @@
"label.none": "Yok",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "TAMAM",
+ "label.online": "Online",
+ "label.organic-search": "Organik arama",
+ "label.organic-shopping": "Organik alışveriş",
+ "label.organic-social": "Organik sosyal",
+ "label.organic-video": "Organik video",
"label.os": "OS",
+ "label.other": "Diğer",
"label.overview": "Genel bakış",
"label.owner": "Sahibi",
+ "label.page": "Sayfa",
"label.page-of": "{total} sayfada {current} ",
"label.page-views": "Sayfa görünümü",
"label.pageTitle": "Sayfa başlığı",
"label.pages": "Sayfalar",
+ "label.paid-ads": "Ücretli reklamlar",
+ "label.paid-search": "Ücretli arama",
+ "label.paid-shopping": "Ücretli alışveriş",
+ "label.paid-social": "Ücretli sosyal",
+ "label.paid-video": "Ücretli video",
"label.password": "Parola",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "Yol",
+ "label.paths": "Yollar",
+ "label.pixels": "Pikseller",
"label.powered-by": "Sağlayıcı: {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "Önceki",
+ "label.previous-period": "Önceki dönem",
+ "label.previous-year": "Önceki yıl",
"label.profile": "Profil",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Özellikler",
+ "label.property": "Özellik",
"label.queries": "Sorgular",
"label.query": "Sorgu",
"label.query-parameters": "Sorgu parametreleri",
"label.realtime": "Gerçek Zamanlı",
+ "label.referral": "Yönlendirme",
"label.referrer": "Referrer",
"label.referrers": "Yönlendirenler",
"label.refresh": "Yenile",
"label.regenerate": "Yeniden Oluştur",
"label.region": "Bölge",
"label.regions": "Bölgeler",
+ "label.remaining": "Kalan",
"label.remove": "Kaldır",
"label.remove-member": "Üyeyi kaldır",
"label.reports": "Raporlar",
@@ -161,9 +211,8 @@
"label.reset-website": "İstatistikleri sıfırla",
"label.retention": "Geri dönüş",
"label.retention-description": "Kullanıcıların ne sıklıkla geri döndüğünü takip ederek web sitenizin kalıcılığını ölçün.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Gelir",
+ "label.revenue-description": "Gelirinizi zaman içinde inceleyin.",
"label.role": "Rol",
"label.run-query": "Sorgu çalıştır",
"label.save": "Kaydet",
@@ -171,26 +220,35 @@
"label.search": "Ara",
"label.select": "Seç",
"label.select-date": "Tarih seç",
+ "label.select-filter": "Filtre seç",
"label.select-role": "Rol seç",
"label.select-website": "Web sitesi seç",
- "label.session": "Session",
+ "label.session": "Oturum",
+ "label.session-data": "Oturum verisi",
"label.sessions": "Sessions",
"label.settings": "Ayarlar",
+ "label.share": "Paylaş",
"label.share-url": "Paylaşım adresi",
"label.single-day": "Tekil gün",
+ "label.sms": "SMS",
+ "label.sources": "Kaynaklar",
"label.start-step": "Start Step",
"label.steps": "Adımlar",
"label.sum": "Toplam",
"label.tablet": "Tablet",
+ "label.tag": "Etiket",
+ "label.tags": "Etiketler",
"label.team": "Takım",
"label.team-id": "Takım ID",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Takım yöneticisi",
"label.team-member": "Takım üyesi",
"label.team-name": "Takım ismi",
"label.team-owner": "Takım sahibi",
+ "label.team-settings": "Takım ayarları",
"label.team-view-only": "Yalnızca ekip görünümü",
"label.team-websites": "Takım web siteleri",
"label.teams": "Takımlar",
+ "label.terms": "Koşullar",
"label.theme": "Tema",
"label.this-month": "Bu ay",
"label.this-week": "Bu hafta",
@@ -213,10 +271,7 @@
"label.unknown": "Bilinmeyen",
"label.untitled": "İsimsiz",
"label.update": "Güncelle",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Kullanıcı",
- "label.user-property": "User Property",
"label.username": "Kullanıcı adı",
"label.users": "Kullanıcılar",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Dün",
"message.action-confirmation": "Onaylamak için aşağıdaki kutuya {confirmation} yazın.",
"message.active-users": "{x} aktif ziyaretçi",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?",
"message.confirm-leave": "{target} kaydından ayrılmak istediğinizden emin misiniz?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "İlişkili tüm veriler de silinecektir.",
"message.error": "Bir şeyler ters gitti!",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Ayarlara git",
"message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.",
"message.invalid-domain": "Geçersiz alan adı",
@@ -259,10 +316,13 @@
"message.no-teams": "Herhangi bir takım oluşturmadınız.",
"message.no-users": "Kullanıcı yok.",
"message.no-websites-configured": "Henüz hiç web sitesi tanımlamadınız",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Sayfa bulunamadı.",
"message.reset-website": "Bu websitesini sıfılamak için aşağıdaki kutuya {confirmation} yazın.",
"message.reset-website-warning": "Bu web sitesi için tüm istatistikler silinecek, ancak izleme kodunuz bozulmadan kalacaktır.",
"message.saved": "Başarıyla kaydedildi.",
+ "message.sever-error": "Server error",
"message.share-url": "{target} için kullanılabilir anonim paylaşım adresidir.",
"message.team-already-member": "Zaten bu takımın üyesisiniz",
"message.team-not-found": "Takım bulunamadı",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Bu web sitesinin aktarılacağı takımı seçin.",
"message.transfer-website": "Web sitesi sahipliğini hesabınıza veya başka bir takıma aktarın",
"message.triggered-event": "Tetiklenen olay",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Kullanıcı silindi.",
"message.viewed-page": "Görüntülenen sayfa",
- "message.visitor-log": "Yeni ziyaretçi: {country}, {os}, {device}, {browser}",
- "message.visitors-dropped-off": "Bırakan ziyaretçiler"
+ "message.visitor-log": "Yeni ziyaretçi: {country}, {os}, {device}, {browser}"
}
diff --git a/src/lang/uk-UA.json b/src/lang/uk-UA.json
index c6843055..768015be 100644
--- a/src/lang/uk-UA.json
+++ b/src/lang/uk-UA.json
@@ -3,33 +3,48 @@
"label.actions": "Дії",
"label.activity": "Журнал",
"label.add": "Додати",
+ "label.add-board": "Додати дошку",
"label.add-description": "Додати опис",
"label.add-member": "Додати учасника",
"label.add-step": "Додати крок",
"label.add-website": "Додати сайт",
"label.admin": "Адміністратор",
+ "label.affiliate": "Партнер",
"label.after": "Після",
"label.all": "Всі",
"label.all-time": "Весь час",
"label.analytics": "Аналітика",
+ "label.apply": "Застосувати",
+ "label.attribution": "Атрибуція",
+ "label.attribution-description": "Дивіться, як користувачі взаємодіють з вашим маркетингом і що сприяє конверсіям.",
"label.average": "Середній",
"label.back": "Назад",
"label.before": "До",
+ "label.behavior": "Поведінка",
+ "label.boards": "Дошки",
"label.bounce-rate": "Показник відмов",
"label.breakdown": "Розподіл",
"label.browser": "Браузер",
"label.browsers": "Браузери",
+ "label.campaigns": "Кампанії",
"label.cancel": "Відмінити",
"label.change-password": "Змінити пароль",
+ "label.channels": "Канали",
"label.cities": "Міста",
"label.city": "Місто",
"label.clear-all": "Очистити все",
- "label.compare": "Compare",
+ "label.cohort": "Когорта",
+ "label.compare": "Порівняти",
+ "label.compare-dates": "Порівняти дати",
"label.confirm": "Підтвердити",
"label.confirm-password": "Підтвердити пароль",
"label.contains": "Містить",
+ "label.content": "Вміст",
"label.continue": "Продовжити",
- "label.count": "Count",
+ "label.conversion": "Конверсія",
+ "label.conversion-rate": "Рівень конверсії",
+ "label.conversion-step": "Крок конверсії",
+ "label.count": "Кількість",
"label.countries": "Країни",
"label.country": "Країна",
"label.create": "Створити",
@@ -38,7 +53,8 @@
"label.create-user": "Створити користувача",
"label.created": "Створено",
"label.created-by": "Створено",
- "label.current": "Current",
+ "label.currency": "Валюта",
+ "label.current": "Поточний",
"label.current-password": "Поточний пароль",
"label.custom-range": "Довільний період",
"label.dashboard": "Інформаційна панель",
@@ -57,19 +73,26 @@
"label.details": "Деталі",
"label.device": "Пристрій",
"label.devices": "Пристрої",
+ "label.direct": "Прямий",
"label.dismiss": "Відхилити",
+ "label.distinct-id": "Унікальний ID",
"label.does-not-contain": "Не містить",
+ "label.does-not-include": "Не включає",
+ "label.doest-not-exist": "Не існує",
"label.domain": "Домен",
"label.dropoff": "Відсів",
"label.edit": "Редагувати",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
- "label.enable-share-url": "Enable share URL",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
+ "label.edit-dashboard": "Редагувати панель",
+ "label.edit-member": "Редагувати учасника",
+ "label.email": "Email",
+ "label.enable-share-url": "Увімкнути спільне посилання",
+ "label.end-step": "Кінцевий крок",
+ "label.entry": "Вхідний URL",
"label.event": "Подія",
"label.event-data": "Дані події",
+ "label.event-name": "Назва події",
"label.events": "Події",
+ "label.exists": "Існує",
"label.exit": "Exit URL",
"label.false": "False",
"label.field": "Поле",
@@ -78,46 +101,58 @@
"label.filter-combined": "Об'єднані",
"label.filter-raw": "Сирі дані",
"label.filters": "Фільтри",
+ "label.first-click": "Перший клік",
"label.first-seen": "First seen",
"label.funnel": "Воронка",
"label.funnel-description": "Зрозуміти рівень конверсії та відсіву користувачів.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
+ "label.funnels": "Воронки",
+ "label.goal": "Мета",
+ "label.goals": "Мети",
+ "label.goals-description": "Відстежуйте свої цілі для переглядів сторінок і подій.",
"label.greater-than": "Більше ніж",
"label.greater-than-equals": "Більше або рівно",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Груповано",
+ "label.hostname": "Ім'я хоста",
+ "label.includes": "Включає",
+ "label.insight": "Інсайт",
"label.insights": "Інсайти",
"label.insights-description": "Зануртеся глибше у свої дані за допомогою сегментів та фільтрів.",
"label.is": "Є",
+ "label.is-false": "Хибно",
"label.is-not": "Не є",
"label.is-not-set": "Не встановлено",
"label.is-set": "Встановлено",
+ "label.is-true": "Правдиво",
"label.join": "Приєднатись",
"label.join-team": "Приєднатись до команди",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
+ "label.journey": "Шлях",
+ "label.journey-description": "Зрозумійте, як користувачі переміщаються вашим сайтом.",
+ "label.journeys": "Шляхи",
"label.language": "Мова",
"label.languages": "Мови",
"label.laptop": "Ноутбук",
+ "label.last-click": "Останній клік",
"label.last-days": "Останні {x} днів",
"label.last-hours": "Останні {x} годин",
"label.last-months": "Останні {x} місяців",
- "label.last-seen": "Last seen",
+ "label.last-seen": "Останній перегляд",
"label.leave": "Покинути",
"label.leave-team": "Покинути команду",
"label.less-than": "Менше ніж",
"label.less-than-equals": "Менше або дорівнює",
+ "label.links": "Посилання",
"label.login": "Увійти",
"label.logout": "Вийти",
"label.manage": "Керувати",
"label.manager": "Manager",
"label.max": "Макс.",
+ "label.maximize": "Розгорнути",
+ "label.medium": "Середній",
"label.member": "Учасник",
"label.members": "Учасники",
"label.min": "Мін.",
"label.mobile": "Мобільний",
+ "label.model": "Модель",
"label.more": "Більше",
"label.my-account": "Мій обліковий запис",
"label.my-websites": "Мої сайти",
@@ -126,33 +161,48 @@
"label.none": "Нічого",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Органічний пошук",
+ "label.organic-shopping": "Органічні покупки",
+ "label.organic-social": "Органічні соцмережі",
+ "label.organic-video": "Органічне відео",
"label.os": "ОС",
+ "label.other": "Інше",
"label.overview": "Огляд",
"label.owner": "Власник",
+ "label.page": "Сторінка",
"label.page-of": "Сторінка {current} з {total}",
"label.page-views": "Перегляди сторінок",
"label.pageTitle": "Заголовок сторінки",
"label.pages": "Сторінки",
+ "label.paid-ads": "Платна реклама",
+ "label.paid-search": "Платний пошук",
+ "label.paid-shopping": "Платні покупки",
+ "label.paid-social": "Платні соцмережі",
+ "label.paid-video": "Платне відео",
"label.password": "Пароль",
"label.path": "Path",
"label.paths": "Paths",
+ "label.pixels": "Пікселі",
"label.powered-by": "На базі {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.previous": "Попередній",
+ "label.previous-period": "Попередній період",
+ "label.previous-year": "Попередній рік",
"label.profile": "Профіль",
- "label.properties": "Properties",
- "label.property": "Property",
+ "label.properties": "Властивості",
+ "label.property": "Властивість",
"label.queries": "Запити",
"label.query": "Запит",
"label.query-parameters": "Параметри запиту",
"label.realtime": "У реальному часі",
+ "label.referral": "Реферал",
"label.referrer": "Джерело",
"label.referrers": "Джерела",
"label.refresh": "Оновити",
"label.regenerate": "Згенерувати знову",
"label.region": "Регіон",
"label.regions": "Регіони",
+ "label.remaining": "Залишилось",
"label.remove": "Видалити",
"label.remove-member": "Видалити користувача",
"label.reports": "Звіти",
@@ -161,9 +211,8 @@
"label.reset-website": "Скинути статистику сайту",
"label.retention": "Липкість",
"label.retention-description": "Виміряйте липкість вашого сайту, відстежуючи, як часто користувачі повертаються на нього.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
+ "label.revenue": "Дохід",
+ "label.revenue-description": "Перегляньте свій дохід за певний період.",
"label.role": "Роль",
"label.run-query": "Виконати запит",
"label.save": "Зберегти",
@@ -171,26 +220,35 @@
"label.search": "Пошук",
"label.select": "Вибрати",
"label.select-date": "Вибрати дату",
+ "label.select-filter": "Вибрати фільтр",
"label.select-role": "Вибрати роль",
"label.select-website": "Вибрати сайт",
- "label.session": "Session",
+ "label.session": "Сесія",
+ "label.session-data": "Дані сесії",
"label.sessions": "Сесії",
"label.settings": "Налаштування",
+ "label.share": "Поділитися",
"label.share-url": "Поділитися посилання",
"label.single-day": "Один день",
+ "label.sms": "SMS",
+ "label.sources": "Джерела",
"label.start-step": "Start Step",
"label.steps": "Кроки",
"label.sum": "Сума",
"label.tablet": "Планшет",
+ "label.tag": "Тег",
+ "label.tags": "Теги",
"label.team": "Команда",
"label.team-id": "Ідентифікатор команди",
- "label.team-manager": "Team manager",
+ "label.team-manager": "Менеджер команди",
"label.team-member": "Учасник команди",
"label.team-name": "Назва команди",
"label.team-owner": "Власник команди",
+ "label.team-settings": "Налаштування команди",
"label.team-view-only": "Тільки для командного перегляду",
"label.team-websites": "Сайти команди",
"label.teams": "Команди",
+ "label.terms": "Умови",
"label.theme": "Тема",
"label.this-month": "Цього місяця",
"label.this-week": "Цього тижня",
@@ -213,10 +271,7 @@
"label.unknown": "Невідомо",
"label.untitled": "Без заголовку",
"label.update": "Оновлення",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "Користувач",
- "label.user-property": "User Property",
"label.username": "Ім'я користувача",
"label.users": "Користувачі",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Вчора",
"message.action-confirmation": "Введіть {confirmation} у полі нижче, щоб підтвердити.",
"message.active-users": "{x} поточних відвідувачів",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?",
"message.confirm-leave": "Ви впевнені, що бажаєте покинути {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "Усі пов'язані дані будуть видалені також.",
"message.error": "Щось пішло не так.",
"message.event-log": "{event} на {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "Перейти до налаштувань",
"message.incorrect-username-password": "Невірне ім'я користувача або пароль.",
"message.invalid-domain": "Некоректний домен",
@@ -259,10 +316,13 @@
"message.no-teams": "Ви не створили жодної команди.",
"message.no-users": "Немає жодного користувача.",
"message.no-websites-configured": "У вас немає налаштованих сайтів.",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Сторінку не знайдено.",
"message.reset-website": "Щоб скинути налаштування цього веб-сайту, введіть {confirmation} у полі нижче для підтвердження.",
"message.reset-website-warning": "Вся статистика для цього сайту буде видалена, проте код відслідковування буде продовжувати працювати.",
"message.saved": "Збережено успішно.",
+ "message.sever-error": "Server error",
"message.share-url": "Це публічне посилання для {target}.",
"message.team-already-member": "Ви вже є членом команди.",
"message.team-not-found": "Команду не знайдено.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Виберіть команду, до якої ви хочете передати цей веб-сайт.",
"message.transfer-website": "Передайте право власності на сайт своєму акаунту або іншій команді.",
"message.triggered-event": "Подія, що спрацювала",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "Користувача видалено.",
"message.viewed-page": "Переглянута сторінка",
- "message.visitor-log": "Відвідувач з {country} використовуючи {browser} на {os} {device}",
- "message.visitors-dropped-off": "Користувачі що відсіялись"
+ "message.visitor-log": "Відвідувач з {country} використовуючи {browser} на {os} {device}"
}
diff --git a/src/lang/ur-PK.json b/src/lang/ur-PK.json
index 138ec1b3..5cc31212 100644
--- a/src/lang/ur-PK.json
+++ b/src/lang/ur-PK.json
@@ -3,32 +3,47 @@
"label.actions": "اعمال",
"label.activity": "Activity log",
"label.add": "Add",
+ "label.add-board": "Add board",
"label.add-description": "Add description",
"label.add-member": "Add member",
"label.add-step": "Add step",
"label.add-website": "ویب سائٹ کا اضافہ کریں",
"label.admin": "منتظم",
+ "label.affiliate": "Affiliate",
"label.after": "After",
"label.all": "تمام",
"label.all-time": "تمام وقت",
"label.analytics": "Analytics",
+ "label.apply": "Apply",
+ "label.attribution": "Attribution",
+ "label.attribution-description": "See how users engage with your marketing and what drives conversions.",
"label.average": "Average",
"label.back": "پیچھے",
"label.before": "Before",
+ "label.behavior": "رویے",
+ "label.boards": "Boards",
"label.bounce-rate": "اچھال کی شرح",
"label.breakdown": "Breakdown",
"label.browser": "Browser",
"label.browsers": "براؤزرز",
+ "label.campaigns": "Campaigns",
"label.cancel": "منسوخ",
"label.change-password": "پاس ورڈ تبدیل کریں",
+ "label.channels": "Channels",
"label.cities": "Cities",
"label.city": "City",
"label.clear-all": "Clear all",
+ "label.cohort": "Cohort",
"label.compare": "Compare",
+ "label.compare-dates": "Compare dates",
"label.confirm": "Confirm",
"label.confirm-password": "پاس ورڈ کی تصدیق کریں",
"label.contains": "Contains",
+ "label.content": "Content",
"label.continue": "Continue",
+ "label.conversion": "Conversion",
+ "label.conversion-rate": "Conversion rate",
+ "label.conversion-step": "Conversion step",
"label.count": "Count",
"label.countries": "ممالک",
"label.country": "Country",
@@ -38,6 +53,7 @@
"label.create-user": "Create user",
"label.created": "Created",
"label.created-by": "Created By",
+ "label.currency": "Currency",
"label.current": "Current",
"label.current-password": "موجودہ پاس ورڈ",
"label.custom-range": "اپنی مرضی کی حد",
@@ -57,19 +73,26 @@
"label.details": "Details",
"label.device": "Device",
"label.devices": "آلات",
+ "label.direct": "Direct",
"label.dismiss": "مسترد کریں",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "Does not contain",
+ "label.does-not-include": "Does not include",
+ "label.doest-not-exist": "Does not exist",
"label.domain": "ڈومین",
"label.dropoff": "Dropoff",
"label.edit": "ترمیم",
"label.edit-dashboard": "Edit dashboard",
"label.edit-member": "Edit member",
+ "label.email": "Email",
"label.enable-share-url": "شیئر یو آر ایل کو فعال کریں",
"label.end-step": "End Step",
"label.entry": "Entry URL",
"label.event": "Event",
"label.event-data": "Event data",
+ "label.event-name": "Event name",
"label.events": "واقعات",
+ "label.exists": "Exists",
"label.exit": "Exit URL",
"label.false": "False",
"label.field": "Field",
@@ -78,29 +101,37 @@
"label.filter-combined": "مشترکہ",
"label.filter-raw": "خام",
"label.filters": "Filters",
+ "label.first-click": "First click",
"label.first-seen": "First seen",
"label.funnel": "Funnel",
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
+ "label.funnels": "Funnels",
"label.goal": "Goal",
"label.goals": "Goals",
"label.goals-description": "Track your goals for pageviews and events.",
"label.greater-than": "Greater than",
"label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
+ "label.grouped": "Grouped",
+ "label.hostname": "Hostname",
+ "label.includes": "Includes",
+ "label.insight": "Insight",
"label.insights": "Insights",
"label.insights-description": "Dive deeper into your data by using segments and filters.",
"label.is": "Is",
+ "label.is-false": "Is false",
"label.is-not": "Is not",
"label.is-not-set": "Is not set",
"label.is-set": "Is set",
+ "label.is-true": "Is true",
"label.join": "Join",
"label.join-team": "Join team",
"label.journey": "Journey",
"label.journey-description": "Understand how users navigate through your website.",
+ "label.journeys": "Journeys",
"label.language": "Language",
"label.languages": "زبانیں",
"label.laptop": "لیپ ٹاپ",
+ "label.last-click": "Last click",
"label.last-days": "پچھلے {x} دن",
"label.last-hours": "پچھلے {x} گھنٹے",
"label.last-months": "Last {x} months",
@@ -109,15 +140,19 @@
"label.leave-team": "Leave team",
"label.less-than": "Less than",
"label.less-than-equals": "Less than or equals",
+ "label.links": "Links",
"label.login": "لاگ ان",
"label.logout": "لاگ آوٹ",
"label.manage": "Manage",
"label.manager": "Manager",
"label.max": "Max",
+ "label.maximize": "Expand",
+ "label.medium": "Medium",
"label.member": "Member",
"label.members": "Members",
"label.min": "Min",
"label.mobile": "موبائل",
+ "label.model": "Model",
"label.more": "مزید",
"label.my-account": "My account",
"label.my-websites": "My websites",
@@ -126,16 +161,29 @@
"label.none": "None",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organic search",
+ "label.organic-shopping": "Organic shopping",
+ "label.organic-social": "Organic social",
+ "label.organic-video": "Organic video",
"label.os": "OS",
+ "label.other": "Other",
"label.overview": "Overview",
"label.owner": "مالک",
+ "label.page": "Page",
"label.page-of": "Page {current} of {total}",
"label.page-views": "صفحہ کے نظارے",
"label.pageTitle": "Page title",
"label.pages": "صفحات",
+ "label.paid-ads": "Paid ads",
+ "label.paid-search": "Paid search",
+ "label.paid-shopping": "Paid shopping",
+ "label.paid-social": "Paid social",
+ "label.paid-video": "Paid video",
"label.password": "پاس ورڈ",
"label.path": "Path",
"label.paths": "Paths",
+ "label.pixels": "Pixels",
"label.powered-by": "تقویت یافتہ بذریعہ {name}",
"label.previous": "Previous",
"label.previous-period": "Previous period",
@@ -147,12 +195,14 @@
"label.query": "Query",
"label.query-parameters": "Query parameters",
"label.realtime": "براہ راست",
+ "label.referral": "Referral",
"label.referrer": "Referrer",
"label.referrers": "بھیجنے والے",
"label.refresh": "تازہ دم کریں",
"label.regenerate": "Regenerate",
"label.region": "Region",
"label.regions": "Regions",
+ "label.remaining": "Remaining",
"label.remove": "Remove",
"label.remove-member": "Remove member",
"label.reports": "Reports",
@@ -163,7 +213,6 @@
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
"label.role": "Role",
"label.run-query": "Run query",
"label.save": "محفوظ کریں",
@@ -171,26 +220,35 @@
"label.search": "Search",
"label.select": "Select",
"label.select-date": "Select date",
+ "label.select-filter": "Select filter",
"label.select-role": "Select role",
"label.select-website": "Select website",
"label.session": "Session",
+ "label.session-data": "Session data",
"label.sessions": "Sessions",
"label.settings": "ترتیبات",
+ "label.share": "Share",
"label.share-url": "URL کا اشتراک کریں",
"label.single-day": "ایک دن",
+ "label.sms": "SMS",
+ "label.sources": "Sources",
"label.start-step": "Start Step",
"label.steps": "Steps",
"label.sum": "Sum",
"label.tablet": "ٹیبلیٹ",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "Team",
"label.team-id": "Team ID",
"label.team-manager": "Team manager",
"label.team-member": "Team member",
"label.team-name": "Team name",
"label.team-owner": "Team owner",
+ "label.team-settings": "Team settings",
"label.team-view-only": "Team view only",
"label.team-websites": "Team websites",
"label.teams": "Teams",
+ "label.terms": "Terms",
"label.theme": "Theme",
"label.this-month": "اس مہینے",
"label.this-week": "اس ہفتے",
@@ -213,10 +271,7 @@
"label.unknown": "نامعلوم",
"label.untitled": "Untitled",
"label.update": "Update",
- "label.url": "URL",
- "label.urls": "URLs",
"label.user": "User",
- "label.user-property": "User Property",
"label.username": "صارف نام",
"label.users": "Users",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "Yesterday",
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
"message.active-users": "{x} موجودہ {x, plural, one {زائر} other {زائرین}}",
+ "message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "کیا آپ واقعی {target} کو حذف کرنا چاہتے ہیں؟",
"message.confirm-leave": "Are you sure you want to leave {target}?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "تمام متعلقہ ڈیٹا بھی حذف کر دیا جائے گا۔",
"message.error": "کچھ غلط ہو گیا.",
"message.event-log": "{event} on {url}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "ترتیبات پر جائیں",
"message.incorrect-username-password": "غلط صارف نام/پاس ورڈ۔",
"message.invalid-domain": "غلط ڈومین",
@@ -259,10 +316,13 @@
"message.no-teams": "You have not created any teams.",
"message.no-users": "There are no users.",
"message.no-websites-configured": "آپ کے پاس کوئی ویب سائٹ کنفیگر نہیں ہے۔",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "صفحہ نہیں ملا.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website-warning": "اس ویب سائٹ کے تمام اعدادوشمار کو حذف کر دیا جائے گا، لیکن آپ کا ٹریکنگ کوڈ برقرار رہے گا۔",
"message.saved": "کامیابی سے محفوظ ہو گیا۔",
+ "message.sever-error": "Server error",
"message.share-url": "یہ {target} کے لیے عوامی طور پر اشتراک کردہ URL ہے۔",
"message.team-already-member": "You are already a member of the team.",
"message.team-not-found": "Team not found.",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
"message.transfer-website": "Transfer website ownership to your account or another team.",
"message.triggered-event": "Triggered event",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "User deleted.",
"message.viewed-page": "Viewed page",
- "message.visitor-log": "{os} {device} پر {browser} کا استعمال کرتے ہوئے {country} سے آنے والا",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.visitor-log": "{os} {device} پر {browser} کا استعمال کرتے ہوئے {country} سے آنے والا"
}
diff --git a/src/lang/uz-UZ.json b/src/lang/uz-UZ.json
new file mode 100644
index 00000000..cf58945f
--- /dev/null
+++ b/src/lang/uz-UZ.json
@@ -0,0 +1,280 @@
+{
+ "label.access-code": "Kirish kodi",
+ "label.actions": "Amallar",
+ "label.activity": "Faoliyat",
+ "label.add": "Qoʻshish",
+ "label.add-description": "Tavsif qoʻshish",
+ "label.add-member": "A'zo qoʻshish",
+ "label.add-step": "Qadam qoʻshish",
+ "label.add-website": "Veb-sayt qoʻshish",
+ "label.admin": "Administrator",
+ "label.after": "Keyin",
+ "label.all": "Barchasi",
+ "label.all-time": "Barcha vaqtlar",
+ "label.analytics": "Tahlil",
+ "label.average": "Oʻrtacha",
+ "label.back": "Orqaga",
+ "label.before": "Oldin",
+ "label.behavior": "Xulq-atvor",
+ "label.bounce-rate": "Chiqib ketish darajasi",
+ "label.breakdown": "Tahlil",
+ "label.browser": "Brauzer",
+ "label.browsers": "Brauzerlar",
+ "label.cancel": "Bekor qilish",
+ "label.change-password": "Parolni oʻzgartirish",
+ "label.cities": "Shaharlar",
+ "label.city": "Shahar",
+ "label.clear-all": "Barchasini tozalash",
+ "label.compare": "Taqqoslash",
+ "label.confirm": "Tasdiqlash",
+ "label.confirm-password": "Parolni tasdiqlash",
+ "label.contains": "Oʻz ichiga oladi",
+ "label.continue": "Davom etish",
+ "label.count": "Soni",
+ "label.countries": "Davlatlar",
+ "label.country": "Davlat",
+ "label.create": "Yaratish",
+ "label.create-report": "Hisobot yaratish",
+ "label.create-team": "Jamoa yaratish",
+ "label.create-user": "Foydalanuvchi yaratish",
+ "label.created": "Yaratilgan",
+ "label.created-by": "Kim tomonidan yaratilgan",
+ "label.current": "Joriy",
+ "label.current-password": "Joriy parol",
+ "label.custom-range": "Maxsus oraliq",
+ "label.dashboard": "Boshqaruv paneli",
+ "label.data": "Ma'lumotlar",
+ "label.date": "Sana",
+ "label.date-range": "Sana oraligʻi",
+ "label.day": "Kun",
+ "label.default-date-range": "Standart sana oraligʻi",
+ "label.delete": "Oʻchirish",
+ "label.delete-report": "Hisobotni oʻchirish",
+ "label.delete-team": "Jamoani oʻchirish",
+ "label.delete-user": "Foydalanuvchini oʻchirish",
+ "label.delete-website": "Veb-saytni oʻchirish",
+ "label.description": "Tavsif",
+ "label.desktop": "Ish stoli",
+ "label.details": "Batafsil ma'lumot",
+ "label.device": "Qurilma",
+ "label.devices": "Qurilmalar",
+ "label.dismiss": "Yopish",
+ "label.does-not-contain": "Oʻz ichiga olmaydi",
+ "label.domain": "Domen",
+ "label.dropoff": "Tashlab ketish",
+ "label.edit": "Tahrirlash",
+ "label.edit-dashboard": "Boshqaruv panelini tahrirlash",
+ "label.edit-member": "A'zoni tahrirlash",
+ "label.enable-share-url": "Ulashish URL'ini yoqish",
+ "label.end-step": "Yakuniy qadam",
+ "label.entry": "Kirish yoʻli",
+ "label.event": "Hodisa",
+ "label.event-data": "Hodisa ma'lumotlari",
+ "label.events": "Hodisalar",
+ "label.exit": "Chiqish yoʻli",
+ "label.false": "Yolgʻon",
+ "label.field": "Maydon",
+ "label.fields": "Maydonlar",
+ "label.filter": "Filtr",
+ "label.filter-combined": "Birlashtirilgan",
+ "label.filter-raw": "Xom",
+ "label.filters": "Filtrlar",
+ "label.first-seen": "Birinchi koʻrilgan",
+ "label.funnel": "Voronka",
+ "label.funnel-description": "Foydalanuvchilarning konversiya va tashlab ketish darajasini tushunish.",
+ "label.goal": "Maqsad",
+ "label.goals": "Maqsadlar",
+ "label.goals-description": "Sahifa koʻrishlari va hodisalar uchun maqsadlaringizni kuzatib boring.",
+ "label.greater-than": "Kattaroq",
+ "label.greater-than-equals": "Kattaroq yoki teng",
+ "label.host": "Xost",
+ "label.hosts": "Xostlar",
+ "label.insights": "Tushunchalar",
+ "label.insights-description": "Segmentlar va filtrlardan foydalanib ma'lumotlaringizga chuqurroq kiring.",
+ "label.is": "Teng",
+ "label.is-not": "Teng emas",
+ "label.is-not-set": "Oʻrnatilmagan",
+ "label.is-set": "Oʻrnatilgan",
+ "label.join": "Qoʻshilish",
+ "label.join-team": "Jamoaga qoʻshilish",
+ "label.journey": "Sayohat",
+ "label.journey-description": "Foydalanuvchilar veb-saytingizda qanday harakat qilishlarini tushunish.",
+ "label.language": "Til",
+ "label.languages": "Tillar",
+ "label.laptop": "Noutbuk",
+ "label.last-days": "Oxirgi {x} kun",
+ "label.last-hours": "Oxirgi {x} soat",
+ "label.last-months": "Oxirgi {x} oy",
+ "label.last-seen": "Oxirgi koʻrilgan",
+ "label.leave": "Tark etish",
+ "label.leave-team": "Jamoani tark etish",
+ "label.less-than": "Kichikroq",
+ "label.less-than-equals": "Kichikroq yoki teng",
+ "label.login": "Kirish",
+ "label.logout": "Chiqish",
+ "label.manage": "Boshqarish",
+ "label.manager": "Menejer",
+ "label.max": "Maksimal",
+ "label.member": "A'zo",
+ "label.members": "A'zolar",
+ "label.min": "Minimal",
+ "label.mobile": "Mobil",
+ "label.more": "Koʻproq",
+ "label.my-account": "Mening hisobim",
+ "label.my-websites": "Mening veb-saytlarim",
+ "label.name": "Ism",
+ "label.new-password": "Yangi parol",
+ "label.none": "Hech biri",
+ "label.number-of-records": "{x} yozuv",
+ "label.ok": "OK",
+ "label.os": "OT (Operatsion tizim)",
+ "label.overview": "Umumiy koʻrinish",
+ "label.owner": "Egasi",
+ "label.page-of": "Sahifa {current} dan {total}",
+ "label.page-views": "Sahifa koʻrishlari",
+ "label.pageTitle": "Sahifa sarlavhasi",
+ "label.pages": "Sahifalar",
+ "label.password": "Parol",
+ "label.path": "Yoʻl",
+ "label.paths": "Yoʻllar",
+ "label.powered-by": "{name} tomonidan quvvatlanadi",
+ "label.previous": "Oldingi",
+ "label.previous-period": "Oldingi davr",
+ "label.previous-year": "Oldingi yil",
+ "label.profile": "Profil",
+ "label.properties": "Xususiyatlar",
+ "label.property": "Xususiyat",
+ "label.queries": "Soʻrovlar",
+ "label.query": "Soʻrov",
+ "label.query-parameters": "Soʻrov parametrlari",
+ "label.realtime": "Haqiqiy vaqt",
+ "label.referrer": "Tavsiya etuvchi",
+ "label.referrers": "Tavsiya etuvchilar",
+ "label.refresh": "Yangilash",
+ "label.regenerate": "Qayta yaratish",
+ "label.region": "Viloyat/Mintaqa",
+ "label.regions": "Viloyatlar/Mintaqalar",
+ "label.remove": "Olib tashlash",
+ "label.remove-member": "A'zoni olib tashlash",
+ "label.reports": "Hisobotlar",
+ "label.required": "Majburiy",
+ "label.reset": "Qayta tiklash",
+ "label.reset-website": "Veb-saytni qayta tiklash",
+ "label.retention": "Saqlanish",
+ "label.retention-description": "Foydalanuvchilarning qaytish chastotasini kuzatib, veb-saytingizning jozibadorligini oʻlchang.",
+ "label.revenue": "Daromad",
+ "label.revenue-description": "Vaqt oʻtishi bilan daromadingizni tekshiring.",
+ "label.revenue-property": "Daromad xususiyati",
+ "label.role": "Rol",
+ "label.run-query": "Soʻrovni ishga tushirish",
+ "label.save": "Saqlash",
+ "label.screens": "Ekranlar",
+ "label.search": "Qidiruv",
+ "label.select": "Tanlash",
+ "label.select-date": "Sanani tanlash",
+ "label.select-role": "Rolni tanlash",
+ "label.select-website": "Veb-saytni tanlash",
+ "label.session": "Sessiya",
+ "label.sessions": "Sessiyalar",
+ "label.settings": "Sozlamalar",
+ "label.share-url": "Ulashish URL'i",
+ "label.single-day": "Bir kun",
+ "label.start-step": "Boshlanish qadami",
+ "label.steps": "Qadamlar",
+ "label.sum": "Yigʻindi",
+ "label.tablet": "Planshet",
+ "label.team": "Jamoa",
+ "label.team-id": "Jamoa ID'si",
+ "label.team-manager": "Jamoa menejeri",
+ "label.team-member": "Jamoa a'zosi",
+ "label.team-name": "Jamoa nomi",
+ "label.team-owner": "Jamoa egasi",
+ "label.team-view-only": "Jamoa faqat koʻrish",
+ "label.team-websites": "Jamoa veb-saytlari",
+ "label.teams": "Jamoalar",
+ "label.theme": "Mavzu",
+ "label.this-month": "Shu oy",
+ "label.this-week": "Shu hafta",
+ "label.this-year": "Shu yil",
+ "label.timezone": "Vaqt zonasi",
+ "label.title": "Sarlavha",
+ "label.today": "Bugun",
+ "label.toggle-charts": "Grafiklarni almashtirish",
+ "label.total": "Jami",
+ "label.total-records": "Jami yozuvlar",
+ "label.tracking-code": "Kuzatuv kodi",
+ "label.transactions": "Tranzaksiyalar",
+ "label.transfer": "Oʻtkazish",
+ "label.transfer-website": "Veb-saytni oʻtkazish",
+ "label.true": "Rost",
+ "label.type": "Tur",
+ "label.unique": "Noyob",
+ "label.unique-visitors": "Noyob tashrif buyuruvchilar",
+ "label.uniqueCustomers": "Noyob mijozlar",
+ "label.unknown": "Noma'lum",
+ "label.untitled": "Sarlavhasiz",
+ "label.update": "Yangilash",
+ "label.url": "URL",
+ "label.urls": "URL'lar",
+ "label.user": "Foydalanuvchi",
+ "label.user-property": "Foydalanuvchi xususiyati",
+ "label.username": "Foydalanuvchi nomi",
+ "label.users": "Foydalanuvchilar",
+ "label.utm": "UTM",
+ "label.utm-description": "UTM parametrlari orqali kampaniyalaringizni kuzatib boring.",
+ "label.value": "Qiymat",
+ "label.view": "Koʻrish",
+ "label.view-details": "Batafsil koʻrish",
+ "label.view-only": "Faqat koʻrish",
+ "label.views": "Koʻrishlar",
+ "label.views-per-visit": "Tashrifga koʻrishlar soni",
+ "label.visit-duration": "Tashrif davomiyligi",
+ "label.visitors": "Tashrif buyuruvchilar",
+ "label.visits": "Tashriflar",
+ "label.website": "Veb-sayt",
+ "label.website-id": "Veb-sayt ID'si",
+ "label.websites": "Veb-saytlar",
+ "label.window": "Oyna",
+ "label.yesterday": "Kecha",
+ "message.action-confirmation": "Tasdiqlash uchun pastdagi qutiga **{confirmation}** yozing.",
+ "message.active-users": "{x} joriy {x, plural, one {tashrif buyuruvchi} other {tashrif buyuruvchilar}}",
+ "message.collected-data": "Yigʻilgan ma'lumotlar",
+ "message.confirm-delete": "**{target}** ni oʻchirmoqchi ekanligingizga ishonchingiz komilmi?",
+ "message.confirm-leave": "**{target}** ni tark etmoqchi ekanligingizga ishonchingiz komilmi?",
+ "message.confirm-remove": "**{target}** ni olib tashlamoqchi ekanligingizga ishonchingiz komilmi?",
+ "message.confirm-reset": "**{target}** ni qayta tiklamoqchi ekanligingizga ishonchingiz komilmi?",
+ "message.delete-team-warning": "Jamoani oʻchirish, shuningdek, barcha jamoa veb-saytlarini ham oʻchiradi.",
+ "message.delete-website-warning": "Barcha veb-sayt ma'lumotlari oʻchiriladi.",
+ "message.error": "Nimadir xato ketdi.",
+ "message.event-log": "**{url}** da **{event}** hodisasi",
+ "message.go-to-settings": "Sozlamalarga oʻtish",
+ "message.incorrect-username-password": "Notoʻgʻri foydalanuvchi nomi va/yoki parol.",
+ "message.invalid-domain": "Notoʻgʻri domen. http/https qoʻshmang.",
+ "message.min-password-length": "Minimal uzunligi {n} belgidan",
+ "message.new-version-available": "Umami'ning yangi **{version}** versiyasi mavjud!",
+ "message.no-data-available": "Ma'lumotlar mavjud emas.",
+ "message.no-event-data": "Hodisa ma'lumotlari mavjud emas.",
+ "message.no-match-password": "Parollar mos kelmadi.",
+ "message.no-results-found": "Hech qanday natija topilmadi.",
+ "message.no-team-websites": "Bu jamoada hech qanday veb-sayt yoʻq.",
+ "message.no-teams": "Siz hech qanday jamoa yaratmagansiz.",
+ "message.no-users": "Hech qanday foydalanuvchi yoʻq.",
+ "message.no-websites-configured": "Sizda hech qanday veb-sayt sozlanmagan.",
+ "message.page-not-found": "Sahifa topilmadi",
+ "message.reset-website": "Bu veb-saytni qayta tiklash uchun tasdiqlash uchun pastdagi qutiga **{confirmation}** yozing.",
+ "message.reset-website-warning": "Bu veb-sayt uchun barcha statistik ma'lumotlar oʻchiriladi, lekin sozlamalaringiz saqlanib qoladi.",
+ "message.saved": "Saqlandi.",
+ "message.share-url": "Sizning veb-sayt statistikalaringiz quyidagi URL'da ochiqdir:",
+ "message.team-already-member": "Siz allaqachon jamoa a'zosisiz.",
+ "message.team-not-found": "Jamoa topilmadi.",
+ "message.team-websites-info": "Veb-saytlarni jamoaning har bir a'zosi koʻrishi mumkin.",
+ "message.tracking-code": "Bu veb-sayt uchun statistikani kuzatish uchun quyidagi kodni HTML'ingizdagi **...** qismiga joylashtiring.",
+ "message.transfer-team-website-to-user": "Bu veb-saytni oʻz hisobingizga oʻtkazasizmi?",
+ "message.transfer-user-website-to-team": "Bu veb-saytni oʻtkazish uchun jamoani tanlang.",
+ "message.transfer-website": "Veb-sayt egaligini oʻz hisobingizga yoki boshqa jamoaga oʻtkazish.",
+ "message.triggered-event": "Hodisa ishga tushirildi",
+ "message.user-deleted": "Foydalanuvchi oʻchirildi.",
+ "message.viewed-page": "Sahifa koʻrildi",
+ "message.visitor-log": "{os} {device} da {browser} dan foydalanayotgan {country} dan tashrif buyuruvchi",
+ "message.visitors-dropped-off": "Tashrif buyuruvchilar tashlab ketishdi"
+}
diff --git a/src/lang/vi-VN.json b/src/lang/vi-VN.json
index 22a4eaf7..fc0a8c13 100644
--- a/src/lang/vi-VN.json
+++ b/src/lang/vi-VN.json
@@ -1,279 +1,280 @@
{
- "label.access-code": "Access code",
+ "label.access-code": "Mã truy cập",
"label.actions": "Hành động",
- "label.activity": "Activity log",
- "label.add": "Add",
- "label.add-description": "Add description",
- "label.add-member": "Add member",
- "label.add-step": "Add step",
+ "label.activity": "Nhật ký hoạt động",
+ "label.add": "Thêm",
+ "label.add-description": "Thêm mô tả",
+ "label.add-member": "Thêm thành viên",
+ "label.add-step": "Thêm bước",
"label.add-website": "Thêm website",
"label.admin": "Quản trị",
- "label.after": "After",
+ "label.after": "Sau đó",
"label.all": "Tất cả",
"label.all-time": "Toàn thời gian",
- "label.analytics": "Analytics",
- "label.average": "Average",
- "label.back": "Quay về",
- "label.before": "Before",
+ "label.analytics": "Phân tích",
+ "label.average": "Trung bình",
+ "label.back": "Quay lại",
+ "label.before": "Trước đó",
+ "label.behavior": "Hành vi",
"label.bounce-rate": "Tỷ lệ thoát trang",
- "label.breakdown": "Breakdown",
- "label.browser": "Browser",
- "label.browsers": "Trình duyệt",
- "label.cancel": "Huỷ bỏ",
+ "label.breakdown": "Phân tích chi tiết",
+ "label.browser": "Trình duyệt",
+ "label.browsers": "Các trình duyệt",
+ "label.cancel": "Hủy bỏ",
"label.change-password": "Đổi mật khẩu",
- "label.cities": "Cities",
- "label.city": "City",
- "label.clear-all": "Clear all",
- "label.compare": "Compare",
- "label.confirm": "Confirm",
+ "label.cities": "Các thành phố",
+ "label.city": "Thành phố",
+ "label.clear-all": "Xóa tất cả",
+ "label.compare": "So sánh",
+ "label.confirm": "Xác nhận",
"label.confirm-password": "Xác nhận mật khẩu",
- "label.contains": "Contains",
- "label.continue": "Continue",
- "label.count": "Count",
- "label.countries": "Quốc gia",
- "label.country": "Country",
- "label.create": "Create",
- "label.create-report": "Create report",
- "label.create-team": "Create team",
- "label.create-user": "Create user",
- "label.created": "Created",
- "label.created-by": "Created By",
- "label.current": "Current",
+ "label.contains": "Chứa",
+ "label.continue": "Tiếp tục",
+ "label.count": "Số lượng",
+ "label.countries": "Các quốc gia",
+ "label.country": "Quốc gia",
+ "label.create": "Tạo",
+ "label.create-report": "Tạo báo cáo",
+ "label.create-team": "Tạo nhóm",
+ "label.create-user": "Tạo người dùng",
+ "label.created": "Đã tạo",
+ "label.created-by": "Được tạo bởi",
+ "label.current": "Hiện tại",
"label.current-password": "Mật khẩu hiện tại",
- "label.custom-range": "Phạm vi ngày tuỳ chọn",
+ "label.custom-range": "Phạm vi tùy chỉnh",
"label.dashboard": "Bảng điều khiển",
- "label.data": "Data",
- "label.date": "Date",
+ "label.data": "Dữ liệu",
+ "label.date": "Ngày",
"label.date-range": "Phạm vi ngày",
- "label.day": "Day",
+ "label.day": "Ngày",
"label.default-date-range": "Khoảng thời gian mặc định",
- "label.delete": "Xoá",
- "label.delete-report": "Delete report",
- "label.delete-team": "Delete team",
- "label.delete-user": "Delete user",
+ "label.delete": "Xóa",
+ "label.delete-report": "Xóa báo cáo",
+ "label.delete-team": "Xóa nhóm",
+ "label.delete-user": "Xóa người dùng",
"label.delete-website": "Xóa website",
- "label.description": "Description",
- "label.desktop": "Máy bàn",
- "label.details": "Details",
- "label.device": "Device",
- "label.devices": "Thiết bị",
- "label.dismiss": "Loại trừ",
- "label.does-not-contain": "Does not contain",
+ "label.description": "Mô tả",
+ "label.desktop": "Máy tính để bàn",
+ "label.details": "Chi tiết",
+ "label.device": "Thiết bị",
+ "label.devices": "Các thiết bị",
+ "label.dismiss": "Bỏ qua",
+ "label.does-not-contain": "Không chứa",
"label.domain": "Tên miền",
- "label.dropoff": "Dropoff",
+ "label.dropoff": "Tỷ lệ bỏ qua",
"label.edit": "Chỉnh sửa",
- "label.edit-dashboard": "Edit dashboard",
- "label.edit-member": "Edit member",
- "label.enable-share-url": "Bật khả năng chia sẻ URL",
- "label.end-step": "End Step",
- "label.entry": "Entry URL",
- "label.event": "Event",
- "label.event-data": "Event data",
- "label.events": "Sự kiện",
- "label.exit": "Exit URL",
- "label.false": "False",
- "label.field": "Field",
- "label.fields": "Fields",
- "label.filter": "Filter",
- "label.filter-combined": "Kết hợp",
- "label.filter-raw": "Gốc",
- "label.filters": "Filters",
- "label.first-seen": "First seen",
- "label.funnel": "Funnel",
- "label.funnel-description": "Understand the conversion and drop-off rate of users.",
- "label.goal": "Goal",
- "label.goals": "Goals",
- "label.goals-description": "Track your goals for pageviews and events.",
- "label.greater-than": "Greater than",
- "label.greater-than-equals": "Greater than or equals",
- "label.host": "Host",
- "label.hosts": "Hosts",
- "label.insights": "Insights",
- "label.insights-description": "Dive deeper into your data by using segments and filters.",
- "label.is": "Is",
- "label.is-not": "Is not",
- "label.is-not-set": "Is not set",
- "label.is-set": "Is set",
- "label.join": "Join",
- "label.join-team": "Join team",
- "label.journey": "Journey",
- "label.journey-description": "Understand how users navigate through your website.",
- "label.language": "Language",
- "label.languages": "Ngôn ngữ",
- "label.laptop": "Laptop",
+ "label.edit-dashboard": "Chỉnh sửa bảng điều khiển",
+ "label.edit-member": "Chỉnh sửa thành viên",
+ "label.enable-share-url": "Bật chia sẻ URL",
+ "label.end-step": "Bước kết thúc",
+ "label.entry": "URL truy cập",
+ "label.event": "Sự kiện",
+ "label.event-data": "Dữ liệu sự kiện",
+ "label.events": "Các sự kiện",
+ "label.exit": "URL thoát",
+ "label.false": "Sai",
+ "label.field": "Trường",
+ "label.fields": "Các trường",
+ "label.filter": "Lọc",
+ "label.filter-combined": "Kết hợp lọc",
+ "label.filter-raw": "Lọc thô",
+ "label.filters": "Bộ lọc",
+ "label.first-seen": "Lần đầu tiên nhìn thấy",
+ "label.funnel": "Phễu",
+ "label.funnel-description": "Tìm hiểu tỷ lệ chuyển đổi và bỏ qua của người dùng.",
+ "label.goal": "Mục tiêu",
+ "label.goals": "Các mục tiêu",
+ "label.goals-description": "Theo dõi các mục tiêu của bạn cho lượt xem trang và sự kiện.",
+ "label.greater-than": "Lớn hơn",
+ "label.greater-than-equals": "Lớn hơn hoặc bằng",
+ "label.host": "Máy chủ",
+ "label.hosts": "Các máy chủ",
+ "label.insights": "Thông tin chi tiết",
+ "label.insights-description": "Tìm hiểu sâu hơn về dữ liệu của bạn bằng cách sử dụng phân đoạn và bộ lọc.",
+ "label.is": "Là",
+ "label.is-not": "Không phải là",
+ "label.is-not-set": "Chưa được đặt",
+ "label.is-set": "Đã đặt",
+ "label.join": "Tham gia",
+ "label.join-team": "Tham gia nhóm",
+ "label.journey": "Hành trình",
+ "label.journey-description": "Hiểu cách người dùng điều hướng qua website của bạn.",
+ "label.language": "Ngôn ngữ",
+ "label.languages": "Các ngôn ngữ",
+ "label.laptop": "Máy tính xách tay",
"label.last-days": "{x} ngày gần nhất",
"label.last-hours": "{x} giờ gần nhất",
- "label.last-months": "Last {x} months",
- "label.last-seen": "Last seen",
- "label.leave": "Leave",
- "label.leave-team": "Leave team",
- "label.less-than": "Less than",
- "label.less-than-equals": "Less than or equals",
+ "label.last-months": "{x} tháng gần nhất",
+ "label.last-seen": "Lần cuối cùng nhìn thấy",
+ "label.leave": "Rời khỏi",
+ "label.leave-team": "Rời nhóm",
+ "label.less-than": "Nhỏ hơn",
+ "label.less-than-equals": "Nhỏ hơn hoặc bằng",
"label.login": "Đăng nhập",
"label.logout": "Đăng xuất",
- "label.manage": "Manage",
- "label.manager": "Manager",
- "label.max": "Max",
- "label.member": "Member",
- "label.members": "Members",
- "label.min": "Min",
+ "label.manage": "Quản lý",
+ "label.manager": "Quản lý",
+ "label.max": "Tối đa",
+ "label.member": "Thành viên",
+ "label.members": "Các thành viên",
+ "label.min": "Tối thiểu",
"label.mobile": "Di động",
"label.more": "Thêm",
- "label.my-account": "My account",
- "label.my-websites": "My websites",
+ "label.my-account": "Tài khoản của tôi",
+ "label.my-websites": "Các website của tôi",
"label.name": "Tên",
"label.new-password": "Mật khẩu mới",
- "label.none": "None",
- "label.number-of-records": "{x} {x, plural, one {record} other {records}}",
+ "label.none": "Không",
+ "label.number-of-records": "{x} {x, plural, one {bản ghi} other {bản ghi}}",
"label.ok": "OK",
- "label.os": "OS",
- "label.overview": "Overview",
+ "label.os": "Hệ điều hành",
+ "label.overview": "Tổng quan",
"label.owner": "Chủ sở hữu",
- "label.page-of": "Page {current} of {total}",
- "label.page-views": "Lượt xem",
- "label.pageTitle": "Page title",
- "label.pages": "Trang",
+ "label.page-of": "Trang {current} trên {total}",
+ "label.page-views": "Lượt xem trang",
+ "label.pageTitle": "Tiêu đề trang",
+ "label.pages": "Các trang",
"label.password": "Mật khẩu",
- "label.path": "Path",
- "label.paths": "Paths",
- "label.powered-by": "Bản quyền thuộc về {name}",
- "label.previous": "Previous",
- "label.previous-period": "Previous period",
- "label.previous-year": "Previous year",
+ "label.path": "Đường dẫn",
+ "label.paths": "Các đường dẫn",
+ "label.powered-by": "Được cung cấp bởi {name}",
+ "label.previous": "Trước",
+ "label.previous-period": "Kỳ trước",
+ "label.previous-year": "Năm trước",
"label.profile": "Hồ sơ",
- "label.properties": "Properties",
- "label.property": "Property",
- "label.queries": "Queries",
- "label.query": "Query",
- "label.query-parameters": "Query parameters",
+ "label.properties": "Thuộc tính",
+ "label.property": "Thuộc tính",
+ "label.queries": "Truy vấn",
+ "label.query": "Truy vấn",
+ "label.query-parameters": "Tham số truy vấn",
"label.realtime": "Thời gian thực",
- "label.referrer": "Referrer",
- "label.referrers": "Liên kết giới thiệu",
+ "label.referrer": "Nguồn giới thiệu",
+ "label.referrers": "Các nguồn giới thiệu",
"label.refresh": "Làm mới",
- "label.regenerate": "Regenerate",
- "label.region": "Region",
- "label.regions": "Regions",
- "label.remove": "Remove",
- "label.remove-member": "Remove member",
- "label.reports": "Reports",
+ "label.regenerate": "Tạo lại",
+ "label.region": "Vùng",
+ "label.regions": "Các vùng",
+ "label.remove": "Xóa",
+ "label.remove-member": "Xóa thành viên",
+ "label.reports": "Báo cáo",
"label.required": "Yêu cầu",
- "label.reset": "Tái thiết lập",
- "label.reset-website": "Tái thiết lập thống kê",
- "label.retention": "Retention",
- "label.retention-description": "Measure your website stickiness by tracking how often users return.",
- "label.revenue": "Revenue",
- "label.revenue-description": "Look into your revenue across time.",
- "label.revenue-property": "Revenue Property",
- "label.role": "Role",
- "label.run-query": "Run query",
+ "label.reset": "Đặt lại",
+ "label.reset-website": "Đặt lại thống kê website",
+ "label.retention": "Tỷ lệ giữ chân",
+ "label.retention-description": "Đo lường mức độ gắn bó của website bằng cách theo dõi tần suất người dùng quay lại.",
+ "label.revenue": "Doanh thu",
+ "label.revenue-description": "Xem xét doanh thu của bạn theo thời gian.",
+ "label.revenue-property": "Thuộc tính doanh thu",
+ "label.role": "Vai trò",
+ "label.run-query": "Chạy truy vấn",
"label.save": "Lưu",
- "label.screens": "Screens",
- "label.search": "Search",
- "label.select": "Select",
- "label.select-date": "Select date",
- "label.select-role": "Select role",
- "label.select-website": "Select website",
- "label.session": "Session",
- "label.sessions": "Sessions",
+ "label.screens": "Màn hình",
+ "label.search": "Tìm kiếm",
+ "label.select": "Chọn",
+ "label.select-date": "Chọn ngày",
+ "label.select-role": "Chọn vai trò",
+ "label.select-website": "Chọn website",
+ "label.session": "Phiên",
+ "label.sessions": "Các phiên",
"label.settings": "Cài đặt",
"label.share-url": "Chia sẻ URL",
- "label.single-day": "Trong ngày",
- "label.start-step": "Start Step",
- "label.steps": "Steps",
- "label.sum": "Sum",
+ "label.single-day": "Một ngày",
+ "label.start-step": "Bước bắt đầu",
+ "label.steps": "Các bước",
+ "label.sum": "Tổng",
"label.tablet": "Máy tính bảng",
- "label.team": "Team",
- "label.team-id": "Team ID",
- "label.team-manager": "Team manager",
- "label.team-member": "Team member",
- "label.team-name": "Team name",
- "label.team-owner": "Team owner",
- "label.team-view-only": "Team view only",
- "label.team-websites": "Team websites",
- "label.teams": "Teams",
- "label.theme": "Giao diện",
+ "label.team": "Nhóm",
+ "label.team-id": "ID nhóm",
+ "label.team-manager": "Quản lý nhóm",
+ "label.team-member": "Thành viên nhóm",
+ "label.team-name": "Tên nhóm",
+ "label.team-owner": "Chủ sở hữu nhóm",
+ "label.team-view-only": "Chỉ xem nhóm",
+ "label.team-websites": "Các website của nhóm",
+ "label.teams": "Các nhóm",
+ "label.theme": "Chủ đề",
"label.this-month": "Tháng này",
"label.this-week": "Tuần này",
"label.this-year": "Năm nay",
"label.timezone": "Múi giờ",
- "label.title": "Title",
+ "label.title": "Tiêu đề",
"label.today": "Hôm nay",
"label.toggle-charts": "Bật/tắt biểu đồ",
- "label.total": "Total",
- "label.total-records": "Total records",
+ "label.total": "Tổng",
+ "label.total-records": "Tổng số bản ghi",
"label.tracking-code": "Mã theo dõi",
- "label.transactions": "Transactions",
- "label.transfer": "Transfer",
- "label.transfer-website": "Transfer website",
- "label.true": "True",
- "label.type": "Type",
- "label.unique": "Unique",
- "label.unique-visitors": "Khách truy cập một lần",
- "label.uniqueCustomers": "Unique Customers",
+ "label.transactions": "Giao dịch",
+ "label.transfer": "Chuyển giao",
+ "label.transfer-website": "Chuyển giao website",
+ "label.true": "Đúng",
+ "label.type": "Loại",
+ "label.unique": "Duy nhất",
+ "label.unique-visitors": "Khách truy cập duy nhất",
+ "label.uniqueCustomers": "Khách hàng duy nhất",
"label.unknown": "Không rõ",
- "label.untitled": "Untitled",
- "label.update": "Update",
+ "label.untitled": "Không có tiêu đề",
+ "label.update": "Cập nhật",
"label.url": "URL",
- "label.urls": "URLs",
- "label.user": "User",
- "label.user-property": "User Property",
+ "label.urls": "Các URL",
+ "label.user": "Người dùng",
+ "label.user-property": "Thuộc tính người dùng",
"label.username": "Tên đăng nhập",
- "label.users": "Users",
+ "label.users": "Người dùng",
"label.utm": "UTM",
- "label.utm-description": "Track your campaigns through UTM parameters.",
- "label.value": "Value",
- "label.view": "View",
+ "label.utm-description": "Theo dõi các chiến dịch của bạn thông qua các tham số UTM.",
+ "label.value": "Giá trị",
+ "label.view": "Xem",
"label.view-details": "Xem chi tiết",
- "label.view-only": "View only",
- "label.views": "Xem",
- "label.views-per-visit": "Views per visit",
- "label.visit-duration": "Thời gian truy cập trung bình",
- "label.visitors": "Khách",
- "label.visits": "Visits",
+ "label.view-only": "Chỉ xem",
+ "label.views": "Lượt xem",
+ "label.views-per-visit": "Lượt xem trên mỗi lượt truy cập",
+ "label.visit-duration": "Thời lượng truy cập",
+ "label.visitors": "Khách truy cập",
+ "label.visits": "Lượt truy cập",
"label.website": "Website",
- "label.website-id": "Website ID",
- "label.websites": "Websites",
- "label.window": "Window",
- "label.yesterday": "Yesterday",
- "message.action-confirmation": "Type {confirmation} in the box below to confirm.",
- "message.active-users": "{x} hiện tại {x, plural, one {một} other {trên}}",
- "message.collected-data": "Collected data",
- "message.confirm-delete": "Bạn có chắc chắn muốn xoá {target}?",
- "message.confirm-leave": "Are you sure you want to leave {target}?",
- "message.confirm-remove": "Are you sure you want to remove {target}?",
- "message.confirm-reset": "Bạn có chắc chắn muốn tái thiết lập thống kê {target}?",
- "message.delete-team-warning": "Deleting a team will also delete all team websites.",
- "message.delete-website-warning": "Tất cả các dữ liệu liên quan cũng sẽ bị xoá.",
+ "label.website-id": "ID website",
+ "label.websites": "Các website",
+ "label.window": "Cửa sổ",
+ "label.yesterday": "Hôm qua",
+ "message.action-confirmation": "Nhập {confirmation} vào ô bên dưới để xác nhận.",
+ "message.active-users": "{x} {x, plural, one {người dùng} other {người dùng}} đang hoạt động",
+ "message.collected-data": "Dữ liệu đã thu thập",
+ "message.confirm-delete": "Bạn có chắc chắn muốn xóa {target}?",
+ "message.confirm-leave": "Bạn có chắc chắn muốn rời {target}?",
+ "message.confirm-remove": "Bạn có chắc chắn muốn xóa {target}?",
+ "message.confirm-reset": "Bạn có chắc chắn muốn đặt lại thống kê {target}?",
+ "message.delete-team-warning": "Việc xóa một nhóm cũng sẽ xóa tất cả các website của nhóm.",
+ "message.delete-website-warning": "Tất cả dữ liệu liên quan cũng sẽ bị xóa.",
"message.error": "Đã xảy ra lỗi.",
- "message.event-log": "{event} on {url}",
- "message.go-to-settings": "Chuyển tới cài đặt",
+ "message.event-log": "{event} trên {url}",
+ "message.go-to-settings": "Chuyển đến cài đặt",
"message.incorrect-username-password": "Sai tên đăng nhập/mật khẩu.",
"message.invalid-domain": "Tên miền không hợp lệ",
- "message.min-password-length": "Minimum length of {n} characters",
- "message.new-version-available": "A new version of Umami {version} is available!",
+ "message.min-password-length": "Độ dài tối thiểu {n} ký tự",
+ "message.new-version-available": "Có phiên bản mới của Umami {version}!",
"message.no-data-available": "Không có dữ liệu.",
- "message.no-event-data": "No event data is available.",
- "message.no-match-password": "Mật khẩu không đồng nhất",
- "message.no-results-found": "No results were found.",
- "message.no-team-websites": "This team does not have any websites.",
- "message.no-teams": "You have not created any teams.",
- "message.no-users": "There are no users.",
- "message.no-websites-configured": "Bạn chưa có bất cứ website nào.",
- "message.page-not-found": "Trang không tìm thấy.",
- "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
- "message.reset-website-warning": "Tất cả số liệu thống kê của website này sẽ bị xoá, nhưng mã theo dõi sẽ vẫn giữ nguyên.",
+ "message.no-event-data": "Không có dữ liệu sự kiện.",
+ "message.no-match-password": "Mật khẩu không khớp",
+ "message.no-results-found": "Không tìm thấy kết quả nào.",
+ "message.no-team-websites": "Nhóm này không có bất kỳ website nào.",
+ "message.no-teams": "Bạn chưa tạo nhóm nào.",
+ "message.no-users": "Không có người dùng nào.",
+ "message.no-websites-configured": "Bạn chưa cấu hình bất kỳ website nào.",
+ "message.page-not-found": "Không tìm thấy trang.",
+ "message.reset-website": "Để đặt lại website này, nhập {confirmation} vào ô bên dưới để xác nhận.",
+ "message.reset-website-warning": "Tất cả số liệu thống kê của website này sẽ bị xóa, nhưng mã theo dõi sẽ vẫn giữ nguyên.",
"message.saved": "Đã lưu thành công.",
"message.share-url": "Đây là đường dẫn URL cho {target}.",
- "message.team-already-member": "You are already a member of the team.",
- "message.team-not-found": "Team not found.",
- "message.team-websites-info": "Websites can be viewed by anyone on the team.",
+ "message.team-already-member": "Bạn đã là thành viên của nhóm.",
+ "message.team-not-found": "Không tìm thấy nhóm.",
+ "message.team-websites-info": "Bất kỳ ai trong nhóm đều có thể xem các website.",
"message.tracking-code": "Mã theo dõi",
- "message.transfer-team-website-to-user": "Transfer this website to your account?",
- "message.transfer-user-website-to-team": "Select the team to transfer this website to.",
- "message.transfer-website": "Transfer website ownership to your account or another team.",
- "message.triggered-event": "Triggered event",
- "message.user-deleted": "User deleted.",
- "message.viewed-page": "Viewed page",
- "message.visitor-log": "Khách từ {country} đang dùng {browser} trên {os} {device}",
- "message.visitors-dropped-off": "Visitors dropped off"
+ "message.transfer-team-website-to-user": "Chuyển website này sang tài khoản của bạn?",
+ "message.transfer-user-website-to-team": "Chọn nhóm để chuyển website này đến.",
+ "message.transfer-website": "Chuyển quyền sở hữu website sang tài khoản của bạn hoặc một nhóm khác.",
+ "message.triggered-event": "Sự kiện được kích hoạt",
+ "message.user-deleted": "Người dùng đã bị xóa.",
+ "message.viewed-page": "Đã xem trang",
+ "message.visitor-log": "Khách từ {country} đang sử dụng {browser} trên {os} {device}",
+ "message.visitors-dropped-off": "Khách truy cập đã rời đi"
}
diff --git a/src/lang/zh-CN.json b/src/lang/zh-CN.json
index 6892f2e5..c6f01dd5 100644
--- a/src/lang/zh-CN.json
+++ b/src/lang/zh-CN.json
@@ -3,33 +3,47 @@
"label.actions": "用户行为",
"label.activity": "活动日志",
"label.add": "添加",
+ "label.add-board": "添加看板",
"label.add-description": "添加描述",
"label.add-member": "添加成员",
"label.add-step": "添加步骤",
"label.add-website": "添加网站",
"label.admin": "管理员",
+ "label.affiliate": "联盟",
"label.after": "之后",
"label.all": "所有",
"label.all-time": "所有时间段",
"label.analytics": "分析",
+ "label.apply": "应用",
+ "label.attribution": "归因",
+ "label.attribution-description": "查看用户如何与您的营销互动,以及是什么促成了转化。",
"label.average": "平均",
"label.back": "返回",
"label.before": "之前",
+ "label.behavior": "行为",
+ "label.boards": "看板",
"label.bounce-rate": "跳出率",
"label.breakdown": "故障",
"label.browser": "浏览器",
"label.browsers": "浏览器",
+ "label.campaigns": "活动",
"label.cancel": "取消",
"label.change-password": "修改密码",
"label.channels": "渠道",
"label.cities": "市/县",
"label.city": "市/县",
"label.clear-all": "清除全部",
+ "label.cohort": "队列",
"label.compare": "比较",
+ "label.compare-dates": "比较日期",
"label.confirm": "确认",
"label.confirm-password": "确认密码",
"label.contains": "包含",
+ "label.content": "内容",
"label.continue": "继续",
+ "label.conversion": "转化",
+ "label.conversion-rate": "转化率",
+ "label.conversion-step": "转化步骤",
"label.count": "统计",
"label.countries": "国家/地区",
"label.country": "国家/地区",
@@ -39,6 +53,7 @@
"label.create-user": "创建用户",
"label.created": "已创建",
"label.created-by": "创建者",
+ "label.currency": "货币",
"label.current": "当前",
"label.current-password": "当前密码",
"label.custom-range": "自定义时间段",
@@ -58,19 +73,26 @@
"label.details": "详细信息",
"label.device": "设备",
"label.devices": "设备",
+ "label.direct": "直接",
"label.dismiss": "关闭",
+ "label.distinct-id": "唯一ID",
"label.does-not-contain": "不包含",
+ "label.does-not-include": "不包括",
+ "label.doest-not-exist": "不存在",
"label.domain": "域名",
"label.dropoff": "丢弃",
"label.edit": "编辑",
"label.edit-dashboard": "编辑仪表盘",
"label.edit-member": "编辑成员",
+ "label.email": "Email",
"label.enable-share-url": "启用共享链接",
"label.end-step": "结束步骤",
"label.entry": "入口 URL",
"label.event": "事件",
"label.event-data": "事件数据",
+ "label.event-name": "事件名称",
"label.events": "行为类别",
+ "label.exists": "存在",
"label.exit": "退出 URL",
"label.false": "否",
"label.field": "字段",
@@ -79,29 +101,37 @@
"label.filter-combined": "合并",
"label.filter-raw": "原始",
"label.filters": "筛选",
+ "label.first-click": "首次点击",
"label.first-seen": "首次出现",
"label.funnel": "分析",
"label.funnel-description": "了解用户的转化率和跳出率。",
+ "label.funnels": "漏斗",
"label.goal": "目标",
"label.goals": "目标",
"label.goals-description": "跟踪页面浏览量和事件的目标。",
"label.greater-than": "大于",
"label.greater-than-equals": "大于或等于",
- "label.host": "主机",
- "label.hosts": "主机",
+ "label.grouped": "分组",
+ "label.hostname": "主机名",
+ "label.includes": "包括",
+ "label.insight": "洞察",
"label.insights": "见解",
"label.insights-description": "通过使用筛选器和划分时间段来更深入地研究数据。",
"label.is": "等于",
+ "label.is-false": "否",
"label.is-not": "不等于",
"label.is-not-set": "未设置",
"label.is-set": "已设置",
+ "label.is-true": "是",
"label.join": "加入",
"label.join-team": "加入团队",
"label.journey": "用户浏览轨迹",
"label.journey-description": "了解用户如何浏览网站。",
+ "label.journeys": "用户路径",
"label.language": "语言",
"label.languages": "语言",
"label.laptop": "笔记本",
+ "label.last-click": "最后点击",
"label.last-days": "最近 {x} 天",
"label.last-hours": "最近 {x} 小时",
"label.last-months": "最近 {x} 个月",
@@ -110,15 +140,19 @@
"label.leave-team": "离开团队",
"label.less-than": "少于",
"label.less-than-equals": "少于等于",
+ "label.links": "链接",
"label.login": "登录",
"label.logout": "退出",
"label.manage": "管理",
"label.manager": "管理者",
"label.max": "最大",
+ "label.maximize": "展开",
+ "label.medium": "中等",
"label.member": "成员",
"label.members": "成员",
"label.min": "最小",
"label.mobile": "手机",
+ "label.model": "模型",
"label.more": "更多",
"label.my-account": "我的账户",
"label.my-websites": "我的网站",
@@ -127,16 +161,29 @@
"label.none": "无",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "好的",
+ "label.online": "Online",
+ "label.organic-search": "自然搜索",
+ "label.organic-shopping": "自然购物",
+ "label.organic-social": "自然社交",
+ "label.organic-video": "自然视频",
"label.os": "操作系统",
+ "label.other": "其他",
"label.overview": "概览",
"label.owner": "所有者",
+ "label.page": "页面",
"label.page-of": "总 {total} 中的第 {current} 页",
"label.page-views": "页面浏览量",
"label.pageTitle": "标题",
"label.pages": "网页",
+ "label.paid-ads": "付费广告",
+ "label.paid-search": "付费搜索",
+ "label.paid-shopping": "付费购物",
+ "label.paid-social": "付费社交",
+ "label.paid-video": "付费视频",
"label.password": "密码",
- "label.path": "Path",
- "label.paths": "Paths",
+ "label.path": "路径",
+ "label.paths": "路径",
+ "label.pixels": "像素",
"label.powered-by": "由 {name} 提供支持",
"label.previous": "先前",
"label.previous-period": "上一时期",
@@ -148,12 +195,14 @@
"label.query": "查询",
"label.query-parameters": "查询参数",
"label.realtime": "实时",
+ "label.referral": "Referral",
"label.referrer": "来源",
"label.referrers": "来源域名",
"label.refresh": "刷新",
"label.regenerate": "重新生成",
"label.region": "州/省",
"label.regions": "州/省",
+ "label.remaining": "剩余",
"label.remove": "移除",
"label.remove-member": "移除成员",
"label.reports": "报告",
@@ -164,7 +213,6 @@
"label.retention-description": "通过追踪用户回访频率来衡量您网站的用户粘性。",
"label.revenue": "收入",
"label.revenue-description": "查看随时间变化的收入数据。",
- "label.revenue-property": "收入值",
"label.role": "角色",
"label.run-query": "查询",
"label.save": "保存",
@@ -172,26 +220,35 @@
"label.search": "搜索",
"label.select": "选择",
"label.select-date": "选择日期",
+ "label.select-filter": "选择筛选器",
"label.select-role": "选择角色",
"label.select-website": "选择网站",
- "label.session": "Session",
+ "label.session": "会话",
+ "label.session-data": "会话数据",
"label.sessions": "会话",
"label.settings": "设置",
+ "label.share": "分享",
"label.share-url": "共享链接",
"label.single-day": "单日",
+ "label.sms": "SMS",
+ "label.sources": "来源",
"label.start-step": "开始步骤",
"label.steps": "步骤",
"label.sum": "总和",
"label.tablet": "平板",
+ "label.tag": "标签",
+ "label.tags": "标签",
"label.team": "团队",
"label.team-id": "团队 ID",
"label.team-manager": "团队管理员",
"label.team-member": "团队成员",
"label.team-name": "团队名称",
"label.team-owner": "团队所有者",
+ "label.team-settings": "团队设置",
"label.team-view-only": "仅团队视图",
"label.team-websites": "团队网站",
"label.teams": "团队",
+ "label.terms": "条款",
"label.theme": "主题",
"label.this-month": "本月",
"label.this-week": "本周",
@@ -214,10 +271,7 @@
"label.unknown": "未知",
"label.untitled": "未命名",
"label.update": "更新",
- "label.url": "网址",
- "label.urls": "网址",
"label.user": "用户",
- "label.user-property": "用户属性",
"label.username": "用户名",
"label.users": "用户",
"label.utm": "UTM",
@@ -238,6 +292,7 @@
"label.yesterday": "昨天",
"message.action-confirmation": "请在下方输入框中输入 {confirmation} 以确认操作。",
"message.active-users": "当前在线 {x} 位访客",
+ "message.bad-request": "Bad request",
"message.collected-data": "已收集的数据",
"message.confirm-delete": "你确定要删除 {target} 吗?",
"message.confirm-leave": "你确定要离开 {target} 吗?",
@@ -247,6 +302,7 @@
"message.delete-website-warning": "所有相关数据将会被删除。",
"message.error": "发生错误。",
"message.event-log": "{url} 上的 {event}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "去设置",
"message.incorrect-username-password": "用户名或密码不正确。",
"message.invalid-domain": "无效域名",
@@ -260,10 +316,13 @@
"message.no-teams": "您尚未创建任何团队。",
"message.no-users": "暂无用户。",
"message.no-websites-configured": "你还没有设置任何网站。",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "页面未找到。",
"message.reset-website": "如确定要重置该网站,请在下面输入 {confirmation} 以确认。",
"message.reset-website-warning": "此网站的所有统计数据将被删除,但您的跟踪代码将保持不变。",
"message.saved": "保存成功。",
+ "message.sever-error": "Server error",
"message.share-url": "这是 {target} 的共享链接。",
"message.team-already-member": "你已是该团队的成员。",
"message.team-not-found": "未找到团队。",
@@ -273,8 +332,8 @@
"message.transfer-user-website-to-team": "选择要转移此网站的团队。",
"message.transfer-website": "将网站所有权转移到您的账户或其他团队。",
"message.triggered-event": "触发事件",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "用户已删除。",
"message.viewed-page": "已浏览页面",
- "message.visitor-log": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。",
- "message.visitors-dropped-off": "访客减少"
+ "message.visitor-log": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。"
}
diff --git a/src/lang/zh-TW.json b/src/lang/zh-TW.json
index 46cdbde1..030d11dc 100644
--- a/src/lang/zh-TW.json
+++ b/src/lang/zh-TW.json
@@ -3,32 +3,47 @@
"label.actions": "行為",
"label.activity": "活動紀錄",
"label.add": "新增",
+ "label.add-board": "新增看板",
"label.add-description": "新增描述",
"label.add-member": "新增成員",
"label.add-step": "新增步驟",
"label.add-website": "新增網站",
"label.admin": "管理員",
+ "label.affiliate": "聯盟",
"label.after": "之後",
"label.all": "全部",
"label.all-time": "所有時間",
"label.analytics": "分析",
+ "label.apply": "套用",
+ "label.attribution": "歸因",
+ "label.attribution-description": "查看使用者如何與您的行銷互動,以及什麼促成了轉換。",
"label.average": "平均",
"label.back": "返回",
"label.before": "之前",
+ "label.behavior": "行為",
+ "label.boards": "看板",
"label.bounce-rate": "跳出率",
"label.breakdown": "細項分析",
"label.browser": "瀏覽器",
"label.browsers": "瀏覽器",
+ "label.campaigns": "活動",
"label.cancel": "取消",
"label.change-password": "更改密碼",
+ "label.channels": "Channels",
"label.cities": "城市",
"label.city": "城市",
"label.clear-all": "全部清除",
+ "label.cohort": "群組",
"label.compare": "比較",
+ "label.compare-dates": "比較日期",
"label.confirm": "確認",
"label.confirm-password": "確認密碼",
"label.contains": "包含",
+ "label.content": "內容",
"label.continue": "繼續",
+ "label.conversion": "轉換",
+ "label.conversion-rate": "轉換率",
+ "label.conversion-step": "轉換步驟",
"label.count": "數量",
"label.countries": "國家",
"label.country": "國家",
@@ -38,6 +53,7 @@
"label.create-user": "建立使用者",
"label.created": "已建立",
"label.created-by": "建立者",
+ "label.currency": "Currency",
"label.current": "目前",
"label.current-password": "目前密碼",
"label.custom-range": "自訂範圍",
@@ -57,19 +73,26 @@
"label.details": "詳細資訊",
"label.device": "裝置",
"label.devices": "裝置",
+ "label.direct": "Direct",
"label.dismiss": "關閉",
+ "label.distinct-id": "Distinct ID",
"label.does-not-contain": "不包含",
+ "label.does-not-include": "Does not include",
+ "label.doest-not-exist": "Does not exist",
"label.domain": "網域",
"label.dropoff": "離開",
"label.edit": "編輯",
"label.edit-dashboard": "編輯儀表板",
"label.edit-member": "編輯成員",
+ "label.email": "Email",
"label.enable-share-url": "啟用分享連結",
"label.end-step": "結束步驟",
"label.entry": "進入網址",
"label.event": "事件",
"label.event-data": "事件資料",
+ "label.event-name": "Event name",
"label.events": "事件",
+ "label.exists": "Exists",
"label.exit": "離開網址",
"label.false": "否",
"label.field": "欄位",
@@ -78,29 +101,37 @@
"label.filter-combined": "組合",
"label.filter-raw": "原始",
"label.filters": "篩選條件",
+ "label.first-click": "First click",
"label.first-seen": "首次造訪",
"label.funnel": "漏斗分析",
"label.funnel-description": "瞭解使用者的轉換率與流失率。",
+ "label.funnels": "Funnels",
"label.goal": "目標",
"label.goals": "目標",
"label.goals-description": "追蹤網頁瀏覽和事件的目標。",
"label.greater-than": "大於",
"label.greater-than-equals": "大於或等於",
- "label.host": "主機名稱",
- "label.hosts": "主機名稱",
+ "label.grouped": "Grouped",
+ "label.hostname": "Hostname",
+ "label.includes": "Includes",
+ "label.insight": "Insight",
"label.insights": "洞察",
"label.insights-description": "使用區段和篩選器來深入分析您的資料。",
"label.is": "是",
+ "label.is-false": "Is false",
"label.is-not": "不是",
"label.is-not-set": "未設定",
"label.is-set": "已設定",
+ "label.is-true": "Is true",
"label.join": "加入",
"label.join-team": "加入團隊",
"label.journey": "使用者旅程",
"label.journey-description": "瞭解使用者如何瀏覽您的網站。",
+ "label.journeys": "Journeys",
"label.language": "語言",
"label.languages": "語言",
"label.laptop": "筆記型電腦",
+ "label.last-click": "Last click",
"label.last-days": "最近 {x} 天",
"label.last-hours": "最近 {x} 小時",
"label.last-months": "最近 {x} 個月",
@@ -109,15 +140,19 @@
"label.leave-team": "離開團隊",
"label.less-than": "小於",
"label.less-than-equals": "小於或等於",
+ "label.links": "Links",
"label.login": "登入",
"label.logout": "登出",
"label.manage": "管理",
"label.manager": "管理者",
"label.max": "最大值",
+ "label.maximize": "Expand",
+ "label.medium": "Medium",
"label.member": "成員",
"label.members": "成員",
"label.min": "最小值",
"label.mobile": "行動裝置",
+ "label.model": "Model",
"label.more": "更多",
"label.my-account": "我的帳號",
"label.my-websites": "我的網站",
@@ -126,16 +161,29 @@
"label.none": "無",
"label.number-of-records": "{x} 筆紀錄",
"label.ok": "OK",
+ "label.online": "Online",
+ "label.organic-search": "Organic search",
+ "label.organic-shopping": "Organic shopping",
+ "label.organic-social": "Organic social",
+ "label.organic-video": "Organic video",
"label.os": "作業系統",
+ "label.other": "Other",
"label.overview": "總覽",
"label.owner": "擁有者",
+ "label.page": "Page",
"label.page-of": "第 {current} 頁,共 {total} 頁",
"label.page-views": "網頁瀏覽次數",
"label.pageTitle": "網頁標題",
"label.pages": "網頁",
+ "label.paid-ads": "Paid ads",
+ "label.paid-search": "Paid search",
+ "label.paid-shopping": "Paid shopping",
+ "label.paid-social": "Paid social",
+ "label.paid-video": "Paid video",
"label.password": "密碼",
"label.path": "路徑",
"label.paths": "路徑",
+ "label.pixels": "Pixels",
"label.powered-by": "由 {name} 提供技術支援",
"label.previous": "上一個",
"label.previous-period": "上一期間",
@@ -147,12 +195,14 @@
"label.query": "查詢",
"label.query-parameters": "查詢參數",
"label.realtime": "即時",
+ "label.referral": "Referral",
"label.referrer": "參照來源",
"label.referrers": "參照來源",
"label.refresh": "重新整理",
"label.regenerate": "重新產生",
"label.region": "地區",
"label.regions": "地區",
+ "label.remaining": "Remaining",
"label.remove": "移除",
"label.remove-member": "移除成員",
"label.reports": "報表",
@@ -163,7 +213,6 @@
"label.retention-description": "透過追蹤使用者回訪的頻率來衡量您的網站黏著度。",
"label.revenue": "營收",
"label.revenue-description": "查看您的營收趨勢。",
- "label.revenue-property": "營收屬性",
"label.role": "角色",
"label.run-query": "執行查詢",
"label.save": "儲存",
@@ -171,26 +220,35 @@
"label.search": "搜尋",
"label.select": "選取",
"label.select-date": "選取日期",
+ "label.select-filter": "Select filter",
"label.select-role": "選取角色",
"label.select-website": "選取網站",
"label.session": "工作階段",
+ "label.session-data": "Session data",
"label.sessions": "工作階段",
"label.settings": "設定",
+ "label.share": "Share",
"label.share-url": "分享連結",
"label.single-day": "單日",
+ "label.sms": "SMS",
+ "label.sources": "Sources",
"label.start-step": "起始步驟",
"label.steps": "步驟",
"label.sum": "總和",
"label.tablet": "平板",
+ "label.tag": "Tag",
+ "label.tags": "Tags",
"label.team": "團隊",
"label.team-id": "團隊 ID",
"label.team-manager": "團隊管理者",
"label.team-member": "團隊成員",
"label.team-name": "團隊名稱",
"label.team-owner": "團隊擁有者",
+ "label.team-settings": "Team settings",
"label.team-view-only": "團隊僅供檢視",
"label.team-websites": "團隊網站",
"label.teams": "團隊",
+ "label.terms": "Terms",
"label.theme": "主題",
"label.this-month": "本月",
"label.this-week": "本週",
@@ -213,10 +271,7 @@
"label.unknown": "未知",
"label.untitled": "未命名",
"label.update": "更新",
- "label.url": "網址",
- "label.urls": "網址",
"label.user": "使用者",
- "label.user-property": "使用者屬性",
"label.username": "使用者名稱",
"label.users": "使用者",
"label.utm": "UTM",
@@ -237,6 +292,7 @@
"label.yesterday": "昨天",
"message.action-confirmation": "請在下方欄位輸入 {confirmation} 以確認。",
"message.active-users": "目前有 {x} 位訪客",
+ "message.bad-request": "Bad request",
"message.collected-data": "已蒐集的資料",
"message.confirm-delete": "您確定要刪除 {target} 嗎?",
"message.confirm-leave": "您確定要離開 {target} 嗎?",
@@ -246,6 +302,7 @@
"message.delete-website-warning": "所有網站資料都將被刪除。",
"message.error": "發生錯誤。",
"message.event-log": "在 {url} 上的 {event}",
+ "message.forbidden": "Forbidden",
"message.go-to-settings": "前往設定",
"message.incorrect-username-password": "使用者名稱或密碼不正確。",
"message.invalid-domain": "無效的網域。請勿包含 http/https。",
@@ -259,10 +316,13 @@
"message.no-teams": "您尚未建立任何團隊。",
"message.no-users": "沒有任何使用者。",
"message.no-websites-configured": "您尚未設定任何網站。",
+ "message.not-found": "Not found",
+ "message.nothing-selected": "Nothing selected.",
"message.page-not-found": "找不到網頁",
"message.reset-website": "要重設此網站的統計資料,請在下方欄位輸入 {confirmation} 以確認。",
"message.reset-website-warning": "此網站的所有統計資料都將被刪除,但您的設定將保持不變。",
"message.saved": "已儲存。",
+ "message.sever-error": "Server error",
"message.share-url": "您的網站統計資料可在以下網址公開檢視:",
"message.team-already-member": "您已是該團隊的成員。",
"message.team-not-found": "找不到團隊。",
@@ -272,8 +332,8 @@
"message.transfer-user-website-to-team": "請選擇要轉移此網站的團隊。",
"message.transfer-website": "將網站所有權轉移至您的帳號或其他團隊。",
"message.triggered-event": "已觸發的事件",
+ "message.unauthorized": "Unauthorized",
"message.user-deleted": "使用者已刪除。",
"message.viewed-page": "已瀏覽的網頁",
- "message.visitor-log": "來自 {country} 的訪客在 {device} 上的 {os} 使用 {browser} 瀏覽。",
- "message.visitors-dropped-off": "訪客已離開"
+ "message.visitor-log": "來自 {country} 的訪客在 {device} 上的 {os} 使用 {browser} 瀏覽。"
}
diff --git a/src/lib/__tests__/charts.test.ts b/src/lib/__tests__/charts.test.ts
index 601ee63d..e81be161 100644
--- a/src/lib/__tests__/charts.test.ts
+++ b/src/lib/__tests__/charts.test.ts
@@ -14,23 +14,21 @@ describe('renderNumberLabels', () => {
expect(renderNumberLabels(input)).toBe(expected);
});
- test.each([['12500', '12.5k']])(
- "formats numbers ≥ 10K as 'X.Xk' (%s → %s)",
- (input, expected) => {
- expect(renderNumberLabels(input)).toBe(expected);
- },
- );
+ test.each([
+ ['12500', '12.5k'],
+ ])("formats numbers ≥ 10K as 'X.Xk' (%s → %s)", (input, expected) => {
+ expect(renderNumberLabels(input)).toBe(expected);
+ });
test.each([['1500', '1.50k']])("formats numbers ≥ 1K as 'X.XXk' (%s → %s)", (input, expected) => {
expect(renderNumberLabels(input)).toBe(expected);
});
- test.each([['999', '999']])(
- 'calls formatNumber for values < 1000 (%s → %s)',
- (input, expected) => {
- expect(renderNumberLabels(input)).toBe(expected);
- },
- );
+ test.each([
+ ['999', '999'],
+ ])('calls formatNumber for values < 1000 (%s → %s)', (input, expected) => {
+ expect(renderNumberLabels(input)).toBe(expected);
+ });
test.each([
['0', '0'],
diff --git a/src/lib/__tests__/detect.test.ts b/src/lib/__tests__/detect.test.ts
index 1cb558ad..0395aef5 100644
--- a/src/lib/__tests__/detect.test.ts
+++ b/src/lib/__tests__/detect.test.ts
@@ -1,22 +1,22 @@
-import * as detect from '../detect';
-import { expect } from '@jest/globals';
+import { getIpAddress } from '../ip';
const IP = '127.0.0.1';
+const BAD_IP = '127.127.127.127';
test('getIpAddress: Custom header', () => {
process.env.CLIENT_IP_HEADER = 'x-custom-ip-header';
- expect(detect.getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP);
+ expect(getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP);
});
test('getIpAddress: CloudFlare header', () => {
- expect(detect.getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP);
+ expect(getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP);
});
test('getIpAddress: Standard header', () => {
- expect(detect.getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP);
+ expect(getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP);
});
test('getIpAddress: No header', () => {
- expect(detect.getIpAddress(new Headers())).toEqual(null);
+ expect(getIpAddress(new Headers())).toEqual(null);
});
diff --git a/src/lib/__tests__/format.test.ts b/src/lib/__tests__/format.test.ts
index 2f68dc18..6e1b319f 100644
--- a/src/lib/__tests__/format.test.ts
+++ b/src/lib/__tests__/format.test.ts
@@ -1,5 +1,4 @@
import * as format from '../format';
-import { expect } from '@jest/globals';
test('parseTime', () => {
expect(format.parseTime(86400 + 3600 + 60 + 1)).toEqual({
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index d67566b8..ba6d8b09 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -1,33 +1,27 @@
-import bcrypt from 'bcryptjs';
-import { Report } from '@prisma/client';
-import redis from '@/lib/redis';
import debug from 'debug';
-import { PERMISSIONS, ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants';
-import { secret, getRandomChars } from '@/lib/crypto';
+import { ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants';
+import { secret } from '@/lib/crypto';
+import { getRandomChars } from '@/lib/generate';
import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt';
+import redis from '@/lib/redis';
import { ensureArray } from '@/lib/utils';
-import { getTeamUser, getUser, getWebsite } from '@/queries';
-import { Auth } from './types';
+import { getUser } from '@/queries/prisma/user';
const log = debug('umami:auth');
-const cloudMode = process.env.CLOUD_MODE;
-const SALT_ROUNDS = 10;
-export function hashPassword(password: string, rounds = SALT_ROUNDS) {
- return bcrypt.hashSync(password, rounds);
-}
+export function getBearerToken(request: Request) {
+ const auth = request.headers.get('authorization');
-export function checkPassword(password: string, passwordHash: string) {
- return bcrypt.compareSync(password, passwordHash);
+ return auth?.split(' ')[1];
}
export async function checkAuth(request: Request) {
- const token = request.headers.get('authorization')?.split(' ')?.[1];
+ const token = getBearerToken(request);
const payload = parseSecureToken(token, secret());
- const shareToken = await parseShareToken(request.headers);
+ const shareToken = await parseShareToken(request);
let user = null;
- const { userId, authKey, grant } = payload || {};
+ const { userId, authKey } = payload || {};
if (userId) {
user = await getUser(userId);
@@ -39,12 +33,10 @@ export async function checkAuth(request: Request) {
}
}
- if (process.env.NODE_ENV === 'development') {
- log('checkAuth:', { token, shareToken, payload, user, grant });
- }
+ log({ token, payload, authKey, shareToken, user });
if (!user?.id && !shareToken) {
- log('checkAuth: User not authorized');
+ log('User not authorized');
return null;
}
@@ -53,11 +45,10 @@ export async function checkAuth(request: Request) {
}
return {
- user,
- grant,
token,
- shareToken,
authKey,
+ shareToken,
+ user,
};
}
@@ -75,247 +66,15 @@ export async function saveAuth(data: any, expire = 0) {
return createSecureToken({ authKey }, secret());
}
-export function parseShareToken(headers: Headers) {
+export async function hasPermission(role: string, permission: string | string[]) {
+ return ensureArray(permission).some(e => ROLE_PERMISSIONS[role]?.includes(e));
+}
+
+export function parseShareToken(request: Request) {
try {
- return parseToken(headers.get(SHARE_TOKEN_HEADER), secret());
+ return parseToken(request.headers.get(SHARE_TOKEN_HEADER), secret());
} catch (e) {
log(e);
return null;
}
}
-
-export async function canViewWebsite({ user, shareToken }: Auth, websiteId: string) {
- if (user?.isAdmin) {
- return true;
- }
-
- if (shareToken?.websiteId === websiteId) {
- return true;
- }
-
- const website = await getWebsite(websiteId);
-
- if (website.userId) {
- return user.id === website.userId;
- }
-
- if (website.teamId) {
- const teamUser = await getTeamUser(website.teamId, user.id);
-
- return !!teamUser;
- }
-
- return false;
-}
-
-export async function canViewAllWebsites({ user }: Auth) {
- return user.isAdmin;
-}
-
-export async function canCreateWebsite({ user, grant }: Auth) {
- if (cloudMode) {
- return !!grant?.find(a => a === PERMISSIONS.websiteCreate);
- }
-
- if (user.isAdmin) {
- return true;
- }
-
- return hasPermission(user.role, PERMISSIONS.websiteCreate);
-}
-
-export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
- if (user.isAdmin) {
- return true;
- }
-
- const website = await getWebsite(websiteId);
-
- if (website.userId) {
- return user.id === website.userId;
- }
-
- if (website.teamId) {
- const teamUser = await getTeamUser(website.teamId, user.id);
-
- return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteUpdate);
- }
-
- return false;
-}
-
-export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string, userId: string) {
- const website = await getWebsite(websiteId);
-
- if (website.teamId && user.id === userId) {
- const teamUser = await getTeamUser(website.teamId, userId);
-
- return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteTransferToUser);
- }
-
- return false;
-}
-
-export async function canTransferWebsiteToTeam({ user }: Auth, websiteId: string, teamId: string) {
- const website = await getWebsite(websiteId);
-
- if (website.userId && website.userId === user.id) {
- const teamUser = await getTeamUser(teamId, user.id);
-
- return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteTransferToTeam);
- }
-
- return false;
-}
-
-export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
- if (user.isAdmin) {
- return true;
- }
-
- const website = await getWebsite(websiteId);
-
- if (website.userId) {
- return user.id === website.userId;
- }
-
- if (website.teamId) {
- const teamUser = await getTeamUser(website.teamId, user.id);
-
- return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteDelete);
- }
-
- return false;
-}
-
-export async function canViewReport(auth: Auth, report: Report) {
- if (auth.user.isAdmin) {
- return true;
- }
-
- if (auth.user.id == report.userId) {
- return true;
- }
-
- return !!(await canViewWebsite(auth, report.websiteId));
-}
-
-export async function canUpdateReport({ user }: Auth, report: Report) {
- if (user.isAdmin) {
- return true;
- }
-
- return user.id == report.userId;
-}
-
-export async function canDeleteReport(auth: Auth, report: Report) {
- return canUpdateReport(auth, report);
-}
-
-export async function canCreateTeam({ user, grant }: Auth) {
- if (cloudMode) {
- return !!grant?.find(a => a === PERMISSIONS.teamCreate);
- }
-
- if (user.isAdmin) {
- return true;
- }
-
- return !!user;
-}
-
-export async function canViewTeam({ user }: Auth, teamId: string) {
- if (user.isAdmin) {
- return true;
- }
-
- return getTeamUser(teamId, user.id);
-}
-
-export async function canUpdateTeam({ user, grant }: Auth, teamId: string) {
- if (user.isAdmin) {
- return true;
- }
-
- if (cloudMode) {
- return !!grant?.find(a => a === PERMISSIONS.teamUpdate);
- }
-
- const teamUser = await getTeamUser(teamId, user.id);
-
- return teamUser && hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
-}
-
-export async function canAddUserToTeam({ user, grant }: Auth) {
- if (cloudMode) {
- return !!grant?.find(a => a === PERMISSIONS.teamUpdate);
- }
-
- return user.isAdmin;
-}
-
-export async function canDeleteTeam({ user }: Auth, teamId: string) {
- if (user.isAdmin) {
- return true;
- }
-
- const teamUser = await getTeamUser(teamId, user.id);
-
- return teamUser && hasPermission(teamUser.role, PERMISSIONS.teamDelete);
-}
-
-export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUserId: string) {
- if (user.isAdmin) {
- return true;
- }
-
- if (removeUserId === user.id) {
- return true;
- }
-
- const teamUser = await getTeamUser(teamId, user.id);
-
- return teamUser && hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
-}
-
-export async function canCreateTeamWebsite({ user }: Auth, teamId: string) {
- if (user.isAdmin) {
- return true;
- }
-
- const teamUser = await getTeamUser(teamId, user.id);
-
- return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteCreate);
-}
-
-export async function canCreateUser({ user }: Auth) {
- return user.isAdmin;
-}
-
-export async function canViewUser({ user }: Auth, viewedUserId: string) {
- if (user.isAdmin) {
- return true;
- }
-
- return user.id === viewedUserId;
-}
-
-export async function canViewUsers({ user }: Auth) {
- return user.isAdmin;
-}
-
-export async function canUpdateUser({ user }: Auth, viewedUserId: string) {
- if (user.isAdmin) {
- return true;
- }
-
- return user.id === viewedUserId;
-}
-
-export async function canDeleteUser({ user }: Auth) {
- return user.isAdmin;
-}
-
-export async function hasPermission(role: string, permission: string | string[]) {
- return ensureArray(permission).some(e => ROLE_PERMISSIONS[role]?.includes(e));
-}
diff --git a/src/lib/charts.ts b/src/lib/charts.ts
index 957d4962..7d4208e2 100644
--- a/src/lib/charts.ts
+++ b/src/lib/charts.ts
@@ -11,7 +11,7 @@ export function renderDateLabels(unit: string, locale: string) {
switch (unit) {
case 'minute':
- return formatDate(d, 'p', locale).split(' ')[0];
+ return formatDate(d, 'h:mm', locale);
case 'hour':
return formatDate(d, 'p', locale);
case 'day':
diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts
index 480084dc..f2ebbb72 100644
--- a/src/lib/clickhouse.ts
+++ b/src/lib/clickhouse.ts
@@ -1,12 +1,10 @@
-import { ClickHouseClient, createClient } from '@clickhouse/client';
+import { type ClickHouseClient, createClient } from '@clickhouse/client';
import { formatInTimeZone } from 'date-fns-tz';
import debug from 'debug';
import { CLICKHOUSE } from '@/lib/db';
-import { getWebsite } from '@/queries';
-import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants';
-import { maxDate } from './date';
-import { filtersToArray } from './params';
-import { PageParams, QueryFilters, QueryOptions } from './types';
+import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS } from './constants';
+import { filtersObjectToArray } from './params';
+import type { QueryFilters, QueryOptions } from './types';
export const CLICKHOUSE_DATE_FORMATS = {
utc: '%Y-%m-%dT%H:%i:%SZ',
@@ -41,7 +39,7 @@ function getClient() {
});
if (process.env.NODE_ENV !== 'production') {
- global[CLICKHOUSE] = client;
+ globalThis[CLICKHOUSE] = client;
}
log('Clickhouse initialized');
@@ -63,7 +61,7 @@ function getDateStringSQL(data: any, unit: string = 'utc', timezone?: string) {
function getDateSQL(field: string, unit: string, timezone?: string) {
if (timezone) {
- return `toDateTime(date_trunc('${unit}', ${field}, '${timezone}'), '${timezone}')`;
+ return `toDateTime(date_trunc('${unit}', ${field}, '${timezone}'))`;
}
return `toDateTime(date_trunc('${unit}', ${field}))`;
}
@@ -89,10 +87,20 @@ function mapFilter(column: string, operator: string, name: string, type: string
}
}
-function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) {
- const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => {
+function getFilterQuery(filters: Record, options: QueryOptions = {}) {
+ const query = filtersObjectToArray(filters, options).reduce((arr, { name, column, operator }) => {
+ const isCohort = options?.isCohort;
+
+ if (isCohort) {
+ column = FILTER_COLUMNS[name.slice('cohort_'.length)];
+ }
+
if (column) {
- arr.push(`and ${mapFilter(column, operator, name)}`);
+ if (name === 'eventType') {
+ arr.push(`and ${mapFilter(column, operator, name, 'UInt32')}`);
+ } else {
+ arr.push(`and ${mapFilter(column, operator, name)}`);
+ }
if (name === 'referrer') {
arr.push(`and referrer_domain != hostname`);
@@ -105,7 +113,25 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {})
return query.join('\n');
}
-function getDateQuery(filters: QueryFilters = {}) {
+function getCohortQuery(filters: Record) {
+ if (!filters || Object.keys(filters).length === 0) {
+ return '';
+ }
+
+ const filterQuery = getFilterQuery(filters, { isCohort: true });
+
+ return `join (
+ select distinct session_id
+ from website_event
+ where website_id = {websiteId:UUID}
+ and created_at between {cohort_startDate:DateTime64} and {cohort_endDate:DateTime64}
+ ${filterQuery}
+ ) as cohort
+ on cohort.session_id = website_event.session_id
+ `;
+}
+
+function getDateQuery(filters: Record) {
const { startDate, endDate, timezone } = filters;
if (startDate) {
@@ -125,36 +151,39 @@ function getDateQuery(filters: QueryFilters = {}) {
return '';
}
-function getFilterParams(filters: QueryFilters = {}) {
- return filtersToArray(filters).reduce((obj, { name, value }) => {
- if (name && value !== undefined) {
- obj[name] = value;
- }
+function getQueryParams(filters: Record) {
+ return {
+ ...filters,
+ ...filtersObjectToArray(filters).reduce((obj, { name, value }) => {
+ if (name && value !== undefined) {
+ obj[name] = value;
+ }
- return obj;
- }, {});
+ return obj;
+ }, {}),
+ };
}
-async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
- const website = await getWebsite(websiteId);
+function parseFilters(filters: Record, options?: QueryOptions) {
+ const cohortFilters = Object.fromEntries(
+ Object.entries(filters).filter(([key]) => key.startsWith('cohort_')),
+ );
return {
filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters),
- params: {
- ...getFilterParams(filters),
- websiteId,
- startDate: maxDate(filters.startDate, new Date(website?.resetAt)),
- },
+ queryParams: getQueryParams(filters),
+ cohortQuery: getCohortQuery(cohortFilters),
};
}
-async function pagedQuery(
+async function pagedRawQuery(
query: string,
- queryParams: { [key: string]: any },
- pageParams: PageParams = {},
+ queryParams: Record,
+ filters: QueryFilters,
+ name?: string,
) {
- const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams;
+ const { page = 1, pageSize, orderBy, sortDescending = false, search } = filters;
const size = +pageSize || DEFAULT_PAGE_SIZE;
const offset = +size * (+page - 1);
const direction = sortDescending ? 'desc' : 'asc';
@@ -170,18 +199,18 @@ async function pagedQuery(
res => res[0].num,
);
- const data = await rawQuery(`${query}${statements}`, queryParams);
+ const data = await rawQuery(`${query}${statements}`, queryParams, name);
- return { data, count, page: +page, pageSize: size, orderBy };
+ return { data, count, page: +page, pageSize: size, orderBy, search };
}
async function rawQuery(
query: string,
params: Record = {},
+ name?: string,
): Promise {
if (process.env.LOG_QUERY) {
- log('QUERY:\n', query);
- log('PARAMETERS:\n', params);
+ log({ query, params, name });
}
await connect();
@@ -219,7 +248,7 @@ async function findFirst(data: any[]) {
async function connect() {
if (enabled && !clickhouse) {
- clickhouse = process.env.CLICKHOUSE_URL && (global[CLICKHOUSE] || getClient());
+ clickhouse = process.env.CLICKHOUSE_URL && (globalThis[CLICKHOUSE] || getClient());
}
return clickhouse;
@@ -236,7 +265,7 @@ export default {
getFilterQuery,
getUTCString,
parseFilters,
- pagedQuery,
+ pagedRawQuery,
findUnique,
findFirst,
rawQuery,
diff --git a/src/lib/client.ts b/src/lib/client.ts
index 795e7780..e176215e 100644
--- a/src/lib/client.ts
+++ b/src/lib/client.ts
@@ -1,4 +1,4 @@
-import { getItem, setItem, removeItem } from '@/lib/storage';
+import { getItem, removeItem, setItem } from '@/lib/storage';
import { AUTH_TOKEN } from './constants';
export function getClientAuthToken() {
diff --git a/src/lib/colors.ts b/src/lib/colors.ts
index ba329805..2ae9bda5 100644
--- a/src/lib/colors.ts
+++ b/src/lib/colors.ts
@@ -1,4 +1,15 @@
-import md5 from 'md5';
+import { colord } from 'colord';
+import { THEME_COLORS } from '@/lib/constants';
+
+export function hex6(str: string) {
+ let h = 0x811c9dc5; // FNV-1a 32-bit offset
+ for (let i = 0; i < str.length; i++) {
+ h ^= str.charCodeAt(i);
+ h = (h >>> 0) * 0x01000193; // FNV prime
+ }
+ // use lower 24 bits; pad to 6 hex chars
+ return ((h >>> 0) & 0xffffff).toString(16).padStart(6, '0');
+}
export const pick = (num: number, arr: any[]) => {
return arr[num % arr.length];
@@ -38,8 +49,43 @@ export function getPastel(color: string, factor: number = 0.5, prefix = '') {
}
export function getColor(seed: string, min: number = 0, max: number = 255) {
- const color = md5(seed).substring(0, 6);
+ const color = hex6(seed);
const { r, g, b } = hex2RGB(color, min, max);
return rgb2Hex(r, g, b);
}
+
+export function getThemeColors(theme: string) {
+ const { primary, text, line, fill } = THEME_COLORS[theme];
+ const primaryColor = colord(THEME_COLORS[theme].primary);
+
+ return {
+ colors: {
+ theme: {
+ ...THEME_COLORS[theme],
+ },
+ chart: {
+ text,
+ line,
+ views: {
+ hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(),
+ backgroundColor: primaryColor.alpha(0.4).toRgbString(),
+ borderColor: primaryColor.alpha(0.7).toRgbString(),
+ hoverBorderColor: primaryColor.toRgbString(),
+ },
+ visitors: {
+ hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(),
+ backgroundColor: primaryColor.alpha(0.6).toRgbString(),
+ borderColor: primaryColor.alpha(0.9).toRgbString(),
+ hoverBorderColor: primaryColor.toRgbString(),
+ },
+ },
+ map: {
+ baseColor: primary,
+ fillColor: fill,
+ strokeColor: primary,
+ hoverColor: primary,
+ },
+ },
+ };
+}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index ccbd0044..e5090c3c 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -5,35 +5,44 @@ export const TIMEZONE_CONFIG = 'umami.timezone';
export const DATE_RANGE_CONFIG = 'umami.date-range';
export const THEME_CONFIG = 'umami.theme';
export const DASHBOARD_CONFIG = 'umami.dashboard';
+export const LAST_TEAM_CONFIG = 'umami.last-team';
export const VERSION_CHECK = 'umami.version-check';
export const SHARE_TOKEN_HEADER = 'x-umami-share-token';
export const HOMEPAGE_URL = 'https://umami.is';
+export const DOCS_URL = 'https://umami.is/docs';
export const REPO_URL = 'https://github.com/umami-software/umami';
export const UPDATES_URL = 'https://api.umami.is/v1/updates';
export const TELEMETRY_PIXEL = 'https://i.umami.is/a.png';
export const FAVICON_URL = 'https://icons.duckduckgo.com/ip3/{{domain}}.ico';
+export const LINKS_URL = `${globalThis?.location?.origin}/q`;
+export const PIXELS_URL = `${globalThis?.location?.origin}/p`;
-export const DEFAULT_LOCALE = process.env.defaultLocale || 'en-US';
+export const DEFAULT_LOCALE = 'en-US';
export const DEFAULT_THEME = 'light';
export const DEFAULT_ANIMATION_DURATION = 300;
-export const DEFAULT_DATE_RANGE = '24hour';
+export const DEFAULT_DATE_RANGE_VALUE = '24hour';
export const DEFAULT_WEBSITE_LIMIT = 10;
export const DEFAULT_RESET_DATE = '2000-01-01';
-export const DEFAULT_PAGE_SIZE = 10;
+export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_DATE_COMPARE = 'prev';
export const REALTIME_RANGE = 30;
export const REALTIME_INTERVAL = 10000;
-export const FILTER_COMBINED = 'filter-combined';
-export const FILTER_RAW = 'filter-raw';
-export const FILTER_DAY = 'filter-day';
-export const FILTER_RANGE = 'filter-range';
-export const FILTER_REFERRERS = 'filter-referrers';
-export const FILTER_PAGES = 'filter-pages';
-
export const UNIT_TYPES = ['year', 'month', 'hour', 'day', 'minute'];
-export const EVENT_COLUMNS = ['url', 'entry', 'exit', 'referrer', 'title', 'query', 'event', 'tag'];
+
+export const EVENT_COLUMNS = [
+ 'path',
+ 'entry',
+ 'exit',
+ 'referrer',
+ 'domain',
+ 'title',
+ 'query',
+ 'event',
+ 'tag',
+ 'hostname',
+];
export const SESSION_COLUMNS = [
'browser',
@@ -44,15 +53,20 @@ export const SESSION_COLUMNS = [
'country',
'city',
'region',
- 'host',
];
+export const SEGMENT_TYPES = {
+ segment: 'segment',
+ cohort: 'cohort',
+};
+
export const FILTER_COLUMNS = {
- url: 'url_path',
+ path: 'url_path',
entry: 'url_path',
exit: 'url_path',
referrer: 'referrer_domain',
- host: 'hostname',
+ domain: 'referrer_domain',
+ hostname: 'hostname',
title: 'page_title',
query: 'url_query',
os: 'os',
@@ -64,16 +78,19 @@ export const FILTER_COLUMNS = {
language: 'language',
event: 'event_name',
tag: 'tag',
+ eventType: 'event_type',
};
export const COLLECTION_TYPE = {
event: 'event',
identify: 'identify',
-};
+} as const;
export const EVENT_TYPE = {
pageView: 1,
customEvent: 2,
+ linkEvent: 3,
+ pixelEvent: 4,
} as const;
export const DATA_TYPE = {
@@ -101,41 +118,12 @@ export const OPERATORS = {
after: 'af',
} as const;
-export const OPERATOR_PREFIXES = {
- [OPERATORS.equals]: '',
- [OPERATORS.notEquals]: '!',
- [OPERATORS.contains]: '~',
- [OPERATORS.doesNotContain]: '!~',
-};
-
export const DATA_TYPES = {
[DATA_TYPE.string]: 'string',
[DATA_TYPE.number]: 'number',
[DATA_TYPE.boolean]: 'boolean',
[DATA_TYPE.date]: 'date',
[DATA_TYPE.array]: 'array',
-};
-
-export const REPORT_TYPES = {
- funnel: 'funnel',
- goals: 'goals',
- insights: 'insights',
- retention: 'retention',
- utm: 'utm',
- journey: 'journey',
- revenue: 'revenue',
- attribution: 'attribution',
-} as const;
-
-export const REPORT_PARAMETERS = {
- fields: 'fields',
- filters: 'filters',
- groups: 'groups',
-} as const;
-
-export const KAFKA_TOPIC = {
- event: 'event',
- eventData: 'event_data',
} as const;
export const ROLES = {
@@ -196,33 +184,17 @@ export const ROLE_PERMISSIONS = {
export const THEME_COLORS = {
light: {
primary: '#2680eb',
- gray50: '#ffffff',
- gray75: '#fafafa',
- gray100: '#f5f5f5',
- gray200: '#eaeaea',
- gray300: '#e1e1e1',
- gray400: '#cacaca',
- gray500: '#b3b3b3',
- gray600: '#8e8e8e',
- gray700: '#6e6e6e',
- gray800: '#4b4b4b',
- gray900: '#2c2c2c',
+ text: '#838383',
+ line: '#d9d9d9',
+ fill: '#f9f9f9',
},
dark: {
primary: '#2680eb',
- gray50: '#252525',
- gray75: '#2f2f2f',
- gray100: '#323232',
- gray200: '#3e3e3e',
- gray300: '#4a4a4a',
- gray400: '#5a5a5a',
- gray500: '#6e6e6e',
- gray600: '#909090',
- gray700: '#b9b9b9',
- gray800: '#e3e3e3',
- gray900: '#ffffff',
+ text: '#7b7b7b',
+ line: '#3a3a3a',
+ fill: '#191919',
},
-};
+} as const;
export const CHART_COLORS = [
'#2680eb',
@@ -241,44 +213,15 @@ export const CHART_COLORS = [
export const DOMAIN_REGEX =
/^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-_]{1,63}\.)(xn--)?[a-z0-9-_]+(-[a-z0-9-_]+)*\.)+(xn--)?[a-z0-9-_]{2,63})$/;
-export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{8,16}$/;
+export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{8,50}$/;
export const DATETIME_REGEX =
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3}(Z|\+[0-9]{2}:[0-9]{2})?)?$/;
-export const DESKTOP_SCREEN_WIDTH = 1920;
-export const LAPTOP_SCREEN_WIDTH = 1024;
-export const MOBILE_SCREEN_WIDTH = 479;
-
export const URL_LENGTH = 500;
export const PAGE_TITLE_LENGTH = 500;
export const EVENT_NAME_LENGTH = 50;
-export const UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
-
-export const DESKTOP_OS = [
- 'BeOS',
- 'Chrome OS',
- 'Linux',
- 'Mac OS',
- 'Open BSD',
- 'OS/2',
- 'QNX',
- 'Sun OS',
- 'Windows 10',
- 'Windows 2000',
- 'Windows 3.11',
- 'Windows 7',
- 'Windows 8',
- 'Windows 8.1',
- 'Windows 95',
- 'Windows 98',
- 'Windows ME',
- 'Windows Server 2003',
- 'Windows Vista',
- 'Windows XP',
-];
-
-export const MOBILE_OS = ['Amazon OS', 'Android OS', 'BlackBerry OS', 'iOS', 'Windows Mobile'];
+export const UTM_PARAMS = ['utm_campaign', 'utm_content', 'utm_medium', 'utm_source', 'utm_term'];
export const OS_NAMES = {
'Android OS': 'Android',
@@ -286,13 +229,13 @@ export const OS_NAMES = {
'Mac OS': 'macOS',
'Sun OS': 'SunOS',
'Windows 10': 'Windows 10/11',
-};
+} as const;
export const BROWSERS = {
android: 'Android',
aol: 'AOL',
- beaker: 'Beaker',
bb10: 'BlackBerry 10',
+ beaker: 'Beaker',
chrome: 'Chrome',
'chromium-webview': 'Chrome (webview)',
crios: 'Chrome (iOS)',
@@ -314,366 +257,426 @@ export const BROWSERS = {
phantomjs: 'PhantomJS',
safari: 'Safari',
samsung: 'Samsung',
- silk: 'Silk',
searchbot: 'Searchbot',
+ silk: 'Silk',
yandexbrowser: 'Yandex',
-};
-
-export const IP_ADDRESS_HEADERS = [
- 'cf-connecting-ip',
- 'x-client-ip',
- 'x-forwarded-for',
- 'do-connecting-ip',
- 'fastly-client-ip',
- 'true-client-ip',
- 'x-real-ip',
- 'x-cluster-client-ip',
- 'x-forwarded',
- 'forwarded',
- 'x-appengine-user-ip',
-];
+} as const;
export const SOCIAL_DOMAINS = [
+ 'bsky.app',
'facebook.com',
'fb.com',
- 'instagram.com',
'ig.com',
- 'twitter.com',
- 't.co',
- 'x.com',
+ 'instagram.com',
'linkedin.',
- 'tiktok.',
- 'reddit.',
- 'threads.net',
- 'bsky.app',
'news.ycombinator.com',
- 'snapchat.',
'pinterest.',
+ 'reddit.',
+ 'snapchat.',
+ 't.co',
+ 'threads.net',
+ 'tiktok.',
+ 'twitter.com',
+ 'x.com',
];
export const SEARCH_DOMAINS = [
- 'google.',
+ 'baidu.com',
'bing.com',
- 'msn.com',
+ 'chatgpt.com',
'duckduckgo.com',
+ 'ecosia.org',
+ 'google.',
+ 'msn.com',
+ 'perplexity.ai',
'search.brave.com',
'yandex.',
- 'baidu.com',
- 'ecosia.org',
- 'chatgpt.com',
- 'perplexity.ai',
];
export const SHOPPING_DOMAINS = [
- 'amazon.',
- 'ebay.com',
- 'walmart.com',
- 'alibab.com',
+ 'alibaba.com',
'aliexpress.com',
- 'etsy.com',
+ 'amazon.',
'bestbuy.com',
- 'target.com',
+ 'ebay.com',
+ 'etsy.com',
'newegg.com',
+ 'target.com',
+ 'walmart.com',
];
export const EMAIL_DOMAINS = [
'gmail.',
+ 'hotmail.',
'mail.yahoo.',
'outlook.',
- 'hotmail.',
- 'protonmail.',
'proton.me',
+ 'protonmail.',
];
-export const VIDEO_DOMAINS = ['youtube.', 'twitch.'];
+export const VIDEO_DOMAINS = ['twitch.', 'youtube.'];
export const PAID_AD_PARAMS = [
- 'utm_source=google',
- 'gclid=',
- 'fbclid=',
- 'msclkid=',
- 'dclid=',
- 'twclid=',
- 'li_fat_id=',
- 'epik=',
- 'ttclid=',
- 'scid=',
- 'aid=',
- 'pc_id=',
'ad_id=',
- 'rdt_cid=',
+ 'aid=',
+ 'dclid=',
+ 'epik=',
+ 'fbclid=',
+ 'gclid=',
+ 'li_fat_id=',
+ 'msclkid=',
'ob_click_id=',
+ 'pc_id=',
+ 'rdt_cid=',
+ 'scid=',
+ 'ttclid=',
+ 'twclid=',
'utm_medium=cpc',
'utm_medium=paid',
'utm_medium=paid_social',
+ 'utm_source=google',
];
export const GROUPED_DOMAINS = [
- { name: 'Google', domain: 'google.com', match: 'google.' },
- { name: 'Facebook', domain: 'facebook.com', match: 'facebook.' },
- { name: 'Reddit', domain: 'reddit.com', match: 'reddit.' },
- { name: 'LinkedIn', domain: 'linkedin.com', match: 'linkedin.' },
- { name: 'GitHub', domain: 'github.com', match: 'github.' },
- { name: 'Hacker News', domain: 'news.ycombinator.com', match: 'news.ycombinator.com' },
+ { name: 'Baidu', domain: 'baidu.com', match: 'baidu.' },
{ name: 'Bing', domain: 'bing.com', match: 'bing.' },
{ name: 'Brave', domain: 'brave.com', match: 'brave.' },
- { name: 'DuckDuckGo', domain: 'duckduckgo.com', match: 'duckduckgo.' },
- { name: 'Twitter', domain: 'twitter.com', match: ['twitter.', 't.co', 'x.com'] },
- { name: 'Instagram', domain: 'instagram.com', match: ['instagram.', 'ig.com'] },
- { name: 'Snapchat', domain: 'snapchat.com', match: 'snapchat.' },
- { name: 'Pinterest', domain: 'pinterest.com', match: 'pinterest.' },
{ name: 'ChatGPT', domain: 'chatgpt.com', match: 'chatgpt.' },
+ { name: 'DuckDuckGo', domain: 'duckduckgo.com', match: 'duckduckgo.' },
+ { name: 'Facebook', domain: 'facebook.com', match: 'facebook.' },
+ { name: 'GitHub', domain: 'github.com', match: 'github.' },
+ { name: 'Google', domain: 'google.com', match: 'google.' },
+ { name: 'Hacker News', domain: 'news.ycombinator.com', match: 'news.ycombinator.com' },
+ { name: 'Instagram', domain: 'instagram.com', match: ['instagram.', 'ig.com'] },
+ { name: 'LinkedIn', domain: 'linkedin.com', match: 'linkedin.' },
+ { name: 'Pinterest', domain: 'pinterest.com', match: 'pinterest.' },
+ { name: 'Reddit', domain: 'reddit.com', match: 'reddit.' },
+ { name: 'Snapchat', domain: 'snapchat.com', match: 'snapchat.' },
+ { name: 'Twitter', domain: 'twitter.com', match: ['twitter.', 't.co', 'x.com'] },
+ { name: 'Yahoo', domain: 'yahoo.com', match: 'yahoo.' },
+ { name: 'Yandex', domain: 'yandex.ru', match: 'yandex.' },
];
export const MAP_FILE = '/datamaps.world.json';
export const ISO_COUNTRIES = {
+ ABW: 'AW',
AFG: 'AF',
- ALA: 'AX',
- ALB: 'AL',
- DZA: 'DZ',
- ASM: 'AS',
- AND: 'AD',
AGO: 'AO',
AIA: 'AI',
- ATA: 'AQ',
- ATG: 'AG',
+ ALA: 'AX',
+ ALB: 'AL',
+ AND: 'AD',
+ ANT: 'AN',
+ ARE: 'AE',
ARG: 'AR',
ARM: 'AM',
- ABW: 'AW',
+ ASM: 'AS',
+ ATF: 'TF',
+ ATG: 'AG',
AUS: 'AU',
AUT: 'AT',
AZE: 'AZ',
- BHS: 'BS',
- BHR: 'BH',
- BGD: 'BD',
- BRB: 'BB',
- BLR: 'BY',
- BEL: 'BE',
- BLZ: 'BZ',
- BEN: 'BJ',
- BMU: 'BM',
- BTN: 'BT',
- BOL: 'BO',
- BIH: 'BA',
- BWA: 'BW',
- BVT: 'BV',
- BRA: 'BR',
- VGB: 'VG',
- IOT: 'IO',
- BRN: 'BN',
- BGR: 'BG',
- BFA: 'BF',
BDI: 'BI',
- KHM: 'KH',
- CMR: 'CM',
- CAN: 'CA',
- CPV: 'CV',
- CYM: 'KY',
+ BEL: 'BE',
+ BEN: 'BJ',
+ BFA: 'BF',
+ BGD: 'BD',
+ BGR: 'BG',
+ BHR: 'BH',
+ BHS: 'BS',
+ BIH: 'BA',
+ BLR: 'BY',
+ BLZ: 'BZ',
+ BLM: 'BL',
+ BMU: 'BM',
+ BOL: 'BO',
+ BRA: 'BR',
+ BRB: 'BB',
+ BRN: 'BN',
+ BTN: 'BT',
+ BVT: 'BV',
+ BWA: 'BW',
CAF: 'CF',
- TCD: 'TD',
+ CAN: 'CA',
+ CCK: 'CC',
+ CHE: 'CH',
CHL: 'CL',
CHN: 'CN',
- HKG: 'HK',
- MAC: 'MO',
- CXR: 'CX',
- CCK: 'CC',
+ CIV: 'CI',
+ CMR: 'CM',
+ COD: 'CD',
+ COG: 'CG',
+ COK: 'CK',
COL: 'CO',
COM: 'KM',
- COG: 'CG',
- COD: 'CD',
- COK: 'CK',
+ CPV: 'CV',
CRI: 'CR',
- CIV: 'CI',
- HRV: 'HR',
CUB: 'CU',
+ CXR: 'CX',
+ CYM: 'KY',
CYP: 'CY',
CZE: 'CZ',
- DNK: 'DK',
+ DEU: 'DE',
DJI: 'DJ',
DMA: 'DM',
+ DNK: 'DK',
DOM: 'DO',
+ DZA: 'DZ',
ECU: 'EC',
EGY: 'EG',
- SLV: 'SV',
- GNQ: 'GQ',
ERI: 'ER',
+ ESH: 'EH',
+ ESP: 'ES',
EST: 'EE',
ETH: 'ET',
- FLK: 'FK',
- FRO: 'FO',
- FJI: 'FJ',
FIN: 'FI',
+ FJI: 'FJ',
+ FLK: 'FK',
FRA: 'FR',
- GUF: 'GF',
- PYF: 'PF',
- ATF: 'TF',
+ FRO: 'FO',
+ FSM: 'FM',
GAB: 'GA',
- GMB: 'GM',
+ GBR: 'GB',
GEO: 'GE',
- DEU: 'DE',
+ GGY: 'GG',
GHA: 'GH',
GIB: 'GI',
- GRC: 'GR',
- GRL: 'GL',
- GRD: 'GD',
- GLP: 'GP',
- GUM: 'GU',
- GTM: 'GT',
- GGY: 'GG',
GIN: 'GN',
+ GLP: 'GP',
+ GMB: 'GM',
GNB: 'GW',
+ GNQ: 'GQ',
+ GRC: 'GR',
+ GRD: 'GD',
+ GRL: 'GL',
+ GTM: 'GT',
+ GUF: 'GF',
+ GUM: 'GU',
GUY: 'GY',
- HTI: 'HT',
+ HKG: 'HK',
HMD: 'HM',
- VAT: 'VA',
HND: 'HN',
+ HRV: 'HR',
+ HTI: 'HT',
HUN: 'HU',
- ISL: 'IS',
- IND: 'IN',
IDN: 'ID',
+ IMN: 'IM',
+ IND: 'IN',
+ IOT: 'IO',
+ IRL: 'IE',
IRN: 'IR',
IRQ: 'IQ',
- IRL: 'IE',
- IMN: 'IM',
+ ISL: 'IS',
ISR: 'IL',
ITA: 'IT',
JAM: 'JM',
- JPN: 'JP',
JEY: 'JE',
JOR: 'JO',
+ JPN: 'JP',
KAZ: 'KZ',
KEN: 'KE',
+ KGZ: 'KG',
+ KHM: 'KH',
KIR: 'KI',
- PRK: 'KP',
+ KNA: 'KN',
KOR: 'KR',
KWT: 'KW',
- KGZ: 'KG',
LAO: 'LA',
- LVA: 'LV',
LBN: 'LB',
- LSO: 'LS',
LBR: 'LR',
LBY: 'LY',
+ LCA: 'LC',
LIE: 'LI',
+ LKA: 'LK',
+ LSO: 'LS',
LTU: 'LT',
LUX: 'LU',
- MKD: 'MK',
+ LVA: 'LV',
+ MAF: 'MF',
+ MAR: 'MA',
+ MCO: 'MC',
+ MDA: 'MD',
MDG: 'MG',
- MWI: 'MW',
- MYS: 'MY',
MDV: 'MV',
+ MEX: 'MX',
+ MHL: 'MH',
+ MKD: 'MK',
MLI: 'ML',
MLT: 'MT',
- MHL: 'MH',
- MTQ: 'MQ',
- MRT: 'MR',
- MUS: 'MU',
- MYT: 'YT',
- MEX: 'MX',
- FSM: 'FM',
- MDA: 'MD',
- MCO: 'MC',
- MNG: 'MN',
- MNE: 'ME',
- MSR: 'MS',
- MAR: 'MA',
- MOZ: 'MZ',
MMR: 'MM',
- NAM: 'NA',
- NRU: 'NR',
- NPL: 'NP',
- NLD: 'NL',
- ANT: 'AN',
- NCL: 'NC',
- NZL: 'NZ',
- NIC: 'NI',
- NER: 'NE',
- NGA: 'NG',
- NIU: 'NU',
- NFK: 'NF',
+ MNE: 'ME',
+ MNG: 'MN',
MNP: 'MP',
+ MOZ: 'MZ',
+ MRT: 'MR',
+ MSR: 'MS',
+ MTQ: 'MQ',
+ MUS: 'MU',
+ MWI: 'MW',
+ MYS: 'MY',
+ MYT: 'YT',
+ NAM: 'NA',
+ NCL: 'NC',
+ NER: 'NE',
+ NFK: 'NF',
+ NGA: 'NG',
+ NIC: 'NI',
+ NIU: 'NU',
+ NLD: 'NL',
NOR: 'NO',
+ NPL: 'NP',
+ NRU: 'NR',
+ NZL: 'NZ',
OMN: 'OM',
PAK: 'PK',
- PLW: 'PW',
- PSE: 'PS',
PAN: 'PA',
- PNG: 'PG',
- PRY: 'PY',
+ PCN: 'PN',
PER: 'PE',
PHL: 'PH',
- PCN: 'PN',
+ PLW: 'PW',
+ PNG: 'PG',
POL: 'PL',
- PRT: 'PT',
PRI: 'PR',
+ PRK: 'KP',
+ PRT: 'PT',
+ PRY: 'PY',
+ PSE: 'PS',
+ PYF: 'PF',
QAT: 'QA',
REU: 'RE',
ROU: 'RO',
RUS: 'RU',
RWA: 'RW',
- BLM: 'BL',
- SHN: 'SH',
- KNA: 'KN',
- LCA: 'LC',
- MAF: 'MF',
- SPM: 'PM',
- VCT: 'VC',
- WSM: 'WS',
- SMR: 'SM',
- STP: 'ST',
SAU: 'SA',
+ SDN: 'SD',
SEN: 'SN',
- SRB: 'RS',
- SYC: 'SC',
- SLE: 'SL',
SGP: 'SG',
+ SGS: 'GS',
+ SHN: 'SH',
+ SJM: 'SJ',
+ SLB: 'SB',
+ SLE: 'SL',
+ SLV: 'SV',
+ SMR: 'SM',
+ SOM: 'SO',
+ SPM: 'PM',
+ SRB: 'RS',
+ SUR: 'SR',
+ STP: 'ST',
SVK: 'SK',
SVN: 'SI',
- SLB: 'SB',
- SOM: 'SO',
- ZAF: 'ZA',
- SGS: 'GS',
- SSD: 'SS',
- ESP: 'ES',
- LKA: 'LK',
- SDN: 'SD',
- SUR: 'SR',
- SJM: 'SJ',
- SWZ: 'SZ',
SWE: 'SE',
- CHE: 'CH',
+ SWZ: 'SZ',
+ SYC: 'SC',
SYR: 'SY',
- TWN: 'TW',
- TJK: 'TJ',
- TZA: 'TZ',
- THA: 'TH',
- TLS: 'TL',
+ TCA: 'TC',
+ TCD: 'TD',
TGO: 'TG',
+ THA: 'TH',
+ TJK: 'TJ',
TKL: 'TK',
+ TKM: 'TM',
+ TLS: 'TL',
TON: 'TO',
TTO: 'TT',
TUN: 'TN',
TUR: 'TR',
- TKM: 'TM',
- TCA: 'TC',
TUV: 'TV',
+ TWN: 'TW',
+ TZA: 'TZ',
UGA: 'UG',
UKR: 'UA',
- ARE: 'AE',
- GBR: 'GB',
- USA: 'US',
UMI: 'UM',
URY: 'UY',
+ USA: 'US',
UZB: 'UZ',
- VUT: 'VU',
+ VAT: 'VA',
+ VCT: 'VC',
VEN: 'VE',
- VNM: 'VN',
+ VGB: 'VG',
VIR: 'VI',
+ VNM: 'VN',
+ VUT: 'VU',
WLF: 'WF',
- ESH: 'EH',
+ WSM: 'WS',
+ XKX: 'XK',
YEM: 'YE',
+ ZAF: 'ZA',
ZMB: 'ZM',
ZWE: 'ZW',
- XKX: 'XK',
+};
+
+export const CURRENCIES = [
+ { id: 'USD', name: 'US Dollar' },
+ { id: 'EUR', name: 'Euro' },
+ { id: 'GBP', name: 'British Pound' },
+ { id: 'JPY', name: 'Japanese Yen' },
+ { id: 'CNY', name: 'Chinese Renminbi (Yuan)' },
+ { id: 'CAD', name: 'Canadian Dollar' },
+ { id: 'HKD', name: 'Hong Kong Dollar' },
+ { id: 'AUD', name: 'Australian Dollar' },
+ { id: 'SGD', name: 'Singapore Dollar' },
+ { id: 'CHF', name: 'Swiss Franc' },
+ { id: 'SEK', name: 'Swedish Krona' },
+ { id: 'PLN', name: 'Polish Złoty' },
+ { id: 'NOK', name: 'Norwegian Krone' },
+ { id: 'DKK', name: 'Danish Krone' },
+ { id: 'NZD', name: 'New Zealand Dollar' },
+ { id: 'ZAR', name: 'South African Rand' },
+ { id: 'MXN', name: 'Mexican Peso' },
+ { id: 'THB', name: 'Thai Baht' },
+ { id: 'HUF', name: 'Hungarian Forint' },
+ { id: 'MYR', name: 'Malaysian Ringgit' },
+ { id: 'INR', name: 'Indian Rupee' },
+ { id: 'KRW', name: 'South Korean Won' },
+ { id: 'BRL', name: 'Brazilian Real' },
+ { id: 'TRY', name: 'Turkish Lira' },
+ { id: 'CZK', name: 'Czech Koruna' },
+ { id: 'ILS', name: 'Israeli New Shekel' },
+ { id: 'RUB', name: 'Russian Ruble' },
+ { id: 'AED', name: 'United Arab Emirates Dirham' },
+ { id: 'IDR', name: 'Indonesian Rupiah' },
+ { id: 'PHP', name: 'Philippine Peso' },
+ { id: 'RON', name: 'Romanian Leu' },
+ { id: 'COP', name: 'Colombian Peso' },
+ { id: 'SAR', name: 'Saudi Riyal' },
+ { id: 'ARS', name: 'Argentine Peso' },
+ { id: 'VND', name: 'Vietnamese Dong' },
+ { id: 'CLP', name: 'Chilean Peso' },
+ { id: 'EGP', name: 'Egyptian Pound' },
+ { id: 'KWD', name: 'Kuwaiti Dinar' },
+ { id: 'PKR', name: 'Pakistani Rupee' },
+ { id: 'QAR', name: 'Qatari Riyal' },
+ { id: 'BHD', name: 'Bahraini Dinar' },
+ { id: 'UAH', name: 'Ukrainian Hryvnia' },
+ { id: 'PEN', name: 'Peruvian Sol' },
+ { id: 'BDT', name: 'Bangladeshi Taka' },
+ { id: 'MAD', name: 'Moroccan Dirham' },
+ { id: 'KES', name: 'Kenyan Shilling' },
+ { id: 'NGN', name: 'Nigerian Naira' },
+ { id: 'TND', name: 'Tunisian Dinar' },
+ { id: 'OMR', name: 'Omani Rial' },
+ { id: 'GHS', name: 'Ghanaian Cedi' },
+];
+
+export const TIMEZONE_LEGACY: Record = {
+ 'Asia/Batavia': 'Asia/Jakarta',
+ 'Asia/Calcutta': 'Asia/Kolkata',
+ 'Asia/Chongqing': 'Asia/Shanghai',
+ 'Asia/Harbin': 'Asia/Shanghai',
+ 'Asia/Jayapura': 'Asia/Pontianak',
+ 'Asia/Katmandu': 'Asia/Kathmandu',
+ 'Asia/Macao': 'Asia/Macau',
+ 'Asia/Rangoon': 'Asia/Yangon',
+ 'Asia/Saigon': 'Asia/Ho_Chi_Minh',
+ 'Europe/Kiev': 'Europe/Kyiv',
+ 'Europe/Zaporozhye': 'Europe/Kyiv',
+ 'Etc/UTC': 'UTC',
+ 'US/Arizona': 'America/Phoenix',
+ 'US/Central': 'America/Chicago',
+ 'US/Eastern': 'America/New_York',
+ 'US/Mountain': 'America/Denver',
+ 'US/Pacific': 'America/Los_Angeles',
+ 'US/Samoa': 'Pacific/Pago_Pago',
};
diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts
index d22bad09..a6d912b8 100644
--- a/src/lib/crypto.ts
+++ b/src/lib/crypto.ts
@@ -1,6 +1,5 @@
-import crypto from 'crypto';
-import prand from 'pure-rand';
-import { v4, v5 } from 'uuid';
+import crypto from 'node:crypto';
+import { v4, v5, v7 } from 'uuid';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
@@ -12,25 +11,6 @@ const ENC_POSITION = TAG_POSITION + TAG_LENGTH;
const HASH_ALGO = 'sha512';
const HASH_ENCODING = 'hex';
-const seed = Date.now() ^ (Math.random() * 0x100000000);
-const rng = prand.xoroshiro128plus(seed);
-
-export function random(min: number, max: number) {
- return prand.unsafeUniformIntDistribution(min, max, rng);
-}
-
-export function getRandomChars(
- n: number,
- chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
-) {
- const arr = chars.split('');
- let s = '';
- for (let i = 0; i < n; i++) {
- s += arr[random(0, arr.length - 1)];
- }
- return s;
-}
-
const getKey = (password: string, salt: Buffer) =>
crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha512');
@@ -77,7 +57,9 @@ export function secret() {
}
export function uuid(...args: any) {
- if (!args.length) return v4();
+ if (args.length) {
+ return v5(hash(...args, secret()), v5.DNS);
+ }
- return v5(hash(...args, secret()), v5.DNS);
+ return process.env.USE_UUIDV7 ? v7() : v4();
}
diff --git a/src/lib/data.ts b/src/lib/data.ts
index cf2722b5..fe69edf0 100644
--- a/src/lib/data.ts
+++ b/src/lib/data.ts
@@ -1,8 +1,8 @@
import { DATA_TYPE, DATETIME_REGEX } from './constants';
-import { DynamicDataType } from './types';
+import type { DynamicDataType } from './types';
export function flattenJSON(
- eventData: { [key: string]: any },
+ eventData: Record,
keyValues: { key: string; value: any; dataType: DynamicDataType }[] = [],
parentKey = '',
): { key: string; value: any; dataType: DynamicDataType }[] {
diff --git a/src/lib/date.ts b/src/lib/date.ts
index 96135845..3c1fd1b7 100644
--- a/src/lib/date.ts
+++ b/src/lib/date.ts
@@ -1,42 +1,45 @@
import {
- addMinutes,
- addHours,
addDays,
+ addHours,
+ addMinutes,
addMonths,
+ addWeeks,
addYears,
- subMinutes,
- subHours,
- subDays,
- subMonths,
- subYears,
- startOfMinute,
- startOfHour,
- startOfDay,
- startOfWeek,
- startOfMonth,
- startOfYear,
- endOfHour,
- endOfDay,
- endOfWeek,
- endOfMonth,
- endOfYear,
- differenceInMinutes,
- differenceInHours,
differenceInCalendarDays,
- differenceInCalendarWeeks,
differenceInCalendarMonths,
+ differenceInCalendarWeeks,
differenceInCalendarYears,
+ differenceInHours,
+ differenceInMinutes,
+ endOfDay,
+ endOfHour,
+ endOfMinute,
+ endOfMonth,
+ endOfWeek,
+ endOfYear,
format,
+ isBefore,
+ isDate,
+ isEqual,
+ isSameDay,
max,
min,
- isDate,
- addWeeks,
+ startOfDay,
+ startOfHour,
+ startOfMinute,
+ startOfMonth,
+ startOfWeek,
+ startOfYear,
+ subDays,
+ subHours,
+ subMinutes,
+ subMonths,
subWeeks,
- endOfMinute,
- isSameDay,
+ subYears,
} from 'date-fns';
+import { utcToZonedTime } from 'date-fns-tz';
import { getDateLocale } from '@/lib/lang';
-import { DateRange } from '@/lib/types';
+import type { DateRange } from '@/lib/types';
export const TIME_UNIT = {
minute: 'minute',
@@ -47,19 +50,7 @@ export const TIME_UNIT = {
year: 'year',
};
-export const CUSTOM_FORMATS = {
- 'en-US': {
- p: 'ha',
- pp: 'h:mm:ss',
- },
- 'fr-FR': {
- 'M/d': 'd/M',
- 'MMM d': 'd MMM',
- 'EEE M/d': 'EEE d/M',
- },
-};
-
-const DATE_FUNCTIONS = {
+export const DATE_FUNCTIONS = {
minute: {
diff: differenceInMinutes,
add: addMinutes,
@@ -104,11 +95,29 @@ const DATE_FUNCTIONS = {
},
};
+export const DATE_FORMATS = {
+ minute: 'yyyy-MM-dd HH:mm',
+ hour: 'yyyy-MM-dd HH',
+ day: 'yyyy-MM-dd',
+ week: "yyyy-'W'II",
+ month: 'yyyy-MM',
+ year: 'yyyy',
+};
+
+const TIMEZONE_MAPPINGS: Record = {
+ 'Asia/Calcutta': 'Asia/Kolkata',
+};
+
+export function normalizeTimezone(timezone: string): string {
+ return TIMEZONE_MAPPINGS[timezone] || timezone;
+}
+
export function isValidTimezone(timezone: string) {
try {
- Intl.DateTimeFormat(undefined, { timeZone: timezone });
+ const normalizedTimezone = normalizeTimezone(timezone);
+ Intl.DateTimeFormat(undefined, { timeZone: normalizedTimezone });
return true;
- } catch (error) {
+ } catch {
return false;
}
}
@@ -127,43 +136,36 @@ export function parseDateValue(value: string) {
return { num: +num, unit };
}
-export function parseDateRange(value: string | object, locale = 'en-US'): DateRange {
- if (typeof value === 'object') {
- return value as DateRange;
+export function parseDateRange(value: string, locale = 'en-US', timezone?: string): DateRange {
+ if (typeof value !== 'string') {
+ return null;
}
- if (value === 'all') {
- return {
- startDate: new Date(0),
- endDate: new Date(1),
- value,
- };
- }
-
- if (value?.startsWith?.('range')) {
+ if (value.startsWith('range')) {
const [, startTime, endTime] = value.split(':');
const startDate = new Date(+startTime);
const endDate = new Date(+endTime);
+ const unit = getMinimumUnit(startDate, endDate);
return {
startDate,
endDate,
value,
...parseDateValue(value),
- offset: 0,
- unit: getMinimumUnit(startDate, endDate),
+ unit,
};
}
- const now = new Date();
+ const date = new Date();
+ const now = timezone ? utcToZonedTime(date, timezone) : date;
const dateLocale = getDateLocale(locale);
- const { num, unit } = parseDateValue(value);
+ const { num = 1, unit } = parseDateValue(value);
switch (unit) {
case 'hour':
return {
- startDate: num ? subHours(startOfHour(now), num - 1) : startOfHour(now),
+ startDate: num ? subHours(startOfHour(now), num) : startOfHour(now),
endDate: endOfHour(now),
offset: 0,
num: num || 1,
@@ -172,7 +174,7 @@ export function parseDateRange(value: string | object, locale = 'en-US'): DateRa
};
case 'day':
return {
- startDate: num ? subDays(startOfDay(now), num - 1) : startOfDay(now),
+ startDate: num ? subDays(startOfDay(now), num) : startOfDay(now),
endDate: endOfDay(now),
unit: num ? 'day' : 'hour',
offset: 0,
@@ -182,7 +184,7 @@ export function parseDateRange(value: string | object, locale = 'en-US'): DateRa
case 'week':
return {
startDate: num
- ? subWeeks(startOfWeek(now, { locale: dateLocale }), num - 1)
+ ? subWeeks(startOfWeek(now, { locale: dateLocale }), num)
: startOfWeek(now, { locale: dateLocale }),
endDate: endOfWeek(now, { locale: dateLocale }),
unit: 'day',
@@ -192,7 +194,7 @@ export function parseDateRange(value: string | object, locale = 'en-US'): DateRa
};
case 'month':
return {
- startDate: num ? subMonths(startOfMonth(now), num - 1) : startOfMonth(now),
+ startDate: num ? subMonths(startOfMonth(now), num) : startOfMonth(now),
endDate: endOfMonth(now),
unit: num ? 'month' : 'day',
offset: 0,
@@ -201,7 +203,7 @@ export function parseDateRange(value: string | object, locale = 'en-US'): DateRa
};
case 'year':
return {
- startDate: num ? subYears(startOfYear(now), num - 1) : startOfYear(now),
+ startDate: num ? subYears(startOfYear(now), num) : startOfYear(now),
endDate: endOfYear(now),
unit: 'month',
offset: 0,
@@ -211,10 +213,14 @@ export function parseDateRange(value: string | object, locale = 'en-US'): DateRa
}
}
-export function getOffsetDateRange(dateRange: DateRange, increment: number) {
- const { startDate, endDate, unit, num, offset, value } = dateRange;
+export function getOffsetDateRange(dateRange: DateRange, offset: number) {
+ if (offset === 0) {
+ return dateRange;
+ }
- const change = num * increment;
+ const { startDate, endDate, unit, num, value } = dateRange;
+
+ const change = num * offset;
const { add } = DATE_FUNCTIONS[unit];
const { unit: originalUnit } = parseDateValue(value) || {};
@@ -222,39 +228,39 @@ export function getOffsetDateRange(dateRange: DateRange, increment: number) {
case 'day':
return {
...dateRange,
+ offset,
startDate: addDays(startDate, change),
endDate: addDays(endDate, change),
- offset: offset + increment,
};
case 'week':
return {
...dateRange,
+ offset,
startDate: addWeeks(startDate, change),
endDate: addWeeks(endDate, change),
- offset: offset + increment,
};
case 'month':
return {
...dateRange,
+ offset,
startDate: addMonths(startDate, change),
endDate: addMonths(endDate, change),
- offset: offset + increment,
};
case 'year':
return {
...dateRange,
+ offset,
startDate: addYears(startDate, change),
endDate: addYears(endDate, change),
- offset: offset + increment,
};
default:
return {
startDate: add(startDate, change),
endDate: add(endDate, change),
+ offset,
value,
unit,
num,
- offset: offset + increment,
};
}
}
@@ -281,31 +287,6 @@ export function getMinimumUnit(startDate: number | Date, endDate: number | Date)
return 'year';
}
-export function getDateArray(data: any[], startDate: Date, endDate: Date, unit: string) {
- const arr = [];
- const { diff, add, start } = DATE_FUNCTIONS[unit];
- const n = diff(endDate, startDate);
-
- for (let i = 0; i <= n; i++) {
- const t = start(add(startDate, i));
- const y = data.find(({ x }) => start(new Date(x)).getTime() === t.getTime())?.y || 0;
-
- arr.push({ x: t, y });
- }
-
- return arr;
-}
-
-export function formatDate(date: string | number | Date, str: string, locale = 'en-US') {
- return format(
- typeof date === 'string' ? new Date(date) : date,
- CUSTOM_FORMATS?.[locale]?.[str] || str,
- {
- locale: getDateLocale(locale),
- },
- );
-}
-
export function maxDate(...args: Date[]) {
return max(args.filter(n => isDate(n)));
}
@@ -314,23 +295,18 @@ export function minDate(...args: any[]) {
return min(args.filter(n => isDate(n)));
}
-export function getLocalTime(t: string | number | Date) {
- return addMinutes(new Date(t), new Date().getTimezoneOffset());
-}
-
-export function getDateLength(startDate: Date, endDate: Date, unit: string | number) {
- const { diff } = DATE_FUNCTIONS[unit];
- return diff(endDate, startDate) + 1;
-}
-
export function getCompareDate(compare: string, startDate: Date, endDate: Date) {
if (compare === 'yoy') {
- return { startDate: subYears(startDate, 1), endDate: subYears(endDate, 1) };
+ return { compare, startDate: subYears(startDate, 1), endDate: subYears(endDate, 1) };
}
- const diff = differenceInMinutes(endDate, startDate);
+ if (compare === 'prev') {
+ const diff = differenceInMinutes(endDate, startDate);
- return { startDate: subMinutes(startDate, diff), endDate: subMinutes(endDate, diff) };
+ return { compare, startDate: subMinutes(startDate, diff), endDate: subMinutes(endDate, diff) };
+ }
+
+ return {};
}
export function getDayOfWeekAsDate(dayOfWeek: number) {
@@ -345,3 +321,55 @@ export function getDayOfWeekAsDate(dayOfWeek: number) {
return currentDate;
}
+
+export function formatDate(
+ date: string | number | Date,
+ dateFormat: string = 'PPpp',
+ locale = 'en-US',
+) {
+ return format(typeof date === 'string' ? new Date(date) : date, dateFormat, {
+ locale: getDateLocale(locale),
+ });
+}
+
+export function generateTimeSeries(
+ data: { x: string; y: number; d?: string }[],
+ minDate: Date,
+ maxDate: Date,
+ unit: string,
+ locale: string,
+) {
+ const add = DATE_FUNCTIONS[unit].add;
+ const start = DATE_FUNCTIONS[unit].start;
+ const fmt = DATE_FORMATS[unit];
+
+ let current = start(minDate);
+ const end = start(maxDate);
+
+ const timeseries: string[] = [];
+
+ while (isBefore(current, end) || isEqual(current, end)) {
+ timeseries.push(formatDate(current, fmt, locale));
+ current = add(current, 1);
+ }
+
+ const lookup = new Map(data.map(({ x, y, d }) => [formatDate(x, fmt, locale), { x, y, d }]));
+
+ return timeseries.map(t => {
+ const { x, y, d } = lookup.get(t) || {};
+
+ return { x: t, d: d ?? x, y: y ?? null };
+ });
+}
+
+export function getDateRangeValue(startDate: Date, endDate: Date) {
+ return `range:${startDate.getTime()}:${endDate.getTime()}`;
+}
+
+export function getMonthDateRangeValue(date: Date) {
+ return getDateRangeValue(startOfMonth(date), endOfMonth(date));
+}
+
+export function isInvalidDate(date: any) {
+ return date instanceof Date && Number.isNaN(date.getTime());
+}
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 0ffedd0d..7b6e8368 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -1,17 +1,16 @@
export const PRISMA = 'prisma';
export const POSTGRESQL = 'postgresql';
-export const MYSQL = 'mysql';
export const CLICKHOUSE = 'clickhouse';
export const KAFKA = 'kafka';
export const KAFKA_PRODUCER = 'kafka-producer';
// Fixes issue with converting bigint values
-BigInt.prototype['toJSON'] = function () {
+BigInt.prototype.toJSON = function () {
return Number(this);
};
export function getDatabaseType(url = process.env.DATABASE_URL) {
- const type = url && url.split(':')[0];
+ const type = url?.split(':')[0];
if (type === 'postgres') {
return POSTGRESQL;
@@ -31,7 +30,7 @@ export async function runQuery(queries: any) {
const db = getDatabaseType();
- if (db === POSTGRESQL || db === MYSQL) {
+ if (db === POSTGRESQL) {
return queries[PRISMA]();
}
}
diff --git a/src/lib/detect.ts b/src/lib/detect.ts
index a023d27d..68cb6672 100644
--- a/src/lib/detect.ts
+++ b/src/lib/detect.ts
@@ -1,73 +1,47 @@
-import path from 'path';
+import path from 'node:path';
import { browserName, detectOS } from 'detect-browser';
-import isLocalhost from 'is-localhost-ip';
import ipaddr from 'ipaddr.js';
+import isLocalhost from 'is-localhost-ip';
import maxmind from 'maxmind';
-import {
- DESKTOP_OS,
- MOBILE_OS,
- DESKTOP_SCREEN_WIDTH,
- LAPTOP_SCREEN_WIDTH,
- MOBILE_SCREEN_WIDTH,
- IP_ADDRESS_HEADERS,
-} from './constants';
+import { UAParser } from 'ua-parser-js';
+import { getIpAddress, stripPort } from '@/lib/ip';
+import { safeDecodeURIComponent } from '@/lib/url';
const MAXMIND = 'maxmind';
-export function getIpAddress(headers: Headers) {
- const customHeader = process.env.CLIENT_IP_HEADER;
+const PROVIDER_HEADERS = [
+ // Cloudflare headers
+ {
+ countryHeader: 'cf-ipcountry',
+ regionHeader: 'cf-region-code',
+ cityHeader: 'cf-ipcity',
+ },
+ // Vercel headers
+ {
+ countryHeader: 'x-vercel-ip-country',
+ regionHeader: 'x-vercel-ip-country-region',
+ cityHeader: 'x-vercel-ip-city',
+ },
+ // CloudFront headers
+ {
+ countryHeader: 'cloudfront-viewer-country',
+ regionHeader: 'cloudfront-viewer-country-region',
+ cityHeader: 'cloudfront-viewer-city',
+ },
+];
- if (customHeader && headers.get(customHeader)) {
- return headers.get(customHeader);
- }
-
- const header = IP_ADDRESS_HEADERS.find(name => {
- return headers.get(name);
- });
-
- const ip = headers.get(header);
-
- if (header === 'x-forwarded-for') {
- return ip?.split(',')?.[0]?.trim();
- }
-
- if (header === 'forwarded') {
- const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/);
-
- if (match) {
- return match[1];
- }
- }
-
- return ip;
-}
-
-export function getDevice(screen: string, os: string) {
- if (!screen) return;
+export function getDevice(userAgent: string, screen: string = '') {
+ const { device } = UAParser(userAgent);
const [width] = screen.split('x');
- if (DESKTOP_OS.includes(os)) {
- if (os === 'Chrome OS' || +width < DESKTOP_SCREEN_WIDTH) {
- return 'laptop';
- }
- return 'desktop';
- } else if (MOBILE_OS.includes(os)) {
- if (os === 'Amazon OS' || +width > MOBILE_SCREEN_WIDTH) {
- return 'tablet';
- }
- return 'mobile';
+ const type = device?.type || 'desktop';
+
+ if (type === 'desktop' && screen && +width <= 1920) {
+ return 'laptop';
}
- if (+width >= DESKTOP_SCREEN_WIDTH) {
- return 'desktop';
- } else if (+width >= LAPTOP_SCREEN_WIDTH) {
- return 'laptop';
- } else if (+width >= MOBILE_SCREEN_WIDTH) {
- return 'tablet';
- } else {
- return 'mobile';
- }
+ return type;
}
function getRegionCode(country: string, region: string) {
@@ -88,46 +62,37 @@ function decodeHeader(s: string | undefined | null): string | undefined | null {
export async function getLocation(ip: string = '', headers: Headers, hasPayloadIP: boolean) {
// Ignore local ips
- if (await isLocalhost(ip)) {
- return;
+ if (!ip || (await isLocalhost(ip))) {
+ return null;
}
if (!hasPayloadIP && !process.env.SKIP_LOCATION_HEADERS) {
- // Cloudflare headers
- if (headers.get('cf-ipcountry')) {
- const country = decodeHeader(headers.get('cf-ipcountry'));
- const region = decodeHeader(headers.get('cf-region-code'));
- const city = decodeHeader(headers.get('cf-ipcity'));
+ for (const provider of PROVIDER_HEADERS) {
+ const countryHeader = headers.get(provider.countryHeader);
+ if (countryHeader) {
+ const country = decodeHeader(countryHeader);
+ const region = decodeHeader(headers.get(provider.regionHeader));
+ const city = decodeHeader(headers.get(provider.cityHeader));
- return {
- country,
- region: getRegionCode(country, region),
- city,
- };
- }
-
- // Vercel headers
- if (headers.get('x-vercel-ip-country')) {
- const country = decodeHeader(headers.get('x-vercel-ip-country'));
- const region = decodeHeader(headers.get('x-vercel-ip-country-region'));
- const city = decodeHeader(headers.get('x-vercel-ip-city'));
-
- return {
- country,
- region: getRegionCode(country, region),
- city,
- };
+ return {
+ country,
+ region: getRegionCode(country, region),
+ city,
+ };
+ }
}
}
// Database lookup
- if (!global[MAXMIND]) {
+ if (!globalThis[MAXMIND]) {
const dir = path.join(process.cwd(), 'geo');
- global[MAXMIND] = await maxmind.open(path.resolve(dir, 'GeoLite2-City.mmdb'));
+ globalThis[MAXMIND] = await maxmind.open(
+ process.env.GEOLITE_DB_PATH || path.resolve(dir, 'GeoLite2-City.mmdb'),
+ );
}
- const result = global[MAXMIND].get(ip);
+ const result = globalThis[MAXMIND]?.get(stripPort(ip));
if (result) {
const country = result.country?.iso_code ?? result?.registered_country?.iso_code;
@@ -146,12 +111,12 @@ export async function getClientInfo(request: Request, payload: Record {
- const map = data.reduce((obj, { x, y }) => {
- if (x) {
- if (!obj[x]) {
- obj[x] = y;
- } else {
- obj[x] += y;
- }
- }
-
- return obj;
- }, {});
-
- return Object.keys(map).map(key => ({ x: key, y: map[key] }));
-};
-
-export const refFilter = (data: any[]) => {
- const links = {};
-
- const map = data.reduce((obj, { x, y }) => {
- let id;
-
- try {
- const url = new URL(x);
-
- id = url.hostname.replace(/www\./, '') || url.href;
- } catch {
- id = '';
- }
-
- links[id] = x;
-
- if (!obj[id]) {
- obj[id] = y;
- } else {
- obj[id] += y;
- }
-
- return obj;
- }, {});
-
- return Object.keys(map).map(key => ({ x: key, y: map[key], w: links[key] }));
-};
-
-export const emptyFilter = (data: any[]) => {
- return data.map(item => (item.x ? item : null)).filter(n => n);
-};
-
export const percentFilter = (data: any[]) => {
+ if (!Array.isArray(data)) return [];
const total = data.reduce((n, { y }) => n + y, 0);
return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props }));
};
diff --git a/src/lib/format.ts b/src/lib/format.ts
index e497464a..52fd3048 100644
--- a/src/lib/format.ts
+++ b/src/lib/format.ts
@@ -77,20 +77,20 @@ export function stringToColor(str: string) {
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
- color += ('00' + value.toString(16)).slice(-2);
+ color += `00${value.toString(16)}`.slice(-2);
}
return color;
}
export function formatCurrency(value: number, currency: string, locale = 'en-US') {
- let formattedValue;
+ let formattedValue: Intl.NumberFormat;
try {
formattedValue = new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
});
- } catch (error) {
+ } catch {
// Fallback to default currency format if an error occurs
formattedValue = new Intl.NumberFormat(locale, {
style: 'currency',
diff --git a/src/lib/generate.ts b/src/lib/generate.ts
new file mode 100644
index 00000000..8e25aa09
--- /dev/null
+++ b/src/lib/generate.ts
@@ -0,0 +1,20 @@
+import prand from 'pure-rand';
+
+const seed = Date.now() ^ (Math.random() * 0x100000000);
+const rng = prand.xoroshiro128plus(seed);
+
+export function random(min: number, max: number) {
+ return prand.unsafeUniformIntDistribution(min, max, rng);
+}
+
+export function getRandomChars(
+ n: number,
+ chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
+) {
+ const arr = chars.split('');
+ let s = '';
+ for (let i = 0; i < n; i++) {
+ s += arr[random(0, arr.length - 1)];
+ }
+ return s;
+}
diff --git a/src/lib/ip.ts b/src/lib/ip.ts
new file mode 100644
index 00000000..5cd77574
--- /dev/null
+++ b/src/lib/ip.ts
@@ -0,0 +1,60 @@
+export const IP_ADDRESS_HEADERS = [
+ 'true-client-ip', // CDN
+ 'cf-connecting-ip', // Cloudflare
+ 'fastly-client-ip', // Fastly
+ 'x-nf-client-connection-ip', // Netlify
+ 'do-connecting-ip', // Digital Ocean
+ 'x-real-ip', // Reverse proxy
+ 'x-appengine-user-ip', // Google App Engine
+ 'x-forwarded-for',
+ 'forwarded',
+ 'x-client-ip',
+ 'x-cluster-client-ip',
+ 'x-forwarded',
+];
+
+export function getIpAddress(headers: Headers) {
+ const customHeader = process.env.CLIENT_IP_HEADER;
+
+ if (customHeader && headers.get(customHeader)) {
+ return headers.get(customHeader);
+ }
+
+ const header = IP_ADDRESS_HEADERS.find(name => {
+ return headers.get(name);
+ });
+
+ const ip = headers.get(header);
+
+ if (header === 'x-forwarded-for') {
+ return ip?.split(',')?.[0]?.trim();
+ }
+
+ if (header === 'forwarded') {
+ const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/);
+
+ if (match) {
+ return match[1];
+ }
+ }
+
+ return ip;
+}
+
+export function stripPort(ip: string) {
+ if (ip.startsWith('[')) {
+ const endBracket = ip.indexOf(']');
+ if (endBracket !== -1) {
+ return ip.slice(0, endBracket + 1);
+ }
+ }
+
+ const idx = ip.lastIndexOf(':');
+ if (idx !== -1) {
+ if (ip.includes('.') || /^[a-zA-Z0-9.-]+$/.test(ip.slice(0, idx))) {
+ return ip.slice(0, idx);
+ }
+ }
+
+ return ip;
+}
diff --git a/src/lib/kafka.ts b/src/lib/kafka.ts
index e7f06910..1d60e1f2 100644
--- a/src/lib/kafka.ts
+++ b/src/lib/kafka.ts
@@ -1,8 +1,8 @@
-import { serializeError } from 'serialize-error';
+import type * as tls from 'node:tls';
import debug from 'debug';
-import { Kafka, Producer, RecordMetadata, SASLOptions, logLevel } from 'kafkajs';
+import { Kafka, logLevel, type Producer, type RecordMetadata, type SASLOptions } from 'kafkajs';
+import { serializeError } from 'serialize-error';
import { KAFKA, KAFKA_PRODUCER } from '@/lib/db';
-import * as tls from 'tls';
const log = debug('umami:kafka');
const CONNECT_TIMEOUT = 5000;
@@ -16,7 +16,8 @@ const enabled = Boolean(process.env.KAFKA_URL && process.env.KAFKA_BROKER);
function getClient() {
const { username, password } = new URL(process.env.KAFKA_URL);
const brokers = process.env.KAFKA_BROKER.split(',');
- const mechanism = process.env.KAFKA_SASL_MECHANISM as 'plain' | 'scram-sha-256' | 'scram-sha-512';
+ const mechanism =
+ (process.env.KAFKA_SASL_MECHANISM as 'plain' | 'scram-sha-256' | 'scram-sha-512') || 'plain';
const ssl: { ssl?: tls.ConnectionOptions | boolean; sasl?: SASLOptions } =
username && password
@@ -41,7 +42,7 @@ function getClient() {
});
if (process.env.NODE_ENV !== 'production') {
- global[KAFKA] = client;
+ globalThis[KAFKA] = client;
}
log('Kafka initialized');
@@ -54,7 +55,7 @@ async function getProducer(): Promise {
await producer.connect();
if (process.env.NODE_ENV !== 'production') {
- global[KAFKA_PRODUCER] = producer;
+ globalThis[KAFKA_PRODUCER] = producer;
}
log('Kafka producer initialized');
@@ -64,7 +65,7 @@ async function getProducer(): Promise {
async function sendMessage(
topic: string,
- message: { [key: string]: string | number } | { [key: string]: string | number }[],
+ message: Record | Record[],
): Promise {
try {
await connect();
@@ -91,10 +92,10 @@ async function sendMessage(
async function connect(): Promise {
if (!kafka) {
- kafka = process.env.KAFKA_URL && process.env.KAFKA_BROKER && (global[KAFKA] || getClient());
+ kafka = process.env.KAFKA_URL && process.env.KAFKA_BROKER && (globalThis[KAFKA] || getClient());
if (kafka) {
- producer = global[KAFKA_PRODUCER] || (await getProducer());
+ producer = globalThis[KAFKA_PRODUCER] || (await getProducer());
}
}
diff --git a/src/lib/lang.ts b/src/lib/lang.ts
index 48176d0a..f874640c 100644
--- a/src/lib/lang.ts
+++ b/src/lib/lang.ts
@@ -1,23 +1,24 @@
import {
arSA,
be,
- bn,
bg,
+ bn,
bs,
+ ca,
cs,
- sk,
da,
de,
el,
- enUS,
enGB,
+ enUS,
es,
+ faIR,
fi,
fr,
- faIR,
he,
hi,
hr,
+ hu,
id,
it,
ja,
@@ -33,17 +34,17 @@ import {
ptBR,
ro,
ru,
+ sk,
sl,
sv,
ta,
th,
tr,
uk,
+ uz,
+ vi,
zhCN,
zhTW,
- ca,
- hu,
- vi,
} from 'date-fns/locale';
export const languages = {
@@ -95,6 +96,7 @@ export const languages = {
'tr-TR': { label: 'Türkçe', dateLocale: tr },
'uk-UA': { label: 'українська', dateLocale: uk },
'ur-PK': { label: 'Urdu (Pakistan)', dateLocale: uk, dir: 'rtl' },
+ 'uz-UZ': { label: 'O‘zbekcha', dateLocale: uz },
'vi-VN': { label: 'Tiếng Việt', dateLocale: vi },
'zh-CN': { label: '中文', dateLocale: zhCN },
'zh-TW': { label: '中文(繁體)', dateLocale: zhTW },
diff --git a/src/lib/load.ts b/src/lib/load.ts
index d9aa23c2..d4d6c3c7 100644
--- a/src/lib/load.ts
+++ b/src/lib/load.ts
@@ -1,6 +1,7 @@
-import { Website, Session } from '@prisma/client';
+import type { Session, Website } from '@/generated/prisma/client';
import redis from '@/lib/redis';
-import { getWebsiteSession, getWebsite } from '@/queries';
+import { getWebsite } from '@/queries/prisma';
+import { getWebsiteSession } from '@/queries/sql';
export async function fetchWebsite(websiteId: string): Promise {
let website = null;
diff --git a/src/lib/params.ts b/src/lib/params.ts
index 8e631ed8..ab2d5866 100644
--- a/src/lib/params.ts
+++ b/src/lib/params.ts
@@ -1,16 +1,17 @@
-import { FILTER_COLUMNS, OPERATOR_PREFIXES, OPERATORS } from '@/lib/constants';
-import { QueryFilters, QueryOptions } from '@/lib/types';
+import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants';
+import type { Filter, QueryFilters, QueryOptions } from '@/lib/types';
-export function parseParameterValue(param: any) {
+export function parseFilterValue(param: any) {
if (typeof param === 'string') {
- const [, prefix, value] = param.match(/^(!~|!|~)?(.*)$/);
+ const operatorValues = Object.values(OPERATORS).join('|');
- const operator =
- Object.keys(OPERATOR_PREFIXES).find(key => OPERATOR_PREFIXES[key] === prefix) ||
- OPERATORS.equals;
+ const regex = new RegExp(`^(${operatorValues})\\.(.*)$`);
- return { operator, value };
+ const [, operator, value] = param.match(regex) || [];
+
+ return { operator: operator || OPERATORS.equals, value: value || param };
}
+
return { operator: OPERATORS.equals, value: param };
}
@@ -22,7 +23,11 @@ export function isSearchOperator(operator: any) {
return [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator);
}
-export function filtersToArray(filters: QueryFilters = {}, options: QueryOptions = {}) {
+export function filtersObjectToArray(filters: QueryFilters, options: QueryOptions = {}): Filter[] {
+ if (!filters) {
+ return [];
+ }
+
return Object.keys(filters).reduce((arr, key) => {
const filter = filters[key];
@@ -34,13 +39,24 @@ export function filtersToArray(filters: QueryFilters = {}, options: QueryOptions
return arr.concat({ ...filter, column: options?.columns?.[key] ?? FILTER_COLUMNS[key] });
}
- const { operator, value } = parseParameterValue(filter);
+ const { operator, value } = parseFilterValue(filter);
return arr.concat({
name: key,
column: options?.columns?.[key] ?? FILTER_COLUMNS[key],
operator,
value,
+ prefix: options?.prefix,
});
}, []);
}
+
+export function filtersArrayToObject(filters: Filter[]) {
+ return filters.reduce((obj, filter: Filter) => {
+ const { name, operator, value } = filter;
+
+ obj[name] = `${operator}.${value}`;
+
+ return obj;
+ }, {});
+}
diff --git a/src/lib/password.ts b/src/lib/password.ts
new file mode 100644
index 00000000..f5c450bd
--- /dev/null
+++ b/src/lib/password.ts
@@ -0,0 +1,11 @@
+import bcrypt from 'bcryptjs';
+
+const SALT_ROUNDS = 10;
+
+export function hashPassword(password: string, rounds = SALT_ROUNDS) {
+ return bcrypt.hashSync(password, rounds);
+}
+
+export function checkPassword(password: string, passwordHash: string) {
+ return bcrypt.compareSync(password, passwordHash);
+}
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
index b611123a..64cb870f 100644
--- a/src/lib/prisma.ts
+++ b/src/lib/prisma.ts
@@ -1,35 +1,25 @@
-import debug from 'debug';
-import { PrismaClient } from '@prisma/client';
+import { PrismaPg } from '@prisma/adapter-pg';
import { readReplicas } from '@prisma/extension-read-replicas';
-import { formatInTimeZone } from 'date-fns-tz';
-import { MYSQL, POSTGRESQL, getDatabaseType } from '@/lib/db';
-import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants';
-import { fetchWebsite } from './load';
-import { maxDate } from './date';
-import { QueryFilters, QueryOptions, PageParams } from './types';
-import { filtersToArray } from './params';
+import debug from 'debug';
+import { PrismaClient } from '@/generated/prisma/client';
+import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS, OPERATORS, SESSION_COLUMNS } from './constants';
+import { filtersObjectToArray } from './params';
+import type { Operator, QueryFilters, QueryOptions } from './types';
const log = debug('umami:prisma');
const PRISMA = 'prisma';
+
const PRISMA_LOG_OPTIONS = {
log: [
{
- emit: 'event',
- level: 'query',
+ emit: 'event' as const,
+ level: 'query' as const,
},
],
};
-const MYSQL_DATE_FORMATS = {
- minute: '%Y-%m-%dT%H:%i:00',
- hour: '%Y-%m-%d %H:00:00',
- day: '%Y-%m-%d 00:00:00',
- month: '%Y-%m-01 00:00:00',
- year: '%Y-01-01 00:00:00',
-};
-
-const POSTGRESQL_DATE_FORMATS = {
+const DATE_FORMATS = {
minute: 'YYYY-MM-DD HH24:MI:00',
hour: 'YYYY-MM-DD HH24:00:00',
day: 'YYYY-MM-DD HH24:00:00',
@@ -37,108 +27,51 @@ const POSTGRESQL_DATE_FORMATS = {
year: 'YYYY-01-01 HH24:00:00',
};
+const DATE_FORMATS_UTC = {
+ minute: 'YYYY-MM-DD"T"HH24:MI:00"Z"',
+ hour: 'YYYY-MM-DD"T"HH24:00:00"Z"',
+ day: 'YYYY-MM-DD"T"HH24:00:00"Z"',
+ month: 'YYYY-MM-01"T"HH24:00:00"Z"',
+ year: 'YYYY-01-01"T"HH24:00:00"Z"',
+};
+
function getAddIntervalQuery(field: string, interval: string): string {
- const db = getDatabaseType();
-
- if (db === POSTGRESQL) {
- return `${field} + interval '${interval}'`;
- }
-
- if (db === MYSQL) {
- return `DATE_ADD(${field}, interval ${interval})`;
- }
+ return `${field} + interval '${interval}'`;
}
function getDayDiffQuery(field1: string, field2: string): string {
- const db = getDatabaseType();
-
- if (db === POSTGRESQL) {
- return `${field1}::date - ${field2}::date`;
- }
-
- if (db === MYSQL) {
- return `DATEDIFF(${field1}, ${field2})`;
- }
+ return `${field1}::date - ${field2}::date`;
}
function getCastColumnQuery(field: string, type: string): string {
- const db = getDatabaseType();
-
- if (db === POSTGRESQL) {
- return `${field}::${type}`;
- }
-
- if (db === MYSQL) {
- return `${field}`;
- }
+ return `${field}::${type}`;
}
function getDateSQL(field: string, unit: string, timezone?: string): string {
- const db = getDatabaseType();
-
- if (db === POSTGRESQL) {
- if (timezone) {
- return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
- }
- return `to_char(date_trunc('${unit}', ${field}), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
+ if (timezone && timezone !== 'utc') {
+ return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${DATE_FORMATS[unit]}')`;
}
- if (db === MYSQL) {
- if (timezone) {
- const tz = formatInTimeZone(new Date(), timezone, 'xxx');
- return `date_format(convert_tz(${field},'+00:00','${tz}'), '${MYSQL_DATE_FORMATS[unit]}')`;
- }
- return `date_format(${field}, '${MYSQL_DATE_FORMATS[unit]}')`;
- }
+ return `to_char(date_trunc('${unit}', ${field}), '${DATE_FORMATS_UTC[unit]}')`;
}
function getDateWeeklySQL(field: string, timezone?: string) {
- const db = getDatabaseType();
-
- if (db === POSTGRESQL) {
- return `concat(extract(dow from (${field} at time zone '${timezone}')), ':', to_char((${field} at time zone '${timezone}'), 'HH24'))`;
- }
-
- if (db === MYSQL) {
- const tz = formatInTimeZone(new Date(), timezone, 'xxx');
- return `date_format(convert_tz(${field},'+00:00','${tz}'), '%w:%H')`;
- }
+ return `concat(extract(dow from (${field} at time zone '${timezone}')), ':', to_char((${field} at time zone '${timezone}'), 'HH24'))`;
}
export function getTimestampSQL(field: string) {
- const db = getDatabaseType();
-
- if (db === POSTGRESQL) {
- return `floor(extract(epoch from ${field}))`;
- }
-
- if (db === MYSQL) {
- return `UNIX_TIMESTAMP(${field})`;
- }
+ return `floor(extract(epoch from ${field}))`;
}
function getTimestampDiffSQL(field1: string, field2: string): string {
- const db = getDatabaseType();
-
- if (db === POSTGRESQL) {
- return `floor(extract(epoch from (${field2} - ${field1})))`;
- }
-
- if (db === MYSQL) {
- return `timestampdiff(second, ${field1}, ${field2})`;
- }
+ return `floor(extract(epoch from (${field2} - ${field1})))`;
}
function getSearchSQL(column: string, param: string = 'search'): string {
- const db = getDatabaseType();
- const like = db === POSTGRESQL ? 'ilike' : 'like';
-
- return `and ${column} ${like} {{${param}}}`;
+ return `and ${column} ilike {{${param}}}`;
}
function mapFilter(column: string, operator: string, name: string, type: string = '') {
- const db = getDatabaseType();
- const like = db === POSTGRESQL ? 'ilike' : 'like';
const value = `{{${name}${type ? `::${type}` : ''}}}`;
switch (operator) {
@@ -147,33 +80,62 @@ function mapFilter(column: string, operator: string, name: string, type: string
case OPERATORS.notEquals:
return `${column} != ${value}`;
case OPERATORS.contains:
- return `${column} ${like} ${value}`;
+ return `${column} ilike ${value}`;
case OPERATORS.doesNotContain:
- return `${column} not ${like} ${value}`;
+ return `${column} not ilike ${value}`;
default:
return '';
}
}
-function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): string {
- const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => {
- if (column) {
- arr.push(`and ${mapFilter(column, operator, name)}`);
+function getFilterQuery(filters: Record, options: QueryOptions = {}): string {
+ const query = filtersObjectToArray(filters, options).reduce(
+ (arr, { name, column, operator, prefix = '' }) => {
+ const isCohort = options?.isCohort;
- if (name === 'referrer') {
- arr.push(
- `and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`,
- );
+ if (isCohort) {
+ column = FILTER_COLUMNS[name.slice('cohort_'.length)];
}
- }
- return arr;
- }, []);
+ if (column) {
+ arr.push(`and ${mapFilter(`${prefix}${column}`, operator, name)}`);
+
+ if (name === 'referrer') {
+ arr.push(
+ `and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`,
+ );
+ }
+ }
+
+ return arr;
+ },
+ [],
+ );
return query.join('\n');
}
-function getDateQuery(filters: QueryFilters = {}) {
+function getCohortQuery(filters: QueryFilters = {}) {
+ if (!filters || Object.keys(filters).length === 0) {
+ return '';
+ }
+
+ const filterQuery = getFilterQuery(filters, { isCohort: true });
+
+ return `join
+ (select distinct website_event.session_id
+ from website_event
+ join session on session.session_id = website_event.session_id
+ and session.website_id = website_event.website_id
+ where website_event.website_id = {{websiteId}}
+ and website_event.created_at between {{cohort_startDate}} and {{cohort_endDate}}
+ ${filterQuery}
+ ) cohort
+ on cohort.session_id = website_event.session_id
+ `;
+}
+
+function getDateQuery(filters: Record) {
const { startDate, endDate } = filters;
if (startDate) {
@@ -187,52 +149,51 @@ function getDateQuery(filters: QueryFilters = {}) {
return '';
}
-function getFilterParams(filters: QueryFilters = {}) {
- return filtersToArray(filters).reduce((obj, { name, operator, value }) => {
- obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator)
- ? `%${value}%`
- : value;
+function getQueryParams(filters: Record) {
+ return {
+ ...filters,
+ ...filtersObjectToArray(filters).reduce((obj, { name, operator, value }) => {
+ obj[name] = ([OPERATORS.contains, OPERATORS.doesNotContain] as Operator[]).includes(operator)
+ ? `%${value}%`
+ : value;
- return obj;
- }, {});
+ return obj;
+ }, {}),
+ };
}
-async function parseFilters(
- websiteId: string,
- filters: QueryFilters = {},
- options: QueryOptions = {},
-) {
- const website = await fetchWebsite(websiteId);
+function parseFilters(filters: Record, options?: QueryOptions) {
const joinSession = Object.keys(filters).find(key =>
['referrer', ...SESSION_COLUMNS].includes(key),
);
+ const cohortFilters = Object.fromEntries(
+ Object.entries(filters).filter(([key]) => key.startsWith('cohort_')),
+ );
+
return {
- joinSession:
+ joinSessionQuery:
options?.joinSession || joinSession
- ? `inner join session on website_event.session_id = session.session_id`
+ ? `inner join session on website_event.session_id = session.session_id and website_event.website_id = session.website_id`
: '',
- filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters),
- params: {
- ...getFilterParams(filters),
- websiteId,
- startDate: maxDate(filters.startDate, website?.resetAt),
- },
+ filterQuery: getFilterQuery(filters, options),
+ queryParams: getQueryParams(filters),
+ cohortQuery: getCohortQuery(cohortFilters),
};
}
-async function rawQuery(sql: string, data: object): Promise {
+async function rawQuery(sql: string, data: Record, name?: string): Promise {
if (process.env.LOG_QUERY) {
log('QUERY:\n', sql);
log('PARAMETERS:\n', data);
+ log('NAME:\n', name);
}
-
- const db = getDatabaseType();
const params = [];
+ const schema = getSchema();
- if (db !== POSTGRESQL && db !== MYSQL) {
- return Promise.reject(new Error('Unknown database.'));
+ if (schema) {
+ await client.$executeRawUnsafe(`SET search_path TO "${schema}";`);
}
const query = sql?.replaceAll(/\{\{\s*(\w+)(::\w+)?\s*}}/g, (...args) => {
@@ -242,16 +203,18 @@ async function rawQuery(sql: string, data: object): Promise {
params.push(value);
- return db === MYSQL ? '?' : `$${params.length}${type ?? ''}`;
+ return `$${params.length}${type ?? ''}`;
});
- return process.env.DATABASE_REPLICA_URL
- ? client.$replica().$queryRawUnsafe(query, ...params)
- : client.$queryRawUnsafe(query, ...params);
+ if (process.env.DATABASE_REPLICA_URL && '$replica' in client) {
+ return client.$replica().$queryRawUnsafe(query, ...params);
+ }
+
+ return client.$queryRawUnsafe(query, ...params);
}
-async function pagedQuery(model: string, criteria: T, pageParams: PageParams) {
- const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams || {};
+async function pagedQuery(model: string, criteria: T, filters?: QueryFilters) {
+ const { page = 1, pageSize, orderBy, sortDescending = false, search } = filters || {};
const size = +pageSize || DEFAULT_PAGE_SIZE;
const data = await client[model].findMany({
@@ -270,15 +233,16 @@ async function pagedQuery(model: string, criteria: T, pageParams: PageParams)
const count = await client[model].count({ where: (criteria as any).where });
- return { data, count, page: +page, pageSize: size, orderBy };
+ return { data, count, page: +page, pageSize: size, orderBy, search };
}
async function pagedRawQuery(
query: string,
- queryParams: { [key: string]: any },
- pageParams: PageParams = {},
+ queryParams: Record,
+ filters: QueryFilters,
+ name?: string,
) {
- const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams;
+ const { page = 1, pageSize, orderBy, sortDescending = false } = filters;
const size = +pageSize || DEFAULT_PAGE_SIZE;
const offset = +size * (+page - 1);
const direction = sortDescending ? 'desc' : 'asc';
@@ -294,26 +258,15 @@ async function pagedRawQuery(
res => res[0].num,
);
- const data = await rawQuery(`${query}${statements}`, queryParams);
+ const data = await rawQuery(`${query}${statements}`, queryParams, name);
return { data, count, page: +page, pageSize: size, orderBy };
}
-function getQueryMode(): { mode?: 'default' | 'insensitive' } {
- const db = getDatabaseType();
-
- if (db === POSTGRESQL) {
- return { mode: 'insensitive' };
- }
-
- return {};
-}
-
-function getSearchParameters(query: string, filters: { [key: string]: any }[]) {
+function getSearchParameters(query: string, filters: Record[]) {
if (!query) return;
- const mode = getQueryMode();
- const parseFilter = (filter: { [key: string]: any }) => {
+ const parseFilter = (filter: Record) => {
const [[key, value]] = Object.entries(filter);
return {
@@ -321,7 +274,7 @@ function getSearchParameters(query: string, filters: { [key: string]: any }[]) {
typeof value === 'string'
? {
[value]: query,
- ...mode,
+ mode: 'insensitive',
}
: parseFilter(value),
};
@@ -340,47 +293,61 @@ function transaction(input: any, options?: any) {
return client.$transaction(input, options);
}
-function getClient(params?: {
- logQuery?: boolean;
- queryLogger?: () => void;
- replicaUrl?: string;
- options?: any;
-}): PrismaClient {
- const {
- logQuery = !!process.env.LOG_QUERY,
- queryLogger,
- replicaUrl = process.env.DATABASE_REPLICA_URL,
- options,
- } = params || {};
+function getSchema() {
+ const connectionUrl = new URL(process.env.DATABASE_URL);
- const prisma = new PrismaClient({
- errorFormat: 'pretty',
- ...(logQuery && PRISMA_LOG_OPTIONS),
- ...options,
- });
-
- if (replicaUrl) {
- prisma.$extends(
- readReplicas({
- url: replicaUrl,
- }),
- );
- }
-
- if (logQuery) {
- prisma.$on('query' as never, queryLogger || log);
- }
-
- if (process.env.NODE_ENV !== 'production') {
- global[PRISMA] = prisma;
- }
-
- log('Prisma initialized');
-
- return prisma;
+ return connectionUrl.searchParams.get('schema');
}
-const client = global[PRISMA] || getClient();
+function getClient() {
+ const url = process.env.DATABASE_URL;
+ const replicaUrl = process.env.DATABASE_REPLICA_URL;
+ const logQuery = process.env.LOG_QUERY;
+ const schema = getSchema();
+
+ const baseAdapter = new PrismaPg({ connectionString: url }, { schema });
+
+ const baseClient = new PrismaClient({
+ adapter: baseAdapter,
+ errorFormat: 'pretty',
+ ...(logQuery ? PRISMA_LOG_OPTIONS : {}),
+ });
+
+ if (logQuery) {
+ baseClient.$on('query', log);
+ }
+
+ if (!replicaUrl) {
+ log('Prisma initialized');
+ globalThis[PRISMA] ??= baseClient;
+ return baseClient;
+ }
+
+ const replicaAdapter = new PrismaPg({ connectionString: replicaUrl }, { schema });
+
+ const replicaClient = new PrismaClient({
+ adapter: replicaAdapter,
+ errorFormat: 'pretty',
+ ...(logQuery ? PRISMA_LOG_OPTIONS : {}),
+ });
+
+ if (logQuery) {
+ replicaClient.$on('query', log);
+ }
+
+ const extended = baseClient.$extends(
+ readReplicas({
+ replicas: [replicaClient],
+ }),
+ );
+
+ log('Prisma initialized (with replica)');
+ globalThis[PRISMA] ??= extended;
+
+ return extended;
+}
+
+const client = (globalThis[PRISMA] || getClient()) as ReturnType;
export default {
client,
@@ -394,7 +361,6 @@ export default {
getSearchParameters,
getTimestampDiffSQL,
getSearchSQL,
- getQueryMode,
pagedQuery,
pagedRawQuery,
parseFilters,
diff --git a/src/lib/react.ts b/src/lib/react.ts
new file mode 100644
index 00000000..668cdf1f
--- /dev/null
+++ b/src/lib/react.ts
@@ -0,0 +1,77 @@
+import {
+ Children,
+ cloneElement,
+ type FC,
+ Fragment,
+ isValidElement,
+ type ReactElement,
+ type ReactNode,
+} from 'react';
+
+export function getFragmentChildren(children: ReactNode) {
+ return (children as ReactElement)?.type === Fragment
+ ? (children as ReactElement).props.children
+ : children;
+}
+
+export function isValidChild(child: ReactElement, types: FC | FC[]) {
+ if (!isValidElement(child)) {
+ return false;
+ }
+ return (Array.isArray(types) ? types : [types]).find(type => type === child.type);
+}
+
+export function mapChildren(
+ children: ReactNode,
+ handler: (child: ReactElement, index: number) => any,
+) {
+ return Children.map(getFragmentChildren(children) as ReactElement[], (child, index) => {
+ if (!child?.props) {
+ return null;
+ }
+ return handler(child, index);
+ });
+}
+
+export function cloneChildren(
+ children: ReactNode,
+ handler: (child: ReactElement, index: number) => any,
+ options?: { validChildren?: any[]; onlyRenderValid?: boolean },
+): ReactNode {
+ if (!children) {
+ return null;
+ }
+
+ const { validChildren, onlyRenderValid = false } = options || {};
+
+ return mapChildren(children, (child, index) => {
+ const invalid = validChildren && !isValidChild(child as ReactElement, validChildren);
+
+ if (onlyRenderValid && invalid) {
+ return null;
+ }
+
+ if (!invalid && isValidElement(child)) {
+ return cloneElement(child, handler(child, index));
+ }
+
+ return child;
+ });
+}
+
+export function renderChildren(
+ children: ReactNode | ((item: any, index: number, array: any) => ReactNode),
+ items: any[],
+ handler: (child: ReactElement, index: number) => object | undefined,
+ options?: { validChildren?: any[]; onlyRenderValid?: boolean },
+): ReactNode {
+ if (typeof children === 'function' && items?.length > 0) {
+ return cloneChildren(items.map(children), handler, options);
+ }
+
+ return cloneChildren(getFragmentChildren(children as ReactNode), handler, options);
+}
+
+export function countChildren(children: ReactNode): number {
+ return Children.count(getFragmentChildren(children));
+}
diff --git a/src/lib/redis.ts b/src/lib/redis.ts
index 98b08388..edde3d65 100644
--- a/src/lib/redis.ts
+++ b/src/lib/redis.ts
@@ -1,17 +1,18 @@
-import { REDIS, UmamiRedisClient } from '@umami/redis-client';
+import { UmamiRedisClient } from '@umami/redis-client';
+const REDIS = 'redis';
const enabled = !!process.env.REDIS_URL;
function getClient() {
- const redis = new UmamiRedisClient(process.env.REDIS_URL);
+ const redis = new UmamiRedisClient({ url: process.env.REDIS_URL });
if (process.env.NODE_ENV !== 'production') {
- global[REDIS] = redis;
+ globalThis[REDIS] = redis;
}
return redis;
}
-const client = global[REDIS] || getClient();
+const client = globalThis[REDIS] || getClient();
export default { client, enabled };
diff --git a/src/lib/request.ts b/src/lib/request.ts
index 374f1cbc..42c44904 100644
--- a/src/lib/request.ts
+++ b/src/lib/request.ts
@@ -1,44 +1,30 @@
-import { z, ZodSchema } from 'zod';
-import { FILTER_COLUMNS } from '@/lib/constants';
-import { badRequest, unauthorized } from '@/lib/response';
-import { getAllowedUnits, getMinimumUnit } from '@/lib/date';
+import { z } from 'zod';
import { checkAuth } from '@/lib/auth';
-import { getWebsiteDateRange } from '@/queries';
-
-export async function getJsonBody(request: Request) {
- try {
- return await request.clone().json();
- } catch {
- return undefined;
- }
-}
+import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS } from '@/lib/constants';
+import { getAllowedUnits, getMinimumUnit, maxDate, parseDateRange } from '@/lib/date';
+import { fetchWebsite } from '@/lib/load';
+import { filtersArrayToObject } from '@/lib/params';
+import { badRequest, unauthorized } from '@/lib/response';
+import type { QueryFilters } from '@/lib/types';
+import { getWebsiteSegment } from '@/queries/prisma';
export async function parseRequest(
request: Request,
- schema?: ZodSchema,
+ schema?: any,
options?: { skipAuth: boolean },
): Promise {
const url = new URL(request.url);
let query = Object.fromEntries(url.searchParams);
let body = await getJsonBody(request);
- let error: () => void | undefined;
+ let error: () => undefined | undefined;
let auth = null;
- const getErrorMessages = (error: z.ZodError) => {
- return Object.entries(error.format())
- .map(([key, value]) => {
- const messages = (value as any)._errors;
- return messages ? `${key}: ${messages.join(', ')}` : null;
- })
- .filter(Boolean);
- };
-
if (schema) {
const isGet = request.method === 'GET';
const result = schema.safeParse(isGet ? query : body);
if (!result.success) {
- error = () => badRequest(getErrorMessages(result.error));
+ error = () => badRequest(z.treeifyError(result.error));
} else if (isGet) {
query = result.data;
} else {
@@ -57,42 +43,103 @@ export async function parseRequest(
return { url, query, body, auth, error };
}
-export async function getRequestDateRange(query: Record) {
- const { websiteId, startAt, endAt, unit } = query;
-
- // All-time
- if (+startAt === 0 && +endAt === 1) {
- const result = await getWebsiteDateRange(websiteId as string);
- const { min, max } = result[0];
- const startDate = new Date(min);
- const endDate = new Date(max);
-
- return {
- startDate,
- endDate,
- unit: getMinimumUnit(startDate, endDate),
- };
+export async function getJsonBody(request: Request) {
+ try {
+ return await request.clone().json();
+ } catch {
+ return undefined;
}
+}
+
+export function getRequestDateRange(query: Record) {
+ const { startAt, endAt, unit, timezone } = query;
const startDate = new Date(+startAt);
const endDate = new Date(+endAt);
- const minUnit = getMinimumUnit(startDate, endDate);
return {
startDate,
endDate,
- unit: (getAllowedUnits(startDate, endDate).includes(unit as string) ? unit : minUnit) as string,
+ timezone,
+ unit: getAllowedUnits(startDate, endDate).includes(unit)
+ ? unit
+ : getMinimumUnit(startDate, endDate),
};
}
export function getRequestFilters(query: Record) {
- return Object.keys(FILTER_COLUMNS).reduce((obj, key) => {
- const value = query[key];
+ const result: Record = {};
+ for (const key of Object.keys(FILTER_COLUMNS)) {
+ const value = query[key];
if (value !== undefined) {
- obj[key] = value;
+ result[key] = value;
+ }
+ }
+
+ return result;
+}
+
+export async function setWebsiteDate(websiteId: string, data: Record) {
+ const website = await fetchWebsite(websiteId);
+
+ if (website?.resetAt) {
+ data.startDate = maxDate(data.startDate, new Date(website?.resetAt));
+ }
+
+ return data;
+}
+
+export async function getQueryFilters(
+ params: Record,
+ websiteId?: string,
+): Promise {
+ const dateRange = getRequestDateRange(params);
+ const filters = getRequestFilters(params);
+
+ if (websiteId) {
+ await setWebsiteDate(websiteId, dateRange);
+
+ if (params.segment) {
+ const segmentParams = (await getWebsiteSegment(websiteId, params.segment))
+ ?.parameters as Record;
+
+ Object.assign(filters, filtersArrayToObject(segmentParams.filters));
}
- return obj;
- }, {});
+ if (params.cohort) {
+ const cohortParams = (await getWebsiteSegment(websiteId, params.cohort))
+ ?.parameters as Record;
+
+ const { startDate, endDate } = parseDateRange(cohortParams.dateRange);
+
+ const cohortFilters = cohortParams.filters.map(({ name, ...props }) => ({
+ ...props,
+ name: `cohort_${name}`,
+ }));
+
+ cohortFilters.push({
+ name: `cohort_${cohortParams.action.type}`,
+ operator: 'eq',
+ value: cohortParams.action.value,
+ });
+
+ Object.assign(filters, {
+ ...filtersArrayToObject(cohortFilters),
+ cohort_startDate: startDate,
+ cohort_endDate: endDate,
+ });
+ }
+ }
+
+ return {
+ ...dateRange,
+ ...filters,
+ page: params?.page,
+ pageSize: params?.pageSize ? params?.pageSize || DEFAULT_PAGE_SIZE : undefined,
+ orderBy: params?.orderBy,
+ sortDescending: params?.sortDescending,
+ search: params?.search,
+ compare: params?.compare,
+ };
}
diff --git a/src/lib/response.ts b/src/lib/response.ts
index d50b453c..f1ad5c7b 100644
--- a/src/lib/response.ts
+++ b/src/lib/response.ts
@@ -1,29 +1,58 @@
-import { serializeError } from 'serialize-error';
-
export function ok() {
return Response.json({ ok: true });
}
-export function json(data: any) {
+export function json(data: Record = {}) {
return Response.json(data);
}
-export function badRequest(error: any = 'Bad request') {
- return Response.json({ error: serializeError(error) }, { status: 400 });
+export function badRequest(error?: Record) {
+ return Response.json(
+ {
+ error: { message: 'Bad request', code: 'bad-request', status: 400, ...error },
+ },
+ { status: 400 },
+ );
}
-export function unauthorized(error: any = 'Unauthorized') {
- return Response.json({ error: serializeError(error) }, { status: 401 });
+export function unauthorized(error?: Record) {
+ return Response.json(
+ {
+ error: {
+ message: 'Unauthorized',
+ code: 'unauthorized',
+ status: 401,
+ ...error,
+ },
+ },
+ { status: 401 },
+ );
}
-export function forbidden(error: any = 'Forbidden') {
- return Response.json({ error: serializeError(error) }, { status: 403 });
+export function forbidden(error?: Record) {
+ return Response.json(
+ { error: { message: 'Forbidden', code: 'forbidden', status: 403, ...error } },
+ { status: 403 },
+ );
}
-export function notFound(error: any = 'Not found') {
- return Response.json({ error: serializeError(error) }, { status: 404 });
+export function notFound(error?: Record) {
+ return Response.json(
+ { error: { message: 'Not found', code: 'not-found', status: 404, ...error } },
+ { status: 404 },
+ );
}
-export function serverError(error: any = 'Server error') {
- return Response.json({ error: serializeError(error) }, { status: 500 });
+export function serverError(error?: Record) {
+ return Response.json(
+ {
+ error: {
+ message: 'Server error',
+ code: 'server-error',
+ status: 500,
+ ...error,
+ },
+ },
+ { status: 500 },
+ );
}
diff --git a/src/lib/schema.ts b/src/lib/schema.ts
index 1997a54c..38f7339a 100644
--- a/src/lib/schema.ts
+++ b/src/lib/schema.ts
@@ -1,9 +1,30 @@
import { z } from 'zod';
-import { isValidTimezone } from '@/lib/date';
+import { isValidTimezone, normalizeTimezone } from '@/lib/date';
import { UNIT_TYPES } from './constants';
+export const timezoneParam = z
+ .string()
+ .refine((value: string) => isValidTimezone(value), {
+ message: 'Invalid timezone',
+ })
+ .transform((value: string) => normalizeTimezone(value));
+
+export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), {
+ message: 'Invalid unit',
+});
+
+export const dateRangeParams = {
+ startAt: z.coerce.number().optional(),
+ endAt: z.coerce.number().optional(),
+ startDate: z.coerce.date().optional(),
+ endDate: z.coerce.date().optional(),
+ timezone: timezoneParam.optional(),
+ unit: unitParam.optional(),
+ compare: z.string().optional(),
+};
+
export const filterParams = {
- url: z.string().optional(),
+ path: z.string().optional(),
referrer: z.string().optional(),
title: z.string().optional(),
query: z.string().optional(),
@@ -14,29 +35,32 @@ export const filterParams = {
region: z.string().optional(),
city: z.string().optional(),
tag: z.string().optional(),
- host: z.string().optional(),
+ hostname: z.string().optional(),
language: z.string().optional(),
event: z.string().optional(),
+ segment: z.uuid().optional(),
+ cohort: z.uuid().optional(),
+ eventType: z.coerce.number().int().positive().optional(),
+};
+
+export const searchParams = {
+ search: z.string().optional(),
};
export const pagingParams = {
page: z.coerce.number().int().positive().optional(),
pageSize: z.coerce.number().int().positive().optional(),
- orderBy: z.string().optional(),
- search: z.string().optional(),
};
-export const timezoneParam = z.string().refine(value => isValidTimezone(value), {
- message: 'Invalid timezone',
-});
+export const sortingParams = {
+ orderBy: z.string().optional(),
+};
-export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), {
- message: 'Invalid unit',
-});
+export const userRoleParam = z.enum(['admin', 'user', 'view-only']);
-export const roleParam = z.enum(['team-member', 'team-view-only', 'team-manager']);
+export const teamRoleParam = z.enum(['team-member', 'team-view-only', 'team-manager']);
-export const anyObjectParam = z.object({}).passthrough();
+export const anyObjectParam = z.record(z.string(), z.any());
export const urlOrPathParam = z.string().refine(
value => {
@@ -52,25 +76,157 @@ export const urlOrPathParam = z.string().refine(
},
);
-export const reportTypeParam = z.enum([
- 'funnel',
- 'insights',
- 'retention',
- 'utm',
- 'goals',
- 'journey',
- 'revenue',
- 'attribution',
+export const fieldsParam = z.enum([
+ 'path',
+ 'referrer',
+ 'title',
+ 'query',
+ 'os',
+ 'browser',
+ 'device',
+ 'country',
+ 'region',
+ 'city',
+ 'tag',
+ 'hostname',
+ 'language',
+ 'event',
]);
-export const reportParms = {
- websiteId: z.string().uuid(),
- dateRange: z.object({
+export const reportTypeParam = z.enum([
+ 'attribution',
+ 'breakdown',
+ 'funnel',
+ 'goal',
+ 'journey',
+ 'retention',
+ 'revenue',
+ 'utm',
+]);
+
+export const goalReportSchema = z.object({
+ type: z.literal('goal'),
+ parameters: z
+ .object({
+ startDate: z.coerce.date(),
+ endDate: z.coerce.date(),
+ type: z.string(),
+ value: z.string(),
+ operator: z.enum(['count', 'sum', 'average']).optional(),
+ property: z.string().optional(),
+ })
+ .refine(data => {
+ if (data.type === 'event' && data.property) {
+ return data.operator && data.property;
+ }
+ return true;
+ }),
+});
+
+export const funnelReportSchema = z.object({
+ type: z.literal('funnel'),
+ parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
- num: z.coerce.number().optional(),
- offset: z.coerce.number().optional(),
- unit: z.string().optional(),
- value: z.string().optional(),
+ window: z.coerce.number().positive(),
+ steps: z
+ .array(
+ z.object({
+ type: z.enum(['path', 'event']),
+ value: z.string(),
+ }),
+ )
+ .min(2)
+ .max(8),
}),
-};
+});
+
+export const journeyReportSchema = z.object({
+ type: z.literal('journey'),
+ parameters: z.object({
+ startDate: z.coerce.date(),
+ endDate: z.coerce.date(),
+ steps: z.coerce.number().min(2).max(7),
+ startStep: z.string().optional(),
+ endStep: z.string().optional(),
+ }),
+});
+
+export const retentionReportSchema = z.object({
+ type: z.literal('retention'),
+ parameters: z.object({
+ startDate: z.coerce.date(),
+ endDate: z.coerce.date(),
+ timezone: z.string().optional(),
+ }),
+});
+
+export const utmReportSchema = z.object({
+ type: z.literal('utm'),
+ parameters: z.object({
+ startDate: z.coerce.date(),
+ endDate: z.coerce.date(),
+ }),
+});
+
+export const revenueReportSchema = z.object({
+ type: z.literal('revenue'),
+ parameters: z.object({
+ startDate: z.coerce.date(),
+ endDate: z.coerce.date(),
+ timezone: z.string().optional(),
+ currency: z.string(),
+ }),
+});
+
+export const attributionReportSchema = z.object({
+ type: z.literal('attribution'),
+ parameters: z.object({
+ startDate: z.coerce.date(),
+ endDate: z.coerce.date(),
+ model: z.enum(['first-click', 'last-click']),
+ type: z.enum(['path', 'event']),
+ step: z.string(),
+ currency: z.string().optional(),
+ }),
+});
+
+export const breakdownReportSchema = z.object({
+ type: z.literal('breakdown'),
+ parameters: z.object({
+ startDate: z.coerce.date(),
+ endDate: z.coerce.date(),
+ fields: z.array(fieldsParam),
+ }),
+});
+
+export const reportBaseSchema = z.object({
+ websiteId: z.uuid(),
+ type: reportTypeParam,
+ name: z.string().max(200),
+ description: z.string().max(500).optional(),
+ parameters: anyObjectParam,
+});
+
+export const reportTypeSchema = z.discriminatedUnion('type', [
+ goalReportSchema,
+ funnelReportSchema,
+ journeyReportSchema,
+ retentionReportSchema,
+ utmReportSchema,
+ revenueReportSchema,
+ attributionReportSchema,
+ breakdownReportSchema,
+]);
+
+export const reportSchema = reportBaseSchema;
+
+export const reportResultSchema = z.intersection(
+ z.object({
+ websiteId: z.uuid(),
+ filters: z.object({ ...filterParams }),
+ }),
+ reportTypeSchema,
+);
+
+export const segmentTypeParam = z.enum(['segment', 'cohort']);
diff --git a/src/lib/storage.ts b/src/lib/storage.ts
index f08a7f7a..19681a2d 100644
--- a/src/lib/storage.ts
+++ b/src/lib/storage.ts
@@ -1,4 +1,4 @@
-export function setItem(key: string, data: any, session?: boolean): void {
+export function setItem(key: string, data: any, session?: boolean) {
if (typeof window !== 'undefined' && data) {
return (session ? sessionStorage : localStorage).setItem(key, JSON.stringify(data));
}
@@ -9,12 +9,16 @@ export function getItem(key: string, session?: boolean): any {
const value = (session ? sessionStorage : localStorage).getItem(key);
if (value !== 'undefined' && value !== null) {
- return JSON.parse(value);
+ try {
+ return JSON.parse(value);
+ } catch {
+ return null;
+ }
}
}
}
-export function removeItem(key: string, session?: boolean): void {
+export function removeItem(key: string, session?: boolean) {
if (typeof window !== 'undefined') {
return (session ? sessionStorage : localStorage).removeItem(key);
}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 00b6eec0..9c061979 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -1,54 +1,15 @@
-import {
- COLLECTION_TYPE,
- DATA_TYPE,
- EVENT_TYPE,
- KAFKA_TOPIC,
- PERMISSIONS,
- REPORT_TYPES,
- ROLES,
-} from './constants';
-import { TIME_UNIT } from './date';
-import { Dispatch, SetStateAction } from 'react';
+import type { UseQueryOptions } from '@tanstack/react-query';
+import type { DATA_TYPE, OPERATORS, ROLES } from './constants';
+import type { TIME_UNIT } from './date';
-type ObjectValues = T[keyof T];
+export type ObjectValues = T[keyof T];
+
+export type ReactQueryOptions = Omit, 'queryKey' | 'queryFn'>;
export type TimeUnit = ObjectValues;
-export type Permission = ObjectValues;
-
-export type CollectionType = ObjectValues;
export type Role = ObjectValues;
-export type EventType = ObjectValues