mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Share page changes.
This commit is contained in:
parent
c9f6653b62
commit
4a09f2bff6
11 changed files with 60 additions and 140 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -32,6 +32,7 @@ pm2.yml
|
|||
.vscode
|
||||
.tool-versions
|
||||
.claude
|
||||
nul
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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));
|
||||
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">
|
||||
{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>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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' }}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export function PageBody({
|
|||
<Column
|
||||
{...props}
|
||||
width="100%"
|
||||
minHeight="100vh"
|
||||
paddingBottom="6"
|
||||
maxWidth={maxWidth}
|
||||
paddingX={{ xs: '3', md: '6' }}
|
||||
|
|
|
|||
|
|
@ -141,3 +141,9 @@ export interface ApiError extends Error {
|
|||
code?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WhiteLabel {
|
||||
name: string;
|
||||
url: string;
|
||||
image: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue