apply white-labeling to all share pages

This commit is contained in:
Francis Cao
2026-03-13 14:46:10 -07:00
parent 5a7b651f3d
commit b823a6ca3e
5 changed files with 98 additions and 80 deletions
+17 -40
View File
@@ -1,11 +1,11 @@
import { getBoardWebsiteIds } from '@/lib/boards';
import { ENTITY_TYPE, ROLES } from '@/lib/constants';
import { secret } from '@/lib/crypto';
import { createToken } from '@/lib/jwt';
import { getBoardWebsiteIds } from '@/lib/boards';
import prisma from '@/lib/prisma';
import redis from '@/lib/redis';
import { json, notFound } from '@/lib/response';
import type { WhiteLabel } from '@/lib/types';
import type { BoardParameters, WhiteLabel } from '@/lib/types';
import { getBoard, getLink, getPixel, getShareByCode, getWebsite } from '@/queries/prisma';
async function getAccountId(entity: { userId?: string; teamId?: string }): Promise<string | null> {
@@ -53,57 +53,35 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
return notFound();
}
if (share.shareType === ENTITY_TYPE.board) {
const board = await getBoard(share.entityId);
if (!board) {
return notFound();
}
const data: Record<string, any> = {
shareId: share.id,
shareType: share.shareType,
boardId: share.entityId,
parameters: share.parameters,
websiteIds: getBoardWebsiteIds(board),
};
data.token = createToken(data, secret());
return json(data);
}
let entity: { userId?: string; teamId?: string } | null = null;
const data: Record<string, any> = {
shareId: share.id,
shareType: share.shareType,
parameters: share.parameters,
};
if (share.shareType === ENTITY_TYPE.website) {
let entity: { userId?: string; teamId?: string } | null = null;
if (share.shareType === ENTITY_TYPE.board) {
const board = await getBoard(share.entityId);
if (!board) return notFound();
entity = board;
data.boardId = share.entityId;
data.websiteIds = getBoardWebsiteIds({
type: board.type,
parameters: share.parameters as BoardParameters,
});
} else if (share.shareType === ENTITY_TYPE.website) {
entity = await getWebsite(share.entityId);
if (!entity) {
return notFound();
}
if (!entity) return notFound();
data.websiteId = share.entityId;
} else if (share.shareType === ENTITY_TYPE.pixel) {
entity = await getPixel(share.entityId);
if (!entity) {
return notFound();
}
if (!entity) return notFound();
data.websiteId = share.entityId;
data.pixelId = share.entityId;
} else if (share.shareType === ENTITY_TYPE.link) {
entity = await getLink(share.entityId);
if (!entity) {
return notFound();
}
if (!entity) return notFound();
data.websiteId = share.entityId;
data.linkId = share.entityId;
} else {
@@ -116,7 +94,6 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
if (accountId) {
const whiteLabel = await getWhiteLabel(accountId);
if (whiteLabel) {
data.whiteLabel = whiteLabel;
}
@@ -0,0 +1,32 @@
'use client';
import { useShare } from '@/components/hooks';
import { Logo } from '@/components/svg';
import { Icon, Row, Text } from '@umami/react-zen';
const LOGO_SIZE = { sm: 24, md: 32, lg: 40 };
const TEXT_SIZE = { sm: 'sm', md: 'base', lg: 'lg' } as const;
export function ShareBranding({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' }) {
const share = useShare();
const logoDomain = share?.whiteLabel?.domainName || 'https://umami.is';
const logoName = share?.whiteLabel?.displayName || 'umami';
const logoImage = share?.whiteLabel?.logoUrl;
const height = LOGO_SIZE[size];
return (
<a href={logoDomain} target="_blank" rel="noopener" style={{ marginLeft: 12 }}>
<Row alignItems="center" gap>
{logoImage ? (
<img src={logoImage} alt={logoName} style={{ height }} />
) : (
<Icon>
<Logo />
</Icon>
)}
<Text size={TEXT_SIZE[size]} weight="bold">
{logoName}
</Text>
</Row>
</a>
);
}
@@ -0,0 +1,25 @@
'use client';
import { useShare } from '@/components/hooks';
import { Column, Row } from '@umami/react-zen';
import { ShareBranding } from './ShareBranding';
export function ShareFooter() {
const share = useShare();
if (!share?.whiteLabel) {
return null;
}
return (
<Column
paddingX={{ base: '3', md: '6' }}
width="100%"
maxWidth="1320px"
style={{ margin: '0 auto' }}
>
<Row border="top" justifyContent="flex-end" paddingY="4">
<ShareBranding size="sm" />
</Row>
</Column>
);
}
+5 -21
View File
@@ -1,10 +1,11 @@
import { Button, Column, Icon, Row, Text, ThemeButton } from '@umami/react-zen';
import { Button, Column, Icon, Row, ThemeButton } from '@umami/react-zen';
import { NavMenu } from '@/components/common/NavMenu';
import { useMessages, useNavigation, useShare } from '@/components/hooks';
import { AlignEndHorizontal, Clock, Eye, PanelLeft, Sheet, Tag, User } from '@/components/icons';
import { LanguageButton } from '@/components/input/LanguageButton';
import { PreferencesButton } from '@/components/input/PreferencesButton';
import { Funnel, Lightning, Logo, Magnet, Money, Network, Path, Target } from '@/components/svg';
import { Funnel, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg';
import { ShareBranding } from './ShareBranding';
export function ShareNav({
collapsed,
@@ -18,11 +19,7 @@ export function ShareNav({
const share = useShare();
const { t, labels } = useMessages();
const { pathname } = useNavigation();
const { slug, parameters, whiteLabel } = share;
const logoDomain = whiteLabel?.domainName || 'https://umami.is';
const logoName = whiteLabel?.displayName || 'umami';
const logoImage = whiteLabel?.logoUrl;
const { slug, parameters } = share;
const renderPath = (path: string) => `/share/${slug}${path}`;
@@ -149,20 +146,7 @@ export function ShareNav({
border={isMobile ? undefined : 'right'}
>
<Row as="header" gap alignItems="center" justifyContent="space-between">
{!collapsed && (
<a href={logoDomain} 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>
)}
{!collapsed && <ShareBranding size="md" />}
{!onItemClick && (
<Button variant="quiet" onPress={() => onCollapse?.(!collapsed)}>
<Icon color="muted">
+19 -19
View File
@@ -1,7 +1,5 @@
'use client';
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import { BoardViewPage } from '@/app/(main)/boards/[boardId]/BoardViewPage';
import { LinkPage } from '@/app/(main)/links/[linkId]/LinkPage';
import { PixelPage } from '@/app/(main)/pixels/[pixelId]/PixelPage';
import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage';
@@ -19,11 +17,14 @@ import { SessionsPage } from '@/app/(main)/websites/[websiteId]/sessions/Session
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
import { BoardViewPage } from '@/app/(main)/boards/[boardId]/BoardViewPage';
import { PageBody } from '@/components/common/PageBody';
import { useShare } from '@/components/hooks';
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
import { ENTITY_TYPE } from '@/lib/constants';
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import { ShareFooter } from './ShareFooter';
import { ShareNav } from './ShareNav';
const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>> = {
@@ -72,25 +73,24 @@ export function SharePage() {
}
}, [setTheme]);
if (shareType === ENTITY_TYPE.board && boardId) {
const entityPage =
shareType === ENTITY_TYPE.board && boardId ? (
<BoardViewPage boardId={boardId} showActions={false} />
) : shareType === ENTITY_TYPE.pixel && pixelId ? (
<PixelPage pixelId={pixelId} showHeaderActions={false} />
) : shareType === ENTITY_TYPE.link && linkId ? (
<LinkPage linkId={linkId} showHeaderActions={false} />
) : null;
if (entityPage) {
return (
<BoardViewPage
boardId={boardId}
showActions={false}
showControls={false}
showEntityBadges={false}
/>
<Column>
{entityPage}
<ShareFooter />
</Column>
);
}
if (shareType === ENTITY_TYPE.pixel && pixelId) {
return <PixelPage pixelId={pixelId} showHeaderActions={false} />;
}
if (shareType === ENTITY_TYPE.link && linkId) {
return <LinkPage linkId={linkId} showHeaderActions={false} />;
}
// Check if the requested path is allowed
const pageKey = path || '';
const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false;