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 ( +
+ + {SHARE_NAV_ITEMS.map(section => ( + + + {formatMessage((labels as any)[section.section])} + + + {section.items.map(item => ( + + + {formatMessage((labels as any)[item.label])} + + + ))} + + + ))} + + {onClose && ( + + )} + {formatMessage(labels.save)} + + +
+ ); +} 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 ( +
+ + + + + + {SHARE_NAV_ITEMS.map(section => ( + + + {formatMessage((labels as any)[section.section])} + + + {section.items.map(item => ( + + {formatMessage((labels as any)[item.label])} + + ))} + + + ))} + + {onClose && ( + + )} + {formatMessage(labels.save)} + + +
+ ); +} 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.enableShareUrl)} - - {currentId && ( - - - - - - - - - - )} - - - {onClose && } - {formatMessage(labels.save)} - - - -
+ + + {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 ( - -
- - - - -