Use FormattedMessage. Updated icons. Fixed bugs.

This commit is contained in:
Mike Cao 2025-09-23 23:08:40 -07:00
parent 3afe843461
commit 83a014e884
20 changed files with 129 additions and 84 deletions

View file

@ -15,7 +15,7 @@ export function LinkDeleteButton({
name: string; name: string;
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error, touch } = useDeleteQuery(`/links/${linkId}`); const { mutateAsync, isPending, error, touch } = useDeleteQuery(`/links/${linkId}`);
const handleConfirm = async (close: () => void) => { const handleConfirm = async (close: () => void) => {
@ -33,9 +33,14 @@ export function LinkDeleteButton({
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}> <Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => ( {({ close }) => (
<ConfirmationForm <ConfirmationForm
message={formatMessage(messages.confirmRemove, { message={
target: name, <FormattedMessage
})} {...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending} isLoading={isPending}
error={getErrorMessage(error)} error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)} onConfirm={handleConfirm.bind(null, close)}

View file

@ -13,7 +13,7 @@ export function PixelDeleteButton({
name: string; name: string;
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error } = useDeleteQuery(`/pixels/${pixelId}`); const { mutateAsync, isPending, error } = useDeleteQuery(`/pixels/${pixelId}`);
const { touch } = useModified(); const { touch } = useModified();
@ -32,9 +32,14 @@ export function PixelDeleteButton({
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}> <Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => ( {({ close }) => (
<ConfirmationForm <ConfirmationForm
message={formatMessage(messages.confirmRemove, { message={
target: name, <FormattedMessage
})} {...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending} isLoading={isPending}
error={getErrorMessage(error)} error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)} onConfirm={handleConfirm.bind(null, close)}

View file

@ -14,7 +14,7 @@ export function TeamLeaveForm({
onSave: () => void; onSave: () => void;
onClose: () => void; onClose: () => void;
}) { }) {
const { formatMessage, labels, messages, getErrorMessage } = useMessages(); const { formatMessage, labels, messages, getErrorMessage, FormattedMessage } = useMessages();
const { mutateAsync, error, isPending } = useDeleteQuery(`/teams/${teamId}/users/${userId}`); const { mutateAsync, error, isPending } = useDeleteQuery(`/teams/${teamId}/users/${userId}`);
const { touch } = useModified(); const { touch } = useModified();
@ -31,9 +31,14 @@ export function TeamLeaveForm({
return ( return (
<ConfirmationForm <ConfirmationForm
buttonLabel={formatMessage(labels.leave)} buttonLabel={formatMessage(labels.leave)}
message={formatMessage(messages.confirmLeave, { message={
target: teamName, <FormattedMessage
})} {...messages.confirmLeave}
values={{
target: <b>{teamName}</b>,
}}
/>
}
onConfirm={handleConfirm} onConfirm={handleConfirm}
onClose={onClose} onClose={onClose}
isLoading={isPending} isLoading={isPending}

View file

@ -1,16 +1,9 @@
import { ReactNode } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { DataGrid } from '@/components/common/DataGrid'; import { DataGrid } from '@/components/common/DataGrid';
import { TeamsTable } from './TeamsTable'; import { TeamsTable } from './TeamsTable';
import { useLoginQuery, useNavigation, useUserTeamsQuery } from '@/components/hooks'; import { useLoginQuery, useNavigation, useUserTeamsQuery } from '@/components/hooks';
export function TeamsDataTable({ export function TeamsDataTable() {
showActions,
}: {
allowEdit?: boolean;
showActions?: boolean;
children?: ReactNode;
}) {
const { user } = useLoginQuery(); const { user } = useLoginQuery();
const query = useUserTeamsQuery(user.id); const query = useUserTeamsQuery(user.id);
const { pathname } = useNavigation(); const { pathname } = useNavigation();
@ -27,7 +20,7 @@ export function TeamsDataTable({
return ( return (
<DataGrid query={query}> <DataGrid query={query}>
{({ data }) => { {({ data }) => {
return <TeamsTable data={data} showActions={showActions} renderLink={renderLink} />; return <TeamsTable data={data} renderLink={renderLink} />;
}} }}
</DataGrid> </DataGrid>
); );

View file

@ -1,5 +1,5 @@
import { Button, Icon, Modal, DialogTrigger, Dialog, Text, useToast } from '@umami/react-zen'; import { Button, Icon, Modal, DialogTrigger, Dialog, Text, useToast } from '@umami/react-zen';
import { AddUser } from '@/components/icons'; import { UserPlus } from '@/components/icons';
import { useMessages, useModified } from '@/components/hooks'; import { useMessages, useModified } from '@/components/hooks';
import { TeamJoinForm } from './TeamJoinForm'; import { TeamJoinForm } from './TeamJoinForm';
@ -17,7 +17,7 @@ export function TeamsJoinButton() {
<DialogTrigger> <DialogTrigger>
<Button> <Button>
<Icon> <Icon>
<AddUser /> <UserPlus />
</Icon> </Icon>
<Text>{formatMessage(labels.joinTeam)}</Text> <Text>{formatMessage(labels.joinTeam)}</Text>
</Button> </Button>

View file

@ -17,7 +17,7 @@ export function TeamMemberRemoveButton({
disabled?: boolean; disabled?: boolean;
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error } = useDeleteQuery(`/teams/${teamId}/users/${userId}`); const { mutateAsync, isPending, error } = useDeleteQuery(`/teams/${teamId}/users/${userId}`);
const { touch } = useModified(); const { touch } = useModified();
@ -36,9 +36,14 @@ export function TeamMemberRemoveButton({
<Dialog title={formatMessage(labels.removeMember)} style={{ width: 400 }}> <Dialog title={formatMessage(labels.removeMember)} style={{ width: 400 }}>
{({ close }) => ( {({ close }) => (
<ConfirmationForm <ConfirmationForm
message={formatMessage(messages.confirmRemove, { message={
target: userName, <FormattedMessage
})} {...messages.confirmRemove}
values={{
target: <b>{userName}</b>,
}}
/>
}
isLoading={isPending} isLoading={isPending}
error={error} error={error}
onConfirm={handleConfirm.bind(null, close)} onConfirm={handleConfirm.bind(null, close)}

View file

@ -2,7 +2,7 @@ import Link from 'next/link';
import { Column, Icon, Text, Row } from '@umami/react-zen'; import { Column, Icon, Text, Row } from '@umami/react-zen';
import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks'; import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { Users, Arrow } from '@/components/icons'; import { Users, ArrowRight } from '@/components/icons';
import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton'; import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
import { TeamManage } from './TeamManage'; import { TeamManage } from './TeamManage';
import { TeamEditForm } from './TeamEditForm'; import { TeamEditForm } from './TeamEditForm';
@ -35,7 +35,7 @@ export function TeamSettings({ teamId }: { teamId: string }) {
<Link href="/settings/teams"> <Link href="/settings/teams">
<Row marginTop="2" alignItems="center" gap> <Row marginTop="2" alignItems="center" gap>
<Icon rotate={180}> <Icon rotate={180}>
<Arrow /> <ArrowRight />
</Icon> </Icon>
<Text>{formatMessage(labels.teams)}</Text> <Text>{formatMessage(labels.teams)}</Text>
</Row> </Row>

View file

@ -14,7 +14,7 @@ export function WebsitesPage() {
return ( return (
<PageBody> <PageBody>
<Column gap="6" margin="2"> <Column gap="6" margin="2">
<PageHeader title={formatMessage(labels.websites)}> <PageHeader title={formatMessage(labels.websites)} label={'back'} description={'Websites'}>
<WebsiteAddButton teamId={teamId} /> <WebsiteAddButton teamId={teamId} />
</PageHeader> </PageHeader>
<Panel> <Panel>

View file

@ -14,12 +14,12 @@ export function WebsiteFilterButton({
showText?: boolean; showText?: boolean;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { replaceParams, router } = useNavigation(); const { updateParams, router } = useNavigation();
const handleChange = ({ filters, segment, cohort }: any) => { const handleChange = ({ filters, segment, cohort }: any) => {
const params = filtersArrayToObject(filters); const params = filtersArrayToObject(filters);
const url = replaceParams({ ...params, segment, cohort }); const url = updateParams({ ...params, segment, cohort });
router.push(url); router.push(url);
}; };

View file

@ -16,7 +16,7 @@ export function CohortDeleteButton({
name: string; name: string;
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error, touch } = useDeleteQuery( const { mutateAsync, isPending, error, touch } = useDeleteQuery(
`/websites/${websiteId}/segments/${cohortId}`, `/websites/${websiteId}/segments/${cohortId}`,
); );
@ -36,9 +36,14 @@ export function CohortDeleteButton({
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}> <Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => ( {({ close }) => (
<ConfirmationForm <ConfirmationForm
message={formatMessage(messages.confirmRemove, { message={
target: name, <FormattedMessage
})} {...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending} isLoading={isPending}
error={error} error={error}
onConfirm={handleConfirm.bind(null, close)} onConfirm={handleConfirm.bind(null, close)}

View file

@ -3,7 +3,7 @@ import { useFormat, useMessages, useNavigation } from '@/components/hooks';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
import { Avatar } from '@/components/common/Avatar'; import { Avatar } from '@/components/common/Avatar';
import Link from 'next/link'; import Link from 'next/link';
import { Bolt, Eye } from '@/components/icons'; import { LightningSvg, Eye } from '@/components/icons';
import { DateDistance } from '@/components/common/DateDistance'; import { DateDistance } from '@/components/common/DateDistance';
import { TypeIcon } from '@/components/common/TypeIcon'; import { TypeIcon } from '@/components/common/TypeIcon';
@ -25,7 +25,7 @@ export function EventsTable({ data = [] }) {
<Link href={renderUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}> <Link href={renderUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}>
<Avatar seed={row.sessionId} size={32} /> <Avatar seed={row.sessionId} size={32} />
</Link> </Link>
<Icon>{row.eventName ? <Bolt /> : <Eye />}</Icon> <Icon>{row.eventName ? <LightningSvg /> : <Eye />}</Icon>
<Text> <Text>
{formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)} {formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
</Text> </Text>

View file

@ -8,7 +8,7 @@ import {
useTimezone, useTimezone,
useWebsite, useWebsite,
} from '@/components/hooks'; } from '@/components/hooks';
import { Eye, Visitor, Bolt } from '@/components/icons'; import { Eye, User, LightningSvg } from '@/components/icons';
import { BROWSERS, OS_NAMES } from '@/lib/constants'; import { BROWSERS, OS_NAMES } from '@/lib/constants';
import { stringToColor } from '@/lib/format'; import { stringToColor } from '@/lib/format';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
@ -23,14 +23,14 @@ const TYPE_EVENT = 'event';
const icons = { const icons = {
[TYPE_PAGEVIEW]: <Eye />, [TYPE_PAGEVIEW]: <Eye />,
[TYPE_SESSION]: <Visitor />, [TYPE_SESSION]: <User />,
[TYPE_EVENT]: <Bolt />, [TYPE_EVENT]: <LightningSvg />,
}; };
export function RealtimeLog({ data }: { data: any }) { export function RealtimeLog({ data }: { data: any }) {
const website = useWebsite(); const website = useWebsite();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { formatValue } = useFormat(); const { formatValue } = useFormat();
const { locale } = useLocale(); const { locale } = useLocale();
const { formatTimezoneDate } = useTimezone(); const { formatTimezoneDate } = useTimezone();
@ -74,20 +74,25 @@ export function RealtimeLog({ data }: { data: any }) {
const { __type, eventName, urlPath, browser, os, country, device } = log; const { __type, eventName, urlPath, browser, os, country, device } = log;
if (__type === TYPE_EVENT) { if (__type === TYPE_EVENT) {
return formatMessage(messages.eventLog, { return (
event: <b key="b">{eventName || formatMessage(labels.unknown)}</b>, <FormattedMessage
url: ( {...messages.eventLog}
<a values={{
key="a" event: <b key="b">{eventName || formatMessage(labels.unknown)}</b>,
href={`//${website?.domain}${urlPath}`} url: (
className={styles.link} <a
target="_blank" key="a"
rel="noreferrer noopener" href={`//${website?.domain}${urlPath}`}
> className={styles.link}
{urlPath} target="_blank"
</a> rel="noreferrer noopener"
), >
}); {urlPath}
</a>
),
}}
/>
);
} }
if (__type === TYPE_PAGEVIEW) { if (__type === TYPE_PAGEVIEW) {
@ -104,12 +109,17 @@ export function RealtimeLog({ data }: { data: any }) {
} }
if (__type === TYPE_SESSION) { if (__type === TYPE_SESSION) {
return formatMessage(messages.visitorLog, { return (
country: <b key="country">{countryNames[country] || formatMessage(labels.unknown)}</b>, <FormattedMessage
browser: <b key="browser">{BROWSERS[browser]}</b>, {...messages.visitorLog}
os: <b key="os">{OS_NAMES[os] || os}</b>, values={{
device: <b key="device">{formatMessage(labels[device] || labels.unknown)}</b>, country: <b key="country">{countryNames[country] || formatMessage(labels.unknown)}</b>,
}); browser: <b key="browser">{BROWSERS[browser]}</b>,
os: <b key="os">{OS_NAMES[os] || os}</b>,
device: <b key="device">{formatMessage(labels[device] || labels.unknown)}</b>,
}}
/>
);
} }
}; };

View file

@ -16,7 +16,7 @@ export function SegmentDeleteButton({
name: string; name: string;
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error, touch } = useDeleteQuery( const { mutateAsync, isPending, error, touch } = useDeleteQuery(
`/websites/${websiteId}/segments/${segmentId}`, `/websites/${websiteId}/segments/${segmentId}`,
); );
@ -36,9 +36,14 @@ export function SegmentDeleteButton({
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}> <Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => ( {({ close }) => (
<ConfirmationForm <ConfirmationForm
message={formatMessage(messages.confirmRemove, { message={
target: name, <FormattedMessage
})} {...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending} isLoading={isPending}
error={error} error={error}
onConfirm={handleConfirm.bind(null, close)} onConfirm={handleConfirm.bind(null, close)}

View file

@ -12,7 +12,7 @@ import {
Dialog, Dialog,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Bolt, Eye, FileText } from '@/components/icons'; import { LightningSvg, Eye, FileText } from '@/components/icons';
import { useMessages, useSessionActivityQuery, useTimezone } from '@/components/hooks'; import { useMessages, useSessionActivityQuery, useTimezone } from '@/components/hooks';
import { EventData } from '@/components/metrics/EventData'; import { EventData } from '@/components/metrics/EventData';
@ -52,7 +52,7 @@ export function SessionActivity({
{formatTimezoneDate(createdAt, 'pp')} {formatTimezoneDate(createdAt, 'pp')}
</StatusLight> </StatusLight>
<Row alignItems="center" gap="2"> <Row alignItems="center" gap="2">
<Icon>{eventName ? <Bolt /> : <Eye />}</Icon> <Icon>{eventName ? <LightningSvg /> : <Eye />}</Icon>
<Text> <Text>
{eventName {eventName
? formatMessage(labels.triggeredEvent) ? formatMessage(labels.triggeredEvent)

View file

@ -4,6 +4,7 @@ import { Heading, Icon, Row, RowProps, Text, Column } from '@umami/react-zen';
export function PageHeader({ export function PageHeader({
title, title,
description, description,
label,
icon, icon,
showBorder = true, showBorder = true,
children, children,
@ -11,6 +12,7 @@ export function PageHeader({
}: { }: {
title: string; title: string;
description?: string; description?: string;
label?: ReactNode;
icon?: ReactNode; icon?: ReactNode;
showBorder?: boolean; showBorder?: boolean;
allowEdit?: boolean; allowEdit?: boolean;
@ -26,7 +28,8 @@ export function PageHeader({
width="100%" width="100%"
{...props} {...props}
> >
<Column> <Column gap="2">
{label}
<Row alignItems="center" gap="3"> <Row alignItems="center" gap="3">
{icon && ( {icon && (
<Icon size="md" color="muted"> <Icon size="md" color="muted">

View file

@ -1,7 +1,22 @@
import { useIntl } from 'react-intl'; import { useIntl, FormattedMessage, type MessageDescriptor } from 'react-intl';
import { messages, labels } from '@/components/messages'; import { messages, labels } from '@/components/messages';
export function useMessages() { type FormatMessage = (
descriptor: MessageDescriptor,
values?: Record<string, string | number | boolean | null | undefined>,
opts?: any,
) => string | null;
interface UseMessages {
formatMessage: FormatMessage;
messages: typeof messages;
labels: typeof labels;
getMessage: (id: string) => string;
getErrorMessage: (error: unknown) => string | undefined;
FormattedMessage: typeof FormattedMessage;
}
export function useMessages(): UseMessages {
const intl = useIntl(); const intl = useIntl();
const getMessage = (id: string) => { const getMessage = (id: string) => {
@ -21,15 +36,12 @@ export function useMessages() {
}; };
const formatMessage = ( const formatMessage = (
descriptor: { descriptor: MessageDescriptor,
id: string; values?: Record<string, string | number | boolean | null | undefined>,
defaultMessage: string;
},
values?: Record<string, string>,
opts?: any, opts?: any,
) => { ) => {
return descriptor ? intl.formatMessage(descriptor, values, opts) : null; return descriptor ? intl.formatMessage(descriptor, values, opts) : null;
}; };
return { formatMessage, messages, labels, getMessage, getErrorMessage }; return { formatMessage, messages, labels, getMessage, getErrorMessage, FormattedMessage };
} }

View file

@ -1,17 +1,13 @@
export * from 'lucide-react'; export * from 'lucide-react';
export { export {
Logo as LogoSvg, Logo as LogoSvg,
Bolt as BoltSvg,
Change as ChangeSvg,
Compare as CompareSvg, Compare as CompareSvg,
Funnel as FunnelSvg, Funnel as FunnelSvg,
Lightbulb as LightbulbSvg,
Lightning as LightningSvg, Lightning as LightningSvg,
Location as LocationSvg, Location as LocationSvg,
Magnet as MagnetSvg, Magnet as MagnetSvg,
Money as MoneySvg, Money as MoneySvg,
Network as NetworkSvg, Network as NetworkSvg,
Path as PathSvg, Path as PathSvg,
Tag as TagSvg,
Target as TargetSvg, Target as TargetSvg,
} from '@/components/svg'; } from '@/components/svg';

View file

@ -18,7 +18,7 @@ export function WebsiteDateFilter({
showButtons = true, showButtons = true,
allowCompare, allowCompare,
}: WebsiteDateFilterProps) { }: WebsiteDateFilterProps) {
const { dateRange } = useDateRange(websiteId); const { dateRange, saveDateRange } = useDateRange(websiteId);
const { value, endDate } = dateRange; const { value, endDate } = dateRange;
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { const {
@ -32,6 +32,7 @@ export function WebsiteDateFilter({
const disableForward = value === 'all' || isAfter(endDate, new Date()); const disableForward = value === 'all' || isAfter(endDate, new Date());
const handleChange = (date: string) => { const handleChange = (date: string) => {
saveDateRange(date);
router.push(updateParams({ date, offset: undefined })); router.push(updateParams({ date, offset: undefined }));
}; };

View file

@ -85,7 +85,7 @@ export function WeeklyTraffic({ websiteId }: { websiteId: string }) {
height="16px" height="16px"
borderRadius="full" borderRadius="full"
style={{ margin: '0 auto' }} style={{ margin: '0 auto' }}
role="cell" role="button"
> >
<Row <Row
backgroundColor="primary" backgroundColor="primary"

View file

@ -136,7 +136,7 @@ export async function getQueryFilters(
...dateRange, ...dateRange,
...filters, ...filters,
page: params?.page, page: params?.page,
pageSize: params?.page ? params?.pageSize || DEFAULT_PAGE_SIZE : undefined, pageSize: params?.pageSize ? params?.pageSize || DEFAULT_PAGE_SIZE : undefined,
orderBy: params?.orderBy, orderBy: params?.orderBy,
sortDescending: params?.sortDescending, sortDescending: params?.sortDescending,
search: params?.search, search: params?.search,