mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
Converted UTM report to a view.
This commit is contained in:
parent
06f76dda13
commit
d0d11225f4
24 changed files with 1815 additions and 1568 deletions
|
|
@ -235,3 +235,42 @@ model Report {
|
|||
@@index([name])
|
||||
@@map("report")
|
||||
}
|
||||
|
||||
model Board {
|
||||
id String @id() @unique() @map("board_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
teamId String? @map("team_id") @db.Uuid
|
||||
name String @map("name") @db.VarChar(200)
|
||||
description String @map("description") @db.VarChar(500)
|
||||
parameters Json @map("parameters") @db.JsonB
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
team Team? @relation(fields: [teamId], references: [id])
|
||||
|
||||
@@index([userId])
|
||||
@@index([teamId])
|
||||
@@index([type])
|
||||
@@index([name])
|
||||
@@map("board")
|
||||
}
|
||||
|
||||
model Block {
|
||||
id String @id() @unique() @map("block_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
name String @map("name") @db.VarChar(200)
|
||||
type String @map("type") @db.VarChar(20)
|
||||
parameters Json @map("parameters") @db.JsonB
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
website Website @relation(fields: [websiteId], references: [id])
|
||||
|
||||
@@index([userId])
|
||||
@@index([websiteId])
|
||||
@@index([type])
|
||||
@@index([name])
|
||||
@@map("block")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
"@react-spring/web": "^9.7.3",
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@tanstack/react-query": "^5.28.6",
|
||||
"@umami/react-zen": "^0.111.0",
|
||||
"@umami/react-zen": "^0.114.0",
|
||||
"@umami/redis-client": "^0.27.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chalk": "^4.1.1",
|
||||
|
|
@ -125,7 +125,7 @@
|
|||
"serialize-error": "^12.0.0",
|
||||
"thenby": "^1.3.4",
|
||||
"uuid": "^9.0.0",
|
||||
"zod": "^3.24.3",
|
||||
"zod": "^3.25.7",
|
||||
"zustand": "^4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -144,8 +144,8 @@
|
|||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.6.6",
|
||||
"esbuild": "^0.25.0",
|
||||
|
|
|
|||
2980
pnpm-lock.yaml
generated
2980
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
40
src/app/(main)/boards/BoardAddButton.tsx
Normal file
40
src/app/(main)/boards/BoardAddButton.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { useMessages, useModified, useNavigation } from '@/components/hooks';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Icons,
|
||||
Modal,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Text,
|
||||
useToast,
|
||||
} from '@umami/react-zen';
|
||||
import { BoardAddForm } from './BoardAddForm';
|
||||
|
||||
export function BoardAddButton() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { toast } = useToast();
|
||||
const { touch } = useModified();
|
||||
const { teamId } = useNavigation();
|
||||
|
||||
const handleSave = async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('boards');
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button data-test="button-website-add" variant="primary">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.addBoard)}</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.addBoard)}>
|
||||
{({ close }) => <BoardAddForm teamId={teamId} onSave={handleSave} onClose={close} />}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
64
src/app/(main)/boards/BoardAddForm.tsx
Normal file
64
src/app/(main)/boards/BoardAddForm.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { Form, FormField, FormSubmitButton, Row, TextField, Button } from '@umami/react-zen';
|
||||
import { useApi } from '@/components/hooks';
|
||||
import { DOMAIN_REGEX } from '@/lib/constants';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export function BoardAddForm({
|
||||
teamId,
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
teamId?: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post('/websites', { ...data, teamId }),
|
||||
});
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={error?.message}>
|
||||
<FormField
|
||||
label={formatMessage(labels.name)}
|
||||
data-test="input-name"
|
||||
name="name"
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
<TextField autoComplete="off" />
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label={formatMessage(labels.domain)}
|
||||
data-test="input-domain"
|
||||
name="domain"
|
||||
rules={{
|
||||
required: formatMessage(labels.required),
|
||||
pattern: { value: DOMAIN_REGEX, message: formatMessage(messages.invalidDomain) },
|
||||
}}
|
||||
>
|
||||
<TextField autoComplete="off" />
|
||||
</FormField>
|
||||
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||
{onClose && (
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
</Button>
|
||||
)}
|
||||
<FormSubmitButton data-test="button-submit" isDisabled={false}>
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
</Row>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import Link from 'next/link';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { BoardAddButton } from './BoardAddButton';
|
||||
|
||||
export function BoardsPage() {
|
||||
return (
|
||||
<Column>
|
||||
<PageHeader title="My Boards" />
|
||||
<Link href="/teams/3a97e34a-7f9d-4de2-8754-ed81714b528d/boards/86d4095c-a2a8-4fc8-9521-103e858e2b41">
|
||||
Board 1
|
||||
</Link>
|
||||
</Column>
|
||||
<PageBody>
|
||||
<Column>
|
||||
<PageHeader title="My Boards">
|
||||
<BoardAddButton />
|
||||
</PageHeader>
|
||||
<Link href="/teams/3a97e34a-7f9d-4de2-8754-ed81714b528d/boards/86d4095c-a2a8-4fc8-9521-103e858e2b41">
|
||||
Board 1
|
||||
</Link>
|
||||
</Column>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ export function WebsiteControls({
|
|||
showFilter?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Column marginBottom="6" gap="3">
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3" paddingY="3">
|
||||
<Column gap>
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3">
|
||||
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
|
||||
<Row alignItems="center" gap="3">
|
||||
<WebsiteDateFilter websiteId={websiteId} />
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { WebsiteExpandedView } from './WebsiteExpandedView';
|
|||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||
import { WebsiteTableView } from './WebsiteTableView';
|
||||
import { WebsiteCompareTables } from './WebsiteCompareTables';
|
||||
import { WebsiteControls } from './WebsiteControls';
|
||||
|
||||
export function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
|
||||
const {
|
||||
|
|
@ -15,6 +16,7 @@ export function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
|
|||
|
||||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Panel>
|
||||
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} />
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { PageHeader } from '@/components/common/PageHeader';
|
|||
import { useWebsite } from '@/components/hooks/useWebsite';
|
||||
import { Lucide } from '@/components/icons';
|
||||
import { Favicon } from '@/components/common/Favicon';
|
||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||
|
||||
export function WebsiteHeader() {
|
||||
const website = useWebsite();
|
||||
|
|
@ -10,6 +11,7 @@ export function WebsiteHeader() {
|
|||
return (
|
||||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />}>
|
||||
<Row alignItems="center" gap>
|
||||
<ActiveUsers websiteId={website.id} />
|
||||
<Button>
|
||||
<Icon>
|
||||
<Lucide.Share />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Grid, Column } from '@umami/react-zen';
|
||||
import { WebsiteProvider } from './WebsiteProvider';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
|
||||
|
|
@ -11,16 +10,15 @@ export function WebsiteLayout({ websiteId, children }: { websiteId: string; chil
|
|||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<PageBody>
|
||||
<WebsiteHeader />
|
||||
<Grid columns="auto 1fr" justifyContent="center" gap width="100%">
|
||||
<Column position="sticky" top="0px" alignSelf="flex-start" width="200px" paddingTop="3">
|
||||
<WebsiteNav websiteId={websiteId} />
|
||||
</Column>
|
||||
<Column>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
{children}
|
||||
</Column>
|
||||
</Grid>
|
||||
<Column gap="6">
|
||||
<WebsiteHeader />
|
||||
<Grid columns="auto 1fr" justifyContent="center" gap="6" width="100%">
|
||||
<Column position="sticky" top="20px" alignSelf="flex-start" width="200px">
|
||||
<WebsiteNav websiteId={websiteId} />
|
||||
</Column>
|
||||
<Column>{children}</Column>
|
||||
</Grid>
|
||||
</Column>
|
||||
</PageBody>
|
||||
</WebsiteProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { EventsChart } from '@/components/metrics/EventsChart';
|
|||
import { GridRow } from '@/components/common/GridRow';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { EventProperties } from './EventProperties';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
|
||||
export function EventsPage({ websiteId }) {
|
||||
const [label, setLabel] = useState(null);
|
||||
|
|
@ -21,6 +22,7 @@ export function EventsPage({ websiteId }) {
|
|||
|
||||
return (
|
||||
<Column gap="3">
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Panel>
|
||||
<EventsMetricsBar websiteId={websiteId} />
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { Column, Button, Heading } from '@umami/react-zen';
|
||||
|
||||
export function GoalsPage({ websiteId }: { websiteId: string }) {
|
||||
return <Column>Goals {websiteId}</Column>;
|
||||
return (
|
||||
<Column>
|
||||
{websiteId}
|
||||
<Button>Add goal</Button>
|
||||
<Heading>Goal 1</Heading>
|
||||
<Heading>Goal 2</Heading>
|
||||
<Heading>Goal 3</Heading>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { UTMView } from './UTMView';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
|
||||
export function UTMPage({ websiteId }: { websiteId: string }) {
|
||||
return <Column>Goals {websiteId}</Column>;
|
||||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<UTMView websiteId={websiteId} />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
68
src/app/(main)/websites/[websiteId]/utm/UTMView.tsx
Normal file
68
src/app/(main)/websites/[websiteId]/utm/UTMView.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { Column, Heading } from '@umami/react-zen';
|
||||
import { firstBy } from 'thenby';
|
||||
import { CHART_COLORS, UTM_PARAMS } from '@/lib/constants';
|
||||
import { useUTMQuery } from '@/components/hooks';
|
||||
import { PieChart } from '@/components/charts/PieChart';
|
||||
import { ListTable } from '@/components/metrics/ListTable';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { GridRow } from '@/components/common/GridRow';
|
||||
|
||||
function toArray(data: { [key: string]: number } = {}) {
|
||||
return Object.keys(data)
|
||||
.map(key => {
|
||||
return { name: key, value: data[key] };
|
||||
})
|
||||
.sort(firstBy('value', -1));
|
||||
}
|
||||
|
||||
export function UTMView({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data } = useUTMQuery(websiteId);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
{UTM_PARAMS.map(param => {
|
||||
const items = toArray(data[param]);
|
||||
const chartData = {
|
||||
labels: items.map(({ name }) => name),
|
||||
datasets: [
|
||||
{
|
||||
data: items.map(({ value }) => value),
|
||||
backgroundColor: CHART_COLORS,
|
||||
borderWidth: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
const total = items.reduce((sum, { value }) => {
|
||||
return +sum + +value;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<Panel key={param}>
|
||||
<GridRow layout="two">
|
||||
<Column>
|
||||
<Heading>{param.replace(/^utm_/, '')}</Heading>
|
||||
<ListTable
|
||||
metric={formatMessage(labels.views)}
|
||||
data={items.map(({ name, value }) => ({
|
||||
x: name,
|
||||
y: value,
|
||||
z: (value / total) * 100,
|
||||
}))}
|
||||
/>
|
||||
</Column>
|
||||
<Column>
|
||||
<PieChart type="doughnut" data={chartData} />
|
||||
</Column>
|
||||
</GridRow>
|
||||
</Panel>
|
||||
);
|
||||
})}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
42
src/app/api/websites/[websiteId]/utm/route.ts
Normal file
42
src/app/api/websites/[websiteId]/utm/route.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { z } from 'zod';
|
||||
import { canViewWebsite } from '@/lib/auth';
|
||||
import { unauthorized, json } from '@/lib/response';
|
||||
import { getRequestDateRange, parseRequest } from '@/lib/request';
|
||||
import { getUTM } from '@/queries';
|
||||
import { filterParams, timezoneParam, unitParam } from '@/lib/schema';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ websiteId: string }> },
|
||||
) {
|
||||
const schema = z.object({
|
||||
startAt: z.coerce.number().int(),
|
||||
endAt: z.coerce.number().int(),
|
||||
unit: unitParam,
|
||||
timezone: timezoneParam,
|
||||
...filterParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { websiteId } = await params;
|
||||
const { timezone } = query;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const { startDate, endDate } = await getRequestDateRange(query);
|
||||
|
||||
const data = await getUTM(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
timezone,
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ export function FilterRecord({
|
|||
<Row gap alignItems="center">
|
||||
<Select
|
||||
items={operators.filter(({ type }) => type === 'string')}
|
||||
selectedKey={operator}
|
||||
value={operator}
|
||||
onSelectionChange={value => onSelect?.(name, value)}
|
||||
>
|
||||
{({ name, label }: any) => {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export * from './queries/useTeamWebsitesQuery';
|
|||
export * from './queries/useTeamMembersQuery';
|
||||
export * from './queries/useUserQuery';
|
||||
export * from './queries/useUsersQuery';
|
||||
export * from './queries/useUTMQuery';
|
||||
export * from './queries/useWebsiteQuery';
|
||||
export * from './queries/useWebsites';
|
||||
export * from './queries/useWebsiteEventsQuery';
|
||||
|
|
|
|||
20
src/components/hooks/queries/useUTMQuery.ts
Normal file
20
src/components/hooks/queries/useUTMQuery.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useApi } from '../useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
|
||||
export function useUTMQuery(
|
||||
websiteId: string,
|
||||
queryParams?: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number },
|
||||
options?: Omit<UseQueryOptions & { onDataLoad?: (data: any) => void }, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const filterParams = useFilterParams(websiteId);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['utm', websiteId, { ...filterParams, ...queryParams }],
|
||||
queryFn: () =>
|
||||
get(`/websites/${websiteId}/utm`, { websiteId, ...filterParams, ...queryParams }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
|
@ -23,22 +23,8 @@ export function FilterBar() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Row
|
||||
theme="dark"
|
||||
backgroundColor="1"
|
||||
gap
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
paddingY="2"
|
||||
paddingLeft="3"
|
||||
paddingRight="2"
|
||||
border
|
||||
borderRadius="2"
|
||||
>
|
||||
<Row alignItems="center" gap="3" wrap="wrap" paddingX="2">
|
||||
<Text color="11" weight="bold">
|
||||
{formatMessage(labels.filters)}
|
||||
</Text>
|
||||
<Row gap alignItems="center" justifyContent="space-between" paddingY="3">
|
||||
<Row alignItems="center" gap="3" wrap="wrap">
|
||||
{Object.keys(filters).map(key => {
|
||||
const filter = filters[key];
|
||||
const { name, label, operator, value } = filter;
|
||||
|
|
@ -51,9 +37,9 @@ export function FilterBar() {
|
|||
padding
|
||||
backgroundColor
|
||||
borderRadius
|
||||
shadow="1"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
theme="dark"
|
||||
>
|
||||
<Row alignItems="center" gap="4">
|
||||
<Row alignItems="center" gap="2">
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export function WebsiteDateFilter({
|
|||
{!isAllTime && showCompare && (
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="quiet" onPress={handleCompare}>
|
||||
<Icon fillColor="currentColor">{compare ? <Icons.Close /> : <Icons.Compare />}</Icon>
|
||||
<Icon fillColor>{compare ? <Icons.Close /> : <Icons.Compare />}</Icon>
|
||||
</Button>
|
||||
<Tooltip>{formatMessage(compare ? labels.cancel : labels.compareDates)}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
|
|
|
|||
|
|
@ -318,6 +318,7 @@ export const labels = defineMessages({
|
|||
apply: { id: 'label.apply', defaultMessage: 'Apply' },
|
||||
links: { id: 'label.links', defaultMessage: 'Links' },
|
||||
pixels: { id: 'label.pixels', defaultMessage: 'Pixels' },
|
||||
addBoard: { id: 'label.add-board', defaultMessage: 'Add board' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ export function ActiveUsers({
|
|||
|
||||
return (
|
||||
<StatusLight variant="success">
|
||||
<Text size="2">{formatMessage(messages.activeUsers, { x: count })}</Text>
|
||||
<Text size="2" weight="bold">
|
||||
{formatMessage(messages.activeUsers, { x: count })}
|
||||
</Text>
|
||||
</StatusLight>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { z, ZodSchema } from 'zod';
|
||||
import { z } from 'zod/v4';
|
||||
import { FILTER_COLUMNS } from '@/lib/constants';
|
||||
import { badRequest, unauthorized } from '@/lib/response';
|
||||
import { getAllowedUnits, getMinimumUnit } from '@/lib/date';
|
||||
|
|
@ -7,7 +7,7 @@ import { getWebsiteDateRange } from '@/queries';
|
|||
|
||||
export async function parseRequest(
|
||||
request: Request,
|
||||
schema?: ZodSchema,
|
||||
schema?: any,
|
||||
options?: { skipAuth: boolean },
|
||||
): Promise<any> {
|
||||
const url = new URL(request.url);
|
||||
|
|
@ -21,7 +21,7 @@ export async function parseRequest(
|
|||
const result = schema.safeParse(isGet ? query : body);
|
||||
|
||||
if (!result.success) {
|
||||
error = () => badRequest(getErrorMessages(result.error));
|
||||
error = () => badRequest(z.treeifyError(result.error));
|
||||
} else if (isGet) {
|
||||
query = result.data;
|
||||
} else {
|
||||
|
|
@ -87,13 +87,3 @@ export function getRequestFilters(query: Record<string, any>) {
|
|||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function getErrorMessages(error: z.ZodError) {
|
||||
return Object.entries(error.format())
|
||||
.flatMap(([key, value]) => {
|
||||
if (key === '_errors') {
|
||||
return value;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"outDir": "./build",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
|
|
@ -21,9 +20,8 @@
|
|||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": false,
|
||||
"types": ["node", "jest"],
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"baseUrl": ".",
|
||||
"outDir": "./build",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue