mirror of
https://github.com/umami-software/umami.git
synced 2025-12-08 05:12:36 +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",
|
"@react-spring/web": "^9.7.3",
|
||||||
"@svgr/cli": "^8.1.0",
|
"@svgr/cli": "^8.1.0",
|
||||||
"@tanstack/react-query": "^5.80.10",
|
"@tanstack/react-query": "^5.80.10",
|
||||||
"@umami/react-zen": "^0.138.0",
|
"@umami/react-zen": "^0.139.0",
|
||||||
"@umami/redis-client": "^0.27.0",
|
"@umami/redis-client": "^0.27.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
|
|
|
||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -45,8 +45,8 @@ importers:
|
||||||
specifier: ^5.80.10
|
specifier: ^5.80.10
|
||||||
version: 5.80.10(react@19.1.0)
|
version: 5.80.10(react@19.1.0)
|
||||||
'@umami/react-zen':
|
'@umami/react-zen':
|
||||||
specifier: ^0.138.0
|
specifier: ^0.139.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))
|
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':
|
'@umami/redis-client':
|
||||||
specifier: ^0.27.0
|
specifier: ^0.27.0
|
||||||
version: 0.27.0
|
version: 0.27.0
|
||||||
|
|
@ -2549,8 +2549,8 @@ packages:
|
||||||
resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==}
|
resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@umami/react-zen@0.138.0':
|
'@umami/react-zen@0.139.0':
|
||||||
resolution: {integrity: sha512-MlrLu21/WjmzPnYRQgQTofb7o+1fvL8XF7EbCjZFKjW+VHz5Cg6nOZWiFBxGWWCIAWfIVZpvczvK+thi4hHigg==}
|
resolution: {integrity: sha512-NRf27+05z78DLFxK3aQUBfhZW7covl6qtS4OcaBUbZ71VZ7eeRVg7SU7Cn3NvkXlcI16t6bbLXGW4HjvfBhXsw==}
|
||||||
|
|
||||||
'@umami/redis-client@0.27.0':
|
'@umami/redis-client@0.27.0':
|
||||||
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
|
||||||
|
|
@ -9733,7 +9733,7 @@ snapshots:
|
||||||
'@typescript-eslint/types': 8.34.1
|
'@typescript-eslint/types': 8.34.1
|
||||||
eslint-visitor-keys: 4.2.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:
|
dependencies:
|
||||||
'@fontsource/jetbrains-mono': 5.2.6
|
'@fontsource/jetbrains-mono': 5.2.6
|
||||||
'@internationalized/date': 3.8.2
|
'@internationalized/date': 3.8.2
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export function TeamLeaveButton({ teamId, teamName }: { teamId: string; teamName
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Button variant="secondary">
|
<Button>
|
||||||
<Icon>
|
<Icon>
|
||||||
<LogOut />
|
<LogOut />
|
||||||
</Icon>
|
</Icon>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export function TeamsJoinButton() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Button variant="secondary">
|
<Button>
|
||||||
<Icon>
|
<Icon>
|
||||||
<AddUser />
|
<AddUser />
|
||||||
</Icon>
|
</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)}
|
description={formatMessage(messages.transferWebsite)}
|
||||||
>
|
>
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Button variant="secondary" isDisabled={!canTransferWebsite}>
|
<Button isDisabled={!canTransferWebsite}>{formatMessage(labels.transfer)}</Button>
|
||||||
{formatMessage(labels.transfer)}
|
|
||||||
</Button>
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<Dialog title={formatMessage(labels.transferWebsite)}>
|
<Dialog title={formatMessage(labels.transferWebsite)}>
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
|
|
@ -70,7 +68,7 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?:
|
||||||
description={formatMessage(messages.resetWebsiteWarning)}
|
description={formatMessage(messages.resetWebsiteWarning)}
|
||||||
>
|
>
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Button variant="secondary">{formatMessage(labels.reset)}</Button>
|
<Button>{formatMessage(labels.reset)}</Button>
|
||||||
<Modal>
|
<Modal>
|
||||||
<Dialog title={formatMessage(labels.resetWebsite)}>
|
<Dialog title={formatMessage(labels.resetWebsite)}>
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvide
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
import { Globe, Arrow } from '@/components/icons';
|
import { Globe, Arrow } from '@/components/icons';
|
||||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||||
import { ShareUrl } from './ShareUrl';
|
import { WebsiteShareForm } from './WebsiteShareForm';
|
||||||
import { TrackingCode } from './TrackingCode';
|
import { TrackingCode } from './TrackingCode';
|
||||||
import { WebsiteData } from './WebsiteData';
|
import { WebsiteData } from './WebsiteData';
|
||||||
import { WebsiteEditForm } from './WebsiteEditForm';
|
import { WebsiteEditForm } from './WebsiteEditForm';
|
||||||
|
|
@ -48,7 +48,7 @@ export function WebsiteSettings({
|
||||||
<TrackingCode websiteId={websiteId} />
|
<TrackingCode websiteId={websiteId} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="share">
|
<TabPanel id="share">
|
||||||
<ShareUrl />
|
<WebsiteShareForm />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="data">
|
<TabPanel id="data">
|
||||||
<WebsiteData websiteId={websiteId} />
|
<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 { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { useWebsite } from '@/components/hooks/useWebsite';
|
import { useWebsite } from '@/components/hooks/useWebsite';
|
||||||
import { Share, Edit } from '@/components/icons';
|
import { Share, Edit } from '@/components/icons';
|
||||||
import { Favicon } from '@/components/common/Favicon';
|
import { Favicon } from '@/components/common/Favicon';
|
||||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||||
|
import { WebsiteShareForm } from '@/app/(main)/settings/websites/[websiteId]/WebsiteShareForm';
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
|
||||||
export function WebsiteHeader() {
|
export function WebsiteHeader() {
|
||||||
const website = useWebsite();
|
const website = useWebsite();
|
||||||
|
|
@ -12,12 +14,7 @@ export function WebsiteHeader() {
|
||||||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} showBorder={false}>
|
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} showBorder={false}>
|
||||||
<Row alignItems="center" gap>
|
<Row alignItems="center" gap>
|
||||||
<ActiveUsers websiteId={website.id} />
|
<ActiveUsers websiteId={website.id} />
|
||||||
<Button>
|
<ShareButton websiteId={website.id} shareId={website.shareId} />
|
||||||
<Icon>
|
|
||||||
<Share />
|
|
||||||
</Icon>
|
|
||||||
<Text>Share</Text>
|
|
||||||
</Button>
|
|
||||||
<Button>
|
<Button>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Edit />
|
<Edit />
|
||||||
|
|
@ -28,3 +25,25 @@ export function WebsiteHeader() {
|
||||||
</PageHeader>
|
</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 { createContext, ReactNode, useEffect } from 'react';
|
||||||
import { useModified, useWebsiteQuery } from '@/components/hooks';
|
import { useModified, useWebsiteQuery } from '@/components/hooks';
|
||||||
import { Loading } from '@umami/react-zen';
|
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({
|
export function WebsiteProvider({
|
||||||
websiteId,
|
websiteId,
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ export async function POST(
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
) {
|
) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string(),
|
name: z.string().optional(),
|
||||||
domain: z.string(),
|
domain: z.string().optional(),
|
||||||
shareId: z.string().regex(SHARE_ID_REGEX).nullable().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/TeamsTable';
|
||||||
export * from '@/app/(main)/settings/teams/WebsiteTags';
|
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]/TrackingCode';
|
||||||
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteData';
|
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteData';
|
||||||
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm';
|
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm';
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ export const CHART_COLORS = [
|
||||||
|
|
||||||
export const DOMAIN_REGEX =
|
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})$/;
|
/^(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 =
|
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})?)?$/;
|
/^[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);
|
background-color: var(--background-color);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[style*='padding-right'] {
|
||||||
|
padding-right: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue