mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Compare commits
14 commits
b84942b6da
...
489c2712d1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
489c2712d1 | ||
|
|
b43e7fd3a7 | ||
|
|
5880eae4e4 | ||
|
|
6169a58e86 | ||
|
|
452a385c4e | ||
|
|
482d6c1e47 | ||
|
|
4e8be724ac | ||
|
|
c9e14f3bce | ||
|
|
d028bfa1f5 | ||
|
|
78d467b478 | ||
|
|
9d3f5ad0fd | ||
|
|
018e76b067 | ||
|
|
2df24a78ca | ||
|
|
9339383497 |
22 changed files with 313 additions and 264 deletions
|
|
@ -9,11 +9,10 @@ import { WebsiteNav } from './WebsiteNav';
|
||||||
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<WebsiteProvider websiteId={websiteId}>
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%">
|
||||||
<Column
|
<Column
|
||||||
display={{ xs: 'none', lg: 'flex' }}
|
display={{ xs: 'none', lg: 'flex' }}
|
||||||
width="240px"
|
width="240px"
|
||||||
height="100%"
|
|
||||||
border="right"
|
border="right"
|
||||||
backgroundColor
|
backgroundColor
|
||||||
marginRight="2"
|
marginRight="2"
|
||||||
|
|
|
||||||
77
src/app/share/ShareProvider.tsx
Normal file
77
src/app/share/ShareProvider.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
'use client';
|
||||||
|
import { Loading } from '@umami/react-zen';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import { createContext, type ReactNode, useEffect } from 'react';
|
||||||
|
import { useShareTokenQuery } from '@/components/hooks';
|
||||||
|
import type { WhiteLabel } from '@/lib/types';
|
||||||
|
|
||||||
|
export interface ShareData {
|
||||||
|
shareId: string;
|
||||||
|
slug: string;
|
||||||
|
websiteId: string;
|
||||||
|
parameters: any;
|
||||||
|
token: string;
|
||||||
|
whiteLabel?: WhiteLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShareContext = createContext<ShareData>(null);
|
||||||
|
|
||||||
|
const ALL_SECTION_IDS = [
|
||||||
|
'overview',
|
||||||
|
'events',
|
||||||
|
'sessions',
|
||||||
|
'realtime',
|
||||||
|
'compare',
|
||||||
|
'breakdown',
|
||||||
|
'goals',
|
||||||
|
'funnels',
|
||||||
|
'journeys',
|
||||||
|
'retention',
|
||||||
|
'utm',
|
||||||
|
'revenue',
|
||||||
|
'attribution',
|
||||||
|
];
|
||||||
|
|
||||||
|
function getSharePath(pathname: string) {
|
||||||
|
const segments = pathname.split('/');
|
||||||
|
const firstSegment = segments[3];
|
||||||
|
|
||||||
|
// If first segment looks like a domain name, skip it
|
||||||
|
if (firstSegment?.includes('.')) {
|
||||||
|
return segments[4];
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShareProvider({ slug, children }: { slug: string; children: ReactNode }) {
|
||||||
|
const { share, isLoading, isFetching } = useShareTokenQuery(slug);
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const path = getSharePath(pathname);
|
||||||
|
|
||||||
|
const allowedSections = share?.parameters
|
||||||
|
? ALL_SECTION_IDS.filter(id => share.parameters[id] !== false)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const shouldRedirect =
|
||||||
|
allowedSections.length === 1 &&
|
||||||
|
allowedSections[0] !== 'overview' &&
|
||||||
|
(path === undefined || path === '' || path === 'overview');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldRedirect) {
|
||||||
|
router.replace(`/share/${slug}/${allowedSections[0]}`);
|
||||||
|
}
|
||||||
|
}, [shouldRedirect, slug, allowedSections, router]);
|
||||||
|
|
||||||
|
if (isFetching && isLoading) {
|
||||||
|
return <Loading placement="absolute" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!share || shouldRedirect) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ShareContext.Provider value={{ ...share, slug }}>{children}</ShareContext.Provider>;
|
||||||
|
}
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import { Row, Text } from '@umami/react-zen';
|
|
||||||
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
|
|
||||||
import type { WhiteLabel } from '@/lib/types';
|
|
||||||
|
|
||||||
export function ShareFooter({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
|
|
||||||
if (whiteLabel) {
|
|
||||||
return (
|
|
||||||
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
|
||||||
<a href={whiteLabel.url} target="_blank">
|
|
||||||
<Text weight="bold">{whiteLabel.name}</Text>
|
|
||||||
</a>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
|
||||||
<a href={HOMEPAGE_URL} target="_blank">
|
|
||||||
<Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`}
|
|
||||||
</a>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
|
|
||||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
|
||||||
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
|
||||||
import { Logo } from '@/components/svg';
|
|
||||||
import type { WhiteLabel } from '@/lib/types';
|
|
||||||
|
|
||||||
export function ShareHeader({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
|
|
||||||
const logoUrl = whiteLabel?.url || 'https://umami.is';
|
|
||||||
const logoName = whiteLabel?.name || 'umami';
|
|
||||||
const logoImage = whiteLabel?.image;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
|
|
||||||
<a href={logoUrl} target="_blank" rel="noopener">
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
{logoImage ? (
|
|
||||||
<img src={logoImage} alt={logoName} style={{ height: 24 }} />
|
|
||||||
) : (
|
|
||||||
<Icon>
|
|
||||||
<Logo />
|
|
||||||
</Icon>
|
|
||||||
)}
|
|
||||||
<Text weight="bold">{logoName}</Text>
|
|
||||||
</Row>
|
|
||||||
</a>
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
<ThemeButton />
|
|
||||||
<LanguageButton />
|
|
||||||
<PreferencesButton />
|
|
||||||
</Row>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
'use client';
|
|
||||||
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useEffect, useMemo } 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';
|
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
|
||||||
import { useShareTokenQuery } from '@/components/hooks';
|
|
||||||
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
|
||||||
import { ShareFooter } from './ShareFooter';
|
|
||||||
import { ShareHeader } from './ShareHeader';
|
|
||||||
import { ShareNav } from './ShareNav';
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
// All section IDs that can be enabled/disabled via parameters
|
|
||||||
const ALL_SECTION_IDS = [
|
|
||||||
'overview',
|
|
||||||
'events',
|
|
||||||
'sessions',
|
|
||||||
'realtime',
|
|
||||||
'compare',
|
|
||||||
'breakdown',
|
|
||||||
'goals',
|
|
||||||
'funnels',
|
|
||||||
'journeys',
|
|
||||||
'retention',
|
|
||||||
'utm',
|
|
||||||
'revenue',
|
|
||||||
'attribution',
|
|
||||||
];
|
|
||||||
|
|
||||||
export function SharePage({ shareId, path = '' }: { shareId: string; path?: string }) {
|
|
||||||
const { shareToken, isLoading } = useShareTokenQuery(shareId);
|
|
||||||
const { setTheme } = useTheme();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Calculate allowed sections
|
|
||||||
const allowedSections = useMemo(() => {
|
|
||||||
if (!shareToken?.parameters) return [];
|
|
||||||
const params = shareToken.parameters;
|
|
||||||
return ALL_SECTION_IDS.filter(id => params[id] !== false);
|
|
||||||
}, [shareToken?.parameters]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const url = new URL(window?.location?.href);
|
|
||||||
const theme = url.searchParams.get('theme');
|
|
||||||
|
|
||||||
if (theme === 'light' || theme === 'dark') {
|
|
||||||
setTheme(theme);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Redirect to the only allowed section if there's just one and we're on the base path
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
allowedSections.length === 1 &&
|
|
||||||
allowedSections[0] !== 'overview' &&
|
|
||||||
(path === '' || path === 'overview')
|
|
||||||
) {
|
|
||||||
router.replace(`/share/${shareId}/${allowedSections[0]}`);
|
|
||||||
}
|
|
||||||
}, [allowedSections, shareId, path, router]);
|
|
||||||
|
|
||||||
if (isLoading || !shareToken) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { websiteId, parameters = {}, whiteLabel } = shareToken;
|
|
||||||
|
|
||||||
// Redirect to only allowed section - return null while redirecting
|
|
||||||
if (
|
|
||||||
allowedSections.length === 1 &&
|
|
||||||
allowedSections[0] !== 'overview' &&
|
|
||||||
(path === '' || path === 'overview')
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the requested path is allowed
|
|
||||||
const pageKey = path || '';
|
|
||||||
const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false;
|
|
||||||
|
|
||||||
if (!isAllowed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Column backgroundColor="2">
|
|
||||||
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
|
||||||
<Row display={{ xs: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
|
||||||
<Grid columns="auto 1fr" flexGrow={1} backgroundColor="3" borderRadius>
|
|
||||||
<MobileMenuButton>
|
|
||||||
{({ close }) => {
|
|
||||||
return <ShareNav shareId={shareId} parameters={parameters} onItemClick={close} />;
|
|
||||||
}}
|
|
||||||
</MobileMenuButton>
|
|
||||||
</Grid>
|
|
||||||
</Row>
|
|
||||||
<Column
|
|
||||||
display={{ xs: 'none', lg: 'flex' }}
|
|
||||||
width="240px"
|
|
||||||
height="100%"
|
|
||||||
border="right"
|
|
||||||
backgroundColor
|
|
||||||
marginRight="2"
|
|
||||||
>
|
|
||||||
<Column display={{ xs: 'none', lg: 'flex' }}>
|
|
||||||
<ShareNav shareId={shareId} parameters={parameters} />
|
|
||||||
</Column>
|
|
||||||
</Column>
|
|
||||||
<PageBody gap>
|
|
||||||
<WebsiteProvider websiteId={websiteId}>
|
|
||||||
<ShareHeader whiteLabel={whiteLabel} />
|
|
||||||
<Column>
|
|
||||||
<WebsiteHeader showActions={false} />
|
|
||||||
<PageComponent websiteId={websiteId} />
|
|
||||||
</Column>
|
|
||||||
<ShareFooter whiteLabel={whiteLabel} />
|
|
||||||
</WebsiteProvider>
|
|
||||||
</PageBody>
|
|
||||||
</Grid>
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { SharePage } from './SharePage';
|
|
||||||
|
|
||||||
export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
|
|
||||||
const { shareId } = await params;
|
|
||||||
const [slug, ...path] = shareId;
|
|
||||||
|
|
||||||
return <SharePage shareId={slug} path={path.join('/')} />;
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +1,30 @@
|
||||||
'use client';
|
import { Button, Column, Icon, Row, Text, ThemeButton } from '@umami/react-zen';
|
||||||
import { Column } from '@umami/react-zen';
|
|
||||||
import { SideMenu } from '@/components/common/SideMenu';
|
import { SideMenu } from '@/components/common/SideMenu';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation, useShare } from '@/components/hooks';
|
||||||
import { AlignEndHorizontal, Clock, Eye, Sheet, Tag, User } from '@/components/icons';
|
import { AlignEndHorizontal, Clock, Eye, PanelLeft, Sheet, Tag, User } from '@/components/icons';
|
||||||
import { Funnel, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg';
|
import { LanguageButton } from '@/components/input/LanguageButton';
|
||||||
|
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
||||||
|
import { Funnel, Lightning, Logo, Magnet, Money, Network, Path, Target } from '@/components/svg';
|
||||||
|
|
||||||
export function ShareNav({
|
export function ShareNav({
|
||||||
shareId,
|
collapsed,
|
||||||
parameters,
|
onCollapse,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
}: {
|
}: {
|
||||||
shareId: string;
|
collapsed?: boolean;
|
||||||
parameters: Record<string, boolean>;
|
onCollapse?: (collapsed: boolean) => void;
|
||||||
onItemClick?: () => void;
|
onItemClick?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const share = useShare();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { pathname } = useNavigation();
|
const { pathname } = useNavigation();
|
||||||
|
const { slug, parameters, whiteLabel } = share;
|
||||||
|
|
||||||
const renderPath = (path: string) => `/share/${shareId}${path}`;
|
const logoUrl = whiteLabel?.url || 'https://umami.is';
|
||||||
|
const logoName = whiteLabel?.name || 'umami';
|
||||||
|
const logoImage = whiteLabel?.image;
|
||||||
|
|
||||||
|
const renderPath = (path: string) => `/share/${slug}${path}`;
|
||||||
|
|
||||||
const allItems = [
|
const allItems = [
|
||||||
{
|
{
|
||||||
|
|
@ -130,14 +137,70 @@ export function ShareNav({
|
||||||
.flatMap(e => e.items)
|
.flatMap(e => e.items)
|
||||||
.find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
|
.find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
|
||||||
|
|
||||||
|
const isMobile = !!onItemClick;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column padding="3" position="sticky" top="0" gap>
|
<Column
|
||||||
<SideMenu
|
position={isMobile ? undefined : 'fixed'}
|
||||||
items={items}
|
padding="3"
|
||||||
selectedKey={selectedKey}
|
width={isMobile ? '100%' : collapsed ? '60px' : '240px'}
|
||||||
allowMinimize={false}
|
maxHeight="100vh"
|
||||||
onItemClick={onItemClick}
|
height="100vh"
|
||||||
/>
|
border={isMobile ? undefined : 'right'}
|
||||||
|
borderColor={isMobile ? undefined : '4'}
|
||||||
|
>
|
||||||
|
<Row as="header" gap alignItems="center" paddingY="3" justifyContent="space-between">
|
||||||
|
{!collapsed && (
|
||||||
|
<a href={logoUrl} target="_blank" rel="noopener" style={{ marginLeft: 12 }}>
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
{logoImage ? (
|
||||||
|
<img src={logoImage} alt={logoName} style={{ height: 24 }} />
|
||||||
|
) : (
|
||||||
|
<Icon>
|
||||||
|
<Logo />
|
||||||
|
</Icon>
|
||||||
|
)}
|
||||||
|
<Text weight="bold">{logoName}</Text>
|
||||||
|
</Row>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{!onItemClick && (
|
||||||
|
<Button variant="quiet" onPress={() => onCollapse?.(!collapsed)}>
|
||||||
|
<Icon color="muted">
|
||||||
|
<PanelLeft />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
{!collapsed && (
|
||||||
|
<Column flexGrow={1} overflowY="auto">
|
||||||
|
<SideMenu
|
||||||
|
items={items}
|
||||||
|
selectedKey={selectedKey}
|
||||||
|
allowMinimize={false}
|
||||||
|
onItemClick={onItemClick}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
)}
|
||||||
|
<Column
|
||||||
|
flexGrow={collapsed ? 1 : undefined}
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems={collapsed ? 'center' : undefined}
|
||||||
|
>
|
||||||
|
{collapsed ? (
|
||||||
|
<Column gap="2" alignItems="center">
|
||||||
|
<ThemeButton />
|
||||||
|
<LanguageButton />
|
||||||
|
<PreferencesButton />
|
||||||
|
</Column>
|
||||||
|
) : (
|
||||||
|
<Row>
|
||||||
|
<ThemeButton />
|
||||||
|
<LanguageButton />
|
||||||
|
<PreferencesButton />
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Column>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
103
src/app/share/[slug]/[[...path]]/SharePage.tsx
Normal file
103
src/app/share/[slug]/[[...path]]/SharePage.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
'use client';
|
||||||
|
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useEffect, useState } 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';
|
||||||
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
|
import { useShare } from '@/components/hooks';
|
||||||
|
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
||||||
|
import { ShareNav } from './ShareNav';
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSharePath(pathname: string) {
|
||||||
|
const segments = pathname.split('/');
|
||||||
|
const firstSegment = segments[3];
|
||||||
|
|
||||||
|
// If first segment looks like a domain name, skip it
|
||||||
|
if (firstSegment?.includes('.')) {
|
||||||
|
return segments[4];
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SharePage() {
|
||||||
|
const [navCollapsed, setNavCollapsed] = useState(false);
|
||||||
|
const share = useShare();
|
||||||
|
const { setTheme } = useTheme();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const path = getSharePath(pathname);
|
||||||
|
const { websiteId, parameters = {} } = share;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const url = new URL(window?.location?.href);
|
||||||
|
const theme = url.searchParams.get('theme');
|
||||||
|
|
||||||
|
if (theme === 'light' || theme === 'dark') {
|
||||||
|
setTheme(theme);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<Grid columns={{ xs: '1fr', lg: `${navCollapsed ? '60px' : '240px'} 1fr` }} width="100%">
|
||||||
|
<Row display={{ xs: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
||||||
|
<MobileMenuButton>
|
||||||
|
{({ close }) => {
|
||||||
|
return <ShareNav onItemClick={close} />;
|
||||||
|
}}
|
||||||
|
</MobileMenuButton>
|
||||||
|
</Row>
|
||||||
|
<Column display={{ xs: 'none', lg: 'flex' }} marginRight="2">
|
||||||
|
<ShareNav collapsed={navCollapsed} onCollapse={setNavCollapsed} />
|
||||||
|
</Column>
|
||||||
|
<PageBody gap>
|
||||||
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
|
<Column>
|
||||||
|
<WebsiteHeader showActions={false} />
|
||||||
|
<PageComponent websiteId={websiteId} />
|
||||||
|
</Column>
|
||||||
|
</WebsiteProvider>
|
||||||
|
</PageBody>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/app/share/[slug]/[[...path]]/page.tsx
Normal file
5
src/app/share/[slug]/[[...path]]/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { SharePage } from './SharePage';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return <SharePage />;
|
||||||
|
}
|
||||||
13
src/app/share/[slug]/layout.tsx
Normal file
13
src/app/share/[slug]/layout.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { ShareProvider } from '@/app/share/ShareProvider';
|
||||||
|
|
||||||
|
export default async function ({
|
||||||
|
params,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { slug } = await params;
|
||||||
|
|
||||||
|
return <ShareProvider slug={slug}>{children}</ShareProvider>;
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
NavMenuItem,
|
NavMenuItem,
|
||||||
type NavMenuProps,
|
type NavMenuProps,
|
||||||
Row,
|
Row,
|
||||||
|
Text,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
|
@ -42,9 +43,11 @@ export function SideMenu({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={id} href={path}>
|
<Link key={id} href={path}>
|
||||||
<NavMenuItem isSelected={isSelected}>
|
<Row padding hoverBackgroundColor="3">
|
||||||
<IconLabel icon={icon}>{label}</IconLabel>
|
<IconLabel icon={icon}>
|
||||||
</NavMenuItem>
|
<Text weight={isSelected ? 'bold' : undefined}>{label}</Text>
|
||||||
|
</IconLabel>
|
||||||
|
</Row>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
6
src/components/hooks/context/useShare.ts
Normal file
6
src/components/hooks/context/useShare.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { ShareContext } from '@/app/share/ShareProvider';
|
||||||
|
|
||||||
|
export function useShare() {
|
||||||
|
return useContext(ShareContext);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
// Context hooks
|
// Context hooks
|
||||||
export * from './context/useLink';
|
export * from './context/useLink';
|
||||||
export * from './context/usePixel';
|
export * from './context/usePixel';
|
||||||
|
export * from './context/useShare';
|
||||||
export * from './context/useTeam';
|
export * from './context/useTeam';
|
||||||
export * from './context/useUser';
|
export * from './context/useUser';
|
||||||
export * from './context/useWebsite';
|
export * from './context/useWebsite';
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,21 @@
|
||||||
import { setShareToken, useApp } from '@/store/app';
|
import { setShare, useApp } from '@/store/app';
|
||||||
import { useApi } from '../useApi';
|
import { useApi } from '../useApi';
|
||||||
|
|
||||||
const selector = (state: { shareToken: string }) => state.shareToken;
|
const selector = state => state.share;
|
||||||
|
|
||||||
export function useShareTokenQuery(slug: string): {
|
export function useShareTokenQuery(slug: string) {
|
||||||
shareToken: any;
|
const share = useApp(selector);
|
||||||
isLoading?: boolean;
|
|
||||||
error?: Error;
|
|
||||||
} {
|
|
||||||
const shareToken = useApp(selector);
|
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { isLoading, error } = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['share', slug],
|
queryKey: ['share', slug],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const data = await get(`/share/${slug}`);
|
const data = await get(`/share/${slug}`);
|
||||||
|
|
||||||
setShareToken(data);
|
setShare(data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { shareToken, isLoading, error };
|
return { share, ...query };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ export function MobileMenuButton(props: DialogProps) {
|
||||||
<Menu />
|
<Menu />
|
||||||
</Icon>
|
</Icon>
|
||||||
</Button>
|
</Button>
|
||||||
<Modal placement="left" offset="80px">
|
<Modal placement="left">
|
||||||
<Dialog variant="sheet" {...props} />
|
<Dialog variant="sheet" {...props} style={{ width: 'auto' }} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,11 @@ export function WebsiteDateFilter({
|
||||||
const showCompare = allowCompare && !isAllTime;
|
const showCompare = allowCompare && !isAllTime;
|
||||||
|
|
||||||
const websiteDateRange = useDateRangeQuery(websiteId);
|
const websiteDateRange = useDateRangeQuery(websiteId);
|
||||||
|
const { startDate, endDate } = websiteDateRange;
|
||||||
|
const hasData = startDate && endDate;
|
||||||
|
|
||||||
const handleChange = (date: string) => {
|
const handleChange = (date: string) => {
|
||||||
if (date === 'all') {
|
if (date === 'all' && hasData) {
|
||||||
router.push(
|
router.push(
|
||||||
updateParams({
|
updateParams({
|
||||||
date: `${getDateRangeValue(websiteDateRange.startDate, websiteDateRange.endDate)}:all`,
|
date: `${getDateRangeValue(websiteDateRange.startDate, websiteDateRange.endDate)}:all`,
|
||||||
|
|
@ -78,7 +80,7 @@ export function WebsiteDateFilter({
|
||||||
<DateFilter
|
<DateFilter
|
||||||
value={dateValue}
|
value={dateValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
showAllTime={showAllTime}
|
showAllTime={hasData && showAllTime}
|
||||||
renderDate={+offset !== 0}
|
renderDate={+offset !== 0}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}
|
||||||
|
|
||||||
if (name === 'referrer') {
|
if (name === 'referrer') {
|
||||||
arr.push(
|
arr.push(
|
||||||
`and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`,
|
`and (website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '') or website_event.referrer_domain is null)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query
|
||||||
let excludeDomain = '';
|
let excludeDomain = '';
|
||||||
|
|
||||||
if (column === 'referrer_domain') {
|
if (column === 'referrer_domain') {
|
||||||
excludeDomain = `and website_event.referrer_domain != website_event.hostname
|
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
||||||
and website_event.referrer_domain != ''`;
|
and website_event.referrer_domain != ''`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ async function relationalQuery(
|
||||||
let excludeDomain = '';
|
let excludeDomain = '';
|
||||||
|
|
||||||
if (column === 'referrer_domain') {
|
if (column === 'referrer_domain') {
|
||||||
excludeDomain = `and website_event.referrer_domain != website_event.hostname
|
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
||||||
and website_event.referrer_domain != ''`;
|
and website_event.referrer_domain != ''`;
|
||||||
if (type === 'domain') {
|
if (type === 'domain') {
|
||||||
column = toPostgresGroupedReferrer(GROUPED_DOMAINS);
|
column = toPostgresGroupedReferrer(GROUPED_DOMAINS);
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ async function relationalQuery(
|
||||||
let excludeDomain = '';
|
let excludeDomain = '';
|
||||||
|
|
||||||
if (column === 'referrer_domain') {
|
if (column === 'referrer_domain') {
|
||||||
excludeDomain = `and website_event.referrer_domain != website_event.hostname
|
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
||||||
and website_event.referrer_domain != ''`;
|
and website_event.referrer_domain != ''`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ async function relationalQuery(
|
||||||
${
|
${
|
||||||
currency
|
currency
|
||||||
? ''
|
? ''
|
||||||
: `and we.referrer_domain != hostname
|
: `and we.referrer_domain != regexp_replace(we.hostname, '^www.', '')
|
||||||
and we.referrer_domain != ''`
|
and we.referrer_domain != ''`
|
||||||
}
|
}
|
||||||
group by 1
|
group by 1
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const initialState = {
|
||||||
theme: getItem(THEME_CONFIG) || DEFAULT_THEME,
|
theme: getItem(THEME_CONFIG) || DEFAULT_THEME,
|
||||||
timezone: getItem(TIMEZONE_CONFIG) || getTimezone(),
|
timezone: getItem(TIMEZONE_CONFIG) || getTimezone(),
|
||||||
dateRangeValue: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
|
dateRangeValue: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
|
||||||
shareToken: null,
|
share: null,
|
||||||
user: null,
|
user: null,
|
||||||
config: null,
|
config: null,
|
||||||
};
|
};
|
||||||
|
|
@ -31,8 +31,8 @@ export function setLocale(locale: string) {
|
||||||
store.setState({ locale });
|
store.setState({ locale });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setShareToken(shareToken: string) {
|
export function setShare(share: object) {
|
||||||
store.setState({ shareToken });
|
store.setState({ share });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setUser(user: object) {
|
export function setUser(user: object) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue