mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
New share URL form.
This commit is contained in:
parent
543674c7f2
commit
6d1603fa28
14 changed files with 144 additions and 116 deletions
|
|
@ -80,7 +80,7 @@
|
|||
"@react-spring/web": "^9.7.3",
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@tanstack/react-query": "^5.80.10",
|
||||
"@umami/react-zen": "^0.138.0",
|
||||
"@umami/react-zen": "^0.139.0",
|
||||
"@umami/redis-client": "^0.27.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chalk": "^4.1.1",
|
||||
|
|
|
|||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -45,8 +45,8 @@ importers:
|
|||
specifier: ^5.80.10
|
||||
version: 5.80.10(react@19.1.0)
|
||||
'@umami/react-zen':
|
||||
specifier: ^0.138.0
|
||||
version: 0.138.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
|
||||
specifier: ^0.139.0
|
||||
version: 0.139.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
|
||||
'@umami/redis-client':
|
||||
specifier: ^0.27.0
|
||||
version: 0.27.0
|
||||
|
|
@ -2549,8 +2549,8 @@ packages:
|
|||
resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@umami/react-zen@0.138.0':
|
||||
resolution: {integrity: sha512-MlrLu21/WjmzPnYRQgQTofb7o+1fvL8XF7EbCjZFKjW+VHz5Cg6nOZWiFBxGWWCIAWfIVZpvczvK+thi4hHigg==}
|
||||
'@umami/react-zen@0.139.0':
|
||||
resolution: {integrity: sha512-NRf27+05z78DLFxK3aQUBfhZW7covl6qtS4OcaBUbZ71VZ7eeRVg7SU7Cn3NvkXlcI16t6bbLXGW4HjvfBhXsw==}
|
||||
|
||||
'@umami/redis-client@0.27.0':
|
||||
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
||||
|
|
@ -9733,7 +9733,7 @@ snapshots:
|
|||
'@typescript-eslint/types': 8.34.1
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@umami/react-zen@0.138.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
|
||||
'@umami/react-zen@0.139.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
|
||||
dependencies:
|
||||
'@fontsource/jetbrains-mono': 5.2.6
|
||||
'@internationalized/date': 3.8.2
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function TeamLeaveButton({ teamId, teamName }: { teamId: string; teamName
|
|||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button variant="secondary">
|
||||
<Button>
|
||||
<Icon>
|
||||
<LogOut />
|
||||
</Icon>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function TeamsJoinButton() {
|
|||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button variant="secondary">
|
||||
<Button>
|
||||
<Icon>
|
||||
<AddUser />
|
||||
</Icon>
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
import {
|
||||
Form,
|
||||
FormField,
|
||||
FormButtons,
|
||||
TextField,
|
||||
Button,
|
||||
Switch,
|
||||
FormSubmitButton,
|
||||
Box,
|
||||
useToast,
|
||||
} from '@umami/react-zen';
|
||||
import { useContext, useState } from 'react';
|
||||
import { getRandomChars } from '@/lib/crypto';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
|
||||
|
||||
const generateId = () => getRandomChars(16);
|
||||
|
||||
export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () => void }) {
|
||||
const website = useContext(WebsiteContext);
|
||||
const { domain, shareId } = website;
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const [id, setId] = useState(shareId);
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post(`/websites/${website.id}`, data),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
const { toast } = useToast();
|
||||
|
||||
const url = `${hostUrl || window?.location.origin || ''}${
|
||||
process.env.basePath || ''
|
||||
}/share/${id}/${domain}`;
|
||||
|
||||
const handleGenerate = () => {
|
||||
setId(generateId());
|
||||
};
|
||||
|
||||
const handleSwitch = (checked: boolean) => {
|
||||
const data = {
|
||||
name: website.name,
|
||||
domain: website.domain,
|
||||
shareId: checked ? generateId() : null,
|
||||
};
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch(`website:${website.id}`);
|
||||
onSave?.();
|
||||
},
|
||||
});
|
||||
setId(data.shareId);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
mutate(
|
||||
{ name: website.name, domain: website.domain, shareId: id },
|
||||
{
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch(`website:${website.id}`);
|
||||
onSave?.();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box marginBottom="6">
|
||||
<Switch defaultSelected={!!id} isSelected={!!id} onChange={handleSwitch}>
|
||||
{formatMessage(labels.enableShareUrl)}
|
||||
</Switch>
|
||||
</Box>
|
||||
{id && (
|
||||
<Form onSubmit={handleSave} error={error} values={{ id, url }}>
|
||||
<FormField label={formatMessage(messages.shareUrl)} name="url">
|
||||
<TextField isReadOnly allowCopy />
|
||||
</FormField>
|
||||
<FormButtons justifyContent="space-between">
|
||||
<Button onPress={handleGenerate}>{formatMessage(labels.regenerate)}</Button>
|
||||
<FormSubmitButton variant="primary" isDisabled={id === shareId} isLoading={isPending}>
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -52,9 +52,7 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?:
|
|||
description={formatMessage(messages.transferWebsite)}
|
||||
>
|
||||
<DialogTrigger>
|
||||
<Button variant="secondary" isDisabled={!canTransferWebsite}>
|
||||
{formatMessage(labels.transfer)}
|
||||
</Button>
|
||||
<Button isDisabled={!canTransferWebsite}>{formatMessage(labels.transfer)}</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.transferWebsite)}>
|
||||
{({ close }) => (
|
||||
|
|
@ -70,7 +68,7 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?:
|
|||
description={formatMessage(messages.resetWebsiteWarning)}
|
||||
>
|
||||
<DialogTrigger>
|
||||
<Button variant="secondary">{formatMessage(labels.reset)}</Button>
|
||||
<Button>{formatMessage(labels.reset)}</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.resetWebsite)}>
|
||||
{({ close }) => (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvide
|
|||
import { useMessages } from '@/components/hooks';
|
||||
import { Globe, Arrow } from '@/components/icons';
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
import { ShareUrl } from './ShareUrl';
|
||||
import { WebsiteShareForm } from './WebsiteShareForm';
|
||||
import { TrackingCode } from './TrackingCode';
|
||||
import { WebsiteData } from './WebsiteData';
|
||||
import { WebsiteEditForm } from './WebsiteEditForm';
|
||||
|
|
@ -48,7 +48,7 @@ export function WebsiteSettings({
|
|||
<TrackingCode websiteId={websiteId} />
|
||||
</TabPanel>
|
||||
<TabPanel id="share">
|
||||
<ShareUrl />
|
||||
<WebsiteShareForm />
|
||||
</TabPanel>
|
||||
<TabPanel id="data">
|
||||
<WebsiteData websiteId={websiteId} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import {
|
||||
Form,
|
||||
FormButtons,
|
||||
TextField,
|
||||
Button,
|
||||
Switch,
|
||||
FormSubmitButton,
|
||||
Column,
|
||||
Icon,
|
||||
Grid,
|
||||
Label,
|
||||
useToast,
|
||||
TooltipTrigger,
|
||||
Tooltip,
|
||||
} from '@umami/react-zen';
|
||||
import { useState } from 'react';
|
||||
import { getRandomChars } from '@/lib/crypto';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { Refresh } from '@/components/icons';
|
||||
|
||||
const generateId = () => getRandomChars(16);
|
||||
|
||||
export interface WebsiteShareFormProps {
|
||||
websiteId: string;
|
||||
shareId: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const [id, setId] = useState(shareId);
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post(`/websites/${websiteId}`, data),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
const { toast } = useToast();
|
||||
|
||||
const url = `${window?.location.origin || ''}${process.env.basePath || ''}/share/${id}`;
|
||||
|
||||
const handleGenerate = () => {
|
||||
setId(generateId());
|
||||
};
|
||||
|
||||
const handleSwitch = () => {
|
||||
setId(id ? null : generateId());
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const data = {
|
||||
shareId: id,
|
||||
};
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch(`website:${websiteId}`);
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSave} error={error} values={{ url }}>
|
||||
<Column gap>
|
||||
<Switch isSelected={!!id} onChange={handleSwitch}>
|
||||
{formatMessage(labels.enableShareUrl)}
|
||||
</Switch>
|
||||
{id && (
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||
<Grid columns="1fr auto" gap>
|
||||
<TextField value={url} isReadOnly allowCopy />
|
||||
<TooltipTrigger>
|
||||
<Button onPress={handleGenerate} variant="quiet" size="sm">
|
||||
<Icon>
|
||||
<Refresh />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Tooltip>{formatMessage(labels.regenerate)}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Grid>
|
||||
</Column>
|
||||
)}
|
||||
<FormButtons>
|
||||
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
<FormSubmitButton isDisabled={false} isLoading={isPending}>
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
</Column>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import { Button, Icon, Text, Row } from '@umami/react-zen';
|
||||
import { Button, Icon, Text, Row, DialogTrigger, Dialog, Modal } from '@umami/react-zen';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { useWebsite } from '@/components/hooks/useWebsite';
|
||||
import { Share, Edit } from '@/components/icons';
|
||||
import { Favicon } from '@/components/common/Favicon';
|
||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||
import { WebsiteShareForm } from '@/app/(main)/settings/websites/[websiteId]/WebsiteShareForm';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export function WebsiteHeader() {
|
||||
const website = useWebsite();
|
||||
|
|
@ -12,12 +14,7 @@ export function WebsiteHeader() {
|
|||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} showBorder={false}>
|
||||
<Row alignItems="center" gap>
|
||||
<ActiveUsers websiteId={website.id} />
|
||||
<Button>
|
||||
<Icon>
|
||||
<Share />
|
||||
</Icon>
|
||||
<Text>Share</Text>
|
||||
</Button>
|
||||
<ShareButton websiteId={website.id} shareId={website.shareId} />
|
||||
<Button>
|
||||
<Icon>
|
||||
<Edit />
|
||||
|
|
@ -28,3 +25,25 @@ export function WebsiteHeader() {
|
|||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
const ShareButton = ({ websiteId, shareId }) => {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button>
|
||||
<Icon>
|
||||
<Share />
|
||||
</Icon>
|
||||
<Text>Share</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.share)} style={{ width: 600 }}>
|
||||
{({ close }) => {
|
||||
return <WebsiteShareForm websiteId={websiteId} shareId={shareId} onClose={close} />;
|
||||
}}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
import { createContext, ReactNode, useEffect } from 'react';
|
||||
import { useModified, useWebsiteQuery } from '@/components/hooks';
|
||||
import { Loading } from '@umami/react-zen';
|
||||
import { Website } from '@/generated/prisma/client';
|
||||
|
||||
export const WebsiteContext = createContext(null);
|
||||
export const WebsiteContext = createContext<Website>(null);
|
||||
|
||||
export function WebsiteProvider({
|
||||
websiteId,
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ export async function POST(
|
|||
{ params }: { params: Promise<{ websiteId: string }> },
|
||||
) {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
domain: z.string(),
|
||||
name: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export * from '@/app/(main)/settings/teams/TeamsJoinButton';
|
|||
export * from '@/app/(main)/settings/teams/TeamsTable';
|
||||
export * from '@/app/(main)/settings/teams/WebsiteTags';
|
||||
|
||||
export * from '@/app/(main)/settings/websites/[websiteId]/ShareUrl';
|
||||
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteShareForm';
|
||||
export * from '@/app/(main)/settings/websites/[websiteId]/TrackingCode';
|
||||
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteData';
|
||||
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm';
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ export const CHART_COLORS = [
|
|||
|
||||
export const DOMAIN_REGEX =
|
||||
/^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-_]{1,63}\.)(xn--)?[a-z0-9-_]+(-[a-z0-9-_]+)*\.)+(xn--)?[a-z0-9-_]{2,63})$/;
|
||||
export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{8,16}$/;
|
||||
export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{8,50}$/;
|
||||
export const DATETIME_REGEX =
|
||||
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3}(Z|\+[0-9]{2}:[0-9]{2})?)?$/;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ body {
|
|||
background-color: var(--background-color);
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html[style*='padding-right'] {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
a,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue