From ac9edb8b5f2c7963b6f2320eb0da9ddf7b60ef7f Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 9 Oct 2025 18:08:59 -0700 Subject: [PATCH] Updated realtime page. --- .../realtime/RealtimeCountries.module.css | 5 - .../realtime/RealtimeCountries.tsx | 8 +- .../realtime/RealtimeHeader.module.css | 21 ----- .../[websiteId]/realtime/RealtimeHeader.tsx | 3 +- .../realtime/RealtimeLog.module.css | 85 ----------------- .../[websiteId]/realtime/RealtimeLog.tsx | 61 ++++++------ .../[websiteId]/realtime/RealtimePage.tsx | 10 +- .../[websiteId]/realtime/RealtimePaths.tsx | 45 +++++++++ .../realtime/RealtimeReferrers.tsx | 45 +++++++++ .../[websiteId]/realtime/RealtimeUrls.tsx | 94 ------------------- src/components/common/Avatar.tsx | 31 +----- 11 files changed, 131 insertions(+), 277 deletions(-) delete mode 100644 src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.module.css delete mode 100644 src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.module.css delete mode 100644 src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.module.css create mode 100644 src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx create mode 100644 src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx delete mode 100644 src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.module.css b/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.module.css deleted file mode 100644 index e55063c3..00000000 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.row { - display: flex; - align-items: center; - gap: 10px; -} diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx index 2c16bced..c4ee9c02 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx @@ -1,8 +1,7 @@ import { useCallback } from 'react'; +import { IconLabel } from '@umami/react-zen'; import { ListTable } from '@/components/metrics/ListTable'; import { useLocale, useCountryNames, useMessages } from '@/components/hooks'; -import classNames from 'classnames'; -import styles from './RealtimeCountries.module.css'; import { TypeIcon } from '@/components/common/TypeIcon'; export function RealtimeCountries({ data }) { @@ -12,10 +11,7 @@ export function RealtimeCountries({ data }) { const renderCountryName = useCallback( ({ label: code }) => ( - - - {countryNames[code]} - + } label={countryNames[code]} /> ), [countryNames, locale], ); diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.module.css b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.module.css deleted file mode 100644 index f87d86e8..00000000 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.module.css +++ /dev/null @@ -1,21 +0,0 @@ -.header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 20px; -} - -.metrics { - display: flex; - flex-wrap: wrap; -} - -.card { - justify-self: flex-start; -} - -@media only screen and (max-width: 992px) { - .card { - flex-basis: calc(50% - 20px); - } -} diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx index 7f4df048..cdd67e7a 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx @@ -1,9 +1,8 @@ import { MetricCard } from '@/components/metrics/MetricCard'; import { useMessages } from '@/components/hooks'; -import { RealtimeData } from '@/lib/types'; import { MetricsBar } from '@/components/metrics/MetricsBar'; -export function RealtimeHeader({ data }: { data: RealtimeData }) { +export function RealtimeHeader({ data }: { data: any }) { const { formatMessage, labels } = useMessages(); const { totals }: any = data || {}; diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.module.css b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.module.css deleted file mode 100644 index 3703baa3..00000000 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.module.css +++ /dev/null @@ -1,85 +0,0 @@ -.table { - font-size: var(--font-size-sm); - overflow: hidden; - height: 100%; -} - -.header { - display: flex; - align-items: center; - justify-content: space-between; - font-size: var(--font-size-md); - line-height: 40px; - font-weight: 700; -} - -.row { - display: flex; - align-items: center; - gap: 10px; - height: 50px; - border-bottom: 1px solid var(--base300); -} - -.body { - overflow: auto; - height: 100%; -} - -.icon { - margin-inline-end: 10px; -} - -.time { - min-width: 60px; - overflow: hidden; -} - -.detail { - display: flex; - align-items: center; - flex: 1; - gap: 10px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -} - -.detail > span { - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} - -.row .link { - color: var(--base900); - text-decoration: none; -} - -.row .link:hover { - color: var(--primary-color); -} - -.search { - max-width: 300px; -} - -.actions { - display: flex; - gap: 20px; - align-items: center; - justify-content: space-between; - margin-bottom: 10px; -} - -@media only screen and (max-width: 992px) { - .actions { - flex-direction: column; - } - - .search { - max-width: 100%; - } -} diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx index 8cd31016..9ae19bf8 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx @@ -1,3 +1,7 @@ +import { useMemo, useState } from 'react'; +import { FixedSizeList } from 'react-window'; +import { SearchField, Text, Column, Row, IconLabel, Heading } from '@umami/react-zen'; +import Link from 'next/link'; import { useFormat } from '@/components//hooks/useFormat'; import { Empty } from '@/components/common/Empty'; import { FilterButtons } from '@/components/input/FilterButtons'; @@ -5,17 +9,15 @@ import { useCountryNames, useLocale, useMessages, + useNavigation, useTimezone, useWebsite, } from '@/components/hooks'; import { Eye, User } from '@/components/icons'; import { Lightning } from '@/components/svg'; import { BROWSERS, OS_NAMES } from '@/lib/constants'; -import { stringToColor } from '@/lib/format'; -import { useMemo, useState } from 'react'; -import { Icon, SearchField, StatusLight, Text } from '@umami/react-zen'; -import { FixedSizeList } from 'react-window'; -import styles from './RealtimeLog.module.css'; +import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal'; +import { Avatar } from '@/components/common/Avatar'; const TYPE_ALL = 'all'; const TYPE_PAGEVIEW = 'pageview'; @@ -37,6 +39,7 @@ export function RealtimeLog({ data }: { data: any }) { const { formatTimezoneDate } = useTimezone(); const { countryNames } = useCountryNames(locale); const [filter, setFilter] = useState(TYPE_ALL); + const { updateParams } = useNavigation(); const buttons = [ { @@ -59,8 +62,6 @@ export function RealtimeLog({ data }: { data: any }) { const getTime = ({ createdAt, firstAt }) => formatTimezoneDate(firstAt || createdAt, 'pp'); - const getColor = ({ id, sessionId }) => stringToColor(sessionId || id); - const getIcon = ({ __type }) => icons[__type]; const getDetail = (log: { @@ -84,7 +85,6 @@ export function RealtimeLog({ data }: { data: any }) { @@ -98,12 +98,7 @@ export function RealtimeLog({ data }: { data: any }) { if (__type === TYPE_PAGEVIEW) { return ( - + {urlPath} ); @@ -124,19 +119,18 @@ export function RealtimeLog({ data }: { data: any }) { } }; - const Row = ({ index, style }) => { + const TableRow = ({ index, style }) => { const row = logs[index]; return ( -
-
- -
-
{getTime(row)}
-
- {getIcon(row)} + + + + + {getTime(row)} + {getDetail(row)} -
-
+
+ ); }; @@ -172,20 +166,21 @@ export function RealtimeLog({ data }: { data: any }) { }, [data, filter, formatValue, search]); return ( -
-
- + + {formatMessage(labels.activity)} + + -
-
{formatMessage(labels.activity)}
-
+ + {logs?.length === 0 && } {logs?.length > 0 && ( - {Row} + {TableRow} )} -
-
+ + + ); } diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx index e0ffd00c..7f9ab608 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimePage.tsx @@ -9,7 +9,8 @@ import { WorldMap } from '@/components/metrics/WorldMap'; import { useRealtimeQuery } from '@/components/hooks'; import { RealtimeLog } from './RealtimeLog'; import { RealtimeHeader } from './RealtimeHeader'; -import { RealtimeUrls } from './RealtimeUrls'; +import { RealtimePaths } from './RealtimePaths'; +import { RealtimeReferrers } from './RealtimeReferrers'; import { RealtimeCountries } from './RealtimeCountries'; import { percentFilter } from '@/lib/filters'; @@ -32,12 +33,15 @@ export function RealtimePage({ websiteId }: { websiteId: string }) { + + + - + - + diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx new file mode 100644 index 00000000..91c6b4d8 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx @@ -0,0 +1,45 @@ +import thenby from 'thenby'; +import { percentFilter } from '@/lib/filters'; +import { ListTable } from '@/components/metrics/ListTable'; +import { useMessages, useWebsite } from '@/components/hooks'; + +export function RealtimePaths({ data }: { data: any }) { + const website = useWebsite(); + const { formatMessage, labels } = useMessages(); + const { urls } = data || {}; + const limit = 15; + + const renderLink = ({ label: x }) => { + const domain = x.startsWith('/') ? website?.domain : ''; + return ( + + {x} + + ); + }; + + const pages = percentFilter( + Object.keys(urls) + .map(key => { + return { + x: key, + y: urls[key], + }; + }) + .sort(thenby.firstBy('y', -1)) + .slice(0, limit), + ); + + return ( + ({ + label: x, + count: y, + percent: z, + }))} + /> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx new file mode 100644 index 00000000..85326cd0 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx @@ -0,0 +1,45 @@ +import thenby from 'thenby'; +import { percentFilter } from '@/lib/filters'; +import { ListTable } from '@/components/metrics/ListTable'; +import { useMessages, useWebsite } from '@/components/hooks'; + +export function RealtimeReferrers({ data }: { data: any }) { + const website = useWebsite(); + const { formatMessage, labels } = useMessages(); + const { referrers } = data || {}; + const limit = 15; + + const renderLink = ({ label: x }) => { + const domain = x.startsWith('/') ? website?.domain : ''; + return ( + + {x} + + ); + }; + + const domains = percentFilter( + Object.keys(referrers) + .map(key => { + return { + x: key, + y: referrers[key], + }; + }) + .sort(thenby.firstBy('y', -1)) + .slice(0, limit), + ); + + return ( + ({ + label: x, + count: y, + percent: z, + }))} + /> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx deleted file mode 100644 index 5bb51c92..00000000 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useState } from 'react'; -import { Row } from '@umami/react-zen'; -import thenby from 'thenby'; -import { percentFilter } from '@/lib/filters'; -import { ListTable } from '@/components/metrics/ListTable'; -import { useMessages, useWebsite } from '@/components/hooks'; -import { FilterButtons } from '@/components/input/FilterButtons'; - -const FILTER_REFERRERS = 'filter-referrers'; -const FILTER_PAGES = 'filter-pages'; - -export function RealtimeUrls({ data }: { data: any }) { - const website = useWebsite(); - const { formatMessage, labels } = useMessages(); - const { referrers, urls } = data || {}; - const [filter, setFilter] = useState(FILTER_REFERRERS); - const limit = 15; - - const buttons = [ - { - id: FILTER_REFERRERS, - label: formatMessage(labels.referrers), - }, - { - id: FILTER_PAGES, - label: formatMessage(labels.pages), - }, - ]; - - const renderLink = ({ label: x }) => { - const domain = x.startsWith('/') ? website?.domain : ''; - return ( - - {x} - - ); - }; - - const domains = percentFilter( - Object.keys(referrers) - .map(key => { - return { - x: key, - y: referrers[key], - }; - }) - .sort(thenby.firstBy('y', -1)) - .slice(0, limit), - ); - - const pages = percentFilter( - Object.keys(urls) - .map(key => { - return { - x: key, - y: urls[key], - }; - }) - .sort(thenby.firstBy('y', -1)) - .slice(0, limit), - ); - - return ( - <> - - - - {filter === FILTER_REFERRERS && ( - ({ - label: x, - count: y, - percent: z, - }))} - /> - )} - {filter === FILTER_PAGES && ( - ({ - label: x, - count: y, - percent: z, - }))} - /> - )} - - ); -} diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 74c3d7ae..f8ed7c94 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -1,4 +1,3 @@ -'use client'; import { useMemo } from 'react'; import { createAvatar } from '@dicebear/core'; import { lorelei } from '@dicebear/collection'; @@ -6,41 +5,17 @@ import { getColor, getPastel } from '@/lib/colors'; const lib = lorelei; -// ✅ Modern UTF-8 safe base64 encoder (no deprecated APIs) -function toBase64(str: string): string { - if (typeof window === 'undefined') { - // Server (Node.js) - return Buffer.from(str, 'utf-8').toString('base64'); - } else { - // Browser (UTF-8 safe) - const encoder = new TextEncoder(); - const bytes = encoder.encode(str); - let binary = ''; - const chunkSize = 0x8000; - - for (let i = 0; i < bytes.length; i += chunkSize) { - const chunk = bytes.subarray(i, i + chunkSize); - binary += String.fromCharCode(...chunk); - } - - return btoa(binary); - } -} - export function Avatar({ seed, size = 128, ...props }: { seed: string; size?: number }) { const backgroundColor = getPastel(getColor(seed), 4); const avatar = useMemo(() => { - const svg = createAvatar(lib, { + return createAvatar(lib, { ...props, seed, size, backgroundColor: [backgroundColor], - }).toString(); - - const base64 = toBase64(svg); - return `data:image/svg+xml;base64,${base64}`; - }, [seed, size, backgroundColor, props]); + }).toDataUri(); + }, []); return Avatar; }