diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
index 7dd1d771..e79576dd 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
@@ -1,11 +1,9 @@
import { Icon, Row, Text } from '@umami/react-zen';
-import { WebsiteShareForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteShareForm';
import { Favicon } from '@/components/common/Favicon';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
-import { Edit, Share } from '@/components/icons';
-import { DialogButton } from '@/components/input/DialogButton';
+import { Edit } from '@/components/icons';
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
@@ -29,29 +27,14 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
{showActions && (
-
-
-
-
-
-
- {formatMessage(labels.edit)}
-
-
+
+
+
+
+ {formatMessage(labels.edit)}
+
)}
);
}
-
-const ShareButton = ({ websiteId, shareId }) => {
- const { formatMessage, labels } = useMessages();
-
- return (
- } label={formatMessage(labels.share)} width="800px">
- {({ close }) => {
- return ;
- }}
-
- );
-};
diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareCreateForm.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareCreateForm.tsx
new file mode 100644
index 00000000..024fc10b
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/ShareCreateForm.tsx
@@ -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 = {};
+ SHARE_NAV_ITEMS.forEach(section => {
+ section.items.forEach(item => {
+ defaultValues[item.id] = true;
+ });
+ });
+
+ const handleSubmit = async (data: any) => {
+ setIsPending(true);
+ try {
+ const parameters: Record = {};
+ 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 (
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareDeleteButton.tsx
new file mode 100644
index 00000000..35e96df3
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/ShareDeleteButton.tsx
@@ -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 (
+ }
+ title={formatMessage(labels.confirm)}
+ variant="quiet"
+ width="400px"
+ >
+ {({ close }) => (
+ {slug},
+ }}
+ />
+ }
+ isLoading={isPending}
+ error={getErrorMessage(error)}
+ onConfirm={handleConfirm.bind(null, close)}
+ onClose={close}
+ buttonLabel={formatMessage(labels.delete)}
+ buttonVariant="danger"
+ />
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareEditButton.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareEditButton.tsx
new file mode 100644
index 00000000..df1c2e64
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/ShareEditButton.tsx
@@ -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 (
+ } title={formatMessage(labels.share)} variant="quiet" width="600px">
+ {({ close }) => {
+ return ;
+ }}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx
new file mode 100644
index 00000000..5e8f8a74
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx
@@ -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(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 = {};
+ 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 ;
+ }
+
+ const url = getUrl(share?.slug || '');
+
+ // Build default values from share parameters
+ const defaultValues: Record = {};
+ SHARE_NAV_ITEMS.forEach(section => {
+ section.items.forEach(item => {
+ defaultValues[item.id] = share?.parameters?.[item.id] ?? true;
+ });
+ });
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx b/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
new file mode 100644
index 00000000..05e8b357
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
@@ -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 (
+
+
+ {({ slug }: any) => {
+ const url = getUrl(slug);
+ return (
+
+ {url}
+
+ );
+ }}
+
+
+ {(row: any) => }
+
+
+ {({ id, slug }: any) => {
+ return (
+
+
+
+
+ );
+ }}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx
index 3970cdbd..d39c4531 100644
--- a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx
@@ -1,14 +1,11 @@
import { Column } from '@umami/react-zen';
import { Panel } from '@/components/common/Panel';
-import { useWebsite } from '@/components/hooks';
import { WebsiteData } from './WebsiteData';
import { WebsiteEditForm } from './WebsiteEditForm';
import { WebsiteShareForm } from './WebsiteShareForm';
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) {
- const website = useWebsite();
-
return (
@@ -18,7 +15,7 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal
-
+
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx
index 56c6f436..7453b402 100644
--- a/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx
@@ -1,93 +1,43 @@
-import {
- Button,
- Column,
- Form,
- FormButtons,
- 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);
+import { Column, Heading, Row, Text } from '@umami/react-zen';
+import { Plus } from 'lucide-react';
+import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
+import { DialogButton } from '@/components/input/DialogButton';
+import { ShareCreateForm } from './ShareCreateForm';
+import { SharesTable } from './SharesTable';
export interface WebsiteShareFormProps {
websiteId: string;
- shareId?: string;
- onSave?: () => void;
- onClose?: () => void;
}
-export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
- const { formatMessage, labels, messages, getErrorMessage } = useMessages();
- const [currentId, setCurrentId] = useState(shareId);
- const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
- const { cloudMode } = useConfig();
+export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
+ const { formatMessage, labels, messages } = useMessages();
+ const { data, isLoading } = useWebsiteSharesQuery({ websiteId });
- const getUrl = (shareId: string) => {
- 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?.();
- },
- });
- };
+ const shares = data?.data || [];
+ const hasShares = shares.length > 0;
return (
-
+
+
+ {formatMessage(labels.share)}
+ }
+ label={formatMessage(labels.add)}
+ title={formatMessage(labels.share)}
+ variant="primary"
+ width="400px"
+ >
+ {({ close }) => }
+
+
+ {hasShares ? (
+ <>
+ {formatMessage(messages.shareUrl)}
+
+ >
+ ) : (
+ {formatMessage(messages.noDataAvailable)}
+ )}
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/settings/constants.ts b/src/app/(main)/websites/[websiteId]/settings/constants.ts
new file mode 100644
index 00000000..f4a3df80
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/constants.ts
@@ -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' },
+ ],
+ },
+];
diff --git a/src/app/api/share/[slug]/route.ts b/src/app/api/share/[slug]/route.ts
index 678795e0..ed3271ea 100644
--- a/src/app/api/share/[slug]/route.ts
+++ b/src/app/api/share/[slug]/route.ts
@@ -12,7 +12,11 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
return notFound();
}
- const data = { shareId: share.id };
+ const data = {
+ shareId: share.id,
+ websiteId: share.entityId,
+ parameters: share.parameters,
+ };
const token = createToken(data, secret());
return json({ ...data, token });
diff --git a/src/app/api/share/route.ts b/src/app/api/share/route.ts
index 99f5df0e..a772b4ab 100644
--- a/src/app/api/share/route.ts
+++ b/src/app/api/share/route.ts
@@ -1,5 +1,6 @@
import z from 'zod';
import { uuid } from '@/lib/crypto';
+import { getRandomChars } from '@/lib/generate';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { anyObjectParam } from '@/lib/schema';
@@ -10,7 +11,7 @@ export async function POST(request: Request) {
const schema = z.object({
entityId: z.uuid(),
shareType: z.coerce.number().int(),
- slug: z.string().max(100),
+ slug: z.string().max(100).optional(),
parameters: anyObjectParam,
});
@@ -30,7 +31,7 @@ export async function POST(request: Request) {
id: uuid(),
entityId,
shareType,
- slug,
+ slug: slug || getRandomChars(16),
parameters,
});
diff --git a/src/app/api/websites/[websiteId]/route.ts b/src/app/api/websites/[websiteId]/route.ts
index b4c0e7e8..59f314d3 100644
--- a/src/app/api/websites/[websiteId]/route.ts
+++ b/src/app/api/websites/[websiteId]/route.ts
@@ -1,7 +1,6 @@
import { z } from 'zod';
-import { SHARE_ID_REGEX } from '@/lib/constants';
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 { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma';
@@ -33,7 +32,6 @@ export async function POST(
const schema = z.object({
name: z.string().optional(),
domain: z.string().optional(),
- shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
});
const { auth, body, error } = await parseRequest(request, schema);
@@ -43,23 +41,15 @@ export async function POST(
}
const { websiteId } = await params;
- const { name, domain, shareId } = body;
+ const { name, domain } = body;
if (!(await canUpdateWebsite(auth, websiteId))) {
return unauthorized();
}
- try {
- const website = await updateWebsite(websiteId, { name, domain, shareId });
+ const website = await updateWebsite(websiteId, { name, domain });
- 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);
- }
+ return Response.json(website);
}
export async function DELETE(
diff --git a/src/app/api/websites/[websiteId]/shares/route.ts b/src/app/api/websites/[websiteId]/shares/route.ts
new file mode 100644
index 00000000..db079d49
--- /dev/null
+++ b/src/app/api/websites/[websiteId]/shares/route.ts
@@ -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);
+}
diff --git a/src/app/share/[...shareId]/ShareNav.tsx b/src/app/share/[...shareId]/ShareNav.tsx
new file mode 100644
index 00000000..b494046d
--- /dev/null
+++ b/src/app/share/[...shareId]/ShareNav.tsx
@@ -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;
+ 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: ,
+ path: renderPath(''),
+ },
+ {
+ id: 'events',
+ label: formatMessage(labels.events),
+ icon: ,
+ path: renderPath('/events'),
+ },
+ {
+ id: 'sessions',
+ label: formatMessage(labels.sessions),
+ icon: ,
+ path: renderPath('/sessions'),
+ },
+ {
+ id: 'realtime',
+ label: formatMessage(labels.realtime),
+ icon: ,
+ path: renderPath('/realtime'),
+ },
+ {
+ id: 'compare',
+ label: formatMessage(labels.compare),
+ icon: ,
+ path: renderPath('/compare'),
+ },
+ {
+ id: 'breakdown',
+ label: formatMessage(labels.breakdown),
+ icon: ,
+ path: renderPath('/breakdown'),
+ },
+ ],
+ },
+ {
+ section: 'behavior',
+ label: formatMessage(labels.behavior),
+ items: [
+ {
+ id: 'goals',
+ label: formatMessage(labels.goals),
+ icon: ,
+ path: renderPath('/goals'),
+ },
+ {
+ id: 'funnels',
+ label: formatMessage(labels.funnels),
+ icon: ,
+ path: renderPath('/funnels'),
+ },
+ {
+ id: 'journeys',
+ label: formatMessage(labels.journeys),
+ icon: ,
+ path: renderPath('/journeys'),
+ },
+ {
+ id: 'retention',
+ label: formatMessage(labels.retention),
+ icon: ,
+ path: renderPath('/retention'),
+ },
+ ],
+ },
+ {
+ section: 'growth',
+ label: formatMessage(labels.growth),
+ items: [
+ {
+ id: 'utm',
+ label: formatMessage(labels.utm),
+ icon: ,
+ path: renderPath('/utm'),
+ },
+ {
+ id: 'revenue',
+ label: formatMessage(labels.revenue),
+ icon: ,
+ path: renderPath('/revenue'),
+ },
+ {
+ id: 'attribution',
+ label: formatMessage(labels.attribution),
+ icon: ,
+ 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 (
+
+
+
+ );
+}
diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[...shareId]/SharePage.tsx
index 7ed06673..3e1cedc0 100644
--- a/src/app/share/[...shareId]/SharePage.tsx
+++ b/src/app/share/[...shareId]/SharePage.tsx
@@ -1,6 +1,18 @@
'use client';
-import { Column, useTheme } from '@umami/react-zen';
+import { Column, Grid, useTheme } from '@umami/react-zen';
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 { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
@@ -8,8 +20,26 @@ import { PageBody } from '@/components/common/PageBody';
import { useShareTokenQuery } from '@/components/hooks';
import { Footer } from './Footer';
import { Header } from './Header';
+import { ShareNav } from './ShareNav';
-export function SharePage({ shareId }) {
+const PAGE_COMPONENTS: Record> = {
+ '': 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 { setTheme } = useTheme();
@@ -26,16 +56,42 @@ export function SharePage({ shareId }) {
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 (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/app/share/[...shareId]/page.tsx b/src/app/share/[...shareId]/page.tsx
index b9900eb7..3a21f836 100644
--- a/src/app/share/[...shareId]/page.tsx
+++ b/src/app/share/[...shareId]/page.tsx
@@ -2,6 +2,7 @@ import { SharePage } from './SharePage';
export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
const { shareId } = await params;
+ const [slug, ...path] = shareId;
- return ;
+ return ;
}
diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts
index e8e5c135..f47f11f0 100644
--- a/src/components/hooks/index.ts
+++ b/src/components/hooks/index.ts
@@ -51,6 +51,7 @@ export * from './queries/useWebsiteSegmentsQuery';
export * from './queries/useWebsiteSessionQuery';
export * from './queries/useWebsiteSessionStatsQuery';
export * from './queries/useWebsiteSessionsQuery';
+export * from './queries/useWebsiteSharesQuery';
export * from './queries/useWebsiteStatsQuery';
export * from './queries/useWebsitesQuery';
export * from './queries/useWebsiteValuesQuery';
diff --git a/src/components/hooks/queries/useWebsiteSharesQuery.ts b/src/components/hooks/queries/useWebsiteSharesQuery.ts
new file mode 100644
index 00000000..298e4d26
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSharesQuery.ts
@@ -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,
+ });
+}
diff --git a/src/queries/prisma/share.ts b/src/queries/prisma/share.ts
index e37dc95b..53246ffb 100644
--- a/src/queries/prisma/share.ts
+++ b/src/queries/prisma/share.ts
@@ -1,14 +1,15 @@
import type { Prisma } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
+import type { QueryFilters } from '@/lib/types';
export async function findShare(criteria: Prisma.ShareFindUniqueArgs) {
return prisma.client.share.findUnique(criteria);
}
-export async function getShare(entityId: string) {
+export async function getShare(shareId: string) {
return findShare({
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(
data: Prisma.ShareCreateInput | Prisma.ShareUncheckedCreateInput,
) {