mirror of
https://github.com/umami-software/umami.git
synced 2026-02-24 06:25:43 +01:00
Compare commits
No commits in common. "d47ee6e8e8e217c4862367edaadc62cfbe0f8827" and "3a498333a688ffd16a505b567e2ec4e8fc6e14d8" have entirely different histories.
d47ee6e8e8
...
3a498333a6
21 changed files with 142 additions and 738 deletions
|
|
@ -1,9 +1,11 @@
|
||||||
import { Icon, Row, Text } from '@umami/react-zen';
|
import { Icon, Row, Text } from '@umami/react-zen';
|
||||||
|
import { WebsiteShareForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteShareForm';
|
||||||
import { Favicon } from '@/components/common/Favicon';
|
import { Favicon } from '@/components/common/Favicon';
|
||||||
import { LinkButton } from '@/components/common/LinkButton';
|
import { LinkButton } from '@/components/common/LinkButton';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
|
import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
|
||||||
import { Edit } from '@/components/icons';
|
import { Edit, Share } from '@/components/icons';
|
||||||
|
import { DialogButton } from '@/components/input/DialogButton';
|
||||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||||
|
|
||||||
export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
||||||
|
|
@ -27,14 +29,29 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
||||||
<ActiveUsers websiteId={website.id} />
|
<ActiveUsers websiteId={website.id} />
|
||||||
|
|
||||||
{showActions && (
|
{showActions && (
|
||||||
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
<Row alignItems="center" gap>
|
||||||
<Icon>
|
<ShareButton websiteId={website.id} shareId={website.shareId} />
|
||||||
<Edit />
|
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
||||||
</Icon>
|
<Icon>
|
||||||
<Text>{formatMessage(labels.edit)}</Text>
|
<Edit />
|
||||||
</LinkButton>
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.edit)}</Text>
|
||||||
|
</LinkButton>
|
||||||
|
</Row>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ShareButton = ({ websiteId, shareId }) => {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogButton icon={<Share />} label={formatMessage(labels.share)} width="800px">
|
||||||
|
{({ close }) => {
|
||||||
|
return <WebsiteShareForm websiteId={websiteId} shareId={shareId} onClose={close} />;
|
||||||
|
}}
|
||||||
|
</DialogButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
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 - all enabled by default
|
|
||||||
const defaultValues: Record<string, boolean> = {};
|
|
||||||
SHARE_NAV_ITEMS.forEach(section => {
|
|
||||||
section.items.forEach(item => {
|
|
||||||
defaultValues[item.id] = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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] ?? true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
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>
|
|
||||||
<Text size="1">{formatMessage((labels as any)[item.label])}</Text>
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
|
||||||
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
|
|
||||||
import { Trash } from '@/components/icons';
|
|
||||||
import { DialogButton } from '@/components/input/DialogButton';
|
|
||||||
import { messages } from '@/components/messages';
|
|
||||||
|
|
||||||
export function ShareDeleteButton({
|
|
||||||
shareId,
|
|
||||||
slug,
|
|
||||||
onSave,
|
|
||||||
}: {
|
|
||||||
shareId: string;
|
|
||||||
slug: string;
|
|
||||||
onSave?: () => void;
|
|
||||||
}) {
|
|
||||||
const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
|
|
||||||
const { mutateAsync, isPending, error } = useDeleteQuery(`/share/id/${shareId}`);
|
|
||||||
const { touch } = useModified();
|
|
||||||
|
|
||||||
const handleConfirm = async (close: () => void) => {
|
|
||||||
await mutateAsync(null, {
|
|
||||||
onSuccess: () => {
|
|
||||||
touch('shares');
|
|
||||||
onSave?.();
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogButton
|
|
||||||
icon={<Trash />}
|
|
||||||
title={formatMessage(labels.confirm)}
|
|
||||||
variant="quiet"
|
|
||||||
width="400px"
|
|
||||||
>
|
|
||||||
{({ close }) => (
|
|
||||||
<ConfirmationForm
|
|
||||||
message={
|
|
||||||
<FormattedMessage
|
|
||||||
{...messages.confirmRemove}
|
|
||||||
values={{
|
|
||||||
target: <b>{slug}</b>,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isLoading={isPending}
|
|
||||||
error={getErrorMessage(error)}
|
|
||||||
onConfirm={handleConfirm.bind(null, close)}
|
|
||||||
onClose={close}
|
|
||||||
buttonLabel={formatMessage(labels.delete)}
|
|
||||||
buttonVariant="danger"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DialogButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { useMessages } from '@/components/hooks';
|
|
||||||
import { Edit } from '@/components/icons';
|
|
||||||
import { DialogButton } from '@/components/input/DialogButton';
|
|
||||||
import { ShareEditForm } from './ShareEditForm';
|
|
||||||
|
|
||||||
export function ShareEditButton({ shareId }: { shareId: string }) {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogButton icon={<Edit />} title={formatMessage(labels.share)} variant="quiet" width="600px">
|
|
||||||
{({ close }) => {
|
|
||||||
return <ShareEditForm shareId={shareId} onClose={close} />;
|
|
||||||
}}
|
|
||||||
</DialogButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Column,
|
|
||||||
Form,
|
|
||||||
FormField,
|
|
||||||
FormSubmitButton,
|
|
||||||
Label,
|
|
||||||
Loading,
|
|
||||||
Row,
|
|
||||||
Text,
|
|
||||||
TextField,
|
|
||||||
} from '@umami/react-zen';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
|
|
||||||
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
|
||||||
import { SHARE_NAV_ITEMS } from './constants';
|
|
||||||
|
|
||||||
export function ShareEditForm({
|
|
||||||
shareId,
|
|
||||||
onSave,
|
|
||||||
onClose,
|
|
||||||
}: {
|
|
||||||
shareId: string;
|
|
||||||
onSave?: () => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
}) {
|
|
||||||
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
|
|
||||||
const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(`/share/id/${shareId}`);
|
|
||||||
const { cloudMode } = useConfig();
|
|
||||||
const { get } = useApi();
|
|
||||||
const { modified } = useModified('shares');
|
|
||||||
const [share, setShare] = useState<any>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
const getUrl = (slug: string) => {
|
|
||||||
if (cloudMode) {
|
|
||||||
return `${process.env.cloudUrl}/share/${slug}`;
|
|
||||||
}
|
|
||||||
return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadShare = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const data = await get(`/share/id/${shareId}`);
|
|
||||||
setShare(data);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadShare();
|
|
||||||
}, [shareId, modified]);
|
|
||||||
|
|
||||||
const handleSubmit = async (data: any) => {
|
|
||||||
const parameters: Record<string, boolean> = {};
|
|
||||||
SHARE_NAV_ITEMS.forEach(section => {
|
|
||||||
section.items.forEach(item => {
|
|
||||||
parameters[item.id] = data[item.id] ?? true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await mutateAsync(
|
|
||||||
{ slug: share.slug, parameters },
|
|
||||||
{
|
|
||||||
onSuccess: async () => {
|
|
||||||
toast(formatMessage(messages.saved));
|
|
||||||
touch('shares');
|
|
||||||
onSave?.();
|
|
||||||
onClose?.();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Loading placement="absolute" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = getUrl(share?.slug || '');
|
|
||||||
|
|
||||||
// Build default values from share parameters
|
|
||||||
const defaultValues: Record<string, boolean> = {};
|
|
||||||
SHARE_NAV_ITEMS.forEach(section => {
|
|
||||||
section.items.forEach(item => {
|
|
||||||
defaultValues[item.id] = share?.parameters?.[item.id] ?? true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}>
|
|
||||||
<Column gap="3">
|
|
||||||
<Column>
|
|
||||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
|
||||||
<TextField value={url} isReadOnly allowCopy />
|
|
||||||
</Column>
|
|
||||||
{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 variant="primary">{formatMessage(labels.save)}</FormSubmitButton>
|
|
||||||
</Row>
|
|
||||||
</Column>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
|
|
||||||
import { DateDistance } from '@/components/common/DateDistance';
|
|
||||||
import { ExternalLink } from '@/components/common/ExternalLink';
|
|
||||||
import { useConfig, useMessages } from '@/components/hooks';
|
|
||||||
import { ShareDeleteButton } from './ShareDeleteButton';
|
|
||||||
import { ShareEditButton } from './ShareEditButton';
|
|
||||||
|
|
||||||
export function SharesTable(props: DataTableProps) {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
const { cloudMode } = useConfig();
|
|
||||||
|
|
||||||
const getUrl = (slug: string) => {
|
|
||||||
if (cloudMode) {
|
|
||||||
return `${process.env.cloudUrl}/share/${slug}`;
|
|
||||||
}
|
|
||||||
return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataTable {...props}>
|
|
||||||
<DataColumn id="slug" label={formatMessage(labels.shareUrl)}>
|
|
||||||
{({ slug }: any) => {
|
|
||||||
const url = getUrl(slug);
|
|
||||||
return (
|
|
||||||
<ExternalLink href={url} prefetch={false}>
|
|
||||||
{url}
|
|
||||||
</ExternalLink>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</DataColumn>
|
|
||||||
<DataColumn id="created" label={formatMessage(labels.created)} width="200px">
|
|
||||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
|
||||||
</DataColumn>
|
|
||||||
<DataColumn id="action" align="end" width="100px">
|
|
||||||
{({ id, slug }: any) => {
|
|
||||||
return (
|
|
||||||
<Row>
|
|
||||||
<ShareEditButton shareId={id} />
|
|
||||||
<ShareDeleteButton shareId={id} slug={slug} />
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</DataColumn>
|
|
||||||
</DataTable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column } from '@umami/react-zen';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
|
import { useWebsite } from '@/components/hooks';
|
||||||
import { WebsiteData } from './WebsiteData';
|
import { WebsiteData } from './WebsiteData';
|
||||||
import { WebsiteEditForm } from './WebsiteEditForm';
|
import { WebsiteEditForm } from './WebsiteEditForm';
|
||||||
import { WebsiteShareForm } from './WebsiteShareForm';
|
import { WebsiteShareForm } from './WebsiteShareForm';
|
||||||
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
|
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
|
||||||
|
|
||||||
export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) {
|
export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) {
|
||||||
|
const website = useWebsite();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap="6">
|
<Column gap="6">
|
||||||
<Panel>
|
<Panel>
|
||||||
|
|
@ -15,7 +18,7 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal
|
||||||
<WebsiteTrackingCode websiteId={websiteId} />
|
<WebsiteTrackingCode websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsiteShareForm websiteId={websiteId} />
|
<WebsiteShareForm websiteId={websiteId} shareId={website.shareId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsiteData websiteId={websiteId} />
|
<WebsiteData websiteId={websiteId} />
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,93 @@
|
||||||
import { Column, Heading, Row, Text } from '@umami/react-zen';
|
import {
|
||||||
import { Plus } from 'lucide-react';
|
Button,
|
||||||
import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
|
Column,
|
||||||
import { DialogButton } from '@/components/input/DialogButton';
|
Form,
|
||||||
import { ShareCreateForm } from './ShareCreateForm';
|
FormButtons,
|
||||||
import { SharesTable } from './SharesTable';
|
FormSubmitButton,
|
||||||
|
IconLabel,
|
||||||
|
Label,
|
||||||
|
Row,
|
||||||
|
Switch,
|
||||||
|
TextField,
|
||||||
|
} from '@umami/react-zen';
|
||||||
|
import { RefreshCcw } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useConfig, useMessages, useUpdateQuery } from '@/components/hooks';
|
||||||
|
import { getRandomChars } from '@/lib/generate';
|
||||||
|
|
||||||
|
const generateId = () => getRandomChars(16);
|
||||||
|
|
||||||
export interface WebsiteShareFormProps {
|
export interface WebsiteShareFormProps {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
|
shareId?: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
|
||||||
const { formatMessage, labels, messages } = useMessages();
|
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
|
||||||
const { data, isLoading } = useWebsiteSharesQuery({ websiteId });
|
const [currentId, setCurrentId] = useState(shareId);
|
||||||
|
const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
|
||||||
|
const { cloudMode } = useConfig();
|
||||||
|
|
||||||
const shares = data?.data || [];
|
const getUrl = (shareId: string) => {
|
||||||
const hasShares = shares.length > 0;
|
if (cloudMode) {
|
||||||
|
return `${process.env.cloudUrl}/share/${shareId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${window?.location.origin}${process.env.basePath || ''}/share/${shareId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = getUrl(currentId);
|
||||||
|
|
||||||
|
const handleGenerate = () => {
|
||||||
|
setCurrentId(generateId());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSwitch = () => {
|
||||||
|
setCurrentId(currentId ? null : generateId());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const data = {
|
||||||
|
shareId: currentId,
|
||||||
|
};
|
||||||
|
await mutateAsync(data, {
|
||||||
|
onSuccess: async () => {
|
||||||
|
toast(formatMessage(messages.saved));
|
||||||
|
touch(`website:${websiteId}`);
|
||||||
|
onSave?.();
|
||||||
|
onClose?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap="4">
|
<Form onSubmit={handleSave} error={getErrorMessage(error)} values={{ url }}>
|
||||||
<Row justifyContent="space-between" alignItems="center">
|
<Column gap>
|
||||||
<Heading>{formatMessage(labels.share)}</Heading>
|
<Switch isSelected={!!currentId} onChange={handleSwitch}>
|
||||||
<DialogButton
|
{formatMessage(labels.enableShareUrl)}
|
||||||
icon={<Plus size={16} />}
|
</Switch>
|
||||||
label={formatMessage(labels.add)}
|
{currentId && (
|
||||||
title={formatMessage(labels.share)}
|
<Row alignItems="flex-end" gap>
|
||||||
variant="primary"
|
<Column flexGrow={1}>
|
||||||
width="400px"
|
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||||
>
|
<TextField value={url} isReadOnly allowCopy />
|
||||||
{({ close }) => <ShareCreateForm websiteId={websiteId} onClose={close} />}
|
</Column>
|
||||||
</DialogButton>
|
<Column>
|
||||||
</Row>
|
<Button onPress={handleGenerate}>
|
||||||
{hasShares ? (
|
<IconLabel icon={<RefreshCcw />} label={formatMessage(labels.regenerate)} />
|
||||||
<>
|
</Button>
|
||||||
<Text>{formatMessage(messages.shareUrl)}</Text>
|
</Column>
|
||||||
<SharesTable data={shares} />
|
</Row>
|
||||||
</>
|
)}
|
||||||
) : (
|
<FormButtons justifyContent="flex-end">
|
||||||
<Text color="muted">{formatMessage(messages.noDataAvailable)}</Text>
|
<Row alignItems="center" gap>
|
||||||
)}
|
{onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>}
|
||||||
</Column>
|
<FormSubmitButton isDisabled={false}>{formatMessage(labels.save)}</FormSubmitButton>
|
||||||
|
</Row>
|
||||||
|
</FormButtons>
|
||||||
|
</Column>
|
||||||
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
export const SHARE_NAV_ITEMS = [
|
|
||||||
{
|
|
||||||
section: 'traffic',
|
|
||||||
items: [
|
|
||||||
{ id: 'overview', label: 'overview' },
|
|
||||||
{ id: 'events', label: 'events' },
|
|
||||||
{ id: 'sessions', label: 'sessions' },
|
|
||||||
{ id: 'realtime', label: 'realtime' },
|
|
||||||
{ id: 'compare', label: 'compare' },
|
|
||||||
{ id: 'breakdown', label: 'breakdown' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
section: 'behavior',
|
|
||||||
items: [
|
|
||||||
{ id: 'goals', label: 'goals' },
|
|
||||||
{ id: 'funnels', label: 'funnels' },
|
|
||||||
{ id: 'journeys', label: 'journeys' },
|
|
||||||
{ id: 'retention', label: 'retention' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
section: 'growth',
|
|
||||||
items: [
|
|
||||||
{ id: 'utm', label: 'utm' },
|
|
||||||
{ id: 'revenue', label: 'revenue' },
|
|
||||||
{ id: 'attribution', label: 'attribution' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
@ -12,11 +12,7 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = { shareId: share.id };
|
||||||
shareId: share.id,
|
|
||||||
websiteId: share.entityId,
|
|
||||||
parameters: share.parameters,
|
|
||||||
};
|
|
||||||
const token = createToken(data, secret());
|
const token = createToken(data, secret());
|
||||||
|
|
||||||
return json({ ...data, token });
|
return json({ ...data, token });
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { uuid } from '@/lib/crypto';
|
import { uuid } from '@/lib/crypto';
|
||||||
import { getRandomChars } from '@/lib/generate';
|
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { anyObjectParam } from '@/lib/schema';
|
import { anyObjectParam } from '@/lib/schema';
|
||||||
|
|
@ -11,7 +10,7 @@ export async function POST(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
entityId: z.uuid(),
|
entityId: z.uuid(),
|
||||||
shareType: z.coerce.number().int(),
|
shareType: z.coerce.number().int(),
|
||||||
slug: z.string().max(100).optional(),
|
slug: z.string().max(100),
|
||||||
parameters: anyObjectParam,
|
parameters: anyObjectParam,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -31,7 +30,7 @@ export async function POST(request: Request) {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
entityId,
|
entityId,
|
||||||
shareType,
|
shareType,
|
||||||
slug: slug || getRandomChars(16),
|
slug,
|
||||||
parameters,
|
parameters,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { SHARE_ID_REGEX } from '@/lib/constants';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { json, ok, unauthorized } from '@/lib/response';
|
import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
|
||||||
import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions';
|
import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions';
|
||||||
import { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma';
|
import { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma';
|
||||||
|
|
||||||
|
|
@ -32,6 +33,7 @@ export async function POST(
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
domain: z.string().optional(),
|
domain: z.string().optional(),
|
||||||
|
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
@ -41,15 +43,23 @@ export async function POST(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = await params;
|
const { websiteId } = await params;
|
||||||
const { name, domain } = body;
|
const { name, domain, shareId } = body;
|
||||||
|
|
||||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const website = await updateWebsite(websiteId, { name, domain });
|
try {
|
||||||
|
const website = await updateWebsite(websiteId, { name, domain, shareId });
|
||||||
|
|
||||||
return Response.json(website);
|
return Response.json(website);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('share_id')) {
|
||||||
|
return badRequest({ message: 'That share ID is already taken.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import { z } from 'zod';
|
|
||||||
import { ENTITY_TYPE } from '@/lib/constants';
|
|
||||||
import { uuid } from '@/lib/crypto';
|
|
||||||
import { getRandomChars } from '@/lib/generate';
|
|
||||||
import { parseRequest } from '@/lib/request';
|
|
||||||
import { json, unauthorized } from '@/lib/response';
|
|
||||||
import { anyObjectParam, filterParams, pagingParams } from '@/lib/schema';
|
|
||||||
import { canUpdateWebsite, canViewWebsite } from '@/permissions';
|
|
||||||
import { createShare, getSharesByEntityId } from '@/queries/prisma';
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: Request,
|
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
|
||||||
) {
|
|
||||||
const schema = z.object({
|
|
||||||
...filterParams,
|
|
||||||
...pagingParams,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { auth, query, error } = await parseRequest(request, schema);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return error();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { websiteId } = await params;
|
|
||||||
const { page, pageSize, search } = query;
|
|
||||||
|
|
||||||
if (!(await canViewWebsite(auth, websiteId))) {
|
|
||||||
return unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await getSharesByEntityId(websiteId, {
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
|
|
||||||
return json(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: Request,
|
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
|
||||||
) {
|
|
||||||
const schema = z.object({
|
|
||||||
parameters: anyObjectParam.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return error();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { websiteId } = await params;
|
|
||||||
const { parameters = {} } = body;
|
|
||||||
|
|
||||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
|
||||||
return unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
const slug = getRandomChars(16);
|
|
||||||
|
|
||||||
const share = await createShare({
|
|
||||||
id: uuid(),
|
|
||||||
entityId: websiteId,
|
|
||||||
shareType: ENTITY_TYPE.website,
|
|
||||||
slug,
|
|
||||||
parameters,
|
|
||||||
});
|
|
||||||
|
|
||||||
return json(share);
|
|
||||||
}
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
'use client';
|
|
||||||
import { Column } from '@umami/react-zen';
|
|
||||||
import { SideMenu } from '@/components/common/SideMenu';
|
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
|
||||||
import { AlignEndHorizontal, Clock, Eye, Sheet, Tag, User } from '@/components/icons';
|
|
||||||
import { Funnel, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg';
|
|
||||||
|
|
||||||
export function ShareNav({
|
|
||||||
shareId,
|
|
||||||
parameters,
|
|
||||||
onItemClick,
|
|
||||||
}: {
|
|
||||||
shareId: string;
|
|
||||||
parameters: Record<string, boolean>;
|
|
||||||
onItemClick?: () => void;
|
|
||||||
}) {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
const { pathname } = useNavigation();
|
|
||||||
|
|
||||||
const renderPath = (path: string) => `/share/${shareId}${path}`;
|
|
||||||
|
|
||||||
const allItems = [
|
|
||||||
{
|
|
||||||
section: 'traffic',
|
|
||||||
label: formatMessage(labels.traffic),
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'overview',
|
|
||||||
label: formatMessage(labels.overview),
|
|
||||||
icon: <Eye />,
|
|
||||||
path: renderPath(''),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'events',
|
|
||||||
label: formatMessage(labels.events),
|
|
||||||
icon: <Lightning />,
|
|
||||||
path: renderPath('/events'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sessions',
|
|
||||||
label: formatMessage(labels.sessions),
|
|
||||||
icon: <User />,
|
|
||||||
path: renderPath('/sessions'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'realtime',
|
|
||||||
label: formatMessage(labels.realtime),
|
|
||||||
icon: <Clock />,
|
|
||||||
path: renderPath('/realtime'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'compare',
|
|
||||||
label: formatMessage(labels.compare),
|
|
||||||
icon: <AlignEndHorizontal />,
|
|
||||||
path: renderPath('/compare'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'breakdown',
|
|
||||||
label: formatMessage(labels.breakdown),
|
|
||||||
icon: <Sheet />,
|
|
||||||
path: renderPath('/breakdown'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
section: 'behavior',
|
|
||||||
label: formatMessage(labels.behavior),
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'goals',
|
|
||||||
label: formatMessage(labels.goals),
|
|
||||||
icon: <Target />,
|
|
||||||
path: renderPath('/goals'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'funnels',
|
|
||||||
label: formatMessage(labels.funnels),
|
|
||||||
icon: <Funnel />,
|
|
||||||
path: renderPath('/funnels'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'journeys',
|
|
||||||
label: formatMessage(labels.journeys),
|
|
||||||
icon: <Path />,
|
|
||||||
path: renderPath('/journeys'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'retention',
|
|
||||||
label: formatMessage(labels.retention),
|
|
||||||
icon: <Magnet />,
|
|
||||||
path: renderPath('/retention'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
section: 'growth',
|
|
||||||
label: formatMessage(labels.growth),
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'utm',
|
|
||||||
label: formatMessage(labels.utm),
|
|
||||||
icon: <Tag />,
|
|
||||||
path: renderPath('/utm'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'revenue',
|
|
||||||
label: formatMessage(labels.revenue),
|
|
||||||
icon: <Money />,
|
|
||||||
path: renderPath('/revenue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'attribution',
|
|
||||||
label: formatMessage(labels.attribution),
|
|
||||||
icon: <Network />,
|
|
||||||
path: renderPath('/attribution'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Filter items based on parameters
|
|
||||||
const items = allItems
|
|
||||||
.map(section => ({
|
|
||||||
label: section.label,
|
|
||||||
items: section.items.filter(item => parameters[item.id] !== false),
|
|
||||||
}))
|
|
||||||
.filter(section => section.items.length > 0);
|
|
||||||
|
|
||||||
const selectedKey = items
|
|
||||||
.flatMap(e => e.items)
|
|
||||||
.find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column padding="3" position="sticky" top="0" gap>
|
|
||||||
<SideMenu
|
|
||||||
items={items}
|
|
||||||
selectedKey={selectedKey}
|
|
||||||
allowMinimize={false}
|
|
||||||
onItemClick={onItemClick}
|
|
||||||
/>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column, Grid, useTheme } from '@umami/react-zen';
|
import { Column, useTheme } from '@umami/react-zen';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage';
|
|
||||||
import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage';
|
|
||||||
import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage';
|
|
||||||
import { GoalsPage } from '@/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage';
|
|
||||||
import { JourneysPage } from '@/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage';
|
|
||||||
import { RetentionPage } from '@/app/(main)/websites/[websiteId]/(reports)/retention/RetentionPage';
|
|
||||||
import { RevenuePage } from '@/app/(main)/websites/[websiteId]/(reports)/revenue/RevenuePage';
|
|
||||||
import { UTMPage } from '@/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage';
|
|
||||||
import { ComparePage } from '@/app/(main)/websites/[websiteId]/compare/ComparePage';
|
|
||||||
import { EventsPage } from '@/app/(main)/websites/[websiteId]/events/EventsPage';
|
|
||||||
import { RealtimePage } from '@/app/(main)/websites/[websiteId]/realtime/RealtimePage';
|
|
||||||
import { SessionsPage } from '@/app/(main)/websites/[websiteId]/sessions/SessionsPage';
|
|
||||||
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
|
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';
|
||||||
|
|
@ -20,26 +8,8 @@ import { PageBody } from '@/components/common/PageBody';
|
||||||
import { useShareTokenQuery } from '@/components/hooks';
|
import { useShareTokenQuery } from '@/components/hooks';
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
import { ShareNav } from './ShareNav';
|
|
||||||
|
|
||||||
const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>> = {
|
export function SharePage({ shareId }) {
|
||||||
'': WebsitePage,
|
|
||||||
overview: WebsitePage,
|
|
||||||
events: EventsPage,
|
|
||||||
sessions: SessionsPage,
|
|
||||||
realtime: RealtimePage,
|
|
||||||
compare: ComparePage,
|
|
||||||
breakdown: BreakdownPage,
|
|
||||||
goals: GoalsPage,
|
|
||||||
funnels: FunnelsPage,
|
|
||||||
journeys: JourneysPage,
|
|
||||||
retention: RetentionPage,
|
|
||||||
utm: UTMPage,
|
|
||||||
revenue: RevenuePage,
|
|
||||||
attribution: AttributionPage,
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
|
|
@ -56,42 +26,16 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId, parameters = {} } = shareToken;
|
|
||||||
|
|
||||||
// Check if the requested path is allowed
|
|
||||||
const pageKey = path || '';
|
|
||||||
const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false;
|
|
||||||
|
|
||||||
if (!isAllowed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column backgroundColor="2">
|
<Column backgroundColor="2">
|
||||||
<Header />
|
<PageBody gap>
|
||||||
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
<Header />
|
||||||
<Column
|
<WebsiteProvider websiteId={shareToken.websiteId}>
|
||||||
display={{ xs: 'none', lg: 'flex' }}
|
<WebsiteHeader showActions={false} />
|
||||||
width="240px"
|
<WebsitePage websiteId={shareToken.websiteId} />
|
||||||
height="100%"
|
</WebsiteProvider>
|
||||||
border="right"
|
<Footer />
|
||||||
backgroundColor
|
</PageBody>
|
||||||
marginRight="2"
|
|
||||||
>
|
|
||||||
<ShareNav shareId={shareId} parameters={parameters} />
|
|
||||||
</Column>
|
|
||||||
<PageBody gap>
|
|
||||||
<WebsiteProvider websiteId={websiteId}>
|
|
||||||
<WebsiteHeader showActions={false} />
|
|
||||||
<Column>
|
|
||||||
<PageComponent websiteId={websiteId} />
|
|
||||||
</Column>
|
|
||||||
</WebsiteProvider>
|
|
||||||
</PageBody>
|
|
||||||
</Grid>
|
|
||||||
<Footer />
|
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { SharePage } from './SharePage';
|
||||||
|
|
||||||
export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
|
export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
|
||||||
const { shareId } = await params;
|
const { shareId } = await params;
|
||||||
const [slug, ...path] = shareId;
|
|
||||||
|
|
||||||
return <SharePage shareId={slug} path={path.join('/')} />;
|
return <SharePage shareId={shareId[0]} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ export * from './queries/useWebsiteSegmentsQuery';
|
||||||
export * from './queries/useWebsiteSessionQuery';
|
export * from './queries/useWebsiteSessionQuery';
|
||||||
export * from './queries/useWebsiteSessionStatsQuery';
|
export * from './queries/useWebsiteSessionStatsQuery';
|
||||||
export * from './queries/useWebsiteSessionsQuery';
|
export * from './queries/useWebsiteSessionsQuery';
|
||||||
export * from './queries/useWebsiteSharesQuery';
|
|
||||||
export * from './queries/useWebsiteStatsQuery';
|
export * from './queries/useWebsiteStatsQuery';
|
||||||
export * from './queries/useWebsitesQuery';
|
export * from './queries/useWebsitesQuery';
|
||||||
export * from './queries/useWebsiteValuesQuery';
|
export * from './queries/useWebsiteValuesQuery';
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function useResultQuery<T = any>(
|
||||||
) {
|
) {
|
||||||
const { websiteId, ...parameters } = params;
|
const { websiteId, ...parameters } = params;
|
||||||
const { post, useQuery } = useApi();
|
const { post, useQuery } = useApi();
|
||||||
const { startDate, endDate, timezone, unit } = useDateParameters();
|
const { startDate, endDate, timezone } = useDateParameters();
|
||||||
const filters = useFilterParameters();
|
const filters = useFilterParameters();
|
||||||
|
|
||||||
return useQuery<T>({
|
return useQuery<T>({
|
||||||
|
|
@ -22,7 +22,6 @@ export function useResultQuery<T = any>(
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
timezone,
|
timezone,
|
||||||
unit,
|
|
||||||
...params,
|
...params,
|
||||||
...filters,
|
...filters,
|
||||||
},
|
},
|
||||||
|
|
@ -36,7 +35,6 @@ export function useResultQuery<T = any>(
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
timezone,
|
timezone,
|
||||||
unit,
|
|
||||||
...parameters,
|
...parameters,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import type { ReactQueryOptions } from '@/lib/types';
|
|
||||||
import { useApi } from '../useApi';
|
|
||||||
import { useModified } from '../useModified';
|
|
||||||
import { usePagedQuery } from '../usePagedQuery';
|
|
||||||
|
|
||||||
export function useWebsiteSharesQuery(
|
|
||||||
{ websiteId }: { websiteId: string },
|
|
||||||
options?: ReactQueryOptions,
|
|
||||||
) {
|
|
||||||
const { modified } = useModified('shares');
|
|
||||||
const { get } = useApi();
|
|
||||||
|
|
||||||
return usePagedQuery({
|
|
||||||
queryKey: ['websiteShares', { websiteId, modified }],
|
|
||||||
queryFn: pageParams => {
|
|
||||||
return get(`/websites/${websiteId}/shares`, pageParams);
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -174,7 +174,6 @@ export const revenueReportSchema = z.object({
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
startDate: z.coerce.date(),
|
startDate: z.coerce.date(),
|
||||||
endDate: z.coerce.date(),
|
endDate: z.coerce.date(),
|
||||||
unit: unitParam.optional(),
|
|
||||||
timezone: z.string().optional(),
|
timezone: z.string().optional(),
|
||||||
currency: z.string(),
|
currency: z.string(),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import type { Prisma } from '@/generated/prisma/client';
|
import type { Prisma } from '@/generated/prisma/client';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import type { QueryFilters } from '@/lib/types';
|
|
||||||
|
|
||||||
export async function findShare(criteria: Prisma.ShareFindUniqueArgs) {
|
export async function findShare(criteria: Prisma.ShareFindUniqueArgs) {
|
||||||
return prisma.client.share.findUnique(criteria);
|
return prisma.client.share.findUnique(criteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getShare(shareId: string) {
|
export async function getShare(entityId: string) {
|
||||||
return findShare({
|
return findShare({
|
||||||
where: {
|
where: {
|
||||||
id: shareId,
|
id: entityId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -22,23 +21,6 @@ export async function getShareByCode(slug: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSharesByEntityId(entityId: string, filters?: QueryFilters) {
|
|
||||||
const { pagedQuery } = prisma;
|
|
||||||
|
|
||||||
return pagedQuery(
|
|
||||||
'share',
|
|
||||||
{
|
|
||||||
where: {
|
|
||||||
entityId,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
createdAt: 'desc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
filters,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createShare(
|
export async function createShare(
|
||||||
data: Prisma.ShareCreateInput | Prisma.ShareUncheckedCreateInput,
|
data: Prisma.ShareCreateInput | Prisma.ShareUncheckedCreateInput,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue