Share page changes.

This commit is contained in:
Mike Cao 2026-01-24 02:47:09 -08:00
parent c9f6653b62
commit 4a09f2bff6
11 changed files with 60 additions and 140 deletions

1
.gitignore vendored
View file

@ -32,6 +32,7 @@ pm2.yml
.vscode
.tool-versions
.claude
nul
# debug
npm-debug.log*

View file

@ -11,7 +11,7 @@
},
"type": "module",
"scripts": {
"dev": "next dev -p 3003 --turbo",
"dev": "next dev -p 3002 --turbo",
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
"start": "next start",
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",

View file

@ -1,101 +0,0 @@
import {
Button,
Checkbox,
Column,
Form,
FormField,
FormSubmitButton,
Row,
Text,
TextField,
} from '@umami/react-zen';
import { useState } from 'react';
import { useApi, useMessages, useModified } from '@/components/hooks';
import { SHARE_NAV_ITEMS } from './constants';
export interface ShareCreateFormProps {
websiteId: string;
onSave?: () => void;
onClose?: () => void;
}
export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormProps) {
const { formatMessage, labels } = useMessages();
const { post } = useApi();
const { touch } = useModified();
const [isPending, setIsPending] = useState(false);
// Build default values - only overview and events enabled by default
const defaultValues: Record<string, boolean> = {};
SHARE_NAV_ITEMS.forEach(section => {
section.items.forEach(item => {
defaultValues[item.id] = item.id === 'overview' || item.id === 'events';
});
});
// Get all item ids for validation
const allItemIds = SHARE_NAV_ITEMS.flatMap(section => section.items.map(item => item.id));
const handleSubmit = async (data: any) => {
setIsPending(true);
try {
const parameters: Record<string, boolean> = {};
SHARE_NAV_ITEMS.forEach(section => {
section.items.forEach(item => {
parameters[item.id] = data[item.id] ?? false;
});
});
await post(`/websites/${websiteId}/shares`, { name: data.name, parameters });
touch('shares');
onSave?.();
onClose?.();
} finally {
setIsPending(false);
}
};
return (
<Form onSubmit={handleSubmit} defaultValues={defaultValues}>
{({ watch }) => {
const values = watch();
const hasSelection = allItemIds.some(id => values[id]);
return (
<Column gap="3">
<FormField
label={formatMessage(labels.name)}
name="name"
rules={{ required: formatMessage(labels.required) }}
>
<TextField autoComplete="off" autoFocus />
</FormField>
{SHARE_NAV_ITEMS.map(section => (
<Column key={section.section} gap="1">
<Text size="2" weight="bold">
{formatMessage((labels as any)[section.section])}
</Text>
<Column gap="1">
{section.items.map(item => (
<FormField key={item.id} name={item.id}>
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
</FormField>
))}
</Column>
</Column>
))}
<Row justifyContent="flex-end" paddingTop="3" gap="3">
{onClose && (
<Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)}
</Button>
)}
<FormSubmitButton isDisabled={isPending || !hasSelection}>
{formatMessage(labels.save)}
</FormSubmitButton>
</Row>
</Column>
);
}}
</Form>
);
}

View file

@ -14,25 +14,30 @@ import {
} from '@umami/react-zen';
import { useEffect, useState } from 'react';
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
import { SHARE_NAV_ITEMS } from './constants';
export function ShareEditForm({
shareId,
websiteId,
onSave,
onClose,
}: {
shareId: string;
shareId?: string;
websiteId?: string;
onSave?: () => void;
onClose?: () => void;
}) {
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(`/share/id/${shareId}`);
const { formatMessage, labels, getErrorMessage } = useMessages();
const { cloudMode } = useConfig();
const { get } = useApi();
const { get, post } = useApi();
const { touch } = useModified();
const { modified } = useModified('shares');
const [share, setShare] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [isLoading, setIsLoading] = useState(!!shareId);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<any>(null);
const isEditing = !!shareId;
const getUrl = (slug: string) => {
if (cloudMode) {
@ -42,6 +47,8 @@ export function ShareEditForm({
};
useEffect(() => {
if (!shareId) return;
const loadShare = async () => {
setIsLoading(true);
try {
@ -62,24 +69,30 @@ export function ShareEditForm({
});
});
await mutateAsync(
{ name: data.name, slug: share.slug, parameters },
{
onSuccess: async () => {
toast(formatMessage(messages.saved));
touch('shares');
onSave?.();
onClose?.();
},
},
);
setIsPending(true);
setError(null);
try {
if (isEditing) {
await post(`/share/id/${shareId}`, { name: data.name, slug: share.slug, parameters });
} else {
await post(`/websites/${websiteId}/shares`, { name: data.name, parameters });
}
touch('shares');
onSave?.();
onClose?.();
} catch (e) {
setError(e);
} finally {
setIsPending(false);
}
};
if (isLoading) {
return <Loading placement="absolute" />;
}
const url = getUrl(share?.slug || '');
const url = isEditing ? getUrl(share?.slug || '') : null;
// Build default values from share parameters
const defaultValues: Record<string, any> = {
@ -103,16 +116,18 @@ export function ShareEditForm({
return (
<Column gap="6">
<Column>
<Label>{formatMessage(labels.shareUrl)}</Label>
<TextField value={url} isReadOnly allowCopy />
</Column>
{url && (
<Column>
<Label>{formatMessage(labels.shareUrl)}</Label>
<TextField value={url} isReadOnly allowCopy />
</Column>
)}
<FormField
label={formatMessage(labels.name)}
name="name"
rules={{ required: formatMessage(labels.required) }}
>
<TextField autoComplete="off" />
<TextField autoComplete="off" autoFocus={!isEditing} />
</FormField>
<Grid columns="repeat(auto-fit, minmax(150px, 1fr))" gap="3">
{SHARE_NAV_ITEMS.map(section => (
@ -134,7 +149,10 @@ export function ShareEditForm({
{formatMessage(labels.cancel)}
</Button>
)}
<FormSubmitButton variant="primary" isDisabled={!hasSelection || !values.name}>
<FormSubmitButton
variant="primary"
isDisabled={isPending || !hasSelection || !values.name}
>
{formatMessage(labels.save)}
</FormSubmitButton>
</Row>

View file

@ -2,7 +2,7 @@ import { Column, Heading, Row, Text } from '@umami/react-zen';
import { Plus } from 'lucide-react';
import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
import { DialogButton } from '@/components/input/DialogButton';
import { ShareCreateForm } from './ShareCreateForm';
import { ShareEditForm } from './ShareEditForm';
import { SharesTable } from './SharesTable';
export interface WebsiteShareFormProps {
@ -25,9 +25,9 @@ export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
label={formatMessage(labels.add)}
title={formatMessage(labels.share)}
variant="primary"
width="400px"
width="600px"
>
{({ close }) => <ShareCreateForm websiteId={websiteId} onClose={close} />}
{({ close }) => <ShareEditForm websiteId={websiteId} onClose={close} />}
</DialogButton>
</Row>
{hasShares ? (

View file

@ -4,14 +4,9 @@ import { createToken } from '@/lib/jwt';
import prisma from '@/lib/prisma';
import redis from '@/lib/redis';
import { json, notFound } from '@/lib/response';
import type { WhiteLabel } from '@/lib/types';
import { getShareByCode, getWebsite } from '@/queries/prisma';
export interface WhiteLabel {
name: string;
url: string;
image: string;
}
async function getAccountId(website: { userId?: string; teamId?: string }): Promise<string | null> {
if (website.userId) {
return website.userId;

View file

@ -1,8 +1,8 @@
import { Row, Text } from '@umami/react-zen';
import type { WhiteLabel } from '@/app/api/share/[shareId]/route';
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
import type { WhiteLabel } from '@/lib/types';
export function Footer({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
export function ShareFooter({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
if (whiteLabel) {
return (
<Row as="footer" paddingY="6" justifyContent="flex-end">

View file

@ -1,10 +1,10 @@
import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
import type { WhiteLabel } from '@/app/api/share/[shareId]/route';
import { LanguageButton } from '@/components/input/LanguageButton';
import { PreferencesButton } from '@/components/input/PreferencesButton';
import { Logo } from '@/components/svg';
import type { WhiteLabel } from '@/lib/types';
export function Header({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
export function ShareHeader({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
const logoUrl = whiteLabel?.url || 'https://umami.is';
const logoName = whiteLabel?.name || 'umami';
const logoImage = whiteLabel?.image;

View file

@ -115,7 +115,7 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri
const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage;
return (
<Column backgroundColor="2" minHeight="100%">
<Column backgroundColor="2">
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
<Column
display={{ xs: 'none', lg: 'flex' }}

View file

@ -31,6 +31,7 @@ export function PageBody({
<Column
{...props}
width="100%"
minHeight="100vh"
paddingBottom="6"
maxWidth={maxWidth}
paddingX={{ xs: '3', md: '6' }}

View file

@ -141,3 +141,9 @@ export interface ApiError extends Error {
code?: string;
message: string;
}
export interface WhiteLabel {
name: string;
url: string;
image: string;
}