mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 12:47:13 +01:00
Merge branch 'master' into hosts-support
This commit is contained in:
commit
e11c2e452c
69 changed files with 3783 additions and 2197 deletions
|
|
@ -40,6 +40,9 @@ export function TeamMemberEditForm({
|
|||
};
|
||||
|
||||
const renderValue = (value: string) => {
|
||||
if (value === ROLES.teamManager) {
|
||||
return formatMessage(labels.manager);
|
||||
}
|
||||
if (value === ROLES.teamMember) {
|
||||
return formatMessage(labels.member);
|
||||
}
|
||||
|
|
@ -58,6 +61,7 @@ export function TeamMemberEditForm({
|
|||
minWidth: '250px',
|
||||
}}
|
||||
>
|
||||
<Item key={ROLES.teamManager}>{formatMessage(labels.manager)}</Item>
|
||||
<Item key={ROLES.teamMember}>{formatMessage(labels.member)}</Item>
|
||||
<Item key={ROLES.teamViewOnly}>{formatMessage(labels.viewOnly)}</Item>
|
||||
</Dropdown>
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ export function TeamMembersPage({ teamId }: { teamId: string }) {
|
|||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const canEdit =
|
||||
team?.teamUser?.find(({ userId, role }) => role === ROLES.teamOwner && userId === user.id) &&
|
||||
user.role !== ROLES.viewOnly;
|
||||
team?.teamUser?.find(
|
||||
({ userId, role }) =>
|
||||
(role === ROLES.teamOwner || role === ROLES.teamManager) && userId === user.id,
|
||||
) && user.role !== ROLES.viewOnly;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export function TeamMembersTable({
|
|||
|
||||
const roles = {
|
||||
[ROLES.teamOwner]: formatMessage(labels.teamOwner),
|
||||
[ROLES.teamManager]: formatMessage(labels.teamManager),
|
||||
[ROLES.teamMember]: formatMessage(labels.teamMember),
|
||||
[ROLES.teamViewOnly]: formatMessage(labels.viewOnly),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,18 +15,24 @@ export function TeamDetails({ teamId }: { teamId: string }) {
|
|||
const { user } = useLogin();
|
||||
const [tab, setTab] = useState('details');
|
||||
|
||||
const canEdit =
|
||||
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 (
|
||||
<Flexbox direction="column">
|
||||
<PageHeader title={team?.name} icon={<Icons.Users />}>
|
||||
{!canEdit && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
|
||||
{!isTeamOwner && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
|
||||
</PageHeader>
|
||||
<Tabs selectedKey={tab} onSelect={(value: any) => setTab(value)} style={{ marginBottom: 30 }}>
|
||||
<Item key="details">{formatMessage(labels.details)}</Item>
|
||||
{canEdit && <Item key="manage">{formatMessage(labels.manage)}</Item>}
|
||||
{isTeamOwner && <Item key="manage">{formatMessage(labels.manage)}</Item>}
|
||||
</Tabs>
|
||||
{tab === 'details' && <TeamEditForm teamId={teamId} allowEdit={canEdit} />}
|
||||
{tab === 'manage' && <TeamManage teamId={teamId} />}
|
||||
|
|
|
|||
3
src/app/(main)/teams/[teamId]/reports/utm/page.tsx
Normal file
3
src/app/(main)/teams/[teamId]/reports/utm/page.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Page from 'app/(main)/reports/utm/page';
|
||||
|
||||
export default Page;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import Page from 'app/(main)/websites/[websiteId]/event-data/page';
|
||||
|
||||
export default Page;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import Page from 'app/(main)/websites/[websiteId]/realtime/page';
|
||||
|
||||
export default Page;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import Page from 'app/(main)/websites/[websiteId]/reports/page';
|
||||
|
||||
export default Page;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import classNames from 'classnames';
|
||||
import Favicon from 'components/common/Favicon';
|
||||
import { useMessages, useWebsite } from 'components/hooks';
|
||||
import { useMessages, useTeamUrl, useWebsite } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import ActiveUsers from 'components/metrics/ActiveUsers';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -19,6 +19,7 @@ export function WebsiteHeader({
|
|||
children?: ReactNode;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
const pathname = usePathname();
|
||||
const { data: website } = useWebsite(websiteId);
|
||||
const { name, domain } = website || {};
|
||||
|
|
@ -62,7 +63,11 @@ export function WebsiteHeader({
|
|||
: pathname.match(/^\/websites\/[\w-]+$/);
|
||||
|
||||
return (
|
||||
<Link key={label} href={`/websites/${websiteId}${path}`} shallow={true}>
|
||||
<Link
|
||||
key={label}
|
||||
href={renderTeamUrl(`/websites/${websiteId}${path}`)}
|
||||
shallow={true}
|
||||
>
|
||||
<Button
|
||||
variant="quiet"
|
||||
className={classNames({
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { Button, Flexbox, Icon, Icons, Text } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
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 (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<Flexbox alignItems="center" justifyContent="end">
|
||||
<Link href={`/reports/create`}>
|
||||
<Link href={renderTeamUrl('/reports/create')}>
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { useApi } from './useApi';
|
||||
import { FilterResult, SearchFilter, FilterQueryResult } from 'lib/types';
|
||||
import { PageResult, PageParams, FilterQueryResult } from 'lib/types';
|
||||
|
||||
export function useFilterQuery<T = any>({
|
||||
queryKey,
|
||||
queryFn,
|
||||
...options
|
||||
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): FilterQueryResult<T> {
|
||||
const [params, setParams] = useState<T | SearchFilter>({
|
||||
const [params, setParams] = useState<T | PageParams>({
|
||||
query: '',
|
||||
page: 1,
|
||||
});
|
||||
|
|
@ -21,7 +21,7 @@ export function useFilterQuery<T = any>({
|
|||
});
|
||||
|
||||
return {
|
||||
result: data as FilterResult<any>,
|
||||
result: data as PageResult<any>,
|
||||
query,
|
||||
params,
|
||||
setParams,
|
||||
|
|
|
|||
|
|
@ -1,33 +1,17 @@
|
|||
import useApi from './useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useDateRange, useNavigation, useTimezone } from 'components/hooks';
|
||||
import { zonedTimeToUtc } from 'date-fns-tz';
|
||||
|
||||
export function useWebsiteEvents(
|
||||
websiteId: string,
|
||||
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, offset } = dateRange;
|
||||
const { timezone } = useTimezone();
|
||||
const {
|
||||
query: { url, event },
|
||||
} = useNavigation();
|
||||
|
||||
const params = {
|
||||
startAt: +zonedTimeToUtc(startDate, timezone),
|
||||
endAt: +zonedTimeToUtc(endDate, timezone),
|
||||
unit,
|
||||
offset,
|
||||
timezone,
|
||||
url,
|
||||
event,
|
||||
};
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['events', { ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/events`, { ...params }),
|
||||
queryKey: ['websites:events', { websiteId, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/events`, params),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import useApi from './useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import useApi from './useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useWebsiteMetrics(
|
||||
websiteId: string,
|
||||
params?: { [key: string]: any },
|
||||
type: string,
|
||||
limit: number,
|
||||
options?: Omit<UseQueryOptions & { onDataLoad?: (data: any) => void }, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
|
|
@ -14,21 +17,26 @@ export function useWebsiteMetrics(
|
|||
{
|
||||
websiteId,
|
||||
...params,
|
||||
type,
|
||||
limit,
|
||||
},
|
||||
],
|
||||
queryFn: async () => {
|
||||
const filters = { ...params };
|
||||
|
||||
filters[params.type] = undefined;
|
||||
filters[type] = undefined;
|
||||
|
||||
const data = await get(`/websites/${websiteId}/metrics`, {
|
||||
...filters,
|
||||
type,
|
||||
limit,
|
||||
});
|
||||
|
||||
options?.onDataLoad?.(data);
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,18 @@
|
|||
import { zonedTimeToUtc } from 'date-fns-tz';
|
||||
import { useApi, useDateRange, useNavigation, useTimezone } from 'components/hooks';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useApi } from './useApi';
|
||||
import { useFilterParams } from '..//useFilterParams';
|
||||
|
||||
export function useWebsitePageviews(websiteId: string, options?: { [key: string]: string }) {
|
||||
export function useWebsitePageviews(
|
||||
websiteId: string,
|
||||
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit } = dateRange;
|
||||
const { timezone } = useTimezone();
|
||||
const {
|
||||
query: { url, referrer, host, os, browser, device, country, region, city, title },
|
||||
} = useNavigation();
|
||||
|
||||
const params = {
|
||||
startAt: +zonedTimeToUtc(startDate, timezone),
|
||||
endAt: +zonedTimeToUtc(endDate, timezone),
|
||||
unit,
|
||||
timezone,
|
||||
url,
|
||||
referrer,
|
||||
host,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
title,
|
||||
};
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['websites:pageviews', { websiteId, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/pageviews`, params),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,14 @@
|
|||
import { useApi, useDateRange, useNavigation } from 'components/hooks';
|
||||
import { useApi } from './useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useWebsiteStats(websiteId: string, options?: { [key: string]: string }) {
|
||||
const { get, useQuery } = useApi();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
const {
|
||||
query: { url, referrer, host, title, os, browser, device, country, region, city },
|
||||
} = useNavigation();
|
||||
|
||||
const params = {
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
url,
|
||||
referrer,
|
||||
host,
|
||||
title,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
};
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['websites:stats', { websiteId, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/stats`, params),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
32
src/components/hooks/useFilterParams.ts
Normal file
32
src/components/hooks/useFilterParams.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { useNavigation } from './useNavigation';
|
||||
import { useDateRange } from './useDateRange';
|
||||
import { useTimezone } from './useTimezone';
|
||||
import { zonedTimeToUtc } from 'date-fns-tz';
|
||||
|
||||
export function useFilterParams(websiteId: string) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, offset } = dateRange;
|
||||
const { timezone } = useTimezone();
|
||||
const {
|
||||
query: { url, referrer, title, query, os, browser, device, country, region, city, event },
|
||||
} = useNavigation();
|
||||
|
||||
return {
|
||||
startAt: +zonedTimeToUtc(startDate, timezone),
|
||||
endAt: +zonedTimeToUtc(endDate, timezone),
|
||||
unit,
|
||||
offset,
|
||||
timezone,
|
||||
url,
|
||||
referrer,
|
||||
title,
|
||||
query,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
event,
|
||||
};
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ export const labels = defineMessages({
|
|||
createdBy: { id: 'label.created-by', defaultMessage: 'Created By' },
|
||||
edit: { id: 'label.edit', defaultMessage: 'Edit' },
|
||||
name: { id: 'label.name', defaultMessage: 'Name' },
|
||||
manager: { id: 'label.manager', defaultMessage: 'Manager' },
|
||||
member: { id: 'label.member', defaultMessage: 'Member' },
|
||||
members: { id: 'label.members', defaultMessage: 'Members' },
|
||||
accessCode: { id: 'label.access-code', defaultMessage: 'Access code' },
|
||||
|
|
@ -43,6 +44,7 @@ export const labels = defineMessages({
|
|||
settings: { id: 'label.settings', defaultMessage: 'Settings' },
|
||||
owner: { id: 'label.owner', defaultMessage: 'Owner' },
|
||||
teamOwner: { id: 'label.team-owner', defaultMessage: 'Team owner' },
|
||||
teamManager: { id: 'label.team-manager', defaultMessage: 'Team manager' },
|
||||
teamMember: { id: 'label.team-member', defaultMessage: 'Team member' },
|
||||
teamViewOnly: { id: 'label.team-view-only', defaultMessage: 'Team view only' },
|
||||
enableShareUrl: { id: 'label.enable-share-url', defaultMessage: 'Enable share URL' },
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import LinkButton from 'components/common/LinkButton';
|
|||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import {
|
||||
useDateRange,
|
||||
useNavigation,
|
||||
useWebsiteMetrics,
|
||||
useMessages,
|
||||
|
|
@ -45,35 +44,14 @@ export function MetricsTable({
|
|||
}: MetricsTableProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatValue } = useFormat();
|
||||
const [{ startDate, endDate }] = useDateRange(websiteId);
|
||||
const {
|
||||
renderUrl,
|
||||
query: { url, referrer, host, title, os, browser, device, country, region, city },
|
||||
} = useNavigation();
|
||||
const { renderUrl } = useNavigation();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { dir } = useLocale();
|
||||
|
||||
const { data, isLoading, isFetched, error } = useWebsiteMetrics(
|
||||
websiteId,
|
||||
{
|
||||
type,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
url,
|
||||
referrer,
|
||||
host,
|
||||
os,
|
||||
title,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
limit,
|
||||
search,
|
||||
},
|
||||
{ retryDelay: delay || DEFAULT_ANIMATION_DURATION, onDataLoad },
|
||||
);
|
||||
const { data, isLoading, isFetched, error } = useWebsiteMetrics(websiteId, type, limit, {
|
||||
retryDelay: delay || DEFAULT_ANIMATION_DURATION,
|
||||
onDataLoad,
|
||||
});
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (data) {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,11 @@ export function PagesTable({ allowFilter, domainName, ...props }: PagesTableProp
|
|||
id={view}
|
||||
value={x}
|
||||
label={!x && formatMessage(labels.none)}
|
||||
externalUrl={`${domainName.startsWith('http') ? domainName : `https://${domainName}`}${x}`}
|
||||
externalUrl={
|
||||
view === 'url'
|
||||
? `${domainName.startsWith('http') ? domainName : `https://${domainName}`}${x}`
|
||||
: null
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
246
src/lang/bs-BA.json
Normal file
246
src/lang/bs-BA.json
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
{
|
||||
"label.access-code": "Pristupni kod",
|
||||
"label.actions": "Akcije",
|
||||
"label.activity-log": "Log aktivnosti",
|
||||
"label.add": "Dodaj",
|
||||
"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": "Nakon",
|
||||
"label.all": "Sve",
|
||||
"label.all-time": "Cijelo vrijeme",
|
||||
"label.analytics": "Analitike",
|
||||
"label.average": "Prosjek",
|
||||
"label.average-visit-time": "Prosječno vrijeme posjete",
|
||||
"label.back": "Nazad",
|
||||
"label.before": "Prije",
|
||||
"label.bounce-rate": "Bounce rate",
|
||||
"label.breakdown": "Pregled po kategorijama",
|
||||
"label.browser": "Browser",
|
||||
"label.browsers": "Browseri",
|
||||
"label.cancel": "Otkaži",
|
||||
"label.change-password": "Promijeni šifru",
|
||||
"label.cities": "Gradovi",
|
||||
"label.city": "Grad",
|
||||
"label.clear-all": "Očisti sve",
|
||||
"label.confirm": "Potvrdi",
|
||||
"label.confirm-password": "Potvrdi šifru",
|
||||
"label.contains": "Sadrži",
|
||||
"label.continue": "Nastavi",
|
||||
"label.countries": "Zemlje",
|
||||
"label.country": "Zemlja",
|
||||
"label.create": "Kreiraj",
|
||||
"label.create-report": "Kreiraj izvještaj",
|
||||
"label.create-team": "Kreiraj tim",
|
||||
"label.create-user": "Kreiraj korisnika",
|
||||
"label.created": "Kreiraj",
|
||||
"label.created-by": "Kreirao",
|
||||
"label.current-password": "Trenutna šifra",
|
||||
"label.custom-range": "Proizvoljni raspon",
|
||||
"label.dashboard": "Dashboard",
|
||||
"label.data": "Podaci",
|
||||
"label.date": "Datum",
|
||||
"label.date-range": "Datumski raspon",
|
||||
"label.day": "Dan",
|
||||
"label.default-date-range": "Defaultni datumski raspon",
|
||||
"label.delete": "Izbriši",
|
||||
"label.delete-report": "Izbriši report",
|
||||
"label.delete-team": "Izbriši tim",
|
||||
"label.delete-user": "Izbriši korisnika",
|
||||
"label.delete-website": "Izbriši web stranicu",
|
||||
"label.description": "Opis",
|
||||
"label.desktop": "Desktop",
|
||||
"label.details": "Detalji",
|
||||
"label.device": "Uređaj",
|
||||
"label.devices": "Uređaji",
|
||||
"label.dismiss": "Odbaci",
|
||||
"label.does-not-contain": "Ne sadrži",
|
||||
"label.domain": "Domena",
|
||||
"label.dropoff": "Dropoff",
|
||||
"label.edit": "Uredi",
|
||||
"label.edit-dashboard": "Uredi dashboard",
|
||||
"label.edit-member": "Uredi člana",
|
||||
"label.enable-share-url": "Omogući URL za dijeljenje",
|
||||
"label.event": "Događaj",
|
||||
"label.event-data": "Podaci o događaju",
|
||||
"label.events": "Događaji",
|
||||
"label.false": "Ne",
|
||||
"label.field": "Polje",
|
||||
"label.fields": "Polja",
|
||||
"label.filter": "Filter",
|
||||
"label.filter-combined": "Kombinovano",
|
||||
"label.filter-raw": "Sirovo",
|
||||
"label.filters": "Filtri",
|
||||
"label.funnel": "Lijevak",
|
||||
"label.funnel-description": "Razumite koverziju i drop-off učestalost korisnika.",
|
||||
"label.greater-than": "Veće od",
|
||||
"label.greater-than-equals": "Veće od ili jednako",
|
||||
"label.insights": "Uvidi",
|
||||
"label.insights-description": "Zaronite dublje u vaše podatke korištenjem segmenata i filtera",
|
||||
"label.is": "Jeste",
|
||||
"label.is-not": "Nije",
|
||||
"label.is-not-set": "Nije setano",
|
||||
"label.is-set": "Jeste setano",
|
||||
"label.join": "Učlani se",
|
||||
"label.join-team": "Učlani se u tim",
|
||||
"label.language": "Jezik",
|
||||
"label.languages": "Jezici",
|
||||
"label.laptop": "Laptop",
|
||||
"label.last-days": "Zadnjih {x} dana",
|
||||
"label.last-hours": "Zadnjih {x} sati",
|
||||
"label.last-months": "Zadnjih {x} mjeseci",
|
||||
"label.leave": "Napusti",
|
||||
"label.leave-team": "Napusti tim",
|
||||
"label.less-than": "Manje od",
|
||||
"label.less-than-equals": "Manje od ili jednako",
|
||||
"label.login": "Login",
|
||||
"label.logout": "Logout",
|
||||
"label.manage": "Manage",
|
||||
"label.max": "Max",
|
||||
"label.member": "Član",
|
||||
"label.members": "Članovi",
|
||||
"label.min": "Min",
|
||||
"label.mobile": "Mobile",
|
||||
"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.ok": "OK",
|
||||
"label.os": "OS",
|
||||
"label.overview": "Pregled",
|
||||
"label.owner": "Vlasnik",
|
||||
"label.page-of": "Strana {current} od {total}",
|
||||
"label.page-views": "Pregleda stranica",
|
||||
"label.pageTitle": "Naslov stranice",
|
||||
"label.pages": "Stranice",
|
||||
"label.password": "Šifra",
|
||||
"label.powered-by": "Omogućeno s {name}",
|
||||
"label.profile": "Profil",
|
||||
"label.queries": "Queryji",
|
||||
"label.query": "Query",
|
||||
"label.query-parameters": "Query parametri",
|
||||
"label.realtime": "Realno vrijeme",
|
||||
"label.referrer": "Referrer",
|
||||
"label.referrers": "Referrers",
|
||||
"label.refresh": "Refresh",
|
||||
"label.regenerate": "Regeneriši",
|
||||
"label.region": "Region",
|
||||
"label.regions": "Regioni",
|
||||
"label.remove": "Ukloni",
|
||||
"label.remove-member": "Ukloni člana",
|
||||
"label.reports": "Izvještaji",
|
||||
"label.required": "Required",
|
||||
"label.reset": "Resetuj",
|
||||
"label.reset-website": "Resetuj web stranicu",
|
||||
"label.retention": "Retention",
|
||||
"label.retention-description": "Izmjeri 'ljepljivost' svoje web stranice praćenjem koliko često set korisnici vraćaju.",
|
||||
"label.role": "Rola",
|
||||
"label.run-query": "Pokreni query",
|
||||
"label.save": "Sačuvaj",
|
||||
"label.screens": "Ekrani",
|
||||
"label.search": "Traži",
|
||||
"label.select": "Odaberi",
|
||||
"label.select-date": "Odaberi datum",
|
||||
"label.select-role": "Odaberi rolu",
|
||||
"label.select-website": "Odaberi web stranicu",
|
||||
"label.sessions": "Sesije",
|
||||
"label.settings": "Postavke",
|
||||
"label.share-url": "Share URL",
|
||||
"label.single-day": "Jedan dan",
|
||||
"label.steps": "Koraci",
|
||||
"label.sum": "Suma",
|
||||
"label.tablet": "Tablet",
|
||||
"label.team": "Tim",
|
||||
"label.team-id": "Tim ID",
|
||||
"label.team-member": "Član tima",
|
||||
"label.team-name": "Naziv tima",
|
||||
"label.team-owner": "Vlasnik tima",
|
||||
"label.team-view-only": "Samo tim može vidjeti",
|
||||
"label.team-websites": "Timske web stranice",
|
||||
"label.teams": "Timovi",
|
||||
"label.theme": "Teme",
|
||||
"label.this-month": "Ovaj mjesec",
|
||||
"label.this-week": "Ova sedmica",
|
||||
"label.this-year": "Ova godina",
|
||||
"label.timezone": "Vremenska zona",
|
||||
"label.title": "Naslov",
|
||||
"label.today": "Danas",
|
||||
"label.toggle-charts": "Uklj/isklj grafikone",
|
||||
"label.total": "Ukupno",
|
||||
"label.total-records": "Ukupno redova",
|
||||
"label.tracking-code": "Kod za praćenje",
|
||||
"label.transfer": "Transfer",
|
||||
"label.transfer-website": "Transfer web stranice",
|
||||
"label.true": "Da",
|
||||
"label.type": "Tip",
|
||||
"label.unique": "Jedinstveno",
|
||||
"label.unique-visitors": "Jedinstvenih posjetitelja",
|
||||
"label.unknown": "Nepoznato",
|
||||
"label.untitled": "Bezimeno",
|
||||
"label.update": "Update",
|
||||
"label.url": "URL",
|
||||
"label.urls": "URLs",
|
||||
"label.user": "Korisnik",
|
||||
"label.username": "Korisničko ime",
|
||||
"label.users": "Korisnici",
|
||||
"label.utm": "UTM",
|
||||
"label.utm-description": "Pratite vaše kampanje kroz UTM parametre.",
|
||||
"label.value": "Vrijednost",
|
||||
"label.view": "Pregled",
|
||||
"label.view-details": "Pogledaj detalje",
|
||||
"label.view-only": "Samo gledanje",
|
||||
"label.views": "Pregledi",
|
||||
"label.views-per-visit": "Pregledi po posjeti",
|
||||
"label.visitors": "Posjetitelji",
|
||||
"label.visits": "Posjete",
|
||||
"label.website": "Web stranica",
|
||||
"label.website-id": "ID web stranice",
|
||||
"label.websites": "Web stranice",
|
||||
"label.window": "Prozor",
|
||||
"label.yesterday": "Jučer",
|
||||
"message.action-confirmation": "Unesite {confirmation} ispod da potvrdite.",
|
||||
"message.active-users": "{x} trenutno {x, plural, one {posjetitelj} other {posjetitelja}}",
|
||||
"message.confirm-delete": "Jeste li sigurni da želite obrisati {target}?",
|
||||
"message.confirm-leave": "Jeste li sigurni da želite napustiti {target}?",
|
||||
"message.confirm-remove": "Jeste li sigurni da želite ukloniti {target}?",
|
||||
"message.confirm-reset": "Jeste li sigurni da želite resetovati {target}?",
|
||||
"message.delete-team-warning": "Brisanje tima će također obrisati sve web stranice tima.",
|
||||
"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.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.",
|
||||
"message.min-password-length": "Minimalna dužina od {n} karaktera",
|
||||
"message.new-version-available": "Nova verzija Umami {version} je dostupna!",
|
||||
"message.no-data-available": "Nema dostupnih podataka.",
|
||||
"message.no-event-data": "Nema dostupnih podataka o događajima.",
|
||||
"message.no-match-password": "Šifre se ne poklapaju.",
|
||||
"message.no-results-found": "Nema rezultata.",
|
||||
"message.no-team-websites": "Ovaj tim nema nikakvih web stranica.",
|
||||
"message.no-teams": "Niste kreirali nijedan tim.",
|
||||
"message.no-users": "Nema nikakvih korisnika.",
|
||||
"message.no-websites-configured": "Nemate iskonfigurisanu nijednu web stranicu.",
|
||||
"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.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.",
|
||||
"message.team-websites-info": "Web stranice može vidjeti bilo ko iz tima.",
|
||||
"message.tracking-code": "Da pratite statistike ove web stranice, stavite sljedeći kod u <head>...</head> sekciju vašeg HTMLa.",
|
||||
"message.transfer-team-website-to-user": "Prebacite ovu web stranicu na vaš račun?",
|
||||
"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.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"
|
||||
}
|
||||
|
|
@ -1,246 +1,246 @@
|
|||
{
|
||||
"label.access-code": "Access code",
|
||||
"label.access-code": "Codi d'accés",
|
||||
"label.actions": "Accions",
|
||||
"label.activity-log": "Activity log",
|
||||
"label.add": "Add",
|
||||
"label.add-description": "Add description",
|
||||
"label.add-member": "Add member",
|
||||
"label.add-step": "Add step",
|
||||
"label.add-website": "Afegeix lloc web",
|
||||
"label.activity-log": "Registre d'activitat",
|
||||
"label.add": "Afegir",
|
||||
"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.after": "After",
|
||||
"label.after": "Després",
|
||||
"label.all": "Tots",
|
||||
"label.all-time": "Sempre",
|
||||
"label.analytics": "Analytics",
|
||||
"label.average": "Average",
|
||||
"label.analytics": "Analítiques",
|
||||
"label.average": "Mitjana",
|
||||
"label.average-visit-time": "Temps mitjà de visita",
|
||||
"label.back": "Enrere",
|
||||
"label.before": "Before",
|
||||
"label.before": "Abans",
|
||||
"label.bounce-rate": "Percentatge de rebot",
|
||||
"label.breakdown": "Breakdown",
|
||||
"label.browser": "Browser",
|
||||
"label.breakdown": "Desglossament",
|
||||
"label.browser": "Navegador",
|
||||
"label.browsers": "Navegadors",
|
||||
"label.cancel": "Cancel·la",
|
||||
"label.change-password": "Canvia la contrasenya",
|
||||
"label.cities": "Cities",
|
||||
"label.city": "City",
|
||||
"label.clear-all": "Clear all",
|
||||
"label.confirm": "Confirm",
|
||||
"label.cities": "Ciutats",
|
||||
"label.city": "Ciutat",
|
||||
"label.clear-all": "Netejar tot",
|
||||
"label.confirm": "Confirmar",
|
||||
"label.confirm-password": "Confirma la contrasenya",
|
||||
"label.contains": "Contains",
|
||||
"label.continue": "Continue",
|
||||
"label.contains": "Conté",
|
||||
"label.continue": "Continuar",
|
||||
"label.countries": "Països",
|
||||
"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.country": "País",
|
||||
"label.create": "Crear",
|
||||
"label.create-report": "Crear informe",
|
||||
"label.create-team": "Crear equip",
|
||||
"label.create-user": "Crear usuari",
|
||||
"label.created": "Creat",
|
||||
"label.created-by": "Creat Per",
|
||||
"label.current-password": "Contrasenya actual",
|
||||
"label.custom-range": "Rang personalitzat",
|
||||
"label.dashboard": "Panell",
|
||||
"label.data": "Data",
|
||||
"label.date": "Date",
|
||||
"label.data": "Dades",
|
||||
"label.date": "Data",
|
||||
"label.date-range": "Interval de dates",
|
||||
"label.day": "Day",
|
||||
"label.day": "Dia",
|
||||
"label.default-date-range": "Interval de dates per defecte",
|
||||
"label.delete": "Esborra",
|
||||
"label.delete-report": "Delete report",
|
||||
"label.delete-team": "Delete team",
|
||||
"label.delete-user": "Delete user",
|
||||
"label.delete-report": "Eliminar informe",
|
||||
"label.delete-team": "Eliminar equip",
|
||||
"label.delete-user": "Eliminar usuari",
|
||||
"label.delete-website": "Esborra el lloc web",
|
||||
"label.description": "Description",
|
||||
"label.description": "Descripció",
|
||||
"label.desktop": "Escriptori",
|
||||
"label.details": "Details",
|
||||
"label.device": "Device",
|
||||
"label.details": "Detalls",
|
||||
"label.device": "Dispositiu",
|
||||
"label.devices": "Dispositius",
|
||||
"label.dismiss": "Descarta",
|
||||
"label.does-not-contain": "Does not contain",
|
||||
"label.does-not-contain": "No conté",
|
||||
"label.domain": "Domini",
|
||||
"label.dropoff": "Dropoff",
|
||||
"label.dropoff": "Abandonament",
|
||||
"label.edit": "Edita",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.edit-member": "Edit member",
|
||||
"label.edit-dashboard": "Edita panell",
|
||||
"label.edit-member": "Edita membre",
|
||||
"label.enable-share-url": "Activa l'enllaç per compartir",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.event": "Esdeveniment",
|
||||
"label.event-data": "Dades de l'esdeveniment",
|
||||
"label.events": "Esdeveniments",
|
||||
"label.false": "False",
|
||||
"label.field": "Field",
|
||||
"label.fields": "Fields",
|
||||
"label.filter": "Filter",
|
||||
"label.false": "Fals",
|
||||
"label.field": "Camp",
|
||||
"label.fields": "Camps",
|
||||
"label.filter": "Filtre",
|
||||
"label.filter-combined": "Combinat",
|
||||
"label.filter-raw": "En cru",
|
||||
"label.filters": "Filters",
|
||||
"label.funnel": "Funnel",
|
||||
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
|
||||
"label.greater-than": "Greater than",
|
||||
"label.greater-than-equals": "Greater than or equals",
|
||||
"label.filters": "Filtres",
|
||||
"label.funnel": "Embut",
|
||||
"label.funnel-description": "Entengui la taxa de conversió i abandonament dels usuaris.",
|
||||
"label.greater-than": "Més gran que",
|
||||
"label.greater-than-equals": "Més gran que o igual a",
|
||||
"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.language": "Language",
|
||||
"label.languages": "Llengües",
|
||||
"label.insights-description": "Aprofundeixi en les seves dades mitjançant l'ús de segments i filtres.",
|
||||
"label.is": "És igual a",
|
||||
"label.is-not": "No és igual a",
|
||||
"label.is-not-set": "No està establert",
|
||||
"label.is-set": "Està establert",
|
||||
"label.join": "Unir",
|
||||
"label.join-team": "Unir-se al equip",
|
||||
"label.language": "Idioma",
|
||||
"label.languages": "Idiomes",
|
||||
"label.laptop": "Portàtil",
|
||||
"label.last-days": "Últims {x} dies",
|
||||
"label.last-hours": "Últimes {x} hores",
|
||||
"label.last-months": "Last {x} months",
|
||||
"label.leave": "Leave",
|
||||
"label.leave-team": "Leave team",
|
||||
"label.less-than": "Less than",
|
||||
"label.less-than-equals": "Less than or equals",
|
||||
"label.last-months": "Últims {x} mesos",
|
||||
"label.leave": "Abandonar",
|
||||
"label.leave-team": "Abandonar equip",
|
||||
"label.less-than": "Menor que",
|
||||
"label.less-than-equals": "Menor que o igual a",
|
||||
"label.login": "Connecta't",
|
||||
"label.logout": "Desconnecta't",
|
||||
"label.manage": "Manage",
|
||||
"label.max": "Max",
|
||||
"label.member": "Member",
|
||||
"label.members": "Members",
|
||||
"label.min": "Min",
|
||||
"label.manage": "Administrar",
|
||||
"label.max": "Màx",
|
||||
"label.member": "Membre",
|
||||
"label.members": "Membres",
|
||||
"label.min": "Mín",
|
||||
"label.mobile": "Mòbil",
|
||||
"label.more": "Més",
|
||||
"label.my-account": "My account",
|
||||
"label.my-websites": "My websites",
|
||||
"label.my-account": "El meu compte",
|
||||
"label.my-websites": "Els meus llocs web",
|
||||
"label.name": "Nom",
|
||||
"label.new-password": "Contrasenya nova",
|
||||
"label.none": "None",
|
||||
"label.none": "Cap",
|
||||
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
||||
"label.ok": "OK",
|
||||
"label.os": "OS",
|
||||
"label.overview": "Overview",
|
||||
"label.os": "SO",
|
||||
"label.overview": "Resum",
|
||||
"label.owner": "Propietari",
|
||||
"label.page-of": "Page {current} of {total}",
|
||||
"label.page-of": "Pàgina {current} de {total}",
|
||||
"label.page-views": "Pàgines vistes",
|
||||
"label.pageTitle": "Page title",
|
||||
"label.pageTitle": "Títol de la pàgina",
|
||||
"label.pages": "Pàgines",
|
||||
"label.password": "Contrasenya",
|
||||
"label.powered-by": "Funciona amb {name}",
|
||||
"label.profile": "Perfil",
|
||||
"label.queries": "Queries",
|
||||
"label.query": "Query",
|
||||
"label.query-parameters": "Query parameters",
|
||||
"label.queries": "Consultes",
|
||||
"label.query": "Consulta",
|
||||
"label.query-parameters": "Paràmetres de consulta",
|
||||
"label.realtime": "Temps real",
|
||||
"label.referrer": "Referrer",
|
||||
"label.referrer": "Referent",
|
||||
"label.referrers": "Referents",
|
||||
"label.refresh": "Refresca",
|
||||
"label.regenerate": "Regenerate",
|
||||
"label.region": "Region",
|
||||
"label.regenerate": "Regenerar",
|
||||
"label.region": "Regió",
|
||||
"label.regions": "Regions",
|
||||
"label.remove": "Remove",
|
||||
"label.remove-member": "Remove member",
|
||||
"label.reports": "Reports",
|
||||
"label.remove": "Treure",
|
||||
"label.remove-member": "Eliminar membre",
|
||||
"label.reports": "Informes",
|
||||
"label.required": "Obligatori",
|
||||
"label.reset": "Restableix",
|
||||
"label.reset-website": "Restableix estadístiques",
|
||||
"label.retention": "Retention",
|
||||
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
|
||||
"label.role": "Role",
|
||||
"label.run-query": "Run query",
|
||||
"label.retention": "Retenció",
|
||||
"label.retention-description": "Mesuri la retenció del seu lloc web fent un seguiment de la freqüència amb què tornen els usuaris.",
|
||||
"label.role": "Rol",
|
||||
"label.run-query": "Executar consulta",
|
||||
"label.save": "Desa",
|
||||
"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": "Pantalles",
|
||||
"label.search": "Buscar",
|
||||
"label.select": "Seleccionar",
|
||||
"label.select-date": "Seleccionar data",
|
||||
"label.select-role": "Seleccionar rol",
|
||||
"label.select-website": "Seleccionar lloc web",
|
||||
"label.sessions": "Sessions",
|
||||
"label.settings": "Configuració",
|
||||
"label.share-url": "Enllaç per compartir",
|
||||
"label.single-day": "Un sol dia",
|
||||
"label.steps": "Steps",
|
||||
"label.sum": "Sum",
|
||||
"label.steps": "Pasos",
|
||||
"label.sum": "Suma",
|
||||
"label.tablet": "Tauleta",
|
||||
"label.team": "Team",
|
||||
"label.team-id": "Team ID",
|
||||
"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.team": "Equip",
|
||||
"label.team-id": "ID del equip",
|
||||
"label.team-member": "Membre de l'equip",
|
||||
"label.team-name": "Nom de l'equip",
|
||||
"label.team-owner": "Propietari 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.theme": "Tema",
|
||||
"label.this-month": "Aquest mes",
|
||||
"label.this-week": "Aquesta setmana",
|
||||
"label.this-year": "Aquest any",
|
||||
"label.timezone": "Zona horària",
|
||||
"label.title": "Title",
|
||||
"label.title": "Títol",
|
||||
"label.today": "Avui",
|
||||
"label.toggle-charts": "Mostra/amaga gràfics",
|
||||
"label.total": "Total",
|
||||
"label.total-records": "Total records",
|
||||
"label.total-records": "Total de registres",
|
||||
"label.tracking-code": "Codi de seguiment",
|
||||
"label.transfer": "Transfer",
|
||||
"label.transfer-website": "Transfer website",
|
||||
"label.true": "True",
|
||||
"label.type": "Type",
|
||||
"label.unique": "Unique",
|
||||
"label.transfer": "Transferir",
|
||||
"label.transfer-website": "Transferir lloc web",
|
||||
"label.true": "Cert",
|
||||
"label.type": "Tipus",
|
||||
"label.unique": "Únic",
|
||||
"label.unique-visitors": "Visitants únics",
|
||||
"label.unknown": "Desconegut",
|
||||
"label.untitled": "Untitled",
|
||||
"label.update": "Update",
|
||||
"label.untitled": "Sense títol",
|
||||
"label.update": "Actualitzar",
|
||||
"label.url": "URL",
|
||||
"label.urls": "URLs",
|
||||
"label.user": "User",
|
||||
"label.user": "Usuari",
|
||||
"label.username": "Nom d'usuari",
|
||||
"label.users": "Users",
|
||||
"label.users": "Usuaris",
|
||||
"label.utm": "UTM",
|
||||
"label.utm-description": "Track your campaigns through UTM parameters.",
|
||||
"label.value": "Value",
|
||||
"label.view": "View",
|
||||
"label.utm-description": "Rastreji les seves campanyes a través de paràmetres UTM.",
|
||||
"label.value": "Valor",
|
||||
"label.view": "Visualitzar",
|
||||
"label.view-details": "Veure els detalls",
|
||||
"label.view-only": "View only",
|
||||
"label.view-only": "Només veure",
|
||||
"label.views": "Vistes",
|
||||
"label.views-per-visit": "Views per visit",
|
||||
"label.visitors": "Visitants",
|
||||
"label.visits": "Visits",
|
||||
"label.website": "Website",
|
||||
"label.website-id": "Website ID",
|
||||
"label.visits": "Visites",
|
||||
"label.website": "Lloc web",
|
||||
"label.website-id": "ID del lloc web",
|
||||
"label.websites": "Llocs web",
|
||||
"label.window": "Window",
|
||||
"label.window": "Finestra",
|
||||
"label.yesterday": "Ahir",
|
||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
||||
"message.action-confirmation": "Escrigui {confirmation} al cuadre inferior per confirmar.",
|
||||
"message.active-users": "{x} {x, plural, one {visitant actual} other {visitants actuals}}",
|
||||
"message.confirm-delete": "Segur que vols esborrar {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": "Segur que vols restablir les estadístiques de {target}?",
|
||||
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
|
||||
"message.confirm-delete": "Segur que vol esborrar {target}?",
|
||||
"message.confirm-leave": "Segur que vol abandonar {target}?",
|
||||
"message.confirm-remove": "Segur que vol eliminar {target}?",
|
||||
"message.confirm-reset": "Segur que vol restablir les estadístiques de {target}?",
|
||||
"message.delete-team-warning": "Al eliminar un equip també s'eliminaran tots els llocs web de l'equip.",
|
||||
"message.delete-website-warning": "També s'esborraran totes les dades relacionades.",
|
||||
"message.error": "S'ha produït un error.",
|
||||
"message.event-log": "{event} on {url}",
|
||||
"message.event-log": "{event} a {url}",
|
||||
"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",
|
||||
"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": "Longitud mínima de {n} caràcters",
|
||||
"message.new-version-available": "Una nova versió d'Umami {version} està disponible!",
|
||||
"message.no-data-available": "No hi ha dades disponibles.",
|
||||
"message.no-event-data": "No event data is available.",
|
||||
"message.no-event-data": "No hi ha dades d'esdeveniments disponibles.",
|
||||
"message.no-match-password": "Les contrasenyes no coincideixen",
|
||||
"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-results-found": "No s'han trobat resultats.",
|
||||
"message.no-team-websites": "Aquest equip no té cap lloc web.",
|
||||
"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.page-not-found": "No s'ha trobat la pàgina.",
|
||||
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
|
||||
"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.share-url": "Aquest és l'enllaç públic per compartir de {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": "Ja és membre d'aquest equip.",
|
||||
"message.team-not-found": "Equip no trobat.",
|
||||
"message.team-websites-info": "Els llocs web poden ser visualitzats per qualsevol membre de l'equip.",
|
||||
"message.tracking-code": "Codi de seguiment",
|
||||
"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.transfer-team-website-to-user": "Transferir aquest lloc web al seu compte?",
|
||||
"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.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": "Visitors dropped off"
|
||||
"message.visitors-dropped-off": "Els visitants han sortit"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,246 +1,246 @@
|
|||
{
|
||||
"label.access-code": "Access code",
|
||||
"label.access-code": "Erişim Kodu",
|
||||
"label.actions": "Hareketler",
|
||||
"label.activity-log": "Activity log",
|
||||
"label.add": "Add",
|
||||
"label.add-description": "Add description",
|
||||
"label.add-member": "Add member",
|
||||
"label.add-step": "Add step",
|
||||
"label.activity-log": "Aktivite Kaydı",
|
||||
"label.add": "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": "Yönetici",
|
||||
"label.after": "After",
|
||||
"label.administrator": "Yönetici",
|
||||
"label.after": "Sonra",
|
||||
"label.all": "Tümü",
|
||||
"label.all-time": "All time",
|
||||
"label.analytics": "Analytics",
|
||||
"label.average": "Average",
|
||||
"label.all-time": "Tüm zamanlar",
|
||||
"label.analytics": "Analitik",
|
||||
"label.average": "Ortalama",
|
||||
"label.average-visit-time": "Ortalama ziyaret süresi",
|
||||
"label.back": "Geri",
|
||||
"label.before": "Before",
|
||||
"label.bounce-rate": "Çıkma oranı",
|
||||
"label.breakdown": "Breakdown",
|
||||
"label.browser": "Browser",
|
||||
"label.before": "Önce",
|
||||
"label.bounce-rate": "Tek sayfa ziyaret oranı",
|
||||
"label.breakdown": "Dağılım",
|
||||
"label.browser": "Tarayıcı",
|
||||
"label.browsers": "Tarayıcılar",
|
||||
"label.cancel": "İptal",
|
||||
"label.change-password": "Şifre değiştir",
|
||||
"label.cities": "Cities",
|
||||
"label.city": "City",
|
||||
"label.clear-all": "Clear all",
|
||||
"label.confirm": "Confirm",
|
||||
"label.cities": "Şehirler",
|
||||
"label.city": "Şehir",
|
||||
"label.clear-all": "Hepsini temizle",
|
||||
"label.confirm": "Onayla",
|
||||
"label.confirm-password": "Parolayı onayla",
|
||||
"label.contains": "Contains",
|
||||
"label.continue": "Continue",
|
||||
"label.contains": "İçeriği",
|
||||
"label.continue": "Devam et",
|
||||
"label.countries": "Ülkeler",
|
||||
"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.country": "Ülke",
|
||||
"label.create": "Oluştur",
|
||||
"label.create-report": "Rapor oluştur",
|
||||
"label.create-team": "Takım oluştur",
|
||||
"label.create-user": "Kullanıcı oluştur",
|
||||
"label.created": "Oluşturuldu",
|
||||
"label.created-by": "Tarafından oluşturldu",
|
||||
"label.current-password": "Mevcut parola",
|
||||
"label.custom-range": "Özelleştirilmiş aralık",
|
||||
"label.dashboard": "Kontrol Paneli",
|
||||
"label.data": "Data",
|
||||
"label.date": "Date",
|
||||
"label.data": "Veri",
|
||||
"label.date": "Tarih",
|
||||
"label.date-range": "Tarih aralığı",
|
||||
"label.day": "Day",
|
||||
"label.day": "Gün",
|
||||
"label.default-date-range": "Varsayılan tarih aralığı",
|
||||
"label.delete": "Sil",
|
||||
"label.delete-report": "Delete report",
|
||||
"label.delete-team": "Delete team",
|
||||
"label.delete-user": "Delete user",
|
||||
"label.delete-report": "Rapor sil",
|
||||
"label.delete-team": "Takım sil",
|
||||
"label.delete-user": "Kullanıcı sil",
|
||||
"label.delete-website": "Web sitesini sil",
|
||||
"label.description": "Description",
|
||||
"label.description": "Açıklama",
|
||||
"label.desktop": "Masaüstü",
|
||||
"label.details": "Details",
|
||||
"label.device": "Device",
|
||||
"label.details": "Detaylar",
|
||||
"label.device": "Cihaz",
|
||||
"label.devices": "Cihazlar",
|
||||
"label.dismiss": "Reddet",
|
||||
"label.does-not-contain": "Does not contain",
|
||||
"label.does-not-contain": "İçermez",
|
||||
"label.domain": "Alan adı",
|
||||
"label.dropoff": "Dropoff",
|
||||
"label.dropoff": "Bırakma",
|
||||
"label.edit": "Düzenle",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.edit-member": "Edit member",
|
||||
"label.edit-dashboard": "Kontrol panelini düzenle",
|
||||
"label.edit-member": "Üyeyi düzenle",
|
||||
"label.enable-share-url": "Anonim paylaşım URL'i aktif",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.event": "Olay",
|
||||
"label.event-data": "Olay verisi",
|
||||
"label.events": "Olaylar",
|
||||
"label.false": "False",
|
||||
"label.field": "Field",
|
||||
"label.fields": "Fields",
|
||||
"label.filter": "Filter",
|
||||
"label.filter-combined": "Birleşik",
|
||||
"label.filter-raw": "Ham",
|
||||
"label.filters": "Filters",
|
||||
"label.funnel": "Funnel",
|
||||
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
|
||||
"label.greater-than": "Greater than",
|
||||
"label.greater-than-equals": "Greater than or equals",
|
||||
"label.false": "Yanlış",
|
||||
"label.field": "Alan",
|
||||
"label.fields": "Alanlar",
|
||||
"label.filter": "Filtre",
|
||||
"label.filter-combined": "Birleşik filtre",
|
||||
"label.filter-raw": "Ham filtre",
|
||||
"label.filters": "Filtreler",
|
||||
"label.funnel": "Huni",
|
||||
"label.funnel-description": "Kullanıcıların dönüşüm ve ayrılma oranlarını anlayın.",
|
||||
"label.greater-than": "Büyüktür",
|
||||
"label.greater-than-equals": "Büyük veya eşittir",
|
||||
"label.insights": "Insights",
|
||||
"label.insights-description": "Dive deeper into your data by using segments and filters.",
|
||||
"label.insights-description": "Segmentleri ve filtreleri kullanarak verilerinizi derinlemesine inceleyin.",
|
||||
"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.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.is-not": "Değil",
|
||||
"label.is-not-set": "Ayarlanmamış",
|
||||
"label.is-set": "Ayarlandı",
|
||||
"label.join": "Katıl",
|
||||
"label.join-team": "Takıma katıl",
|
||||
"label.language": "Dil",
|
||||
"label.languages": "Diller",
|
||||
"label.laptop": "Dizüstü",
|
||||
"label.last-days": "Son {x} gün",
|
||||
"label.last-hours": "Son {x} saat",
|
||||
"label.last-months": "Last {x} months",
|
||||
"label.leave": "Leave",
|
||||
"label.leave-team": "Leave team",
|
||||
"label.less-than": "Less than",
|
||||
"label.less-than-equals": "Less than or equals",
|
||||
"label.last-months": "Son {x} ay",
|
||||
"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.login": "Giriş Yap",
|
||||
"label.logout": "Çıkış Yap",
|
||||
"label.manage": "Manage",
|
||||
"label.manage": "Yönet",
|
||||
"label.max": "Max",
|
||||
"label.member": "Member",
|
||||
"label.members": "Members",
|
||||
"label.member": "Üye",
|
||||
"label.members": "Üyeler",
|
||||
"label.min": "Min",
|
||||
"label.mobile": "Mobil Cihaz",
|
||||
"label.more": "Detaylı göster",
|
||||
"label.my-account": "My account",
|
||||
"label.my-websites": "My websites",
|
||||
"label.my-account": "Hesabım",
|
||||
"label.my-websites": "Web sitelerim",
|
||||
"label.name": "İsim",
|
||||
"label.new-password": "Yeni parola",
|
||||
"label.none": "None",
|
||||
"label.none": "Yok",
|
||||
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
||||
"label.ok": "OK",
|
||||
"label.ok": "TAMAM",
|
||||
"label.os": "OS",
|
||||
"label.overview": "Overview",
|
||||
"label.owner": "Owner",
|
||||
"label.page-of": "Page {current} of {total}",
|
||||
"label.overview": "Genel bakış",
|
||||
"label.owner": "Sahibi",
|
||||
"label.page-of": "{total} sayfada {current} ",
|
||||
"label.page-views": "Sayfa görünümü",
|
||||
"label.pageTitle": "Page title",
|
||||
"label.pageTitle": "Sayfa başlığı",
|
||||
"label.pages": "Sayfalar",
|
||||
"label.password": "Parola",
|
||||
"label.powered-by": "Sağlayıcı: {name}",
|
||||
"label.profile": "Profil",
|
||||
"label.queries": "Queries",
|
||||
"label.query": "Query",
|
||||
"label.query-parameters": "Query parameters",
|
||||
"label.queries": "Sorgular",
|
||||
"label.query": "Sorgu",
|
||||
"label.query-parameters": "Sorgu parametreleri",
|
||||
"label.realtime": "Gerçek Zamanlı",
|
||||
"label.referrer": "Referrer",
|
||||
"label.referrers": "Yönlendirenler",
|
||||
"label.refresh": "Yenile",
|
||||
"label.regenerate": "Regenerate",
|
||||
"label.region": "Region",
|
||||
"label.regions": "Regions",
|
||||
"label.remove": "Remove",
|
||||
"label.remove-member": "Remove member",
|
||||
"label.reports": "Reports",
|
||||
"label.regenerate": "Yeniden Oluştur",
|
||||
"label.region": "Bölge",
|
||||
"label.regions": "Bölgeler",
|
||||
"label.remove": "Kaldır",
|
||||
"label.remove-member": "Üyeyi kaldır",
|
||||
"label.reports": "Raporlar",
|
||||
"label.required": "Zorunlu alan",
|
||||
"label.reset": "Sıfırla",
|
||||
"label.reset-website": "Reset statistics",
|
||||
"label.retention": "Retention",
|
||||
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
|
||||
"label.role": "Role",
|
||||
"label.run-query": "Run query",
|
||||
"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.role": "Rol",
|
||||
"label.run-query": "Sorgu çalıştır",
|
||||
"label.save": "Kaydet",
|
||||
"label.screens": "Ekranlar",
|
||||
"label.search": "Search",
|
||||
"label.select": "Select",
|
||||
"label.select-date": "Select date",
|
||||
"label.select-role": "Select role",
|
||||
"label.select-website": "Select website",
|
||||
"label.search": "Ara",
|
||||
"label.select": "Seç",
|
||||
"label.select-date": "Tarih seç",
|
||||
"label.select-role": "Rol seç",
|
||||
"label.select-website": "Web sitesi seç",
|
||||
"label.sessions": "Sessions",
|
||||
"label.settings": "Ayarlar",
|
||||
"label.share-url": "Paylaşım adresi",
|
||||
"label.single-day": "Tekil gün",
|
||||
"label.steps": "Steps",
|
||||
"label.sum": "Sum",
|
||||
"label.steps": "Adımlar",
|
||||
"label.sum": "Toplam",
|
||||
"label.tablet": "Tablet",
|
||||
"label.team": "Team",
|
||||
"label.team-id": "Team ID",
|
||||
"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.team": "Takım",
|
||||
"label.team-id": "Takım ID",
|
||||
"label.team-member": "Takım üyesi",
|
||||
"label.team-name": "Takım ismi",
|
||||
"label.team-owner": "Takım sahibi",
|
||||
"label.team-view-only": "Yalnızca ekip görünümü",
|
||||
"label.team-websites": "Takım web siteleri",
|
||||
"label.teams": "Takımlar",
|
||||
"label.theme": "Tema",
|
||||
"label.this-month": "Bu ay",
|
||||
"label.this-week": "Bu hafta",
|
||||
"label.this-year": "Bu yıl",
|
||||
"label.timezone": "Zaman dilimi",
|
||||
"label.title": "Title",
|
||||
"label.title": "Başlık",
|
||||
"label.today": "Bugün",
|
||||
"label.toggle-charts": "Toggle charts",
|
||||
"label.total": "Total",
|
||||
"label.total-records": "Total records",
|
||||
"label.toggle-charts": "Grafikleri değiştir",
|
||||
"label.total": "Toplam",
|
||||
"label.total-records": "Toplam kayıt",
|
||||
"label.tracking-code": "İzleme kodu",
|
||||
"label.transfer": "Transfer",
|
||||
"label.transfer-website": "Transfer website",
|
||||
"label.true": "True",
|
||||
"label.type": "Type",
|
||||
"label.unique": "Unique",
|
||||
"label.transfer-website": "Transfer web sitesi",
|
||||
"label.true": "Doğru",
|
||||
"label.type": "Tip",
|
||||
"label.unique": "Benzersiz",
|
||||
"label.unique-visitors": "Tekil kullanıcı",
|
||||
"label.unknown": "Bilinmeyen",
|
||||
"label.untitled": "Untitled",
|
||||
"label.update": "Update",
|
||||
"label.untitled": "İsimsiz",
|
||||
"label.update": "Güncelle",
|
||||
"label.url": "URL",
|
||||
"label.urls": "URLs",
|
||||
"label.user": "User",
|
||||
"label.user": "Kullanıcı",
|
||||
"label.username": "Kullanıcı adı",
|
||||
"label.users": "Users",
|
||||
"label.users": "Kullanıcılar",
|
||||
"label.utm": "UTM",
|
||||
"label.utm-description": "Track your campaigns through UTM parameters.",
|
||||
"label.value": "Value",
|
||||
"label.view": "View",
|
||||
"label.utm-description": "Kampanyalarınızı UTM parametreleri aracılığıyla takip edin.",
|
||||
"label.value": "Değer",
|
||||
"label.view": "Görünüm",
|
||||
"label.view-details": "Detayı incele",
|
||||
"label.view-only": "View only",
|
||||
"label.view-only": "Sadece görünüm",
|
||||
"label.views": "Görüntüleme",
|
||||
"label.views-per-visit": "Views per visit",
|
||||
"label.views-per-visit": "Ziyaret başına görüntüleme",
|
||||
"label.visitors": "Ziyaretçi",
|
||||
"label.visits": "Visits",
|
||||
"label.website": "Website",
|
||||
"label.visits": "Ziyaretler",
|
||||
"label.website": "Web sitesi",
|
||||
"label.website-id": "Website ID",
|
||||
"label.websites": "Web siteleri",
|
||||
"label.window": "Window",
|
||||
"label.yesterday": "Yesterday",
|
||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
||||
"label.window": "Pencere",
|
||||
"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.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?",
|
||||
"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 your 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": "İlişkili tüm veriler de silinecektir.",
|
||||
"message.confirm-leave": "{target} kaydından ayrılmak istediğinizden emin misiniz?",
|
||||
"message.confirm-remove": "{target} kaydını kaldırmak istediğinizden emin misiniz?",
|
||||
"message.confirm-reset": "{target} istatistiklerini sıfırlamak istediğinizden emin misiniz?",
|
||||
"message.delete-team-warning": "Bir takımı silmek tüm takım web sitelerini de silecektir.",
|
||||
"message.delete-website-warning": "İlişkili tüm veriler de silinecektir.",
|
||||
"message.error": "Bir şeyler ters gitti!",
|
||||
"message.event-log": "{event} on {url}",
|
||||
"message.go-to-settings": "Ayarlara git",
|
||||
"message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.",
|
||||
"message.invalid-domain": "Geçersiz alan adı",
|
||||
"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": "Minimum {n} karakter uzunluğu",
|
||||
"message.new-version-available": "Yeni versiyon Umami {version} mevcut!",
|
||||
"message.no-data-available": "Henüz hiç veri yok.",
|
||||
"message.no-event-data": "No event data is available.",
|
||||
"message.no-event-data": "Hiçbir olay verisi mevcut değil.",
|
||||
"message.no-match-password": "Parolalar uyuşmuyor",
|
||||
"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-results-found": "Hiçbir sonuç bulunamadı.",
|
||||
"message.no-team-websites": "Bu takımın herhangi bir web sitesi yok.",
|
||||
"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.page-not-found": "Sayfa bulunamadı.",
|
||||
"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.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.share-url": "{target} için kullanılabilir anonim paylaşım adresidir.",
|
||||
"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": "Zaten bu takımın üyesisiniz",
|
||||
"message.team-not-found": "Takım bulunamadı",
|
||||
"message.team-websites-info": "Web siteleri takımdaki herkes tarafından görüntülenebilir.",
|
||||
"message.tracking-code": "İzleme kodu",
|
||||
"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.transfer-team-website-to-user": "Bu web sitesi hesbınıza aktarılsın mı?",
|
||||
"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.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": "Visitors dropped off"
|
||||
"message.visitors-dropped-off": "Bırakan ziyaretçiler"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"label.add": "添加",
|
||||
"label.add-description": "添加描述",
|
||||
"label.add-member": "添加成员",
|
||||
"label.add-step": "Add step",
|
||||
"label.add-step": "添加步骤",
|
||||
"label.add-website": "添加网站",
|
||||
"label.admin": "管理员",
|
||||
"label.after": "之后",
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
"label.laptop": "笔记本",
|
||||
"label.last-days": "最近 {x} 天",
|
||||
"label.last-hours": "最近 {x} 小时",
|
||||
"label.last-months": "Last {x} months",
|
||||
"label.last-months": "最近 {x} 个月",
|
||||
"label.leave": "离开",
|
||||
"label.leave-team": "离开团队",
|
||||
"label.less-than": "少于",
|
||||
|
|
@ -152,7 +152,7 @@
|
|||
"label.settings": "设置",
|
||||
"label.share-url": "共享链接",
|
||||
"label.single-day": "单日",
|
||||
"label.steps": "Steps",
|
||||
"label.steps": "步骤",
|
||||
"label.sum": "总和",
|
||||
"label.tablet": "平板",
|
||||
"label.team": "团队",
|
||||
|
|
@ -182,22 +182,22 @@
|
|||
"label.unique-visitors": "独立访客",
|
||||
"label.unknown": "未知",
|
||||
"label.untitled": "未命名",
|
||||
"label.update": "Update",
|
||||
"label.update": "更新",
|
||||
"label.url": "网址",
|
||||
"label.urls": "网址",
|
||||
"label.user": "用户",
|
||||
"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.visitors": "访客",
|
||||
"label.visits": "Visits",
|
||||
"label.visits": "访问次数",
|
||||
"label.website": "网站",
|
||||
"label.website-id": "网站 ID",
|
||||
"label.websites": "网站",
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
import { User, Website } from '@prisma/client';
|
||||
import redis from '@umami/redis-client';
|
||||
import { getSession, getUser, getWebsite } from '../queries';
|
||||
|
||||
async function fetchWebsite(websiteId: string): Promise<Website> {
|
||||
return redis.client.getCache(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
|
||||
}
|
||||
|
||||
async function storeWebsite(data: { id: any }) {
|
||||
const { id } = data;
|
||||
const key = `website:${id}`;
|
||||
|
||||
const obj = await redis.client.setCache(key, data);
|
||||
await redis.client.expire(key, 86400);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function deleteWebsite(id) {
|
||||
return redis.client.deleteCache(`website:${id}`);
|
||||
}
|
||||
|
||||
async function fetchUser(id): Promise<User> {
|
||||
return redis.client.getCache(`user:${id}`, () => getUser(id, { includePassword: true }), 86400);
|
||||
}
|
||||
|
||||
async function storeUser(data) {
|
||||
const { id } = data;
|
||||
const key = `user:${id}`;
|
||||
|
||||
const obj = await redis.client.setCache(key, data);
|
||||
await redis.client.expire(key, 86400);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function deleteUser(id) {
|
||||
return redis.client.deleteCache(`user:${id}`);
|
||||
}
|
||||
|
||||
async function fetchSession(id) {
|
||||
return redis.client.getCache(`session:${id}`, () => getSession(id), 86400);
|
||||
}
|
||||
|
||||
async function storeSession(data) {
|
||||
const { id } = data;
|
||||
const key = `session:${id}`;
|
||||
|
||||
const obj = await redis.client.setCache(key, data);
|
||||
await redis.client.expire(key, 86400);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function deleteSession(id) {
|
||||
return redis.client.deleteCache(`session:${id}`);
|
||||
}
|
||||
|
||||
async function fetchUserBlock(userId: string) {
|
||||
const key = `user:block:${userId}`;
|
||||
return redis.client.get(key);
|
||||
}
|
||||
|
||||
async function incrementUserBlock(userId: string) {
|
||||
const key = `user:block:${userId}`;
|
||||
return redis.client.incr(key);
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchWebsite,
|
||||
storeWebsite,
|
||||
deleteWebsite,
|
||||
fetchUser,
|
||||
storeUser,
|
||||
deleteUser,
|
||||
fetchSession,
|
||||
storeSession,
|
||||
deleteSession,
|
||||
fetchUserBlock,
|
||||
incrementUserBlock,
|
||||
enabled: !!redis.enabled,
|
||||
};
|
||||
|
|
@ -4,7 +4,7 @@ import debug from 'debug';
|
|||
import { CLICKHOUSE } from 'lib/db';
|
||||
import { QueryFilters, QueryOptions } from './types';
|
||||
import { OPERATORS } from './constants';
|
||||
import { loadWebsite } from './load';
|
||||
import { fetchWebsite } from './load';
|
||||
import { maxDate } from './date';
|
||||
import { filtersToArray } from './params';
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ function getFilterParams(filters: QueryFilters = {}) {
|
|||
}
|
||||
|
||||
async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
|
||||
const website = await loadWebsite(websiteId);
|
||||
const website = await fetchWebsite(websiteId);
|
||||
|
||||
return {
|
||||
filterQuery: getFilterQuery(filters, options),
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ 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'];
|
||||
|
||||
export const UNIT_TYPES = ['year', 'month', 'hour', 'day', 'minute'];
|
||||
export const EVENT_COLUMNS = ['url', 'referrer', 'title', 'query', 'event', 'host'];
|
||||
|
||||
export const SESSION_COLUMNS = [
|
||||
|
|
@ -134,6 +135,7 @@ export const ROLES = {
|
|||
user: 'user',
|
||||
viewOnly: 'view-only',
|
||||
teamOwner: 'team-owner',
|
||||
teamManager: 'team-manager',
|
||||
teamMember: 'team-member',
|
||||
teamViewOnly: 'team-view-only',
|
||||
} as const;
|
||||
|
|
@ -164,6 +166,12 @@ export const ROLE_PERMISSIONS = {
|
|||
PERMISSIONS.websiteUpdate,
|
||||
PERMISSIONS.websiteDelete,
|
||||
],
|
||||
[ROLES.teamManager]: [
|
||||
PERMISSIONS.teamUpdate,
|
||||
PERMISSIONS.websiteCreate,
|
||||
PERMISSIONS.websiteUpdate,
|
||||
PERMISSIONS.websiteDelete,
|
||||
],
|
||||
[ROLES.teamMember]: [
|
||||
PERMISSIONS.websiteCreate,
|
||||
PERMISSIONS.websiteUpdate,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { startOfHour, startOfMonth } from 'date-fns';
|
||||
import { hash } from 'next-basics';
|
||||
import { v4, v5, validate } from 'uuid';
|
||||
import { v4, v5 } from 'uuid';
|
||||
|
||||
export function secret() {
|
||||
return hash(process.env.APP_SECRET || process.env.DATABASE_URL);
|
||||
|
|
@ -23,7 +23,3 @@ export function uuid(...args: any) {
|
|||
|
||||
return v5(hash(...args, salt()), v5.DNS);
|
||||
}
|
||||
|
||||
export function isUuid(value: string) {
|
||||
return validate(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export async function getClientInfo(req: NextApiRequestCollect) {
|
|||
const subdivision2 = location?.subdivision2;
|
||||
const city = location?.city;
|
||||
const browser = browserName(userAgent);
|
||||
const os = detectOS(userAgent);
|
||||
const os = detectOS(userAgent) as string;
|
||||
const device = getDevice(req.body?.payload?.screen, os);
|
||||
|
||||
return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device };
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
arSA,
|
||||
be,
|
||||
bn,
|
||||
bs,
|
||||
cs,
|
||||
sk,
|
||||
da,
|
||||
|
|
@ -48,6 +49,7 @@ export const languages = {
|
|||
'ar-SA': { label: 'العربية', dateLocale: arSA, dir: 'rtl' },
|
||||
'be-BY': { label: 'Беларуская', dateLocale: be },
|
||||
'bn-BD': { label: 'বাংলা', dateLocale: bn },
|
||||
'bs-BA': { label: 'Bosanski', dateLocale: bs },
|
||||
'ca-ES': { label: 'Català', dateLocale: ca },
|
||||
'cs-CZ': { label: 'Čeština', dateLocale: cs },
|
||||
'da-DK': { label: 'Dansk', dateLocale: da },
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import cache from 'lib/cache';
|
||||
import { getSession, getUser, getWebsite } from 'queries';
|
||||
import { User, Website, Session } from '@prisma/client';
|
||||
import { getSession, getWebsite } from 'queries';
|
||||
import { Website, Session } from '@prisma/client';
|
||||
import redis from '@umami/redis-client';
|
||||
|
||||
export async function loadWebsite(websiteId: string): Promise<Website> {
|
||||
export async function fetchWebsite(websiteId: string): Promise<Website> {
|
||||
let website;
|
||||
|
||||
if (cache.enabled) {
|
||||
website = await cache.fetchWebsite(websiteId);
|
||||
if (redis.enabled) {
|
||||
website = await redis.client.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
|
||||
} else {
|
||||
website = await getWebsite(websiteId);
|
||||
}
|
||||
|
|
@ -18,11 +18,11 @@ export async function loadWebsite(websiteId: string): Promise<Website> {
|
|||
return website;
|
||||
}
|
||||
|
||||
export async function loadSession(sessionId: string): Promise<Session> {
|
||||
export async function fetchSession(sessionId: string): Promise<Session> {
|
||||
let session;
|
||||
|
||||
if (cache.enabled) {
|
||||
session = await cache.fetchSession(sessionId);
|
||||
if (redis.enabled) {
|
||||
session = await redis.client.fetch(`session:${sessionId}`, () => getSession(sessionId), 86400);
|
||||
} else {
|
||||
session = await getSession(sessionId);
|
||||
}
|
||||
|
|
@ -33,19 +33,3 @@ export async function loadSession(sessionId: string): Promise<Session> {
|
|||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function loadUser(userId: string): Promise<User> {
|
||||
let user;
|
||||
|
||||
if (cache.enabled) {
|
||||
user = await cache.fetchUser(userId);
|
||||
} else {
|
||||
user = await getUser(userId);
|
||||
}
|
||||
|
||||
if (!user || user.deletedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ import redis from '@umami/redis-client';
|
|||
import { getAuthToken, parseShareToken } from 'lib/auth';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { secret } from 'lib/crypto';
|
||||
import { findSession } from 'lib/session';
|
||||
import { getSession } from 'lib/session';
|
||||
import {
|
||||
badRequest,
|
||||
createMiddleware,
|
||||
forbidden,
|
||||
notFound,
|
||||
parseSecureToken,
|
||||
tooManyRequest,
|
||||
unauthorized,
|
||||
} from 'next-basics';
|
||||
import { NextApiRequestCollect } from 'pages/api/send';
|
||||
|
|
@ -27,7 +26,7 @@ export const useCors = createMiddleware(
|
|||
|
||||
export const useSession = createMiddleware(async (req, res, next) => {
|
||||
try {
|
||||
const session = await findSession(req as NextApiRequestCollect);
|
||||
const session = await getSession(req as NextApiRequestCollect);
|
||||
|
||||
if (!session) {
|
||||
log('useSession: Session not found');
|
||||
|
|
@ -36,11 +35,8 @@ export const useSession = createMiddleware(async (req, res, next) => {
|
|||
|
||||
(req as any).session = session;
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Usage Limit.') {
|
||||
return tooManyRequest(res, e.message);
|
||||
}
|
||||
if (e.message.startsWith('Website not found:')) {
|
||||
return forbidden(res, e.message);
|
||||
if (e.message.startsWith('Website not found')) {
|
||||
return notFound(res, e.message);
|
||||
}
|
||||
return badRequest(res, e.message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import prisma from '@umami/prisma-client';
|
|||
import moment from 'moment-timezone';
|
||||
import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
|
||||
import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants';
|
||||
import { loadWebsite } from './load';
|
||||
import { fetchWebsite } from './load';
|
||||
import { maxDate } from './date';
|
||||
import { QueryFilters, QueryOptions, SearchFilter } from './types';
|
||||
import { QueryFilters, QueryOptions, PageParams } from './types';
|
||||
import { filtersToArray } from './params';
|
||||
|
||||
const MYSQL_DATE_FORMATS = {
|
||||
|
|
@ -152,7 +152,7 @@ async function parseFilters(
|
|||
filters: QueryFilters = {},
|
||||
options: QueryOptions = {},
|
||||
) {
|
||||
const website = await loadWebsite(websiteId);
|
||||
const website = await fetchWebsite(websiteId);
|
||||
const joinSession = Object.keys(filters).find(key => SESSION_COLUMNS.includes(key));
|
||||
|
||||
return {
|
||||
|
|
@ -191,7 +191,7 @@ async function rawQuery(sql: string, data: object): Promise<any> {
|
|||
return prisma.rawQuery(query, params);
|
||||
}
|
||||
|
||||
async function pagedQuery<T>(model: string, criteria: T, filters: SearchFilter) {
|
||||
async function pagedQuery<T>(model: string, criteria: T, filters: PageParams) {
|
||||
const { page = 1, pageSize, orderBy, sortDescending = false } = filters || {};
|
||||
const size = +pageSize || DEFAULT_PAGE_SIZE;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as yup from 'yup';
|
|||
|
||||
export const dateRange = {
|
||||
startAt: yup.number().integer().required(),
|
||||
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
|
||||
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
|
||||
};
|
||||
|
||||
export const pageInfo = {
|
||||
|
|
|
|||
|
|
@ -1,28 +1,13 @@
|
|||
import { isUuid, secret, uuid, visitSalt } from 'lib/crypto';
|
||||
import { secret, uuid, visitSalt } from 'lib/crypto';
|
||||
import { getClientInfo } from 'lib/detect';
|
||||
import { parseToken } from 'next-basics';
|
||||
import { NextApiRequestCollect } from 'pages/api/send';
|
||||
import { createSession } from 'queries';
|
||||
import cache from './cache';
|
||||
import clickhouse from './clickhouse';
|
||||
import { loadSession, loadWebsite } from './load';
|
||||
import { fetchSession, fetchWebsite } from './load';
|
||||
import { SessionData } from 'lib/types';
|
||||
|
||||
export async function findSession(req: NextApiRequestCollect): Promise<{
|
||||
id: any;
|
||||
websiteId: string;
|
||||
visitId: string;
|
||||
hostname: string;
|
||||
browser: string;
|
||||
os: any;
|
||||
device: string;
|
||||
screen: string;
|
||||
language: string;
|
||||
country: any;
|
||||
subdivision1: any;
|
||||
subdivision2: any;
|
||||
city: any;
|
||||
ownerId: string;
|
||||
}> {
|
||||
export async function getSession(req: NextApiRequestCollect): Promise<SessionData> {
|
||||
const { payload } = req.body;
|
||||
|
||||
if (!payload) {
|
||||
|
|
@ -35,9 +20,8 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
|
|||
if (cacheToken) {
|
||||
const result = await parseToken(cacheToken, secret());
|
||||
|
||||
// Token is valid
|
||||
if (result) {
|
||||
await checkUserBlock(result?.ownerId);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -45,25 +29,13 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
|
|||
// Verify payload
|
||||
const { website: websiteId, hostname, screen, language } = payload;
|
||||
|
||||
// Check the hostname value for legality to eliminate dirty data
|
||||
const validHostnameRegex = /^[\w-.]+$/;
|
||||
if (!validHostnameRegex.test(hostname)) {
|
||||
throw new Error('Invalid hostname.');
|
||||
}
|
||||
|
||||
if (!isUuid(websiteId)) {
|
||||
throw new Error('Invalid website ID.');
|
||||
}
|
||||
|
||||
// Find website
|
||||
const website = await loadWebsite(websiteId);
|
||||
const website = await fetchWebsite(websiteId);
|
||||
|
||||
if (!website) {
|
||||
throw new Error(`Website not found: ${websiteId}.`);
|
||||
}
|
||||
|
||||
await checkUserBlock(website.userId);
|
||||
|
||||
const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } =
|
||||
await getClientInfo(req);
|
||||
|
||||
|
|
@ -78,7 +50,7 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
|
|||
visitId,
|
||||
hostname,
|
||||
browser,
|
||||
os: os as any,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
|
|
@ -86,12 +58,11 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
|
|||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
ownerId: website.userId,
|
||||
};
|
||||
}
|
||||
|
||||
// Find session
|
||||
let session = await loadSession(sessionId);
|
||||
let session = await fetchSession(sessionId);
|
||||
|
||||
// Create a session if not found
|
||||
if (!session) {
|
||||
|
|
@ -117,13 +88,5 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
|
|||
}
|
||||
}
|
||||
|
||||
return { ...session, ownerId: website.userId, visitId: visitId };
|
||||
}
|
||||
|
||||
async function checkUserBlock(userId: string) {
|
||||
if (process.env.ENABLE_BLOCKER && (await cache.fetchUserBlock(userId))) {
|
||||
await cache.incrementUserBlock(userId);
|
||||
|
||||
throw new Error('Usage Limit.');
|
||||
}
|
||||
return { ...session, visitId: visitId };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,31 +24,7 @@ export type DynamicDataType = ObjectValues<typeof DATA_TYPE>;
|
|||
export type KafkaTopic = ObjectValues<typeof KAFKA_TOPIC>;
|
||||
export type ReportType = ObjectValues<typeof REPORT_TYPES>;
|
||||
|
||||
export interface WebsiteSearchFilter extends SearchFilter {
|
||||
userId?: string;
|
||||
teamId?: string;
|
||||
includeTeams?: boolean;
|
||||
onlyTeams?: boolean;
|
||||
}
|
||||
|
||||
export interface UserSearchFilter extends SearchFilter {
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
export interface TeamSearchFilter extends SearchFilter {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface TeamUserSearchFilter extends SearchFilter {
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
export interface ReportSearchFilter extends SearchFilter {
|
||||
userId?: string;
|
||||
websiteId?: string;
|
||||
}
|
||||
|
||||
export interface SearchFilter {
|
||||
export interface PageParams {
|
||||
query?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
|
|
@ -56,7 +32,7 @@ export interface SearchFilter {
|
|||
sortDescending?: boolean;
|
||||
}
|
||||
|
||||
export interface FilterResult<T> {
|
||||
export interface PageResult<T> {
|
||||
data: T;
|
||||
count: number;
|
||||
page: number;
|
||||
|
|
@ -66,10 +42,10 @@ export interface FilterResult<T> {
|
|||
}
|
||||
|
||||
export interface FilterQueryResult<T> {
|
||||
result: FilterResult<T>;
|
||||
result: PageResult<T>;
|
||||
query: any;
|
||||
params: SearchFilter;
|
||||
setParams: Dispatch<SetStateAction<T | SearchFilter>>;
|
||||
params: PageParams;
|
||||
setParams: Dispatch<SetStateAction<T | PageParams>>;
|
||||
}
|
||||
|
||||
export interface DynamicData {
|
||||
|
|
@ -230,3 +206,19 @@ export interface RealtimeData {
|
|||
countries?: any[];
|
||||
visitors?: any[];
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
id: string;
|
||||
websiteId: string;
|
||||
visitId: string;
|
||||
hostname: string;
|
||||
browser: string;
|
||||
os: string;
|
||||
device: string;
|
||||
screen: string;
|
||||
language: string;
|
||||
country: string;
|
||||
subdivision1: string;
|
||||
subdivision2: string;
|
||||
city: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { canViewUsers } from 'lib/auth';
|
||||
import { useAuth, useValidate } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, Role, SearchFilter, User } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types';
|
||||
import { pageInfo } from 'lib/schema';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { getUsers } from 'queries';
|
||||
import * as yup from 'yup';
|
||||
|
||||
export interface UsersRequestQuery extends SearchFilter {}
|
||||
export interface UsersRequestQuery extends PageParams {}
|
||||
export interface UsersRequestBody {
|
||||
userId: string;
|
||||
username: string;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import { canViewAllWebsites } from 'lib/auth';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { useAuth, useCors, useValidate } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
|
||||
import { pageInfo } from 'lib/schema';
|
||||
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { getWebsites } from 'queries';
|
||||
import * as yup from 'yup';
|
||||
import { pageInfo } from 'lib/schema';
|
||||
|
||||
export interface WebsitesRequestQuery extends SearchFilter {}
|
||||
export interface WebsitesRequestQuery extends PageParams {
|
||||
userId?: string;
|
||||
includeTeams?: boolean;
|
||||
}
|
||||
|
||||
export interface WebsitesRequestBody {
|
||||
name: string;
|
||||
|
|
@ -39,8 +43,29 @@ export default async (
|
|||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { userId, includeOwnedTeams } = req.query;
|
||||
|
||||
const websites = await getWebsites(
|
||||
{
|
||||
where: {
|
||||
OR: [
|
||||
...(userId && [{ userId }]),
|
||||
...(userId &&
|
||||
includeOwnedTeams && [
|
||||
{
|
||||
team: {
|
||||
deletedAt: null,
|
||||
teamUser: {
|
||||
some: {
|
||||
role: ROLES.teamOwner,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
|
|
@ -48,6 +73,18 @@ export default async (
|
|||
id: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
include: {
|
||||
teamUser: {
|
||||
where: {
|
||||
role: ROLES.teamOwner,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
req.query,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const schema = {
|
|||
GET: yup.object().shape({
|
||||
websiteId: yup.string().uuid().required(),
|
||||
startAt: yup.number().integer().required(),
|
||||
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
|
||||
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
|
||||
event: yup.string(),
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const schema = {
|
|||
GET: yup.object().shape({
|
||||
websiteId: yup.string().uuid().required(),
|
||||
startAt: yup.number().integer().required(),
|
||||
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
|
||||
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
|
||||
field: yup.string(),
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const schema = {
|
|||
GET: yup.object().shape({
|
||||
websiteId: yup.string().uuid().required(),
|
||||
startAt: yup.number().integer().required(),
|
||||
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
|
||||
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import ipaddr from 'ipaddr.js';
|
||||
import { isbot } from 'isbot';
|
||||
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
|
||||
import { secret, visitSalt, uuid } from 'lib/crypto';
|
||||
import { getIpAddress } from 'lib/detect';
|
||||
import { useCors, useSession, useValidate } from 'lib/middleware';
|
||||
import { CollectionType, YupRequest } from 'lib/types';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import {
|
||||
badRequest,
|
||||
|
|
@ -15,6 +10,11 @@ import {
|
|||
safeDecodeURI,
|
||||
send,
|
||||
} from 'next-basics';
|
||||
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
|
||||
import { secret, visitSalt, uuid } from 'lib/crypto';
|
||||
import { getIpAddress } from 'lib/detect';
|
||||
import { useCors, useSession, useValidate } from 'lib/middleware';
|
||||
import { CollectionType, YupRequest } from 'lib/types';
|
||||
import { saveEvent, saveSessionData } from 'queries';
|
||||
import * as yup from 'yup';
|
||||
|
||||
|
|
@ -41,7 +41,6 @@ export interface NextApiRequestCollect extends NextApiRequest {
|
|||
id: string;
|
||||
websiteId: string;
|
||||
visitId: string;
|
||||
ownerId: string;
|
||||
hostname: string;
|
||||
browser: string;
|
||||
os: string;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const schema = {
|
|||
POST: yup.object().shape({
|
||||
role: yup
|
||||
.string()
|
||||
.matches(/team-member|team-view-only/i)
|
||||
.matches(/team-member|team-view-only|team-manager/i)
|
||||
.required(),
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { canAddUserToTeam, canViewTeam } from 'lib/auth';
|
||||
import { useAuth, useValidate } from 'lib/middleware';
|
||||
import { pageInfo } from 'lib/schema';
|
||||
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createTeamUser, getTeamUser, getTeamUsers } from 'queries';
|
||||
import * as yup from 'yup';
|
||||
|
||||
export interface TeamUserRequestQuery extends SearchFilter {
|
||||
export interface TeamUserRequestQuery extends PageParams {
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ const schema = {
|
|||
userId: yup.string().uuid().required(),
|
||||
role: yup
|
||||
.string()
|
||||
.matches(/team-member|team-view-only/i)
|
||||
.matches(/team-member|team-view-only|team-manager/i)
|
||||
.required(),
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import * as yup from 'yup';
|
||||
import { canViewTeam } from 'lib/auth';
|
||||
import { useAuth, useValidate } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
|
||||
import { pageInfo } from 'lib/schema';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { ok, unauthorized } from 'next-basics';
|
||||
import { getTeamWebsites } from 'queries';
|
||||
|
||||
export interface TeamWebsiteRequestQuery extends SearchFilter {
|
||||
export interface TeamWebsiteRequestQuery extends PageParams {
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { Team } from '@prisma/client';
|
|||
import { canCreateTeam } from 'lib/auth';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { useAuth, useValidate } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
|
||||
import { pageInfo } from 'lib/schema';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createTeam } from 'queries';
|
||||
|
||||
export interface TeamsRequestQuery extends SearchFilter {}
|
||||
export interface TeamsRequestQuery extends PageParams {}
|
||||
export interface TeamsRequestBody {
|
||||
name: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import * as yup from 'yup';
|
||||
import { useAuth, useCors, useValidate } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
|
||||
import { pageInfo } from 'lib/schema';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { getUserTeams } from 'queries';
|
||||
|
||||
export interface UserTeamsRequestQuery extends SearchFilter {
|
||||
export interface UserTeamsRequestQuery extends PageParams {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const schema = {
|
|||
GET: yup.object().shape({
|
||||
id: yup.string().uuid().required(),
|
||||
startAt: yup.number().integer().required(),
|
||||
endAt: yup.number().integer().moreThan(yup.ref<number>('startAt')).required(),
|
||||
endAt: yup.number().integer().min(yup.ref<number>('startAt')).required(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import { canCreateUser } from 'lib/auth';
|
|||
import { ROLES } from 'lib/constants';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { useAuth, useValidate } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, Role, SearchFilter, User } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types';
|
||||
import { pageInfo } from 'lib/schema';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createUser, getUserByUsername } from 'queries';
|
||||
import * as yup from 'yup';
|
||||
|
||||
export interface UsersRequestQuery extends SearchFilter {}
|
||||
export interface UsersRequestQuery extends PageParams {}
|
||||
export interface UsersRequestBody {
|
||||
username: string;
|
||||
password: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { canViewWebsite } from 'lib/auth';
|
||||
import { useAuth, useCors, useValidate } from 'lib/middleware';
|
||||
import { getRequestDateRange } from 'lib/request';
|
||||
import { getRequestFilters, getRequestDateRange } from 'lib/request';
|
||||
import { NextApiRequestQueryBody, WebsiteMetric } from 'lib/types';
|
||||
import { TimezoneTest, UnitTypeTest } from 'lib/yup';
|
||||
import { NextApiResponse } from 'next';
|
||||
|
|
@ -15,16 +15,32 @@ export interface WebsiteEventsRequestQuery {
|
|||
unit?: string;
|
||||
timezone?: string;
|
||||
url: string;
|
||||
referrer?: string;
|
||||
title?: string;
|
||||
os?: string;
|
||||
browser?: string;
|
||||
device?: string;
|
||||
country?: string;
|
||||
region: string;
|
||||
city?: string;
|
||||
}
|
||||
|
||||
const schema = {
|
||||
GET: yup.object().shape({
|
||||
websiteId: yup.string().uuid().required(),
|
||||
startAt: yup.number().integer().required(),
|
||||
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
|
||||
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
|
||||
unit: UnitTypeTest,
|
||||
timezone: TimezoneTest,
|
||||
url: yup.string(),
|
||||
referrer: yup.string(),
|
||||
title: yup.string(),
|
||||
os: yup.string(),
|
||||
browser: yup.string(),
|
||||
device: yup.string(),
|
||||
country: yup.string(),
|
||||
region: yup.string(),
|
||||
city: yup.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
@ -36,7 +52,7 @@ export default async (
|
|||
await useAuth(req, res);
|
||||
await useValidate(schema, req, res);
|
||||
|
||||
const { websiteId, timezone, url } = req.query;
|
||||
const { websiteId, timezone } = req.query;
|
||||
const { startDate, endDate, unit } = await getRequestDateRange(req);
|
||||
|
||||
if (req.method === 'GET') {
|
||||
|
|
@ -44,13 +60,15 @@ export default async (
|
|||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const events = await getEventMetrics(websiteId, {
|
||||
const filters = {
|
||||
...getRequestFilters(req),
|
||||
startDate,
|
||||
endDate,
|
||||
timezone,
|
||||
unit,
|
||||
url,
|
||||
});
|
||||
};
|
||||
|
||||
const events = await getEventMetrics(websiteId, filters);
|
||||
|
||||
return ok(res, events);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import * as yup from 'yup';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { useAuth, useCors, useValidate } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { getWebsiteReports } from 'queries';
|
||||
import { pageInfo } from 'lib/schema';
|
||||
|
||||
export interface ReportsRequestQuery extends SearchFilter {
|
||||
export interface ReportsRequestQuery extends PageParams {
|
||||
websiteId: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { canCreateTeamWebsite, canCreateWebsite } from 'lib/auth';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { useAuth, useCors, useValidate } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, SearchFilter } from 'lib/types';
|
||||
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createWebsite } from 'queries';
|
||||
|
|
@ -9,7 +9,7 @@ import userWebsitesRoute from 'pages/api/users/[userId]/websites';
|
|||
import * as yup from 'yup';
|
||||
import { pageInfo } from 'lib/schema';
|
||||
|
||||
export interface WebsitesRequestQuery extends SearchFilter {}
|
||||
export interface WebsitesRequestQuery extends PageParams {}
|
||||
|
||||
export interface WebsitesRequestBody {
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Prisma, Report } from '@prisma/client';
|
||||
import prisma from 'lib/prisma';
|
||||
import { FilterResult, ReportSearchFilter } from 'lib/types';
|
||||
import { PageResult, PageParams } from 'lib/types';
|
||||
import ReportFindManyArgs = Prisma.ReportFindManyArgs;
|
||||
|
||||
async function findReport(criteria: Prisma.ReportFindUniqueArgs): Promise<Report> {
|
||||
|
|
@ -17,8 +17,8 @@ export async function getReport(reportId: string): Promise<Report> {
|
|||
|
||||
export async function getReports(
|
||||
criteria: ReportFindManyArgs,
|
||||
filters: ReportSearchFilter = {},
|
||||
): Promise<FilterResult<Report[]>> {
|
||||
filters: PageParams = {},
|
||||
): Promise<PageResult<Report[]>> {
|
||||
const { query } = filters;
|
||||
|
||||
const where: Prisma.ReportWhereInput = {
|
||||
|
|
@ -50,8 +50,8 @@ export async function getReports(
|
|||
|
||||
export async function getUserReports(
|
||||
userId: string,
|
||||
filters?: ReportSearchFilter,
|
||||
): Promise<FilterResult<Report[]>> {
|
||||
filters?: PageParams,
|
||||
): Promise<PageResult<Report[]>> {
|
||||
return getReports(
|
||||
{
|
||||
where: {
|
||||
|
|
@ -72,8 +72,8 @@ export async function getUserReports(
|
|||
|
||||
export async function getWebsiteReports(
|
||||
websiteId: string,
|
||||
filters: ReportSearchFilter = {},
|
||||
): Promise<FilterResult<Report[]>> {
|
||||
filters: PageParams = {},
|
||||
): Promise<PageResult<Report[]>> {
|
||||
return getReports(
|
||||
{
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Prisma, Team } from '@prisma/client';
|
|||
import { ROLES } from 'lib/constants';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import prisma from 'lib/prisma';
|
||||
import { FilterResult, TeamSearchFilter } from 'lib/types';
|
||||
import { PageResult, PageParams } from 'lib/types';
|
||||
import TeamFindManyArgs = Prisma.TeamFindManyArgs;
|
||||
|
||||
export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise<Team> {
|
||||
|
|
@ -22,8 +22,8 @@ export async function getTeam(teamId: string, options: { includeMembers?: boolea
|
|||
|
||||
export async function getTeams(
|
||||
criteria: TeamFindManyArgs,
|
||||
filters: TeamSearchFilter = {},
|
||||
): Promise<FilterResult<Team[]>> {
|
||||
filters: PageParams = {},
|
||||
): Promise<PageResult<Team[]>> {
|
||||
const { getSearchParameters } = prisma;
|
||||
const { query } = filters;
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ export async function getTeams(
|
|||
);
|
||||
}
|
||||
|
||||
export async function getUserTeams(userId: string, filters: TeamSearchFilter = {}) {
|
||||
export async function getUserTeams(userId: string, filters: PageParams = {}) {
|
||||
return getTeams(
|
||||
{
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Prisma, TeamUser } from '@prisma/client';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import prisma from 'lib/prisma';
|
||||
import { FilterResult, TeamUserSearchFilter } from 'lib/types';
|
||||
import { PageResult, PageParams } from 'lib/types';
|
||||
import TeamUserFindManyArgs = Prisma.TeamUserFindManyArgs;
|
||||
|
||||
export async function findTeamUser(criteria: Prisma.TeamUserFindUniqueArgs): Promise<TeamUser> {
|
||||
|
|
@ -19,8 +19,8 @@ export async function getTeamUser(teamId: string, userId: string): Promise<TeamU
|
|||
|
||||
export async function getTeamUsers(
|
||||
criteria: TeamUserFindManyArgs,
|
||||
filters?: TeamUserSearchFilter,
|
||||
): Promise<FilterResult<TeamUser[]>> {
|
||||
filters?: PageParams,
|
||||
): Promise<PageResult<TeamUser[]>> {
|
||||
const { query } = filters;
|
||||
|
||||
const where: Prisma.TeamUserWhereInput = {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { Prisma } from '@prisma/client';
|
||||
import cache from 'lib/cache';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import prisma from 'lib/prisma';
|
||||
import { FilterResult, Role, User, UserSearchFilter } from 'lib/types';
|
||||
import { PageResult, Role, User, PageParams } from 'lib/types';
|
||||
import { getRandomChars } from 'next-basics';
|
||||
import UserFindManyArgs = Prisma.UserFindManyArgs;
|
||||
|
||||
|
|
@ -50,8 +49,8 @@ export async function getUserByUsername(username: string, options: GetUserOption
|
|||
|
||||
export async function getUsers(
|
||||
criteria: UserFindManyArgs,
|
||||
filters?: UserSearchFilter,
|
||||
): Promise<FilterResult<User[]>> {
|
||||
filters?: PageParams,
|
||||
): Promise<PageResult<User[]>> {
|
||||
const { query } = filters;
|
||||
|
||||
const where: Prisma.UserWhereInput = {
|
||||
|
|
@ -221,15 +220,5 @@ export async function deleteUser(
|
|||
id: userId,
|
||||
},
|
||||
}),
|
||||
]).then(async data => {
|
||||
if (cache.enabled) {
|
||||
const ids = websites.map(a => a.id);
|
||||
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
await cache.deleteWebsite(`website:${ids[i]}`);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Prisma, Website } from '@prisma/client';
|
||||
import cache from 'lib/cache';
|
||||
import prisma from 'lib/prisma';
|
||||
import { FilterResult, WebsiteSearchFilter } from 'lib/types';
|
||||
import { PageResult, PageParams } from 'lib/types';
|
||||
import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs;
|
||||
|
||||
async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs): Promise<Website> {
|
||||
|
|
@ -26,8 +25,8 @@ export async function getSharedWebsite(shareId: string) {
|
|||
|
||||
export async function getWebsites(
|
||||
criteria: WebsiteFindManyArgs,
|
||||
filters: WebsiteSearchFilter,
|
||||
): Promise<FilterResult<Website[]>> {
|
||||
filters: PageParams,
|
||||
): Promise<PageResult<Website[]>> {
|
||||
const { query } = filters;
|
||||
|
||||
const where: Prisma.WebsiteWhereInput = {
|
||||
|
|
@ -54,8 +53,8 @@ export async function getAllWebsites(userId: string) {
|
|||
|
||||
export async function getUserWebsites(
|
||||
userId: string,
|
||||
filters?: WebsiteSearchFilter,
|
||||
): Promise<FilterResult<Website[]>> {
|
||||
filters?: PageParams,
|
||||
): Promise<PageResult<Website[]>> {
|
||||
return getWebsites(
|
||||
{
|
||||
where: {
|
||||
|
|
@ -79,8 +78,8 @@ export async function getUserWebsites(
|
|||
|
||||
export async function getTeamWebsites(
|
||||
teamId: string,
|
||||
filters?: WebsiteSearchFilter,
|
||||
): Promise<FilterResult<Website[]>> {
|
||||
filters?: PageParams,
|
||||
): Promise<PageResult<Website[]>> {
|
||||
return getWebsites(
|
||||
{
|
||||
where: {
|
||||
|
|
@ -102,17 +101,9 @@ export async function getTeamWebsites(
|
|||
export async function createWebsite(
|
||||
data: Prisma.WebsiteCreateInput | Prisma.WebsiteUncheckedCreateInput,
|
||||
): Promise<Website> {
|
||||
return prisma.client.website
|
||||
.create({
|
||||
data,
|
||||
})
|
||||
.then(async data => {
|
||||
if (cache.enabled) {
|
||||
await cache.storeWebsite(data);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
return prisma.client.website.create({
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateWebsite(
|
||||
|
|
@ -148,13 +139,7 @@ export async function resetWebsite(
|
|||
resetAt: new Date(),
|
||||
},
|
||||
}),
|
||||
]).then(async data => {
|
||||
if (cache.enabled) {
|
||||
await cache.storeWebsite(data[3]);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
]);
|
||||
}
|
||||
|
||||
export async function deleteWebsite(
|
||||
|
|
@ -188,11 +173,5 @@ export async function deleteWebsite(
|
|||
: client.website.delete({
|
||||
where: { id: websiteId },
|
||||
}),
|
||||
]).then(async data => {
|
||||
if (cache.enabled) {
|
||||
await cache.deleteWebsite(websiteId);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
and event_data.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_name = {{event}}
|
||||
group by website_event.event_name, event_data.data_key, event_data.data_type, event_data.string_value
|
||||
order by 1 asc, 2 asc, 3 asc, 4 desc
|
||||
order by 1 asc, 2 asc, 3 asc, 5 desc
|
||||
`,
|
||||
params,
|
||||
);
|
||||
|
|
@ -81,7 +81,7 @@ async function clickhouseQuery(
|
|||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_name = {event:String}
|
||||
group by data_key, data_type, string_value, event_name
|
||||
order by 1 asc, 2 asc, 3 asc, 4 desc
|
||||
order by 1 asc, 2 asc, 3 asc, 5 desc
|
||||
limit 500
|
||||
`,
|
||||
params,
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
`
|
||||
select
|
||||
event_name x,
|
||||
${getDateQuery('created_at', unit, timezone)} t,
|
||||
${getDateQuery('website_event.created_at', unit, timezone)} t,
|
||||
count(*) y
|
||||
from website_event
|
||||
${joinSession}
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and event_type = {{eventType}}
|
||||
${filterQuery}
|
||||
group by 1, 2
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Prisma } from '@prisma/client';
|
||||
import cache from 'lib/cache';
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function createSession(data: Prisma.SessionCreateInput) {
|
||||
|
|
@ -18,28 +17,20 @@ export async function createSession(data: Prisma.SessionCreateInput) {
|
|||
city,
|
||||
} = data;
|
||||
|
||||
return prisma.client.session
|
||||
.create({
|
||||
data: {
|
||||
id,
|
||||
websiteId,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
},
|
||||
})
|
||||
.then(async data => {
|
||||
if (cache.enabled) {
|
||||
await cache.storeSession(data);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
return prisma.client.session.create({
|
||||
data: {
|
||||
id,
|
||||
websiteId,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue