diff --git a/src/app/share/ShareProvider.tsx b/src/app/share/ShareProvider.tsx new file mode 100644 index 00000000..fe4b5b3a --- /dev/null +++ b/src/app/share/ShareProvider.tsx @@ -0,0 +1,29 @@ +'use client'; +import { Loading } from '@umami/react-zen'; +import { createContext, type ReactNode } from 'react'; +import { useShareTokenQuery } from '@/components/hooks'; +import type { WhiteLabel } from '@/lib/types'; + +export interface ShareData { + shareId: string; + websiteId: string; + parameters: any; + token: string; + whiteLabel?: WhiteLabel; +} + +export const ShareContext = createContext(null); + +export function ShareProvider({ shareId, children }: { shareId: string; children: ReactNode }) { + const { share, isLoading, isFetching } = useShareTokenQuery(shareId); + + if (isFetching && isLoading) { + return ; + } + + if (!share) { + return null; + } + + return {children}; +} diff --git a/src/app/share/[...shareId]/ShareNav.tsx b/src/app/share/[...shareId]/ShareNav.tsx index b494046d..171539ac 100644 --- a/src/app/share/[...shareId]/ShareNav.tsx +++ b/src/app/share/[...shareId]/ShareNav.tsx @@ -1,21 +1,20 @@ -'use client'; -import { Column } from '@umami/react-zen'; +import { Column, Icon, Row, Text, ThemeButton } from '@umami/react-zen'; 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 { 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({ - shareId, - parameters, - onItemClick, -}: { - shareId: string; - parameters: Record; - onItemClick?: () => void; -}) { +export function ShareNav({ onItemClick }: { onItemClick?: () => void }) { + const share = useShare(); const { formatMessage, labels } = useMessages(); const { pathname } = useNavigation(); + const { shareId, parameters, whiteLabel } = share; + + const logoUrl = whiteLabel?.url || 'https://umami.is'; + const logoName = whiteLabel?.name || 'umami'; + const logoImage = whiteLabel?.image; const renderPath = (path: string) => `/share/${shareId}${path}`; @@ -131,13 +130,36 @@ export function ShareNav({ .find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id; return ( - - + + + + + {logoImage ? ( + {logoName} + ) : ( + + + + )} + {logoName} + + + + + + + + + + + + + ); } diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[...shareId]/SharePage.tsx index 91a8b298..aa0361c6 100644 --- a/src/app/share/[...shareId]/SharePage.tsx +++ b/src/app/share/[...shareId]/SharePage.tsx @@ -1,6 +1,6 @@ 'use client'; import { Column, Grid, Row, useTheme } from '@umami/react-zen'; -import { useRouter } from 'next/navigation'; +import { usePathname, 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'; @@ -18,10 +18,8 @@ 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 { useShare } 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> = { @@ -58,17 +56,20 @@ const ALL_SECTION_IDS = [ 'attribution', ]; -export function SharePage({ shareId, path = '' }: { shareId: string; path?: string }) { - const { shareToken, isLoading } = useShareTokenQuery(shareId); +export function SharePage({ shareId }: { shareId: string }) { + const share = useShare(); const { setTheme } = useTheme(); const router = useRouter(); + const pathname = usePathname(); + const path = pathname.split('/')[3]; + const { websiteId, parameters = {} } = share; // Calculate allowed sections const allowedSections = useMemo(() => { - if (!shareToken?.parameters) return []; - const params = shareToken.parameters; + if (!share?.parameters) return []; + const params = share.parameters; return ALL_SECTION_IDS.filter(id => params[id] !== false); - }, [shareToken?.parameters]); + }, [share?.parameters]); useEffect(() => { const url = new URL(window?.location?.href); @@ -90,12 +91,6 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri } }, [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 && @@ -116,40 +111,25 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage; return ( - - - - - - {({ close }) => { - return ; - }} - - - - - - + + + + {({ close }) => { + return ; + }} + + + + + + + + + + - - - - - - - - - - - - - + + + ); } diff --git a/src/app/share/[...shareId]/page.tsx b/src/app/share/[...shareId]/page.tsx index 3a21f836..7d080fe5 100644 --- a/src/app/share/[...shareId]/page.tsx +++ b/src/app/share/[...shareId]/page.tsx @@ -1,8 +1,13 @@ +import { ShareProvider } from '@/app/share/ShareProvider'; import { SharePage } from './SharePage'; export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) { const { shareId } = await params; - const [slug, ...path] = shareId; + const [slug] = shareId; - return ; + return ( + + + + ); } diff --git a/src/components/common/SideMenu.tsx b/src/components/common/SideMenu.tsx index 92ff798a..dd716d78 100644 --- a/src/components/common/SideMenu.tsx +++ b/src/components/common/SideMenu.tsx @@ -7,6 +7,7 @@ import { NavMenuItem, type NavMenuProps, Row, + Text, } from '@umami/react-zen'; import Link from 'next/link'; @@ -42,9 +43,11 @@ export function SideMenu({ return ( - - {label} - + + + {label} + + ); }); diff --git a/src/components/hooks/context/useShare.ts b/src/components/hooks/context/useShare.ts new file mode 100644 index 00000000..c7493c66 --- /dev/null +++ b/src/components/hooks/context/useShare.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react'; +import { ShareContext } from '@/app/share/ShareProvider'; + +export function useShare() { + return useContext(ShareContext); +} diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index f47f11f0..89cb904b 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -3,6 +3,7 @@ // Context hooks export * from './context/useLink'; export * from './context/usePixel'; +export * from './context/useShare'; export * from './context/useTeam'; export * from './context/useUser'; export * from './context/useWebsite'; diff --git a/src/components/hooks/queries/useShareTokenQuery.ts b/src/components/hooks/queries/useShareTokenQuery.ts index 446e33da..28820be0 100644 --- a/src/components/hooks/queries/useShareTokenQuery.ts +++ b/src/components/hooks/queries/useShareTokenQuery.ts @@ -1,25 +1,21 @@ -import { setShareToken, useApp } from '@/store/app'; +import { setShare, useApp } from '@/store/app'; import { useApi } from '../useApi'; -const selector = (state: { shareToken: string }) => state.shareToken; +const selector = state => state.share; -export function useShareTokenQuery(slug: string): { - shareToken: any; - isLoading?: boolean; - error?: Error; -} { - const shareToken = useApp(selector); +export function useShareTokenQuery(slug: string) { + const share = useApp(selector); const { get, useQuery } = useApi(); - const { isLoading, error } = useQuery({ + const query = useQuery({ queryKey: ['share', slug], queryFn: async () => { const data = await get(`/share/${slug}`); - setShareToken(data); + setShare(data); return data; }, }); - return { shareToken, isLoading, error }; + return { share, ...query }; } diff --git a/src/store/app.ts b/src/store/app.ts index bb54e565..e2a54a80 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -16,7 +16,7 @@ const initialState = { theme: getItem(THEME_CONFIG) || DEFAULT_THEME, timezone: getItem(TIMEZONE_CONFIG) || getTimezone(), dateRangeValue: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE, - shareToken: null, + share: null, user: null, config: null, }; @@ -31,8 +31,8 @@ export function setLocale(locale: string) { store.setState({ locale }); } -export function setShareToken(shareToken: string) { - store.setState({ shareToken }); +export function setShare(share: object) { + store.setState({ share }); } export function setUser(user: object) {