mirror of
https://github.com/umami-software/umami.git
synced 2026-02-21 13:05:36 +01:00
Compare commits
No commits in common. "c5aa8be15cbee74f3264bcb09437f6cde12b857b" and "67cdfdfb7e7ee3fea9a2699155df8dfbd826f724" have entirely different histories.
c5aa8be15c
...
67cdfdfb7e
30 changed files with 285 additions and 503 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",
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ export function SharesTable(props: DataTableProps) {
|
||||||
const { cloudMode } = useConfig();
|
const { cloudMode } = useConfig();
|
||||||
|
|
||||||
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)}>
|
|
||||||
{({ name }: any) => name}
|
|
||||||
</DataColumn>
|
|
||||||
<DataColumn id="slug" label={formatMessage(labels.shareUrl)}>
|
<DataColumn id="slug" label={formatMessage(labels.shareUrl)}>
|
||||||
{({ slug }: any) => {
|
{({ slug }: any) => {
|
||||||
const url = getUrl(slug);
|
const url = getUrl(slug);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
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,23 +0,0 @@
|
||||||
import { Row, Text } from '@umami/react-zen';
|
|
||||||
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
|
|
||||||
import type { WhiteLabel } from '@/lib/types';
|
|
||||||
|
|
||||||
export function ShareFooter({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
|
|
||||||
if (whiteLabel) {
|
|
||||||
return (
|
|
||||||
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
|
||||||
<a href={whiteLabel.url} target="_blank">
|
|
||||||
<Text weight="bold">{whiteLabel.name}</Text>
|
|
||||||
</a>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
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';
|
|
||||||
import type { WhiteLabel } from '@/lib/types';
|
|
||||||
|
|
||||||
export function ShareHeader({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
|
|
||||||
const logoUrl = whiteLabel?.url || 'https://umami.is';
|
|
||||||
const logoName = whiteLabel?.name || 'umami';
|
|
||||||
const logoImage = whiteLabel?.image;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
|
|
||||||
<a href={logoUrl} target="_blank" rel="noopener">
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
{logoImage ? (
|
|
||||||
<img src={logoImage} alt={logoName} style={{ height: 24 }} />
|
|
||||||
) : (
|
|
||||||
<Icon>
|
|
||||||
<Logo />
|
|
||||||
</Icon>
|
|
||||||
)}
|
|
||||||
<Text weight="bold">{logoName}</Text>
|
|
||||||
</Row>
|
|
||||||
</a>
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
<ThemeButton />
|
|
||||||
<LanguageButton />
|
|
||||||
<PreferencesButton />
|
|
||||||
</Row>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 { useRouter } from 'next/navigation';
|
import { useEffect } from 'react';
|
||||||
import { useEffect, useMemo } 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';
|
||||||
|
|
@ -19,9 +18,8 @@ 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 { useShareTokenQuery } from '@/components/hooks';
|
import { useShareTokenQuery } from '@/components/hooks';
|
||||||
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
import { Footer } from './Footer';
|
||||||
import { ShareFooter } from './ShareFooter';
|
import { Header } from './Header';
|
||||||
import { ShareHeader } from './ShareHeader';
|
|
||||||
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 }>> = {
|
||||||
|
|
@ -41,34 +39,9 @@ const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>
|
||||||
attribution: AttributionPage,
|
attribution: AttributionPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
// All section IDs that can be enabled/disabled via parameters
|
|
||||||
const ALL_SECTION_IDS = [
|
|
||||||
'overview',
|
|
||||||
'events',
|
|
||||||
'sessions',
|
|
||||||
'realtime',
|
|
||||||
'compare',
|
|
||||||
'breakdown',
|
|
||||||
'goals',
|
|
||||||
'funnels',
|
|
||||||
'journeys',
|
|
||||||
'retention',
|
|
||||||
'utm',
|
|
||||||
'revenue',
|
|
||||||
'attribution',
|
|
||||||
];
|
|
||||||
|
|
||||||
export function SharePage({ shareId, path = '' }: { shareId: string; path?: string }) {
|
export function SharePage({ shareId, path = '' }: { shareId: string; path?: string }) {
|
||||||
const { shareToken, isLoading } = useShareTokenQuery(shareId);
|
const { shareToken, isLoading } = useShareTokenQuery(shareId);
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Calculate allowed sections
|
|
||||||
const allowedSections = useMemo(() => {
|
|
||||||
if (!shareToken?.parameters) return [];
|
|
||||||
const params = shareToken.parameters;
|
|
||||||
return ALL_SECTION_IDS.filter(id => params[id] !== false);
|
|
||||||
}, [shareToken?.parameters]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const url = new URL(window?.location?.href);
|
const url = new URL(window?.location?.href);
|
||||||
|
|
@ -79,31 +52,11 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Redirect to the only allowed section if there's just one and we're on the base path
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
allowedSections.length === 1 &&
|
|
||||||
allowedSections[0] !== 'overview' &&
|
|
||||||
(path === '' || path === 'overview')
|
|
||||||
) {
|
|
||||||
router.replace(`/share/${shareId}/${allowedSections[0]}`);
|
|
||||||
}
|
|
||||||
}, [allowedSections, shareId, path, router]);
|
|
||||||
|
|
||||||
if (isLoading || !shareToken) {
|
if (isLoading || !shareToken) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId, parameters = {}, whiteLabel } = shareToken;
|
const { websiteId, parameters = {} } = shareToken;
|
||||||
|
|
||||||
// Redirect to only allowed section - return null while redirecting
|
|
||||||
if (
|
|
||||||
allowedSections.length === 1 &&
|
|
||||||
allowedSections[0] !== 'overview' &&
|
|
||||||
(path === '' || path === 'overview')
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the requested path is allowed
|
// Check if the requested path is allowed
|
||||||
const pageKey = path || '';
|
const pageKey = path || '';
|
||||||
|
|
@ -117,16 +70,8 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column backgroundColor="2">
|
<Column backgroundColor="2">
|
||||||
|
<Header />
|
||||||
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
||||||
<Row display={{ xs: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
|
||||||
<Grid columns="auto 1fr" flexGrow={1} backgroundColor="3" borderRadius>
|
|
||||||
<MobileMenuButton>
|
|
||||||
{({ close }) => {
|
|
||||||
return <ShareNav shareId={shareId} parameters={parameters} onItemClick={close} />;
|
|
||||||
}}
|
|
||||||
</MobileMenuButton>
|
|
||||||
</Grid>
|
|
||||||
</Row>
|
|
||||||
<Column
|
<Column
|
||||||
display={{ xs: 'none', lg: 'flex' }}
|
display={{ xs: 'none', lg: 'flex' }}
|
||||||
width="240px"
|
width="240px"
|
||||||
|
|
@ -135,21 +80,18 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri
|
||||||
backgroundColor
|
backgroundColor
|
||||||
marginRight="2"
|
marginRight="2"
|
||||||
>
|
>
|
||||||
<Column display={{ xs: 'none', lg: 'flex' }}>
|
<ShareNav shareId={shareId} parameters={parameters} />
|
||||||
<ShareNav shareId={shareId} parameters={parameters} />
|
|
||||||
</Column>
|
|
||||||
</Column>
|
</Column>
|
||||||
<PageBody gap>
|
<PageBody gap>
|
||||||
<WebsiteProvider websiteId={websiteId}>
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
<ShareHeader whiteLabel={whiteLabel} />
|
<WebsiteHeader showActions={false} />
|
||||||
<Column>
|
<Column>
|
||||||
<WebsiteHeader showActions={false} />
|
|
||||||
<PageComponent websiteId={websiteId} />
|
<PageComponent websiteId={websiteId} />
|
||||||
</Column>
|
</Column>
|
||||||
<ShareFooter whiteLabel={whiteLabel} />
|
|
||||||
</WebsiteProvider>
|
</WebsiteProvider>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Footer />
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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' }}
|
||||||
|
|
|
||||||
|
|
@ -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 || [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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':
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue