mirror of
https://github.com/umami-software/umami.git
synced 2025-12-08 05:12:36 +01:00
Pixel/link metrics pages.
This commit is contained in:
parent
789b8b36d8
commit
8e766e2db7
42 changed files with 530 additions and 49 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { TeamSettings } from '@/app/(main)/teams/[teamId]/TeamSettings';
|
import { TeamSettings } from '@/app/(main)/teams/[teamId]/TeamSettings';
|
||||||
import { TeamProvider } from '@/app/(main)/teams/[teamId]/TeamProvider';
|
import { TeamProvider } from '@/app/(main)/teams/TeamProvider';
|
||||||
|
|
||||||
export function AdminTeamPage({ teamId }: { teamId: string }) {
|
export function AdminTeamPage({ teamId }: { teamId: string }) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings';
|
import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings';
|
||||||
import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
|
|
||||||
export function AdminWebsitePage({ websiteId }: { websiteId: string }) {
|
export function AdminWebsitePage({ websiteId }: { websiteId: string }) {
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ export function LinkEditForm({
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
const { mutate, error, isPending, touch } = useUpdateQuery(
|
const { mutate, error, isPending, touch, toast } = useUpdateQuery(
|
||||||
linkId ? `/links/${linkId}` : '/links',
|
linkId ? `/links/${linkId}` : '/links',
|
||||||
{
|
{
|
||||||
id: linkId,
|
id: linkId,
|
||||||
|
|
@ -48,6 +48,7 @@ export function LinkEditForm({
|
||||||
const handleSubmit = async (data: any) => {
|
const handleSubmit = async (data: any) => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
toast(formatMessage(messages.saved));
|
||||||
touch('links');
|
touch('links');
|
||||||
onSave?.();
|
onSave?.();
|
||||||
onClose?.();
|
onClose?.();
|
||||||
|
|
|
||||||
20
src/app/(main)/links/LinkProvider.tsx
Normal file
20
src/app/(main)/links/LinkProvider.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
'use client';
|
||||||
|
import { createContext, ReactNode } from 'react';
|
||||||
|
import { useLinkQuery } from '@/components/hooks';
|
||||||
|
import { Loading } from '@umami/react-zen';
|
||||||
|
|
||||||
|
export const LinkContext = createContext(null);
|
||||||
|
|
||||||
|
export function LinkProvider({ linkId, children }: { linkId?: string; children: ReactNode }) {
|
||||||
|
const { data: link, isLoading, isFetching } = useLinkQuery(linkId);
|
||||||
|
|
||||||
|
if (isFetching && isLoading) {
|
||||||
|
return <Loading position="page" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!link) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <LinkContext.Provider value={link}>{children}</LinkContext.Provider>;
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
|
import Link from 'next/link';
|
||||||
import { DataTable, DataColumn, Row } from '@umami/react-zen';
|
import { DataTable, DataColumn, Row } from '@umami/react-zen';
|
||||||
import { useConfig, useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
||||||
import { Empty } from '@/components/common/Empty';
|
import { Empty } from '@/components/common/Empty';
|
||||||
import { DateDistance } from '@/components/common/DateDistance';
|
import { DateDistance } from '@/components/common/DateDistance';
|
||||||
import { ExternalLink } from '@/components/common/ExternalLink';
|
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||||
import { LinkEditButton } from './LinkEditButton';
|
import { LinkEditButton } from './LinkEditButton';
|
||||||
import { LinkDeleteButton } from './LinkDeleteButton';
|
import { LinkDeleteButton } from './LinkDeleteButton';
|
||||||
import { LINKS_URL } from '@/lib/constants';
|
|
||||||
|
|
||||||
export function LinksTable({ data = [] }) {
|
export function LinksTable({ data = [] }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { websiteId } = useNavigation();
|
const { websiteId, renderUrl } = useNavigation();
|
||||||
const { linksUrl } = useConfig();
|
const { getSlugUrl } = useSlug('link');
|
||||||
const hostUrl = linksUrl || LINKS_URL;
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return <Empty />;
|
return <Empty />;
|
||||||
|
|
@ -19,10 +18,14 @@ export function LinksTable({ data = [] }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable data={data}>
|
<DataTable data={data}>
|
||||||
<DataColumn id="name" label={formatMessage(labels.name)} />
|
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||||
|
{({ id, name }: any) => {
|
||||||
|
return <Link href={renderUrl(`/links/${id}`)}>{name}</Link>;
|
||||||
|
}}
|
||||||
|
</DataColumn>
|
||||||
<DataColumn id="slug" label={formatMessage(labels.link)}>
|
<DataColumn id="slug" label={formatMessage(labels.link)}>
|
||||||
{({ slug }: any) => {
|
{({ slug }: any) => {
|
||||||
const url = `${hostUrl}/${slug}`;
|
const url = getSlugUrl(slug);
|
||||||
return <ExternalLink href={url}>{url}</ExternalLink>;
|
return <ExternalLink href={url}>{url}</ExternalLink>;
|
||||||
}}
|
}}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
|
|
|
||||||
34
src/app/(main)/links/[linkId]/LinkControls.tsx
Normal file
34
src/app/(main)/links/[linkId]/LinkControls.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Column, Row } from '@umami/react-zen';
|
||||||
|
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
||||||
|
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||||
|
import { FilterBar } from '@/components/input/FilterBar';
|
||||||
|
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect';
|
||||||
|
import { ExportButton } from '@/components/input/ExportButton';
|
||||||
|
|
||||||
|
export function LinkControls({
|
||||||
|
linkId: websiteId,
|
||||||
|
allowFilter = true,
|
||||||
|
allowDateFilter = true,
|
||||||
|
allowMonthFilter,
|
||||||
|
allowCompare,
|
||||||
|
allowDownload = false,
|
||||||
|
}: {
|
||||||
|
linkId: string;
|
||||||
|
allowFilter?: boolean;
|
||||||
|
allowCompare?: boolean;
|
||||||
|
allowDateFilter?: boolean;
|
||||||
|
allowMonthFilter?: boolean;
|
||||||
|
allowDownload?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Column gap>
|
||||||
|
<Row alignItems="center" justifyContent="space-between" gap="3">
|
||||||
|
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
||||||
|
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
|
||||||
|
{allowDownload && <ExportButton websiteId={websiteId} />}
|
||||||
|
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
|
||||||
|
</Row>
|
||||||
|
{allowFilter && <FilterBar websiteId={websiteId} />}
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/app/(main)/links/[linkId]/LinkHeader.tsx
Normal file
22
src/app/(main)/links/[linkId]/LinkHeader.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { useLink, useMessages, useSlug } from '@/components/hooks';
|
||||||
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
|
import { Icon, Text } from '@umami/react-zen';
|
||||||
|
import { ExternalLink } from '@/components/icons';
|
||||||
|
import { LinkButton } from '@/components/common/LinkButton';
|
||||||
|
|
||||||
|
export function LinkHeader() {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { getSlugUrl } = useSlug('link');
|
||||||
|
const link = useLink();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageHeader title={link.name} description={link.url}>
|
||||||
|
<LinkButton href={getSlugUrl(link.slug)}>
|
||||||
|
<Icon>
|
||||||
|
<ExternalLink />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.view)}</Text>
|
||||||
|
</LinkButton>
|
||||||
|
</PageHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
src/app/(main)/links/[linkId]/LinkMetricsBar.tsx
Normal file
71
src/app/(main)/links/[linkId]/LinkMetricsBar.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { useDateRange, useMessages } from '@/components/hooks';
|
||||||
|
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||||
|
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||||
|
import { formatLongNumber } from '@/lib/format';
|
||||||
|
import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery';
|
||||||
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
|
|
||||||
|
export function LinkMetricsBar({
|
||||||
|
linkId,
|
||||||
|
}: {
|
||||||
|
linkId: string;
|
||||||
|
showChange?: boolean;
|
||||||
|
compareMode?: boolean;
|
||||||
|
}) {
|
||||||
|
const { dateRange } = useDateRange(linkId);
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId);
|
||||||
|
const isAllTime = dateRange.value === 'all';
|
||||||
|
|
||||||
|
const { pageviews, visitors, visits, comparison } = data || {};
|
||||||
|
|
||||||
|
const metrics = data
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
value: visitors,
|
||||||
|
label: formatMessage(labels.visitors),
|
||||||
|
change: visitors - comparison.visitors,
|
||||||
|
formatValue: formatLongNumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: visits,
|
||||||
|
label: formatMessage(labels.visits),
|
||||||
|
change: visits - comparison.visits,
|
||||||
|
formatValue: formatLongNumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: pageviews,
|
||||||
|
label: formatMessage(labels.views),
|
||||||
|
change: pageviews - comparison.pageviews,
|
||||||
|
formatValue: formatLongNumber,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoadingPanel
|
||||||
|
data={metrics}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isFetching={isFetching}
|
||||||
|
error={error}
|
||||||
|
minHeight="136px"
|
||||||
|
>
|
||||||
|
<MetricsBar>
|
||||||
|
{metrics?.map(({ label, value, prev, change, formatValue, reverseColors }: any) => {
|
||||||
|
return (
|
||||||
|
<MetricCard
|
||||||
|
key={label}
|
||||||
|
value={value}
|
||||||
|
previousValue={prev}
|
||||||
|
label={label}
|
||||||
|
change={change}
|
||||||
|
formatValue={formatValue}
|
||||||
|
reverseColors={reverseColors}
|
||||||
|
showChange={!isAllTime}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MetricsBar>
|
||||||
|
</LoadingPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
src/app/(main)/links/[linkId]/LinkPage.tsx
Normal file
59
src/app/(main)/links/[linkId]/LinkPage.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
'use client';
|
||||||
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
|
import { LinkProvider } from '@/app/(main)/links/LinkProvider';
|
||||||
|
import { LinkHeader } from '@/app/(main)/links/[linkId]/LinkHeader';
|
||||||
|
import { Panel } from '@/components/common/Panel';
|
||||||
|
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
|
||||||
|
import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar';
|
||||||
|
import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls';
|
||||||
|
import { Grid } from '@umami/react-zen';
|
||||||
|
import { GridRow } from '@/components/common/GridRow';
|
||||||
|
import { ReferrersTable } from '@/components/metrics/ReferrersTable';
|
||||||
|
import { BrowsersTable } from '@/components/metrics/BrowsersTable';
|
||||||
|
import { OSTable } from '@/components/metrics/OSTable';
|
||||||
|
import { DevicesTable } from '@/components/metrics/DevicesTable';
|
||||||
|
import { WorldMap } from '@/components/metrics/WorldMap';
|
||||||
|
import { CountriesTable } from '@/components/metrics/CountriesTable';
|
||||||
|
|
||||||
|
export function LinkPage({ linkId }: { linkId: string }) {
|
||||||
|
const props = { websiteId: linkId, limit: 10, allowDownload: false };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinkProvider linkId={linkId}>
|
||||||
|
<PageBody gap>
|
||||||
|
<LinkHeader />
|
||||||
|
<LinkControls linkId={linkId} />
|
||||||
|
<LinkMetricsBar linkId={linkId} showChange={true} />
|
||||||
|
<Panel>
|
||||||
|
<WebsiteChart websiteId={linkId} />
|
||||||
|
</Panel>
|
||||||
|
<GridRow layout="two">
|
||||||
|
<Panel>
|
||||||
|
<ReferrersTable {...props} />
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
<Grid gap="3">
|
||||||
|
<GridRow layout="three">
|
||||||
|
<Panel>
|
||||||
|
<BrowsersTable {...props} />
|
||||||
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<OSTable {...props} />
|
||||||
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<DevicesTable {...props} />
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
<GridRow layout="two-one">
|
||||||
|
<Panel gridColumn="span 2" noPadding>
|
||||||
|
<WorldMap websiteId={linkId} />
|
||||||
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<CountriesTable {...props} />
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
</Grid>
|
||||||
|
</PageBody>
|
||||||
|
</LinkProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/app/(main)/links/[linkId]/page.tsx
Normal file
12
src/app/(main)/links/[linkId]/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { LinkPage } from './LinkPage';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export default async function ({ params }: { params: Promise<{ linkId: string }> }) {
|
||||||
|
const { linkId } = await params;
|
||||||
|
|
||||||
|
return <LinkPage linkId={linkId} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Link',
|
||||||
|
};
|
||||||
|
|
@ -31,8 +31,8 @@ export function PixelEditForm({
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
const { mutate, error, isPending, touch } = useUpdateQuery(
|
const { mutate, error, isPending, touch, toast } = useUpdateQuery(
|
||||||
pixelId ? `/pixels/${pixelId}` : '/pixels',
|
pixelId ? `/pixels/${pixelId}` : '/pixels',
|
||||||
{
|
{
|
||||||
id: pixelId,
|
id: pixelId,
|
||||||
|
|
@ -47,6 +47,7 @@ export function PixelEditForm({
|
||||||
const handleSubmit = async (data: any) => {
|
const handleSubmit = async (data: any) => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
toast(formatMessage(messages.saved));
|
||||||
touch('pixels');
|
touch('pixels');
|
||||||
onSave?.();
|
onSave?.();
|
||||||
onClose?.();
|
onClose?.();
|
||||||
|
|
|
||||||
20
src/app/(main)/pixels/PixelProvider.tsx
Normal file
20
src/app/(main)/pixels/PixelProvider.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
'use client';
|
||||||
|
import { createContext, ReactNode } from 'react';
|
||||||
|
import { usePixelQuery } from '@/components/hooks';
|
||||||
|
import { Loading } from '@umami/react-zen';
|
||||||
|
|
||||||
|
export const PixelContext = createContext(null);
|
||||||
|
|
||||||
|
export function PixelProvider({ pixelId, children }: { pixelId?: string; children: ReactNode }) {
|
||||||
|
const { data: pixel, isLoading, isFetching } = usePixelQuery(pixelId);
|
||||||
|
|
||||||
|
if (isFetching && isLoading) {
|
||||||
|
return <Loading position="page" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pixel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PixelContext.Provider value={pixel}>{children}</PixelContext.Provider>;
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
|
import Link from 'next/link';
|
||||||
import { DataTable, DataColumn, Row } from '@umami/react-zen';
|
import { DataTable, DataColumn, Row } from '@umami/react-zen';
|
||||||
import { useConfig, useMessages } from '@/components/hooks';
|
import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
||||||
import { Empty } from '@/components/common/Empty';
|
import { Empty } from '@/components/common/Empty';
|
||||||
import { DateDistance } from '@/components/common/DateDistance';
|
import { DateDistance } from '@/components/common/DateDistance';
|
||||||
import { PixelEditButton } from './PixelEditButton';
|
import { PixelEditButton } from './PixelEditButton';
|
||||||
import { PixelDeleteButton } from './PixelDeleteButton';
|
import { PixelDeleteButton } from './PixelDeleteButton';
|
||||||
import { PIXELS_URL } from '@/lib/constants';
|
|
||||||
import { ExternalLink } from '@/components/common/ExternalLink';
|
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||||
|
|
||||||
export function PixelsTable({ data = [] }) {
|
export function PixelsTable({ data = [] }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { pixelsUrl } = useConfig();
|
const { renderUrl } = useNavigation();
|
||||||
const hostUrl = pixelsUrl || PIXELS_URL;
|
const { getSlugUrl } = useSlug('pixel');
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return <Empty />;
|
return <Empty />;
|
||||||
|
|
@ -18,10 +18,14 @@ export function PixelsTable({ data = [] }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable data={data}>
|
<DataTable data={data}>
|
||||||
<DataColumn id="name" label={formatMessage(labels.name)} />
|
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||||
|
{({ id, name }: any) => {
|
||||||
|
return <Link href={renderUrl(`/pixels/${id}`)}>{name}</Link>;
|
||||||
|
}}
|
||||||
|
</DataColumn>
|
||||||
<DataColumn id="url" label="URL">
|
<DataColumn id="url" label="URL">
|
||||||
{({ slug }: any) => {
|
{({ slug }: any) => {
|
||||||
const url = `${hostUrl}/${slug}`;
|
const url = getSlugUrl(slug);
|
||||||
return <ExternalLink href={url}>{url}</ExternalLink>;
|
return <ExternalLink href={url}>{url}</ExternalLink>;
|
||||||
}}
|
}}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
|
|
|
||||||
34
src/app/(main)/pixels/[pixelId]/PixelControls.tsx
Normal file
34
src/app/(main)/pixels/[pixelId]/PixelControls.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Column, Row } from '@umami/react-zen';
|
||||||
|
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
||||||
|
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||||
|
import { FilterBar } from '@/components/input/FilterBar';
|
||||||
|
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect';
|
||||||
|
import { ExportButton } from '@/components/input/ExportButton';
|
||||||
|
|
||||||
|
export function PixelControls({
|
||||||
|
pixelId: websiteId,
|
||||||
|
allowFilter = true,
|
||||||
|
allowDateFilter = true,
|
||||||
|
allowMonthFilter,
|
||||||
|
allowCompare,
|
||||||
|
allowDownload = false,
|
||||||
|
}: {
|
||||||
|
pixelId: string;
|
||||||
|
allowFilter?: boolean;
|
||||||
|
allowCompare?: boolean;
|
||||||
|
allowDateFilter?: boolean;
|
||||||
|
allowMonthFilter?: boolean;
|
||||||
|
allowDownload?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Column gap>
|
||||||
|
<Row alignItems="center" justifyContent="space-between" gap="3">
|
||||||
|
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
||||||
|
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
|
||||||
|
{allowDownload && <ExportButton websiteId={websiteId} />}
|
||||||
|
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
|
||||||
|
</Row>
|
||||||
|
{allowFilter && <FilterBar websiteId={websiteId} />}
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/app/(main)/pixels/[pixelId]/PixelHeader.tsx
Normal file
22
src/app/(main)/pixels/[pixelId]/PixelHeader.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { usePixel, useMessages, useSlug } from '@/components/hooks';
|
||||||
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
|
import { Icon, Text } from '@umami/react-zen';
|
||||||
|
import { ExternalLink } from '@/components/icons';
|
||||||
|
import { LinkButton } from '@/components/common/LinkButton';
|
||||||
|
|
||||||
|
export function PixelHeader() {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { getSlugUrl } = useSlug('pixel');
|
||||||
|
const pixel = usePixel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageHeader title={pixel.name} description={pixel.url}>
|
||||||
|
<LinkButton href={getSlugUrl(pixel.slug)}>
|
||||||
|
<Icon>
|
||||||
|
<ExternalLink />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.view)}</Text>
|
||||||
|
</LinkButton>
|
||||||
|
</PageHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx
Normal file
71
src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { useDateRange, useMessages } from '@/components/hooks';
|
||||||
|
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||||
|
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||||
|
import { formatLongNumber } from '@/lib/format';
|
||||||
|
import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery';
|
||||||
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
|
|
||||||
|
export function PixelMetricsBar({
|
||||||
|
pixelId,
|
||||||
|
}: {
|
||||||
|
pixelId: string;
|
||||||
|
showChange?: boolean;
|
||||||
|
compareMode?: boolean;
|
||||||
|
}) {
|
||||||
|
const { dateRange } = useDateRange(pixelId);
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId);
|
||||||
|
const isAllTime = dateRange.value === 'all';
|
||||||
|
|
||||||
|
const { pageviews, visitors, visits, comparison } = data || {};
|
||||||
|
|
||||||
|
const metrics = data
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
value: visitors,
|
||||||
|
label: formatMessage(labels.visitors),
|
||||||
|
change: visitors - comparison.visitors,
|
||||||
|
formatValue: formatLongNumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: visits,
|
||||||
|
label: formatMessage(labels.visits),
|
||||||
|
change: visits - comparison.visits,
|
||||||
|
formatValue: formatLongNumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: pageviews,
|
||||||
|
label: formatMessage(labels.views),
|
||||||
|
change: pageviews - comparison.pageviews,
|
||||||
|
formatValue: formatLongNumber,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoadingPanel
|
||||||
|
data={metrics}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isFetching={isFetching}
|
||||||
|
error={error}
|
||||||
|
minHeight="136px"
|
||||||
|
>
|
||||||
|
<MetricsBar>
|
||||||
|
{metrics?.map(({ label, value, prev, change, formatValue, reverseColors }: any) => {
|
||||||
|
return (
|
||||||
|
<MetricCard
|
||||||
|
key={label}
|
||||||
|
value={value}
|
||||||
|
previousValue={prev}
|
||||||
|
label={label}
|
||||||
|
change={change}
|
||||||
|
formatValue={formatValue}
|
||||||
|
reverseColors={reverseColors}
|
||||||
|
showChange={!isAllTime}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MetricsBar>
|
||||||
|
</LoadingPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
src/app/(main)/pixels/[pixelId]/PixelPage.tsx
Normal file
59
src/app/(main)/pixels/[pixelId]/PixelPage.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
'use client';
|
||||||
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
|
import { PixelProvider } from '@/app/(main)/pixels/PixelProvider';
|
||||||
|
import { PixelHeader } from '@/app/(main)/pixels/[pixelId]/PixelHeader';
|
||||||
|
import { Panel } from '@/components/common/Panel';
|
||||||
|
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
|
||||||
|
import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar';
|
||||||
|
import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls';
|
||||||
|
import { Grid } from '@umami/react-zen';
|
||||||
|
import { GridRow } from '@/components/common/GridRow';
|
||||||
|
import { ReferrersTable } from '@/components/metrics/ReferrersTable';
|
||||||
|
import { BrowsersTable } from '@/components/metrics/BrowsersTable';
|
||||||
|
import { OSTable } from '@/components/metrics/OSTable';
|
||||||
|
import { DevicesTable } from '@/components/metrics/DevicesTable';
|
||||||
|
import { WorldMap } from '@/components/metrics/WorldMap';
|
||||||
|
import { CountriesTable } from '@/components/metrics/CountriesTable';
|
||||||
|
|
||||||
|
export function PixelPage({ pixelId }: { pixelId: string }) {
|
||||||
|
const props = { websiteId: pixelId, limit: 10, allowDownload: false };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PixelProvider pixelId={pixelId}>
|
||||||
|
<PageBody gap>
|
||||||
|
<PixelHeader />
|
||||||
|
<PixelControls pixelId={pixelId} />
|
||||||
|
<PixelMetricsBar pixelId={pixelId} showChange={true} />
|
||||||
|
<Panel>
|
||||||
|
<WebsiteChart websiteId={pixelId} />
|
||||||
|
</Panel>
|
||||||
|
<GridRow layout="two">
|
||||||
|
<Panel>
|
||||||
|
<ReferrersTable {...props} />
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
<Grid gap="3">
|
||||||
|
<GridRow layout="three">
|
||||||
|
<Panel>
|
||||||
|
<BrowsersTable {...props} />
|
||||||
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<OSTable {...props} />
|
||||||
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<DevicesTable {...props} />
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
<GridRow layout="two-one">
|
||||||
|
<Panel gridColumn="span 2" noPadding>
|
||||||
|
<WorldMap websiteId={pixelId} />
|
||||||
|
</Panel>
|
||||||
|
<Panel>
|
||||||
|
<CountriesTable {...props} />
|
||||||
|
</Panel>
|
||||||
|
</GridRow>
|
||||||
|
</Grid>
|
||||||
|
</PageBody>
|
||||||
|
</PixelProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/app/(main)/pixels/[pixelId]/page.tsx
Normal file
12
src/app/(main)/pixels/[pixelId]/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { PixelPage } from './PixelPage';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export default async function ({ params }: { params: Promise<{ pixelId: string }> }) {
|
||||||
|
const { pixelId } = await params;
|
||||||
|
|
||||||
|
return <PixelPage pixelId={pixelId} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Pixel',
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { TeamProvider } from '@/app/(main)/teams/[teamId]/TeamProvider';
|
import { TeamProvider } from '@/app/(main)/teams/TeamProvider';
|
||||||
import { TeamSettings } from '@/app/(main)/teams/[teamId]/TeamSettings';
|
import { TeamSettings } from '@/app/(main)/teams/[teamId]/TeamSettings';
|
||||||
|
|
||||||
export function TeamSettingsPage({ teamId }: { teamId: string }) {
|
export function TeamSettingsPage({ teamId }: { teamId: string }) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column } from '@umami/react-zen';
|
||||||
import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings';
|
import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings';
|
||||||
import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader';
|
import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { getRandomChars } from '@/lib/crypto';
|
import { getRandomChars } from '@/lib/crypto';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||||
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
|
import { TeamContext } from '@/app/(main)/teams/TeamProvider';
|
||||||
|
|
||||||
const generateId = () => `team_${getRandomChars(16)}`;
|
const generateId = () => `team_${getRandomChars(16)}`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
|
import { Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
|
||||||
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
|
import { TeamContext } from '@/app/(main)/teams/TeamProvider';
|
||||||
import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
|
import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
|
||||||
|
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||||
import { WebsiteTableView } from './WebsiteTableView';
|
import { WebsiteTableView } from './WebsiteTableView';
|
||||||
import { WebsiteControls } from './WebsiteControls';
|
import { WebsiteControls } from './WebsiteControls';
|
||||||
|
|
||||||
export function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
|
export function WebsitePage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
router,
|
router,
|
||||||
query: { view, compare },
|
query: { view, compare },
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { WebsiteDetailsPage } from './WebsiteDetailsPage';
|
import { WebsitePage } from './WebsitePage';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
export default async function WebsitePage({ params }: { params: Promise<{ websiteId: string }> }) {
|
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
|
||||||
const { websiteId } = await params;
|
const { websiteId } = await params;
|
||||||
|
|
||||||
return <WebsiteDetailsPage websiteId={websiteId} />;
|
return <WebsitePage websiteId={websiteId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||||
import { DOMAIN_REGEX } from '@/lib/constants';
|
import { DOMAIN_REGEX } from '@/lib/constants';
|
||||||
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
import { WebsiteContext } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
|
|
||||||
export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
|
export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
|
||||||
const website = useContext(WebsiteContext);
|
const website = useContext(WebsiteContext);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
|
import { Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
|
||||||
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
import { WebsiteContext } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
import { WebsiteShareForm } from './WebsiteShareForm';
|
import { WebsiteShareForm } from './WebsiteShareForm';
|
||||||
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
|
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
import { WebsiteContext } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { Globe } from '@/components/icons';
|
import { Globe } from '@/components/icons';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
Text,
|
Text,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useApi, useLoginQuery, useMessages, useUserTeamsQuery } from '@/components/hooks';
|
import { useApi, useLoginQuery, useMessages, useUserTeamsQuery } from '@/components/hooks';
|
||||||
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
import { WebsiteContext } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
|
|
||||||
export function WebsiteTransferForm({
|
export function WebsiteTransferForm({
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { WebsiteDetailsPage } from '@/app/(main)/websites/[websiteId]/WebsiteDetailsPage';
|
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
|
||||||
import { useShareTokenQuery } from '@/components/hooks';
|
import { useShareTokenQuery } from '@/components/hooks';
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
|
|
@ -17,7 +17,7 @@ export function SharePage({ shareId }) {
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<Header />
|
<Header />
|
||||||
<WebsiteProvider websiteId={shareToken.websiteId}>
|
<WebsiteProvider websiteId={shareToken.websiteId}>
|
||||||
<WebsiteDetailsPage websiteId={shareToken.websiteId} />
|
<WebsitePage websiteId={shareToken.websiteId} />
|
||||||
</WebsiteProvider>
|
</WebsiteProvider>
|
||||||
<Footer />
|
<Footer />
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Heading, Icon, Row, RowProps, Text } from '@umami/react-zen';
|
import { Heading, Icon, Row, RowProps, Text, Column } from '@umami/react-zen';
|
||||||
|
|
||||||
export function PageHeader({
|
export function PageHeader({
|
||||||
title,
|
title,
|
||||||
|
|
@ -26,11 +26,13 @@ export function PageHeader({
|
||||||
width="100%"
|
width="100%"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Row alignItems="center" gap="3">
|
<Column>
|
||||||
{icon && <Icon size="md">{icon}</Icon>}
|
<Row alignItems="center" gap="3">
|
||||||
{title && <Heading size="4">{title}</Heading>}
|
{icon && <Icon size="md">{icon}</Icon>}
|
||||||
|
{title && <Heading size="4">{title}</Heading>}
|
||||||
|
</Row>
|
||||||
{description && <Text color="muted">{description}</Text>}
|
{description && <Text color="muted">{description}</Text>}
|
||||||
</Row>
|
</Column>
|
||||||
<Row justifyContent="flex-end">{children}</Row>
|
<Row justifyContent="flex-end">{children}</Row>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
6
src/components/hooks/context/useLink.ts
Normal file
6
src/components/hooks/context/useLink.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { LinkContext } from '@/app/(main)/links/LinkProvider';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
export function useLink() {
|
||||||
|
return useContext(LinkContext);
|
||||||
|
}
|
||||||
6
src/components/hooks/context/usePixel.ts
Normal file
6
src/components/hooks/context/usePixel.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { PixelContext } from '@/app/(main)/pixels/PixelProvider';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
export function usePixel() {
|
||||||
|
return useContext(PixelContext);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
|
import { TeamContext } from '@/app/(main)/teams/TeamProvider';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
export function useTeam() {
|
export function useTeam() {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
import { WebsiteContext } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
export function useWebsite() {
|
export function useWebsite() {
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
// Context hooks
|
||||||
|
export * from './context/useLink';
|
||||||
|
export * from './context/usePixel';
|
||||||
|
export * from './context/useTeam';
|
||||||
|
export * from './context/useWebsite';
|
||||||
|
|
||||||
// Query hooks
|
// Query hooks
|
||||||
export * from './queries/useActiveUsersQuery';
|
export * from './queries/useActiveUsersQuery';
|
||||||
export * from './queries/useDeleteQuery';
|
export * from './queries/useDeleteQuery';
|
||||||
|
|
@ -70,7 +76,6 @@ export * from './useNavigation';
|
||||||
export * from './usePagedQuery';
|
export * from './usePagedQuery';
|
||||||
export * from './usePageParameters';
|
export * from './usePageParameters';
|
||||||
export * from './useRegionNames';
|
export * from './useRegionNames';
|
||||||
|
export * from './useSlug';
|
||||||
export * from './useSticky';
|
export * from './useSticky';
|
||||||
export * from './useTeam';
|
|
||||||
export * from './useTimezone';
|
export * from './useTimezone';
|
||||||
export * from './useWebsite';
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { useApi, useModified } from '@/components/hooks';
|
import { useApi } from '../useApi';
|
||||||
|
import { useModified } from '../useModified';
|
||||||
|
import { useToast } from '@umami/react-zen';
|
||||||
|
|
||||||
export function useUpdateQuery(path: string, params?: Record<string, any>) {
|
export function useUpdateQuery(path: string, params?: Record<string, any>) {
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
|
|
@ -6,6 +8,7 @@ export function useUpdateQuery(path: string, params?: Record<string, any>) {
|
||||||
mutationFn: (data: Record<string, any>) => post(path, { ...data, ...params }),
|
mutationFn: (data: Record<string, any>) => post(path, { ...data, ...params }),
|
||||||
});
|
});
|
||||||
const { touch } = useModified();
|
const { touch } = useModified();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
return { mutate, isPending, error, touch };
|
return { mutate, isPending, error, touch, toast };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
src/components/hooks/useSlug.ts
Normal file
14
src/components/hooks/useSlug.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { useConfig } from '@/components/hooks/useConfig';
|
||||||
|
import { LINKS_URL, PIXELS_URL } from '@/lib/constants';
|
||||||
|
|
||||||
|
export function useSlug(type: 'link' | 'pixel') {
|
||||||
|
const { linksUrl, pixelsUrl } = useConfig();
|
||||||
|
|
||||||
|
const hostUrl = type === 'link' ? linksUrl || LINKS_URL : pixelsUrl || PIXELS_URL;
|
||||||
|
|
||||||
|
const getSlugUrl = (slug: string) => {
|
||||||
|
return `${hostUrl}/${slug}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { getSlugUrl, hostUrl };
|
||||||
|
}
|
||||||
|
|
@ -358,7 +358,7 @@ export const labels = defineMessages({
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
error: { id: 'message.error', defaultMessage: 'Something went wrong.' },
|
error: { id: 'message.error', defaultMessage: 'Something went wrong.' },
|
||||||
saved: { id: 'message.saved', defaultMessage: 'Saved.' },
|
saved: { id: 'message.saved', defaultMessage: 'Saved successfully.' },
|
||||||
noUsers: { id: 'message.no-users', defaultMessage: 'There are no users.' },
|
noUsers: { id: 'message.no-users', defaultMessage: 'There are no users.' },
|
||||||
userDeleted: { id: 'message.user-deleted', defaultMessage: 'User deleted.' },
|
userDeleted: { id: 'message.user-deleted', defaultMessage: 'User deleted.' },
|
||||||
noDataAvailable: { id: 'message.no-data-available', defaultMessage: 'No data available.' },
|
noDataAvailable: { id: 'message.no-data-available', defaultMessage: 'No data available.' },
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
import { WebsiteContext } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { FilterButtons } from '@/components/input/FilterButtons';
|
import { FilterButtons } from '@/components/input/FilterButtons';
|
||||||
import { FilterLink } from '@/components/common/FilterLink';
|
import { FilterLink } from '@/components/common/FilterLink';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export * from '@/app/(main)/teams/TeamAddForm';
|
||||||
export * from '@/app/(main)/teams/TeamJoinForm';
|
export * from '@/app/(main)/teams/TeamJoinForm';
|
||||||
export * from '@/app/(main)/teams/TeamLeaveButton';
|
export * from '@/app/(main)/teams/TeamLeaveButton';
|
||||||
export * from '@/app/(main)/teams/TeamLeaveForm';
|
export * from '@/app/(main)/teams/TeamLeaveForm';
|
||||||
export * from '@/app/(main)/teams/[teamId]/TeamProvider';
|
export * from '@/app/(main)/teams/TeamProvider';
|
||||||
export * from '@/app/(main)/teams/TeamsAddButton';
|
export * from '@/app/(main)/teams/TeamsAddButton';
|
||||||
export * from '@/app/(main)/teams/TeamsDataTable';
|
export * from '@/app/(main)/teams/TeamsDataTable';
|
||||||
export * from '@/app/(main)/teams/TeamsHeader';
|
export * from '@/app/(main)/teams/TeamsHeader';
|
||||||
|
|
@ -38,7 +38,7 @@ export * from '@/app/(main)/websites/WebsitesDataTable';
|
||||||
export * from '@/app/(main)/websites/WebsitesHeader';
|
export * from '@/app/(main)/websites/WebsitesHeader';
|
||||||
export * from '@/app/(main)/websites/WebsitesTable';
|
export * from '@/app/(main)/websites/WebsitesTable';
|
||||||
|
|
||||||
export * from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
export * from '@/app/(main)/websites/WebsiteProvider';
|
||||||
|
|
||||||
export * from '@/components/common/ConfirmationForm';
|
export * from '@/components/common/ConfirmationForm';
|
||||||
export * from '@/components/common/DataGrid';
|
export * from '@/components/common/DataGrid';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue