mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Merge branch 'dev' of https://github.com/umami-software/umami into dev
Some checks are pending
Node.js CI / build (push) Waiting to run
Some checks are pending
Node.js CI / build (push) Waiting to run
This commit is contained in:
commit
d47ee6e8e8
19 changed files with 734 additions and 141 deletions
|
|
@ -1,11 +1,9 @@
|
||||||
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, Share } from '@/components/icons';
|
import { Edit } 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 }) {
|
||||||
|
|
@ -29,29 +27,14 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
||||||
<ActiveUsers websiteId={website.id} />
|
<ActiveUsers websiteId={website.id} />
|
||||||
|
|
||||||
{showActions && (
|
{showActions && (
|
||||||
<Row alignItems="center" gap>
|
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
||||||
<ShareButton websiteId={website.id} shareId={website.shareId} />
|
<Icon>
|
||||||
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
<Edit />
|
||||||
<Icon>
|
</Icon>
|
||||||
<Edit />
|
<Text>{formatMessage(labels.edit)}</Text>
|
||||||
</Icon>
|
</LinkButton>
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
||||||
|
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
|
||||||
|
import { Trash } from '@/components/icons';
|
||||||
|
import { DialogButton } from '@/components/input/DialogButton';
|
||||||
|
import { messages } from '@/components/messages';
|
||||||
|
|
||||||
|
export function 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
123
src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx
Normal file
123
src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
Normal file
46
src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
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,14 +1,11 @@
|
||||||
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>
|
||||||
|
|
@ -18,7 +15,7 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal
|
||||||
<WebsiteTrackingCode websiteId={websiteId} />
|
<WebsiteTrackingCode websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsiteShareForm websiteId={websiteId} shareId={website.shareId} />
|
<WebsiteShareForm websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsiteData websiteId={websiteId} />
|
<WebsiteData websiteId={websiteId} />
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,43 @@
|
||||||
import {
|
import { Column, Heading, Row, Text } from '@umami/react-zen';
|
||||||
Button,
|
import { Plus } from 'lucide-react';
|
||||||
Column,
|
import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
|
||||||
Form,
|
import { DialogButton } from '@/components/input/DialogButton';
|
||||||
FormButtons,
|
import { ShareCreateForm } from './ShareCreateForm';
|
||||||
FormSubmitButton,
|
import { SharesTable } from './SharesTable';
|
||||||
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, shareId, onSave, onClose }: WebsiteShareFormProps) {
|
export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
||||||
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
const [currentId, setCurrentId] = useState(shareId);
|
const { data, isLoading } = useWebsiteSharesQuery({ websiteId });
|
||||||
const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
|
|
||||||
const { cloudMode } = useConfig();
|
|
||||||
|
|
||||||
const getUrl = (shareId: string) => {
|
const shares = data?.data || [];
|
||||||
if (cloudMode) {
|
const hasShares = shares.length > 0;
|
||||||
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 (
|
||||||
<Form onSubmit={handleSave} error={getErrorMessage(error)} values={{ url }}>
|
<Column gap="4">
|
||||||
<Column gap>
|
<Row justifyContent="space-between" alignItems="center">
|
||||||
<Switch isSelected={!!currentId} onChange={handleSwitch}>
|
<Heading>{formatMessage(labels.share)}</Heading>
|
||||||
{formatMessage(labels.enableShareUrl)}
|
<DialogButton
|
||||||
</Switch>
|
icon={<Plus size={16} />}
|
||||||
{currentId && (
|
label={formatMessage(labels.add)}
|
||||||
<Row alignItems="flex-end" gap>
|
title={formatMessage(labels.share)}
|
||||||
<Column flexGrow={1}>
|
variant="primary"
|
||||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
width="400px"
|
||||||
<TextField value={url} isReadOnly allowCopy />
|
>
|
||||||
</Column>
|
{({ close }) => <ShareCreateForm websiteId={websiteId} onClose={close} />}
|
||||||
<Column>
|
</DialogButton>
|
||||||
<Button onPress={handleGenerate}>
|
</Row>
|
||||||
<IconLabel icon={<RefreshCcw />} label={formatMessage(labels.regenerate)} />
|
{hasShares ? (
|
||||||
</Button>
|
<>
|
||||||
</Column>
|
<Text>{formatMessage(messages.shareUrl)}</Text>
|
||||||
</Row>
|
<SharesTable data={shares} />
|
||||||
)}
|
</>
|
||||||
<FormButtons justifyContent="flex-end">
|
) : (
|
||||||
<Row alignItems="center" gap>
|
<Text color="muted">{formatMessage(messages.noDataAvailable)}</Text>
|
||||||
{onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>}
|
)}
|
||||||
<FormSubmitButton isDisabled={false}>{formatMessage(labels.save)}</FormSubmitButton>
|
</Column>
|
||||||
</Row>
|
|
||||||
</FormButtons>
|
|
||||||
</Column>
|
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
src/app/(main)/websites/[websiteId]/settings/constants.ts
Normal file
30
src/app/(main)/websites/[websiteId]/settings/constants.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
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,7 +12,11 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = { shareId: share.id };
|
const data = {
|
||||||
|
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,5 +1,6 @@
|
||||||
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';
|
||||||
|
|
@ -10,7 +11,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),
|
slug: z.string().max(100).optional(),
|
||||||
parameters: anyObjectParam,
|
parameters: anyObjectParam,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -30,7 +31,7 @@ export async function POST(request: Request) {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
entityId,
|
entityId,
|
||||||
shareType,
|
shareType,
|
||||||
slug,
|
slug: slug || getRandomChars(16),
|
||||||
parameters,
|
parameters,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
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 { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
|
import { json, ok, 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';
|
||||||
|
|
||||||
|
|
@ -33,7 +32,6 @@ 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);
|
||||||
|
|
@ -43,23 +41,15 @@ export async function POST(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = await params;
|
const { websiteId } = await params;
|
||||||
const { name, domain, shareId } = body;
|
const { name, domain } = body;
|
||||||
|
|
||||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const website = await updateWebsite(websiteId, { name, domain });
|
||||||
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(
|
||||||
|
|
|
||||||
74
src/app/api/websites/[websiteId]/shares/route.ts
Normal file
74
src/app/api/websites/[websiteId]/shares/route.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
143
src/app/share/[...shareId]/ShareNav.tsx
Normal file
143
src/app/share/[...shareId]/ShareNav.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
'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,6 +1,18 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column, useTheme } from '@umami/react-zen';
|
import { Column, Grid, 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';
|
||||||
|
|
@ -8,8 +20,26 @@ 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';
|
||||||
|
|
||||||
export function SharePage({ shareId }) {
|
const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>> = {
|
||||||
|
'': 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();
|
||||||
|
|
||||||
|
|
@ -26,16 +56,42 @@ export function SharePage({ shareId }) {
|
||||||
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">
|
||||||
<PageBody gap>
|
<Header />
|
||||||
<Header />
|
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
||||||
<WebsiteProvider websiteId={shareToken.websiteId}>
|
<Column
|
||||||
<WebsiteHeader showActions={false} />
|
display={{ xs: 'none', lg: 'flex' }}
|
||||||
<WebsitePage websiteId={shareToken.websiteId} />
|
width="240px"
|
||||||
</WebsiteProvider>
|
height="100%"
|
||||||
<Footer />
|
border="right"
|
||||||
</PageBody>
|
backgroundColor
|
||||||
|
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,6 +2,7 @@ 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={shareId[0]} />;
|
return <SharePage shareId={slug} path={path.join('/')} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ 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';
|
||||||
|
|
|
||||||
20
src/components/hooks/queries/useWebsiteSharesQuery.ts
Normal file
20
src/components/hooks/queries/useWebsiteSharesQuery.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
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(entityId: string) {
|
export async function getShare(shareId: string) {
|
||||||
return findShare({
|
return findShare({
|
||||||
where: {
|
where: {
|
||||||
id: entityId,
|
id: shareId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +22,23 @@ 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