mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Compare commits
No commits in common. "abfb78bb980ea078d6110738c24bf315221be4fb" and "a37de757a098ed7a64525246216b0f30b2c8139c" have entirely different histories.
abfb78bb98
...
a37de757a0
52 changed files with 855 additions and 809 deletions
94
.gitignore
vendored
94
.gitignore
vendored
|
|
@ -1,48 +1,46 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
.pnp
|
.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
.pnpm-store
|
.pnpm-store
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next
|
/.next
|
||||||
/out
|
/out
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
/public/script.js
|
/public/script.js
|
||||||
/geo
|
/geo
|
||||||
/dist
|
/dist
|
||||||
/generated
|
/generated
|
||||||
/src/generated
|
/src/generated
|
||||||
pm2.yml
|
pm2.yml
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
.yarn
|
.yarn
|
||||||
*.iml
|
*.iml
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
.tool-versions
|
.tool-versions
|
||||||
.claude
|
|
||||||
nul
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
# debug
|
yarn-debug.log*
|
||||||
npm-debug.log*
|
yarn-error.log*
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
# local env files
|
||||||
|
.env
|
||||||
# local env files
|
.env.*
|
||||||
.env
|
*.env.*
|
||||||
.env.*
|
|
||||||
*.env.*
|
*.dev.yml
|
||||||
|
|
||||||
*.dev.yml
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3002 --turbo",
|
"dev": "next dev -p 3001 --turbo",
|
||||||
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
|
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
||||||
|
|
@ -102,7 +102,7 @@
|
||||||
"kafkajs": "^2.1.0",
|
"kafkajs": "^2.1.0",
|
||||||
"lucide-react": "^0.543.0",
|
"lucide-react": "^0.543.0",
|
||||||
"maxmind": "^5.0.0",
|
"maxmind": "^5.0.0",
|
||||||
"next": "^15.5.10",
|
"next": "^15.5.9",
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
|
|
@ -164,7 +164,7 @@
|
||||||
"stylelint-config-css-modules": "^4.5.1",
|
"stylelint-config-css-modules": "^4.5.1",
|
||||||
"stylelint-config-prettier": "^9.0.3",
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
"stylelint-config-recommended": "^14.0.0",
|
"stylelint-config-recommended": "^14.0.0",
|
||||||
"tar": "^7.5.7",
|
"tar": "^7.5.4",
|
||||||
"ts-jest": "^29.4.6",
|
"ts-jest": "^29.4.6",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
|
|
|
||||||
617
pnpm-lock.yaml
generated
617
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -42,7 +42,7 @@ export function MobileNav() {
|
||||||
{({ close }) => {
|
{({ close }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavMenu padding="3" onItemClick={close} border="bottom" width="100%">
|
<NavMenu padding="3" onItemClick={close} border="bottom">
|
||||||
<NavButton />
|
<NavButton />
|
||||||
{links.map(link => {
|
{links.map(link => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export function PixelsTable({ showActions, ...props }: PixelsTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { renderUrl } = useNavigation();
|
const { renderUrl } = useNavigation();
|
||||||
const { getSlugUrl } = useSlug('pixel');
|
const { getSlugUrl } = useSlug('pixel');
|
||||||
|
console.log(showActions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable {...props}>
|
<DataTable {...props}>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { useMessages, useNavigation, useResultQuery } from '@/components/hooks';
|
import { useMessages, useResultQuery } from '@/components/hooks';
|
||||||
import { File, User } from '@/components/icons';
|
import { File, User } from '@/components/icons';
|
||||||
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
||||||
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
|
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
|
||||||
|
|
@ -20,8 +20,6 @@ type FunnelResult = {
|
||||||
|
|
||||||
export function Funnel({ id, name, type, parameters, websiteId }) {
|
export function Funnel({ id, name, type, parameters, websiteId }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { pathname } = useNavigation();
|
|
||||||
const isSharePage = pathname.includes('/share/');
|
|
||||||
const { data, error, isLoading } = useResultQuery(type, {
|
const { data, error, isLoading } = useResultQuery(type, {
|
||||||
websiteId,
|
websiteId,
|
||||||
...parameters,
|
...parameters,
|
||||||
|
|
@ -38,22 +36,21 @@ export function Funnel({ id, name, type, parameters, websiteId }) {
|
||||||
</Text>
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
{!isSharePage && (
|
<Column>
|
||||||
<Column>
|
<ReportEditButton id={id} name={name} type={type}>
|
||||||
<ReportEditButton id={id} name={name} type={type}>
|
{({ close }) => {
|
||||||
{({ close }) => {
|
return (
|
||||||
return (
|
<Dialog
|
||||||
<Dialog
|
title={formatMessage(labels.funnel)}
|
||||||
title={formatMessage(labels.funnel)}
|
variant="modal"
|
||||||
style={{ minHeight: 300, minWidth: 400 }}
|
style={{ minHeight: 300, minWidth: 400 }}
|
||||||
>
|
>
|
||||||
<FunnelEditForm id={id} websiteId={websiteId} onClose={close} />
|
<FunnelEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</ReportEditButton>
|
</ReportEditButton>
|
||||||
</Column>
|
</Column>
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
{data?.map(
|
{data?.map(
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||||
import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
|
import { useDateRange, useReportsQuery } from '@/components/hooks';
|
||||||
import { Funnel } from './Funnel';
|
import { Funnel } from './Funnel';
|
||||||
import { FunnelAddButton } from './FunnelAddButton';
|
import { FunnelAddButton } from './FunnelAddButton';
|
||||||
|
|
||||||
|
|
@ -13,17 +13,13 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange();
|
} = useDateRange();
|
||||||
const { pathname } = useNavigation();
|
|
||||||
const isSharePage = pathname.includes('/share/');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
{!isSharePage && (
|
<SectionHeader>
|
||||||
<SectionHeader>
|
<FunnelAddButton websiteId={websiteId} />
|
||||||
<FunnelAddButton websiteId={websiteId} />
|
</SectionHeader>
|
||||||
</SectionHeader>
|
|
||||||
)}
|
|
||||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||||
{data && (
|
{data && (
|
||||||
<Grid gap>
|
<Grid gap>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { useMessages, useNavigation, useResultQuery } from '@/components/hooks';
|
import { useMessages, useResultQuery } from '@/components/hooks';
|
||||||
import { File, User } from '@/components/icons';
|
import { File, User } from '@/components/icons';
|
||||||
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
||||||
import { Lightning } from '@/components/svg';
|
import { Lightning } from '@/components/svg';
|
||||||
|
|
@ -25,8 +25,6 @@ export type GoalData = { num: number; total: number };
|
||||||
|
|
||||||
export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) {
|
export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { pathname } = useNavigation();
|
|
||||||
const isSharePage = pathname.includes('/share/');
|
|
||||||
const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, {
|
const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, {
|
||||||
websiteId,
|
websiteId,
|
||||||
startDate,
|
startDate,
|
||||||
|
|
@ -47,23 +45,21 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate
|
||||||
</Text>
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
{!isSharePage && (
|
<Column>
|
||||||
<Column>
|
<ReportEditButton id={id} name={name} type={type}>
|
||||||
<ReportEditButton id={id} name={name} type={type}>
|
{({ close }) => {
|
||||||
{({ close }) => {
|
return (
|
||||||
return (
|
<Dialog
|
||||||
<Dialog
|
title={formatMessage(labels.goal)}
|
||||||
title={formatMessage(labels.goal)}
|
variant="modal"
|
||||||
variant="modal"
|
style={{ minHeight: 300, minWidth: 400 }}
|
||||||
style={{ minHeight: 300, minWidth: 400 }}
|
>
|
||||||
>
|
<GoalEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||||
<GoalEditForm id={id} websiteId={websiteId} onClose={close} />
|
</Dialog>
|
||||||
</Dialog>
|
);
|
||||||
);
|
}}
|
||||||
}}
|
</ReportEditButton>
|
||||||
</ReportEditButton>
|
</Column>
|
||||||
</Column>
|
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Row alignItems="center" justifyContent="space-between" gap>
|
<Row alignItems="center" justifyContent="space-between" gap>
|
||||||
<Text color="muted">
|
<Text color="muted">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||||
import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
|
import { useDateRange, useReportsQuery } from '@/components/hooks';
|
||||||
import { Goal } from './Goal';
|
import { Goal } from './Goal';
|
||||||
import { GoalAddButton } from './GoalAddButton';
|
import { GoalAddButton } from './GoalAddButton';
|
||||||
|
|
||||||
|
|
@ -13,17 +13,13 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange();
|
} = useDateRange();
|
||||||
const { pathname } = useNavigation();
|
|
||||||
const isSharePage = pathname.includes('/share/');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
{!isSharePage && (
|
<SectionHeader>
|
||||||
<SectionHeader>
|
<GoalAddButton websiteId={websiteId} />
|
||||||
<GoalAddButton websiteId={websiteId} />
|
</SectionHeader>
|
||||||
</SectionHeader>
|
|
||||||
)}
|
|
||||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||||
{data && (
|
{data && (
|
||||||
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
|
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,7 @@ import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
|
||||||
import { Edit } from '@/components/icons';
|
import { Edit } from '@/components/icons';
|
||||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||||
|
|
||||||
export function WebsiteHeader({
|
export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
||||||
showActions,
|
|
||||||
allowLink = true,
|
|
||||||
}: {
|
|
||||||
showActions?: boolean;
|
|
||||||
allowLink?: boolean;
|
|
||||||
}) {
|
|
||||||
const website = useWebsite();
|
const website = useWebsite();
|
||||||
const { renderUrl, pathname } = useNavigation();
|
const { renderUrl, pathname } = useNavigation();
|
||||||
const isSettings = pathname.endsWith('/settings');
|
const isSettings = pathname.endsWith('/settings');
|
||||||
|
|
@ -27,7 +21,7 @@ export function WebsiteHeader({
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={website.name}
|
title={website.name}
|
||||||
icon={<Favicon domain={website.domain} />}
|
icon={<Favicon domain={website.domain} />}
|
||||||
titleHref={allowLink ? renderUrl(`/websites/${website.id}`, false) : undefined}
|
titleHref={renderUrl(`/websites/${website.id}`, false)}
|
||||||
>
|
>
|
||||||
<Row alignItems="center" gap="6" wrap="wrap">
|
<Row alignItems="center" gap="6" wrap="wrap">
|
||||||
<ActiveUsers websiteId={website.id} />
|
<ActiveUsers websiteId={website.id} />
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@ import { WebsiteNav } from './WebsiteNav';
|
||||||
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<WebsiteProvider websiteId={websiteId}>
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%">
|
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
||||||
<Column
|
<Column
|
||||||
display={{ xs: 'none', lg: 'flex' }}
|
display={{ xs: 'none', lg: 'flex' }}
|
||||||
width="240px"
|
width="240px"
|
||||||
|
height="100%"
|
||||||
border="right"
|
border="right"
|
||||||
backgroundColor
|
backgroundColor
|
||||||
marginRight="2"
|
marginRight="2"
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { Grid, Heading, Row, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
|
import { Grid, Heading, Row, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
|
||||||
import { GridRow } from '@/components/common/GridRow';
|
import { GridRow } from '@/components/common/GridRow';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
|
import { EventsChart } from '@/components/metrics/EventsChart';
|
||||||
import { MetricsTable } from '@/components/metrics/MetricsTable';
|
import { MetricsTable } from '@/components/metrics/MetricsTable';
|
||||||
import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic';
|
import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic';
|
||||||
import { WorldMap } from '@/components/metrics/WorldMap';
|
import { WorldMap } from '@/components/metrics/WorldMap';
|
||||||
|
|
||||||
export function WebsitePanels({ websiteId }: { websiteId: string }) {
|
export function WebsitePanels({ websiteId }: { websiteId: string }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { pathname } = useNavigation();
|
||||||
const tableProps = {
|
const tableProps = {
|
||||||
websiteId,
|
websiteId,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
|
@ -16,6 +18,7 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) {
|
||||||
metric: formatMessage(labels.visitors),
|
metric: formatMessage(labels.visitors),
|
||||||
};
|
};
|
||||||
const rowProps = { minHeight: '570px' };
|
const rowProps = { minHeight: '570px' };
|
||||||
|
const isSharePage = pathname.includes('/share/');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid gap="3">
|
<Grid gap="3">
|
||||||
|
|
@ -113,6 +116,25 @@ export function WebsitePanels({ websiteId }: { websiteId: string }) {
|
||||||
<WeeklyTraffic websiteId={websiteId} />
|
<WeeklyTraffic websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
|
{isSharePage && (
|
||||||
|
<GridRow layout="two-one" {...rowProps}>
|
||||||
|
<Panel>
|
||||||
|
<Heading size="2">{formatMessage(labels.events)}</Heading>
|
||||||
|
<Row border="bottom" marginBottom="4" />
|
||||||
|
<MetricsTable
|
||||||
|
websiteId={websiteId}
|
||||||
|
type="event"
|
||||||
|
title={formatMessage(labels.event)}
|
||||||
|
metric={formatMessage(labels.count)}
|
||||||
|
limit={15}
|
||||||
|
filterLink={false}
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
<Panel gridColumn={{ xs: 'span 1', md: 'span 2' }}>
|
||||||
|
<EventsChart websiteId={websiteId} />
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Column,
|
||||||
|
Form,
|
||||||
|
FormField,
|
||||||
|
FormSubmitButton,
|
||||||
|
Row,
|
||||||
|
Text,
|
||||||
|
} from '@umami/react-zen';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||||
|
import { SHARE_NAV_ITEMS } from './constants';
|
||||||
|
|
||||||
|
export interface ShareCreateFormProps {
|
||||||
|
websiteId: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormProps) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { post } = useApi();
|
||||||
|
const { touch } = useModified();
|
||||||
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
|
// Build default values - only overview and events enabled by default
|
||||||
|
const defaultValues: Record<string, boolean> = {};
|
||||||
|
SHARE_NAV_ITEMS.forEach(section => {
|
||||||
|
section.items.forEach(item => {
|
||||||
|
defaultValues[item.id] = item.id === 'overview' || item.id === 'events';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (data: any) => {
|
||||||
|
setIsPending(true);
|
||||||
|
try {
|
||||||
|
const parameters: Record<string, boolean> = {};
|
||||||
|
SHARE_NAV_ITEMS.forEach(section => {
|
||||||
|
section.items.forEach(item => {
|
||||||
|
parameters[item.id] = data[item.id] ?? false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await post(`/websites/${websiteId}/shares`, { parameters });
|
||||||
|
touch('shares');
|
||||||
|
onSave?.();
|
||||||
|
onClose?.();
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit} defaultValues={defaultValues}>
|
||||||
|
<Column gap="3">
|
||||||
|
{SHARE_NAV_ITEMS.map(section => (
|
||||||
|
<Column key={section.section} gap="1">
|
||||||
|
<Text size="2" weight="bold">
|
||||||
|
{formatMessage((labels as any)[section.section])}
|
||||||
|
</Text>
|
||||||
|
<Column gap="1">
|
||||||
|
{section.items.map(item => (
|
||||||
|
<FormField key={item.id} name={item.id}>
|
||||||
|
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
|
||||||
|
</FormField>
|
||||||
|
))}
|
||||||
|
</Column>
|
||||||
|
</Column>
|
||||||
|
))}
|
||||||
|
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||||
|
{onClose && (
|
||||||
|
<Button isDisabled={isPending} onPress={onClose}>
|
||||||
|
{formatMessage(labels.cancel)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<FormSubmitButton isDisabled={isPending}>{formatMessage(labels.save)}</FormSubmitButton>
|
||||||
|
</Row>
|
||||||
|
</Column>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
Form,
|
Form,
|
||||||
FormField,
|
FormField,
|
||||||
FormSubmitButton,
|
FormSubmitButton,
|
||||||
Grid,
|
|
||||||
Label,
|
Label,
|
||||||
Loading,
|
Loading,
|
||||||
Row,
|
Row,
|
||||||
|
|
@ -14,30 +13,25 @@ import {
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
|
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
|
||||||
|
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
||||||
import { SHARE_NAV_ITEMS } from './constants';
|
import { SHARE_NAV_ITEMS } from './constants';
|
||||||
|
|
||||||
export function ShareEditForm({
|
export function ShareEditForm({
|
||||||
shareId,
|
shareId,
|
||||||
websiteId,
|
|
||||||
onSave,
|
onSave,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
shareId?: string;
|
shareId: string;
|
||||||
websiteId?: string;
|
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels, getErrorMessage } = useMessages();
|
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
|
||||||
|
const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(`/share/id/${shareId}`);
|
||||||
const { cloudMode } = useConfig();
|
const { cloudMode } = useConfig();
|
||||||
const { get, post } = useApi();
|
const { get } = useApi();
|
||||||
const { touch } = useModified();
|
|
||||||
const { modified } = useModified('shares');
|
const { modified } = useModified('shares');
|
||||||
const [share, setShare] = useState<any>(null);
|
const [share, setShare] = useState<any>(null);
|
||||||
const [isLoading, setIsLoading] = useState(!!shareId);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isPending, setIsPending] = useState(false);
|
|
||||||
const [error, setError] = useState<any>(null);
|
|
||||||
|
|
||||||
const isEditing = !!shareId;
|
|
||||||
|
|
||||||
const getUrl = (slug: string) => {
|
const getUrl = (slug: string) => {
|
||||||
if (cloudMode) {
|
if (cloudMode) {
|
||||||
|
|
@ -47,8 +41,6 @@ export function ShareEditForm({
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shareId) return;
|
|
||||||
|
|
||||||
const loadShare = async () => {
|
const loadShare = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -69,35 +61,27 @@ export function ShareEditForm({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsPending(true);
|
await mutateAsync(
|
||||||
setError(null);
|
{ slug: share.slug, parameters },
|
||||||
|
{
|
||||||
try {
|
onSuccess: async () => {
|
||||||
if (isEditing) {
|
toast(formatMessage(messages.saved));
|
||||||
await post(`/share/id/${shareId}`, { name: data.name, slug: share.slug, parameters });
|
touch('shares');
|
||||||
} else {
|
onSave?.();
|
||||||
await post(`/websites/${websiteId}/shares`, { name: data.name, parameters });
|
onClose?.();
|
||||||
}
|
},
|
||||||
touch('shares');
|
},
|
||||||
onSave?.();
|
);
|
||||||
onClose?.();
|
|
||||||
} catch (e) {
|
|
||||||
setError(e);
|
|
||||||
} finally {
|
|
||||||
setIsPending(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loading placement="absolute" />;
|
return <Loading placement="absolute" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = isEditing ? getUrl(share?.slug || '') : null;
|
const url = getUrl(share?.slug || '');
|
||||||
|
|
||||||
// Build default values from share parameters
|
// Build default values from share parameters
|
||||||
const defaultValues: Record<string, any> = {
|
const defaultValues: Record<string, boolean> = {};
|
||||||
name: share?.name || '',
|
|
||||||
};
|
|
||||||
SHARE_NAV_ITEMS.forEach(section => {
|
SHARE_NAV_ITEMS.forEach(section => {
|
||||||
section.items.forEach(item => {
|
section.items.forEach(item => {
|
||||||
const defaultSelected = item.id === 'overview' || item.id === 'events';
|
const defaultSelected = item.id === 'overview' || item.id === 'events';
|
||||||
|
|
@ -105,60 +89,34 @@ export function ShareEditForm({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get all item ids for validation
|
|
||||||
const allItemIds = SHARE_NAV_ITEMS.flatMap(section => section.items.map(item => item.id));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}>
|
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}>
|
||||||
{({ watch }) => {
|
<Column gap="3">
|
||||||
const values = watch();
|
<Column>
|
||||||
const hasSelection = allItemIds.some(id => values[id]);
|
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||||
|
<TextField value={url} isReadOnly allowCopy />
|
||||||
return (
|
</Column>
|
||||||
<Column gap="6">
|
{SHARE_NAV_ITEMS.map(section => (
|
||||||
{url && (
|
<Column key={section.section} gap="1">
|
||||||
<Column>
|
<Text weight="bold">{formatMessage((labels as any)[section.section])}</Text>
|
||||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
<Column gap="1">
|
||||||
<TextField value={url} isReadOnly allowCopy />
|
{section.items.map(item => (
|
||||||
</Column>
|
<FormField key={item.id} name={item.id}>
|
||||||
)}
|
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
|
||||||
<FormField
|
</FormField>
|
||||||
label={formatMessage(labels.name)}
|
|
||||||
name="name"
|
|
||||||
rules={{ required: formatMessage(labels.required) }}
|
|
||||||
>
|
|
||||||
<TextField autoComplete="off" autoFocus={!isEditing} />
|
|
||||||
</FormField>
|
|
||||||
<Grid columns="repeat(auto-fit, minmax(150px, 1fr))" gap="3">
|
|
||||||
{SHARE_NAV_ITEMS.map(section => (
|
|
||||||
<Column key={section.section} gap="3">
|
|
||||||
<Text weight="bold">{formatMessage((labels as any)[section.section])}</Text>
|
|
||||||
<Column gap="1">
|
|
||||||
{section.items.map(item => (
|
|
||||||
<FormField key={item.id} name={item.id}>
|
|
||||||
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
|
|
||||||
</FormField>
|
|
||||||
))}
|
|
||||||
</Column>
|
|
||||||
</Column>
|
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Column>
|
||||||
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
|
||||||
{onClose && (
|
|
||||||
<Button isDisabled={isPending} onPress={onClose}>
|
|
||||||
{formatMessage(labels.cancel)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<FormSubmitButton
|
|
||||||
variant="primary"
|
|
||||||
isDisabled={isPending || !hasSelection || !values.name}
|
|
||||||
>
|
|
||||||
{formatMessage(labels.save)}
|
|
||||||
</FormSubmitButton>
|
|
||||||
</Row>
|
|
||||||
</Column>
|
</Column>
|
||||||
);
|
))}
|
||||||
}}
|
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||||
|
{onClose && (
|
||||||
|
<Button isDisabled={isPending} onPress={onClose}>
|
||||||
|
{formatMessage(labels.cancel)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<FormSubmitButton variant="primary">{formatMessage(labels.save)}</FormSubmitButton>
|
||||||
|
</Row>
|
||||||
|
</Column>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,24 @@
|
||||||
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
|
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
|
||||||
import { DateDistance } from '@/components/common/DateDistance';
|
import { DateDistance } from '@/components/common/DateDistance';
|
||||||
import { ExternalLink } from '@/components/common/ExternalLink';
|
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||||
import { useConfig, useMessages, useMobile } from '@/components/hooks';
|
import { useConfig, useMessages } from '@/components/hooks';
|
||||||
import { ShareDeleteButton } from './ShareDeleteButton';
|
import { ShareDeleteButton } from './ShareDeleteButton';
|
||||||
import { ShareEditButton } from './ShareEditButton';
|
import { ShareEditButton } from './ShareEditButton';
|
||||||
|
|
||||||
export function SharesTable(props: DataTableProps) {
|
export function SharesTable(props: DataTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { cloudMode } = useConfig();
|
const { cloudMode } = useConfig();
|
||||||
const { isMobile } = useMobile();
|
|
||||||
|
|
||||||
const getUrl = (slug: string) => {
|
const getUrl = (slug: string) => {
|
||||||
return `${cloudMode ? process.env.cloudUrl : window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
|
if (cloudMode) {
|
||||||
|
return `${process.env.cloudUrl}/share/${slug}`;
|
||||||
|
}
|
||||||
|
return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable {...props}>
|
<DataTable {...props}>
|
||||||
<DataColumn id="name" label={formatMessage(labels.name)}>
|
<DataColumn id="slug" label={formatMessage(labels.shareUrl)}>
|
||||||
{({ name }: any) => name}
|
|
||||||
</DataColumn>
|
|
||||||
<DataColumn id="slug" label={formatMessage(labels.shareUrl)} width="2fr">
|
|
||||||
{({ slug }: any) => {
|
{({ slug }: any) => {
|
||||||
const url = getUrl(slug);
|
const url = getUrl(slug);
|
||||||
return (
|
return (
|
||||||
|
|
@ -29,11 +28,9 @@ export function SharesTable(props: DataTableProps) {
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
{!isMobile && (
|
<DataColumn id="created" label={formatMessage(labels.created)} width="200px">
|
||||||
<DataColumn id="created" label={formatMessage(labels.created)}>
|
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
</DataColumn>
|
||||||
</DataColumn>
|
|
||||||
)}
|
|
||||||
<DataColumn id="action" align="end" width="100px">
|
<DataColumn id="action" align="end" width="100px">
|
||||||
{({ id, slug }: any) => {
|
{({ id, slug }: any) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Column, Heading, Row, Text } from '@umami/react-zen';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
|
import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
|
||||||
import { DialogButton } from '@/components/input/DialogButton';
|
import { DialogButton } from '@/components/input/DialogButton';
|
||||||
import { ShareEditForm } from './ShareEditForm';
|
import { ShareCreateForm } from './ShareCreateForm';
|
||||||
import { SharesTable } from './SharesTable';
|
import { SharesTable } from './SharesTable';
|
||||||
|
|
||||||
export interface WebsiteShareFormProps {
|
export interface WebsiteShareFormProps {
|
||||||
|
|
@ -11,7 +11,7 @@ export interface WebsiteShareFormProps {
|
||||||
|
|
||||||
export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
||||||
const { formatMessage, labels, messages } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
const { data } = useWebsiteSharesQuery({ websiteId });
|
const { data, isLoading } = useWebsiteSharesQuery({ websiteId });
|
||||||
|
|
||||||
const shares = data?.data || [];
|
const shares = data?.data || [];
|
||||||
const hasShares = shares.length > 0;
|
const hasShares = shares.length > 0;
|
||||||
|
|
@ -25,9 +25,9 @@ export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
||||||
label={formatMessage(labels.add)}
|
label={formatMessage(labels.add)}
|
||||||
title={formatMessage(labels.share)}
|
title={formatMessage(labels.share)}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
width="600px"
|
width="400px"
|
||||||
>
|
>
|
||||||
{({ close }) => <ShareEditForm websiteId={websiteId} onClose={close} />}
|
{({ close }) => <ShareCreateForm websiteId={websiteId} onClose={close} />}
|
||||||
</DialogButton>
|
</DialogButton>
|
||||||
</Row>
|
</Row>
|
||||||
{hasShares ? (
|
{hasShares ? (
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,7 @@
|
||||||
import { ROLES } from '@/lib/constants';
|
|
||||||
import { secret } from '@/lib/crypto';
|
import { secret } from '@/lib/crypto';
|
||||||
import { createToken } from '@/lib/jwt';
|
import { createToken } from '@/lib/jwt';
|
||||||
import prisma from '@/lib/prisma';
|
|
||||||
import redis from '@/lib/redis';
|
|
||||||
import { json, notFound } from '@/lib/response';
|
import { json, notFound } from '@/lib/response';
|
||||||
import type { WhiteLabel } from '@/lib/types';
|
import { getShareByCode } from '@/queries/prisma';
|
||||||
import { getShareByCode, getWebsite } from '@/queries/prisma';
|
|
||||||
|
|
||||||
async function getAccountId(website: { userId?: string; teamId?: string }): Promise<string | null> {
|
|
||||||
if (website.userId) {
|
|
||||||
return website.userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (website.teamId) {
|
|
||||||
const teamOwner = await prisma.client.teamUser.findFirst({
|
|
||||||
where: {
|
|
||||||
teamId: website.teamId,
|
|
||||||
role: ROLES.teamOwner,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
userId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return teamOwner?.userId || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getWhiteLabel(accountId: string): Promise<WhiteLabel | null> {
|
|
||||||
if (!redis.enabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await redis.client.get(`white-label:${accountId}`);
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return data as WhiteLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
|
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
|
@ -52,25 +12,12 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const website = await getWebsite(share.entityId);
|
const data = {
|
||||||
|
|
||||||
const data: Record<string, any> = {
|
|
||||||
shareId: share.id,
|
shareId: share.id,
|
||||||
websiteId: share.entityId,
|
websiteId: share.entityId,
|
||||||
parameters: share.parameters,
|
parameters: share.parameters,
|
||||||
};
|
};
|
||||||
|
const token = createToken(data, secret());
|
||||||
|
|
||||||
data.token = createToken(data, secret());
|
return json({ ...data, token });
|
||||||
|
|
||||||
const accountId = await getAccountId(website);
|
|
||||||
|
|
||||||
if (accountId) {
|
|
||||||
const whiteLabel = await getWhiteLabel(accountId);
|
|
||||||
|
|
||||||
if (whiteLabel) {
|
|
||||||
data.whiteLabel = whiteLabel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return json(data);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ shar
|
||||||
|
|
||||||
export async function POST(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
|
export async function POST(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().max(200),
|
|
||||||
slug: z.string().max(100),
|
slug: z.string().max(100),
|
||||||
parameters: anyObjectParam,
|
parameters: anyObjectParam,
|
||||||
});
|
});
|
||||||
|
|
@ -37,7 +36,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ sha
|
||||||
}
|
}
|
||||||
|
|
||||||
const { shareId } = await params;
|
const { shareId } = await params;
|
||||||
const { name, slug, parameters } = body;
|
const { slug, parameters } = body;
|
||||||
|
|
||||||
const share = await getShare(shareId);
|
const share = await getShare(shareId);
|
||||||
|
|
||||||
|
|
@ -50,7 +49,6 @@ export async function POST(request: Request, { params }: { params: Promise<{ sha
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await updateShare(shareId, {
|
const result = await updateShare(shareId, {
|
||||||
name,
|
|
||||||
slug,
|
slug,
|
||||||
parameters,
|
parameters,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ export async function POST(
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
) {
|
) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().max(200),
|
|
||||||
parameters: anyObjectParam.optional(),
|
parameters: anyObjectParam.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -55,8 +54,7 @@ export async function POST(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = await params;
|
const { websiteId } = await params;
|
||||||
const { name, parameters } = body;
|
const { parameters = {} } = body;
|
||||||
const shareParameters = parameters ?? {};
|
|
||||||
|
|
||||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
|
|
@ -68,9 +66,8 @@ export async function POST(
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
entityId: websiteId,
|
entityId: websiteId,
|
||||||
shareType: ENTITY_TYPE.website,
|
shareType: ENTITY_TYPE.website,
|
||||||
name,
|
|
||||||
slug,
|
slug,
|
||||||
parameters: shareParameters,
|
parameters,
|
||||||
});
|
});
|
||||||
|
|
||||||
return json(share);
|
return json(share);
|
||||||
|
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
'use client';
|
|
||||||
import { Loading } from '@umami/react-zen';
|
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
|
||||||
import { createContext, type ReactNode, useEffect } from 'react';
|
|
||||||
import { useShareTokenQuery } from '@/components/hooks';
|
|
||||||
import type { WhiteLabel } from '@/lib/types';
|
|
||||||
|
|
||||||
export interface ShareData {
|
|
||||||
shareId: string;
|
|
||||||
slug: string;
|
|
||||||
websiteId: string;
|
|
||||||
parameters: any;
|
|
||||||
token: string;
|
|
||||||
whiteLabel?: WhiteLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShareContext = createContext<ShareData>(null);
|
|
||||||
|
|
||||||
const ALL_SECTION_IDS = [
|
|
||||||
'overview',
|
|
||||||
'events',
|
|
||||||
'sessions',
|
|
||||||
'realtime',
|
|
||||||
'compare',
|
|
||||||
'breakdown',
|
|
||||||
'goals',
|
|
||||||
'funnels',
|
|
||||||
'journeys',
|
|
||||||
'retention',
|
|
||||||
'utm',
|
|
||||||
'revenue',
|
|
||||||
'attribution',
|
|
||||||
];
|
|
||||||
|
|
||||||
function getSharePath(pathname: string) {
|
|
||||||
const segments = pathname.split('/');
|
|
||||||
const firstSegment = segments[3];
|
|
||||||
|
|
||||||
// If first segment looks like a domain name, skip it
|
|
||||||
if (firstSegment?.includes('.')) {
|
|
||||||
return segments[4];
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstSegment;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShareProvider({ slug, children }: { slug: string; children: ReactNode }) {
|
|
||||||
const { share, isLoading, isFetching } = useShareTokenQuery(slug);
|
|
||||||
const router = useRouter();
|
|
||||||
const pathname = usePathname();
|
|
||||||
const path = getSharePath(pathname);
|
|
||||||
|
|
||||||
const allowedSections = share?.parameters
|
|
||||||
? ALL_SECTION_IDS.filter(id => share.parameters[id] !== false)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const shouldRedirect =
|
|
||||||
allowedSections.length === 1 &&
|
|
||||||
allowedSections[0] !== 'overview' &&
|
|
||||||
(path === undefined || path === '' || path === 'overview');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (shouldRedirect) {
|
|
||||||
router.replace(`/share/${slug}/${allowedSections[0]}`);
|
|
||||||
}
|
|
||||||
}, [shouldRedirect, slug, allowedSections, router]);
|
|
||||||
|
|
||||||
if (isFetching && isLoading) {
|
|
||||||
return <Loading placement="absolute" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!share || shouldRedirect) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ShareContext.Provider value={{ ...share, slug }}>{children}</ShareContext.Provider>;
|
|
||||||
}
|
|
||||||
12
src/app/share/[...shareId]/Footer.tsx
Normal file
12
src/app/share/[...shareId]/Footer.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Row, Text } from '@umami/react-zen';
|
||||||
|
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
||||||
|
<a href={HOMEPAGE_URL} target="_blank">
|
||||||
|
<Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`}
|
||||||
|
</a>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/app/share/[...shareId]/Header.tsx
Normal file
24
src/app/share/[...shareId]/Header.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
|
||||||
|
import { LanguageButton } from '@/components/input/LanguageButton';
|
||||||
|
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
||||||
|
import { Logo } from '@/components/svg';
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
return (
|
||||||
|
<Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
|
||||||
|
<a href="https://umami.is" target="_blank" rel="noopener">
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
<Icon>
|
||||||
|
<Logo />
|
||||||
|
</Icon>
|
||||||
|
<Text weight="bold">umami</Text>
|
||||||
|
</Row>
|
||||||
|
</a>
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
<ThemeButton />
|
||||||
|
<LanguageButton />
|
||||||
|
<PreferencesButton />
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,30 +1,23 @@
|
||||||
import { Button, Column, Icon, Row, Text, ThemeButton } from '@umami/react-zen';
|
'use client';
|
||||||
|
import { Column } from '@umami/react-zen';
|
||||||
import { SideMenu } from '@/components/common/SideMenu';
|
import { SideMenu } from '@/components/common/SideMenu';
|
||||||
import { useMessages, useNavigation, useShare } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { AlignEndHorizontal, Clock, Eye, PanelLeft, Sheet, Tag, User } from '@/components/icons';
|
import { AlignEndHorizontal, Clock, Eye, Sheet, Tag, User } from '@/components/icons';
|
||||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
import { Funnel, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg';
|
||||||
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
|
||||||
import { Funnel, Lightning, Logo, Magnet, Money, Network, Path, Target } from '@/components/svg';
|
|
||||||
|
|
||||||
export function ShareNav({
|
export function ShareNav({
|
||||||
collapsed,
|
shareId,
|
||||||
onCollapse,
|
parameters,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
}: {
|
}: {
|
||||||
collapsed?: boolean;
|
shareId: string;
|
||||||
onCollapse?: (collapsed: boolean) => void;
|
parameters: Record<string, boolean>;
|
||||||
onItemClick?: () => void;
|
onItemClick?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const share = useShare();
|
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { pathname } = useNavigation();
|
const { pathname } = useNavigation();
|
||||||
const { slug, parameters, whiteLabel } = share;
|
|
||||||
|
|
||||||
const logoUrl = whiteLabel?.url || 'https://umami.is';
|
const renderPath = (path: string) => `/share/${shareId}${path}`;
|
||||||
const logoName = whiteLabel?.name || 'umami';
|
|
||||||
const logoImage = whiteLabel?.image;
|
|
||||||
|
|
||||||
const renderPath = (path: string) => `/share/${slug}${path}`;
|
|
||||||
|
|
||||||
const allItems = [
|
const allItems = [
|
||||||
{
|
{
|
||||||
|
|
@ -137,70 +130,14 @@ export function ShareNav({
|
||||||
.flatMap(e => e.items)
|
.flatMap(e => e.items)
|
||||||
.find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
|
.find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
|
||||||
|
|
||||||
const isMobile = !!onItemClick;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column padding="3" position="sticky" top="0" gap>
|
||||||
position={isMobile ? undefined : 'fixed'}
|
<SideMenu
|
||||||
padding="3"
|
items={items}
|
||||||
width={isMobile ? '100%' : collapsed ? '60px' : '240px'}
|
selectedKey={selectedKey}
|
||||||
maxHeight="100dvh"
|
allowMinimize={false}
|
||||||
height="100dvh"
|
onItemClick={onItemClick}
|
||||||
border={isMobile ? undefined : 'right'}
|
/>
|
||||||
borderColor={isMobile ? undefined : '4'}
|
|
||||||
>
|
|
||||||
<Row as="header" gap alignItems="center" justifyContent="space-between">
|
|
||||||
{!collapsed && (
|
|
||||||
<a href={logoUrl} target="_blank" rel="noopener" style={{ marginLeft: 12 }}>
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
{logoImage ? (
|
|
||||||
<img src={logoImage} alt={logoName} style={{ height: 24 }} />
|
|
||||||
) : (
|
|
||||||
<Icon>
|
|
||||||
<Logo />
|
|
||||||
</Icon>
|
|
||||||
)}
|
|
||||||
<Text weight="bold">{logoName}</Text>
|
|
||||||
</Row>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{!onItemClick && (
|
|
||||||
<Button variant="quiet" onPress={() => onCollapse?.(!collapsed)}>
|
|
||||||
<Icon color="muted">
|
|
||||||
<PanelLeft />
|
|
||||||
</Icon>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
{!collapsed && (
|
|
||||||
<Column flexGrow={1} overflowY="auto">
|
|
||||||
<SideMenu
|
|
||||||
items={items}
|
|
||||||
selectedKey={selectedKey}
|
|
||||||
allowMinimize={false}
|
|
||||||
onItemClick={onItemClick}
|
|
||||||
/>
|
|
||||||
</Column>
|
|
||||||
)}
|
|
||||||
<Column
|
|
||||||
flexGrow={collapsed ? 1 : undefined}
|
|
||||||
justifyContent="flex-end"
|
|
||||||
alignItems={collapsed ? 'center' : undefined}
|
|
||||||
>
|
|
||||||
{collapsed ? (
|
|
||||||
<Column gap="2" alignItems="center">
|
|
||||||
<ThemeButton />
|
|
||||||
<LanguageButton />
|
|
||||||
<PreferencesButton />
|
|
||||||
</Column>
|
|
||||||
) : (
|
|
||||||
<Row>
|
|
||||||
<ThemeButton />
|
|
||||||
<LanguageButton />
|
|
||||||
<PreferencesButton />
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
</Column>
|
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
|
import { Column, Grid, useTheme } from '@umami/react-zen';
|
||||||
import { usePathname } from 'next/navigation';
|
import { useEffect } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage';
|
import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage';
|
||||||
import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage';
|
import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage';
|
||||||
import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage';
|
import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage';
|
||||||
|
|
@ -18,8 +17,9 @@ import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
|
||||||
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
|
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
|
||||||
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
import { useShare } from '@/components/hooks';
|
import { useShareTokenQuery } from '@/components/hooks';
|
||||||
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
import { Footer } from './Footer';
|
||||||
|
import { Header } from './Header';
|
||||||
import { ShareNav } from './ShareNav';
|
import { ShareNav } from './ShareNav';
|
||||||
|
|
||||||
const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>> = {
|
const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>> = {
|
||||||
|
|
@ -39,25 +39,9 @@ const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>
|
||||||
attribution: AttributionPage,
|
attribution: AttributionPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSharePath(pathname: string) {
|
export function SharePage({ shareId, path = '' }: { shareId: string; path?: string }) {
|
||||||
const segments = pathname.split('/');
|
const { shareToken, isLoading } = useShareTokenQuery(shareId);
|
||||||
const firstSegment = segments[3];
|
|
||||||
|
|
||||||
// If first segment looks like a domain name, skip it
|
|
||||||
if (firstSegment?.includes('.')) {
|
|
||||||
return segments[4];
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstSegment;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SharePage() {
|
|
||||||
const [navCollapsed, setNavCollapsed] = useState(false);
|
|
||||||
const share = useShare();
|
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
const pathname = usePathname();
|
|
||||||
const path = getSharePath(pathname);
|
|
||||||
const { websiteId, parameters = {} } = share;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const url = new URL(window?.location?.href);
|
const url = new URL(window?.location?.href);
|
||||||
|
|
@ -68,6 +52,12 @@ export function SharePage() {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading || !shareToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { websiteId, parameters = {} } = shareToken;
|
||||||
|
|
||||||
// Check if the requested path is allowed
|
// Check if the requested path is allowed
|
||||||
const pageKey = path || '';
|
const pageKey = path || '';
|
||||||
const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false;
|
const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false;
|
||||||
|
|
@ -79,25 +69,29 @@ export function SharePage() {
|
||||||
const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage;
|
const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid columns={{ xs: '1fr', lg: `${navCollapsed ? '60px' : '240px'} 1fr` }} width="100%">
|
<Column backgroundColor="2">
|
||||||
<Row display={{ xs: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
<Header />
|
||||||
<MobileMenuButton>
|
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
||||||
{({ close }) => {
|
<Column
|
||||||
return <ShareNav onItemClick={close} />;
|
display={{ xs: 'none', lg: 'flex' }}
|
||||||
}}
|
width="240px"
|
||||||
</MobileMenuButton>
|
height="100%"
|
||||||
</Row>
|
border="right"
|
||||||
<Column display={{ xs: 'none', lg: 'flex' }} marginRight="2">
|
backgroundColor
|
||||||
<ShareNav collapsed={navCollapsed} onCollapse={setNavCollapsed} />
|
marginRight="2"
|
||||||
</Column>
|
>
|
||||||
<PageBody gap>
|
<ShareNav shareId={shareId} parameters={parameters} />
|
||||||
<WebsiteProvider websiteId={websiteId}>
|
</Column>
|
||||||
<Column>
|
<PageBody gap>
|
||||||
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
<WebsiteHeader showActions={false} />
|
<WebsiteHeader showActions={false} />
|
||||||
<PageComponent websiteId={websiteId} />
|
<Column>
|
||||||
</Column>
|
<PageComponent websiteId={websiteId} />
|
||||||
</WebsiteProvider>
|
</Column>
|
||||||
</PageBody>
|
</WebsiteProvider>
|
||||||
</Grid>
|
</PageBody>
|
||||||
|
</Grid>
|
||||||
|
<Footer />
|
||||||
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
8
src/app/share/[...shareId]/page.tsx
Normal file
8
src/app/share/[...shareId]/page.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { SharePage } from './SharePage';
|
||||||
|
|
||||||
|
export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
|
||||||
|
const { shareId } = await params;
|
||||||
|
const [slug, ...path] = shareId;
|
||||||
|
|
||||||
|
return <SharePage shareId={slug} path={path.join('/')} />;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { SharePage } from './SharePage';
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
return <SharePage />;
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { ShareProvider } from '@/app/share/ShareProvider';
|
|
||||||
|
|
||||||
export default async function ({
|
|
||||||
params,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
params: Promise<{ slug: string }>;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
const { slug } = await params;
|
|
||||||
|
|
||||||
return <ShareProvider slug={slug}>{children}</ShareProvider>;
|
|
||||||
}
|
|
||||||
|
|
@ -31,7 +31,6 @@ export function PageBody({
|
||||||
<Column
|
<Column
|
||||||
{...props}
|
{...props}
|
||||||
width="100%"
|
width="100%"
|
||||||
minHeight="100vh"
|
|
||||||
paddingBottom="6"
|
paddingBottom="6"
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
paddingX={{ xs: '3', md: '6' }}
|
paddingX={{ xs: '3', md: '6' }}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
NavMenuItem,
|
NavMenuItem,
|
||||||
type NavMenuProps,
|
type NavMenuProps,
|
||||||
Row,
|
Row,
|
||||||
Text,
|
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
|
@ -43,11 +42,9 @@ export function SideMenu({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={id} href={path}>
|
<Link key={id} href={path}>
|
||||||
<Row padding hoverBackgroundColor="3">
|
<NavMenuItem isSelected={isSelected}>
|
||||||
<IconLabel icon={icon}>
|
<IconLabel icon={icon}>{label}</IconLabel>
|
||||||
<Text weight={isSelected ? 'bold' : undefined}>{label}</Text>
|
</NavMenuItem>
|
||||||
</IconLabel>
|
|
||||||
</Row>
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { useContext } from 'react';
|
|
||||||
import { ShareContext } from '@/app/share/ShareProvider';
|
|
||||||
|
|
||||||
export function useShare() {
|
|
||||||
return useContext(ShareContext);
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
// Context hooks
|
// Context hooks
|
||||||
export * from './context/useLink';
|
export * from './context/useLink';
|
||||||
export * from './context/usePixel';
|
export * from './context/usePixel';
|
||||||
export * from './context/useShare';
|
|
||||||
export * from './context/useTeam';
|
export * from './context/useTeam';
|
||||||
export * from './context/useUser';
|
export * from './context/useUser';
|
||||||
export * from './context/useWebsite';
|
export * from './context/useWebsite';
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,25 @@
|
||||||
import { setShare, useApp } from '@/store/app';
|
import { setShareToken, useApp } from '@/store/app';
|
||||||
import { useApi } from '../useApi';
|
import { useApi } from '../useApi';
|
||||||
|
|
||||||
const selector = state => state.share;
|
const selector = (state: { shareToken: string }) => state.shareToken;
|
||||||
|
|
||||||
export function useShareTokenQuery(slug: string) {
|
export function useShareTokenQuery(slug: string): {
|
||||||
const share = useApp(selector);
|
shareToken: any;
|
||||||
|
isLoading?: boolean;
|
||||||
|
error?: Error;
|
||||||
|
} {
|
||||||
|
const shareToken = useApp(selector);
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const query = useQuery({
|
const { isLoading, error } = useQuery({
|
||||||
queryKey: ['share', slug],
|
queryKey: ['share', slug],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const data = await get(`/share/${slug}`);
|
const data = await get(`/share/${slug}`);
|
||||||
|
|
||||||
setShare(data);
|
setShareToken(data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { share, ...query };
|
return { shareToken, isLoading, error };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function MobileMenuButton(props: DialogProps) {
|
||||||
</Icon>
|
</Icon>
|
||||||
</Button>
|
</Button>
|
||||||
<Modal placement="left" offset="80px">
|
<Modal placement="left" offset="80px">
|
||||||
<Dialog variant="sheet" {...props} style={{ width: 'auto' }} />
|
<Dialog variant="sheet" {...props} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,9 @@ export function WebsiteDateFilter({
|
||||||
const showCompare = allowCompare && !isAllTime;
|
const showCompare = allowCompare && !isAllTime;
|
||||||
|
|
||||||
const websiteDateRange = useDateRangeQuery(websiteId);
|
const websiteDateRange = useDateRangeQuery(websiteId);
|
||||||
const { startDate, endDate } = websiteDateRange;
|
|
||||||
const hasData = startDate && endDate;
|
|
||||||
|
|
||||||
const handleChange = (date: string) => {
|
const handleChange = (date: string) => {
|
||||||
if (date === 'all' && hasData) {
|
if (date === 'all') {
|
||||||
router.push(
|
router.push(
|
||||||
updateParams({
|
updateParams({
|
||||||
date: `${getDateRangeValue(websiteDateRange.startDate, websiteDateRange.endDate)}:all`,
|
date: `${getDateRangeValue(websiteDateRange.startDate, websiteDateRange.endDate)}:all`,
|
||||||
|
|
@ -80,7 +78,7 @@ export function WebsiteDateFilter({
|
||||||
<DateFilter
|
<DateFilter
|
||||||
value={dateValue}
|
value={dateValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
showAllTime={hasData && showAllTime}
|
showAllTime={showAllTime}
|
||||||
renderDate={+offset !== 0}
|
renderDate={+offset !== 0}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export function WebsiteSelect({
|
||||||
const { user } = useLoginQuery();
|
const { user } = useLoginQuery();
|
||||||
const { data, isLoading } = useUserWebsitesQuery(
|
const { data, isLoading } = useUserWebsitesQuery(
|
||||||
{ userId: user?.id, teamId },
|
{ userId: user?.id, teamId },
|
||||||
{ search, pageSize: 20, includeTeams },
|
{ search, pageSize: 10, includeTeams },
|
||||||
);
|
);
|
||||||
const listItems: { id: string; name: string }[] = data?.data || [];
|
const listItems: { id: string; name: string }[] = data?.data || [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,5 @@ test('getIpAddress: Standard header', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getIpAddress: No header', () => {
|
test('getIpAddress: No header', () => {
|
||||||
expect(getIpAddress(new Headers())).toEqual(undefined);
|
expect(getIpAddress(new Headers())).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export function renderDateLabels(unit: string, locale: string) {
|
||||||
|
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'minute':
|
case 'minute':
|
||||||
|
return formatDate(d, 'h:mm', locale);
|
||||||
case 'hour':
|
case 'hour':
|
||||||
return formatDate(d, 'p', locale);
|
return formatDate(d, 'p', locale);
|
||||||
case 'day':
|
case 'day':
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}
|
||||||
|
|
||||||
if (name === 'referrer') {
|
if (name === 'referrer') {
|
||||||
arr.push(
|
arr.push(
|
||||||
`and (website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '') or website_event.referrer_domain is null)`,
|
`and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,9 +141,3 @@ export interface ApiError extends Error {
|
||||||
code?: string;
|
code?: string;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WhiteLabel {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
image: string;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,7 @@ import type { Auth } from '@/lib/types';
|
||||||
import { getTeamUser } from '@/queries/prisma';
|
import { getTeamUser } from '@/queries/prisma';
|
||||||
|
|
||||||
export async function canViewEntity({ user }: Auth, entityId: string) {
|
export async function canViewEntity({ user }: Auth, entityId: string) {
|
||||||
if (!user) {
|
if (user?.isAdmin) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,10 +25,6 @@ export async function canViewEntity({ user }: Auth, entityId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateEntity({ user }: Auth, entityId: string) {
|
export async function canUpdateEntity({ user }: Auth, entityId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -53,10 +45,6 @@ export async function canUpdateEntity({ user }: Auth, entityId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteEntity({ user }: Auth, entityId: string) {
|
export async function canDeleteEntity({ user }: Auth, entityId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@ import type { Auth } from '@/lib/types';
|
||||||
import { getLink, getTeamUser } from '@/queries/prisma';
|
import { getLink, getTeamUser } from '@/queries/prisma';
|
||||||
|
|
||||||
export async function canViewLink({ user }: Auth, linkId: string) {
|
export async function canViewLink({ user }: Auth, linkId: string) {
|
||||||
if (!user) {
|
if (user?.isAdmin) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,10 +24,6 @@ export async function canViewLink({ user }: Auth, linkId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateLink({ user }: Auth, linkId: string) {
|
export async function canUpdateLink({ user }: Auth, linkId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -52,10 +44,6 @@ export async function canUpdateLink({ user }: Auth, linkId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteLink({ user }: Auth, linkId: string) {
|
export async function canDeleteLink({ user }: Auth, linkId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@ import type { Auth } from '@/lib/types';
|
||||||
import { getPixel, getTeamUser } from '@/queries/prisma';
|
import { getPixel, getTeamUser } from '@/queries/prisma';
|
||||||
|
|
||||||
export async function canViewPixel({ user }: Auth, pixelId: string) {
|
export async function canViewPixel({ user }: Auth, pixelId: string) {
|
||||||
if (!user) {
|
if (user?.isAdmin) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,10 +24,6 @@ export async function canViewPixel({ user }: Auth, pixelId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdatePixel({ user }: Auth, pixelId: string) {
|
export async function canUpdatePixel({ user }: Auth, pixelId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -52,10 +44,6 @@ export async function canUpdatePixel({ user }: Auth, pixelId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeletePixel({ user }: Auth, pixelId: string) {
|
export async function canDeletePixel({ user }: Auth, pixelId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ import type { Auth } from '@/lib/types';
|
||||||
import { canViewWebsite } from './website';
|
import { canViewWebsite } from './website';
|
||||||
|
|
||||||
export async function canViewReport(auth: Auth, report: Report) {
|
export async function canViewReport(auth: Auth, report: Report) {
|
||||||
if (auth.user?.isAdmin) {
|
if (auth.user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.user?.id === report.userId) {
|
if (auth.user.id === report.userId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15,10 +15,6 @@ export async function canViewReport(auth: Auth, report: Report) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateReport({ user }: Auth, report: Report) {
|
export async function canUpdateReport({ user }: Auth, report: Report) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,6 @@ import type { Auth } from '@/lib/types';
|
||||||
import { getTeamUser } from '@/queries/prisma';
|
import { getTeamUser } from '@/queries/prisma';
|
||||||
|
|
||||||
export async function canViewTeam({ user }: Auth, teamId: string) {
|
export async function canViewTeam({ user }: Auth, teamId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -16,10 +12,6 @@ export async function canViewTeam({ user }: Auth, teamId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canCreateTeam({ user }: Auth) {
|
export async function canCreateTeam({ user }: Auth) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -28,10 +20,6 @@ export async function canCreateTeam({ user }: Auth) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -42,10 +30,6 @@ export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -56,10 +40,6 @@ export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUserId: string) {
|
export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUserId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -74,10 +54,6 @@ export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUs
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canCreateTeamWebsite({ user }: Auth, teamId: string) {
|
export async function canCreateTeamWebsite({ user }: Auth, teamId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -88,5 +64,5 @@ export async function canCreateTeamWebsite({ user }: Auth, teamId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canViewAllTeams({ user }: Auth) {
|
export async function canViewAllTeams({ user }: Auth) {
|
||||||
return user?.isAdmin ?? false;
|
return user.isAdmin;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
import type { Auth } from '@/lib/types';
|
import type { Auth } from '@/lib/types';
|
||||||
|
|
||||||
export async function canCreateUser({ user }: Auth) {
|
export async function canCreateUser({ user }: Auth) {
|
||||||
return user?.isAdmin ?? false;
|
return user.isAdmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canViewUser({ user }: Auth, viewedUserId: string) {
|
export async function canViewUser({ user }: Auth, viewedUserId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -17,14 +13,10 @@ export async function canViewUser({ user }: Auth, viewedUserId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canViewUsers({ user }: Auth) {
|
export async function canViewUsers({ user }: Auth) {
|
||||||
return user?.isAdmin ?? false;
|
return user.isAdmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateUser({ user }: Auth, viewedUserId: string) {
|
export async function canUpdateUser({ user }: Auth, viewedUserId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -33,5 +25,5 @@ export async function canUpdateUser({ user }: Auth, viewedUserId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteUser({ user }: Auth) {
|
export async function canDeleteUser({ user }: Auth) {
|
||||||
return user?.isAdmin ?? false;
|
return user.isAdmin;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
|
||||||
|
|
||||||
const entity = await getEntity(websiteId);
|
const entity = await getEntity(websiteId);
|
||||||
|
|
||||||
if (!entity || !user) {
|
if (!entity) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,14 +33,10 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canViewAllWebsites({ user }: Auth) {
|
export async function canViewAllWebsites({ user }: Auth) {
|
||||||
return user?.isAdmin ?? false;
|
return user.isAdmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canCreateWebsite({ user }: Auth) {
|
export async function canCreateWebsite({ user }: Auth) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -49,10 +45,6 @@ export async function canCreateWebsite({ user }: Auth) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
|
export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -77,10 +69,6 @@ export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
|
export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -105,10 +93,6 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string, userId: string) {
|
export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string, userId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const website = await getWebsite(websiteId);
|
const website = await getWebsite(websiteId);
|
||||||
|
|
||||||
if (!website) {
|
if (!website) {
|
||||||
|
|
@ -125,10 +109,6 @@ export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canTransferWebsiteToTeam({ user }: Auth, websiteId: string, teamId: string) {
|
export async function canTransferWebsiteToTeam({ user }: Auth, websiteId: string, teamId: string) {
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const website = await getWebsite(websiteId);
|
const website = await getWebsite(websiteId);
|
||||||
|
|
||||||
if (!website) {
|
if (!website) {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query
|
||||||
let excludeDomain = '';
|
let excludeDomain = '';
|
||||||
|
|
||||||
if (column === 'referrer_domain') {
|
if (column === 'referrer_domain') {
|
||||||
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
excludeDomain = `and website_event.referrer_domain != website_event.hostname
|
||||||
and website_event.referrer_domain != ''`;
|
and website_event.referrer_domain != ''`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export async function getWeeklyTraffic(...args: [websiteId: string, filters: Que
|
||||||
}
|
}
|
||||||
|
|
||||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||||
const { timezone = 'utc' } = filters;
|
const timezone = 'utc';
|
||||||
const { rawQuery, getDateWeeklySQL, parseFilters } = prisma;
|
const { rawQuery, getDateWeeklySQL, parseFilters } = prisma;
|
||||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||||
...filters,
|
...filters,
|
||||||
|
|
@ -33,7 +33,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||||
${filterQuery}
|
${filterQuery}
|
||||||
group by time
|
group by time
|
||||||
order by 1
|
order by 2
|
||||||
`,
|
`,
|
||||||
queryParams,
|
queryParams,
|
||||||
FUNCTION_NAME,
|
FUNCTION_NAME,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ async function relationalQuery(
|
||||||
let excludeDomain = '';
|
let excludeDomain = '';
|
||||||
|
|
||||||
if (column === 'referrer_domain') {
|
if (column === 'referrer_domain') {
|
||||||
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
excludeDomain = `and website_event.referrer_domain != website_event.hostname
|
||||||
and website_event.referrer_domain != ''`;
|
and website_event.referrer_domain != ''`;
|
||||||
if (type === 'domain') {
|
if (type === 'domain') {
|
||||||
column = toPostgresGroupedReferrer(GROUPED_DOMAINS);
|
column = toPostgresGroupedReferrer(GROUPED_DOMAINS);
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ async function relationalQuery(
|
||||||
let excludeDomain = '';
|
let excludeDomain = '';
|
||||||
|
|
||||||
if (column === 'referrer_domain') {
|
if (column === 'referrer_domain') {
|
||||||
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
excludeDomain = `and website_event.referrer_domain != website_event.hostname
|
||||||
and website_event.referrer_domain != ''`;
|
and website_event.referrer_domain != ''`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ async function relationalQuery(
|
||||||
${
|
${
|
||||||
currency
|
currency
|
||||||
? ''
|
? ''
|
||||||
: `and we.referrer_domain != regexp_replace(we.hostname, '^www.', '')
|
: `and we.referrer_domain != hostname
|
||||||
and we.referrer_domain != ''`
|
and we.referrer_domain != ''`
|
||||||
}
|
}
|
||||||
group by 1
|
group by 1
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const initialState = {
|
||||||
theme: getItem(THEME_CONFIG) || DEFAULT_THEME,
|
theme: getItem(THEME_CONFIG) || DEFAULT_THEME,
|
||||||
timezone: getItem(TIMEZONE_CONFIG) || getTimezone(),
|
timezone: getItem(TIMEZONE_CONFIG) || getTimezone(),
|
||||||
dateRangeValue: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
|
dateRangeValue: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
|
||||||
share: null,
|
shareToken: null,
|
||||||
user: null,
|
user: null,
|
||||||
config: null,
|
config: null,
|
||||||
};
|
};
|
||||||
|
|
@ -31,8 +31,8 @@ export function setLocale(locale: string) {
|
||||||
store.setState({ locale });
|
store.setState({ locale });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setShare(share: object) {
|
export function setShareToken(shareToken: string) {
|
||||||
store.setState({ share });
|
store.setState({ shareToken });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setUser(user: object) {
|
export function setUser(user: object) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue