mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Link editing.
This commit is contained in:
parent
0558563d35
commit
5f4b83b09c
13 changed files with 123 additions and 89 deletions
|
|
@ -7,7 +7,6 @@ import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||||
|
|
||||||
export function LinkDeleteButton({
|
export function LinkDeleteButton({
|
||||||
linkId,
|
linkId,
|
||||||
websiteId,
|
|
||||||
name,
|
name,
|
||||||
onSave,
|
onSave,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -19,7 +18,7 @@ export function LinkDeleteButton({
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { del, useMutation } = useApi();
|
const { del, useMutation } = useApi();
|
||||||
const { mutate, isPending, error } = useMutation({
|
const { mutate, isPending, error } = useMutation({
|
||||||
mutationFn: () => del(`/websites/${websiteId}/links/${linkId}`),
|
mutationFn: () => del(`/links/${linkId}`),
|
||||||
});
|
});
|
||||||
const { touch } = useModified();
|
const { touch } = useModified();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export function LinkEditButton({ linkId }: { linkId: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
|
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
|
||||||
<Dialog title={formatMessage(labels.link)} style={{ width: 800 }}>
|
<Dialog title={formatMessage(labels.link)} style={{ width: 800, minHeight: 300 }}>
|
||||||
{({ close }) => {
|
{({ close }) => {
|
||||||
return <LinkEditForm linkId={linkId} onClose={close} />;
|
return <LinkEditForm linkId={linkId} onClose={close} />;
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormField,
|
FormField,
|
||||||
|
|
@ -5,7 +6,6 @@ import {
|
||||||
Row,
|
Row,
|
||||||
TextField,
|
TextField,
|
||||||
Button,
|
Button,
|
||||||
Text,
|
|
||||||
Label,
|
Label,
|
||||||
Column,
|
Column,
|
||||||
Icon,
|
Icon,
|
||||||
|
|
@ -16,6 +16,8 @@ import { useMessages } from '@/components/hooks';
|
||||||
import { Refresh } from '@/components/icons';
|
import { Refresh } from '@/components/icons';
|
||||||
import { getRandomChars } from '@/lib/crypto';
|
import { getRandomChars } from '@/lib/crypto';
|
||||||
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
||||||
|
import { LINKS_URL } from '@/lib/constants';
|
||||||
|
import { isValidUrl } from '@/lib/url';
|
||||||
|
|
||||||
const generateId = () => getRandomChars(9);
|
const generateId = () => getRandomChars(9);
|
||||||
|
|
||||||
|
|
@ -31,29 +33,55 @@ export function LinkEditForm({
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { mutate, error, isPending } = useUpdateQuery('/links', { id: linkId, teamId });
|
const { mutate, error, isPending, touch } = useUpdateQuery(
|
||||||
const { linkDomain } = useConfig();
|
linkId ? `/links/${linkId}` : '/links',
|
||||||
|
{
|
||||||
|
id: linkId,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { linksUrl } = useConfig();
|
||||||
|
const hostUrl = linksUrl || LINKS_URL;
|
||||||
const { data, isLoading } = useLinkQuery(linkId);
|
const { data, isLoading } = useLinkQuery(linkId);
|
||||||
|
const [slug, setSlug] = useState(generateId());
|
||||||
|
|
||||||
const handleSubmit = async (data: any) => {
|
const handleSubmit = async (data: any) => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
touch('links');
|
||||||
onSave?.();
|
onSave?.();
|
||||||
onClose?.();
|
onClose?.();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (linkId && !isLoading) {
|
const handleSlug = () => {
|
||||||
|
const slug = generateId();
|
||||||
|
|
||||||
|
setSlug(slug);
|
||||||
|
|
||||||
|
return slug;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkUrl = (url: string) => {
|
||||||
|
if (!isValidUrl(url)) {
|
||||||
|
return formatMessage(labels.invalidUrl);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
setSlug(data.slug);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
if (linkId && isLoading) {
|
||||||
return <Loading position="page" />;
|
return <Loading position="page" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form onSubmit={handleSubmit} error={error?.message} defaultValues={{ slug, ...data }}>
|
||||||
onSubmit={handleSubmit}
|
|
||||||
error={error?.message}
|
|
||||||
defaultValues={{ slug: generateId(), ...data }}
|
|
||||||
>
|
|
||||||
{({ setValue }) => {
|
{({ setValue }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -62,33 +90,40 @@ export function LinkEditForm({
|
||||||
name="name"
|
name="name"
|
||||||
rules={{ required: formatMessage(labels.required) }}
|
rules={{ required: formatMessage(labels.required) }}
|
||||||
>
|
>
|
||||||
<TextField autoComplete="off" />
|
<TextField autoComplete="off" autoFocus />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label={formatMessage(labels.destinationUrl)}
|
label={formatMessage(labels.destinationUrl)}
|
||||||
name="url"
|
name="url"
|
||||||
rules={{ required: formatMessage(labels.required) }}
|
rules={{ required: formatMessage(labels.required), validate: checkUrl }}
|
||||||
>
|
>
|
||||||
<TextField placeholder="https://example.com" autoComplete="off" />
|
<TextField placeholder="https://example.com" autoComplete="off" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<Column>
|
|
||||||
<Label>{formatMessage(labels.link)}</Label>
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
<Text>{linkDomain || window.location.origin}/</Text>
|
|
||||||
<FormField
|
<FormField
|
||||||
name="slug"
|
name="slug"
|
||||||
rules={{
|
rules={{
|
||||||
required: formatMessage(labels.required),
|
required: formatMessage(labels.required),
|
||||||
}}
|
}}
|
||||||
style={{ width: '100%' }}
|
style={{ display: 'none' }}
|
||||||
>
|
>
|
||||||
<TextField autoComplete="off" isReadOnly />
|
<input type="hidden" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<Column>
|
||||||
|
<Label>{formatMessage(labels.link)}</Label>
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
<TextField
|
||||||
|
value={`${hostUrl}/${slug}`}
|
||||||
|
autoComplete="off"
|
||||||
|
isReadOnly
|
||||||
|
allowCopy
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="quiet"
|
variant="quiet"
|
||||||
onPress={() => setValue('slug', generateId(), { shouldDirty: true })}
|
onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}
|
||||||
>
|
>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Refresh />
|
<Refresh />
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ 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 } = useNavigation();
|
||||||
const { linksUrl } = useConfig();
|
const { linksUrl } = useConfig();
|
||||||
const hostUrl = linksUrl || `${window.location.origin}/x`;
|
const hostUrl = linksUrl || LINKS_URL;
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return <Empty />;
|
return <Empty />;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
|
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 { useConfig, useMessages, useNavigation } 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 Link from 'next/link';
|
import { PIXELS_URL } from '@/lib/constants';
|
||||||
|
|
||||||
export function PixelsTable({ data = [] }) {
|
export function PixelsTable({ data = [] }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { websiteId } = useNavigation();
|
const { websiteId } = useNavigation();
|
||||||
const { pixelsUrl } = useConfig();
|
const { pixelsUrl } = useConfig();
|
||||||
const defaultUrl = `${window.location.origin}/p`;
|
const hostUrl = pixelsUrl || PIXELS_URL;
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return <Empty />;
|
return <Empty />;
|
||||||
|
|
@ -21,7 +22,7 @@ export function PixelsTable({ data = [] }) {
|
||||||
<DataColumn id="name" label={formatMessage(labels.name)} />
|
<DataColumn id="name" label={formatMessage(labels.name)} />
|
||||||
<DataColumn id="url" label="URL">
|
<DataColumn id="url" label="URL">
|
||||||
{({ slug }: any) => {
|
{({ slug }: any) => {
|
||||||
const url = `${pixelsUrl || defaultUrl}/${slug}`;
|
const url = `${hostUrl}/${slug}`;
|
||||||
return <Link href={url}>{url}</Link>;
|
return <Link href={url}>{url}</Link>;
|
||||||
}}
|
}}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,32 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { canUpdateLink, canDeleteLink, canViewLink } from '@/validations';
|
import { canUpdateLink, canDeleteLink, canViewLink } from '@/validations';
|
||||||
import { SHARE_ID_REGEX } from '@/lib/constants';
|
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { ok, json, unauthorized, serverError } from '@/lib/response';
|
import { ok, json, unauthorized, serverError, badRequest } from '@/lib/response';
|
||||||
import { deleteLink, getLink, updateLink } from '@/queries';
|
import { deleteLink, getLink, updateLink } from '@/queries';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(request: Request, { params }: { params: Promise<{ linkId: string }> }) {
|
||||||
request: Request,
|
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
|
||||||
) {
|
|
||||||
const { auth, error } = await parseRequest(request);
|
const { auth, error } = await parseRequest(request);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = await params;
|
const { linkId } = await params;
|
||||||
|
|
||||||
if (!(await canViewLink(auth, websiteId))) {
|
if (!(await canViewLink(auth, linkId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const website = await getLink(websiteId);
|
const website = await getLink(linkId);
|
||||||
|
|
||||||
return json(website);
|
return json(website);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(request: Request, { params }: { params: Promise<{ linkId: string }> }) {
|
||||||
request: Request,
|
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
|
||||||
) {
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string(),
|
||||||
domain: z.string().optional(),
|
url: z.string(),
|
||||||
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
|
slug: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
@ -42,20 +35,20 @@ export async function POST(
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = await params;
|
const { linkId } = await params;
|
||||||
const { name, domain, shareId } = body;
|
const { name, url, slug } = body;
|
||||||
|
|
||||||
if (!(await canUpdateLink(auth, websiteId))) {
|
if (!(await canUpdateLink(auth, linkId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const website = await updateLink(websiteId, { name, domain, shareId });
|
const result = await updateLink(linkId, { name, url, slug });
|
||||||
|
|
||||||
return Response.json(website);
|
return Response.json(result);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.message.includes('Unique constraint') && e.message.includes('share_id')) {
|
if (e.message.includes('Unique constraint') && e.message.includes('slug')) {
|
||||||
return serverError(new Error('That share ID is already taken.'));
|
return badRequest('That slug is already taken.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverError(e);
|
return serverError(e);
|
||||||
|
|
@ -64,7 +57,7 @@ export async function POST(
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
{ params }: { params: Promise<{ linkId: string }> },
|
||||||
) {
|
) {
|
||||||
const { auth, error } = await parseRequest(request);
|
const { auth, error } = await parseRequest(request);
|
||||||
|
|
||||||
|
|
@ -72,13 +65,13 @@ export async function DELETE(
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = await params;
|
const { linkId } = await params;
|
||||||
|
|
||||||
if (!(await canDeleteLink(auth, websiteId))) {
|
if (!(await canDeleteLink(auth, linkId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteLink(websiteId);
|
await deleteLink(linkId);
|
||||||
|
|
||||||
return ok();
|
return ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,32 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from '@/validations';
|
import { canUpdatePixel, canDeletePixel, canViewPixel } from '@/validations';
|
||||||
import { SHARE_ID_REGEX } from '@/lib/constants';
|
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { ok, json, unauthorized, serverError } from '@/lib/response';
|
import { ok, json, unauthorized, serverError, badRequest } from '@/lib/response';
|
||||||
import { deleteWebsite, getWebsite, updateWebsite } from '@/queries';
|
import { deletePixel, getPixel, updatePixel } from '@/queries';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(request: Request, { params }: { params: Promise<{ pixelId: string }> }) {
|
||||||
request: Request,
|
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
|
||||||
) {
|
|
||||||
const { auth, error } = await parseRequest(request);
|
const { auth, error } = await parseRequest(request);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = await params;
|
const { pixelId } = await params;
|
||||||
|
|
||||||
if (!(await canViewWebsite(auth, websiteId))) {
|
if (!(await canViewPixel(auth, pixelId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const website = await getWebsite(websiteId);
|
const pixel = await getPixel(pixelId);
|
||||||
|
|
||||||
return json(website);
|
return json(pixel);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(request: Request, { params }: { params: Promise<{ pixelId: string }> }) {
|
||||||
request: Request,
|
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
|
||||||
) {
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string(),
|
||||||
domain: z.string().optional(),
|
url: z.string().url(),
|
||||||
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
|
slug: z.string().min(8),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
@ -42,20 +35,20 @@ export async function POST(
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = await params;
|
const { pixelId } = await params;
|
||||||
const { name, domain, shareId } = body;
|
const { name, domain, shareId } = body;
|
||||||
|
|
||||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
if (!(await canUpdatePixel(auth, pixelId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const website = await updateWebsite(websiteId, { name, domain, shareId });
|
const pixel = await updatePixel(pixelId, { name, domain, shareId });
|
||||||
|
|
||||||
return Response.json(website);
|
return Response.json(pixel);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.message.includes('Unique constraint') && e.message.includes('share_id')) {
|
if (e.message.includes('Unique constraint') && e.message.includes('slug')) {
|
||||||
return serverError(new Error('That share ID is already taken.'));
|
return badRequest('That slug is already taken.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverError(e);
|
return serverError(e);
|
||||||
|
|
@ -64,7 +57,7 @@ export async function POST(
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: Promise<{ websiteId: string }> },
|
{ params }: { params: Promise<{ pixelId: string }> },
|
||||||
) {
|
) {
|
||||||
const { auth, error } = await parseRequest(request);
|
const { auth, error } = await parseRequest(request);
|
||||||
|
|
||||||
|
|
@ -72,13 +65,13 @@ export async function DELETE(
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = await params;
|
const { pixelId } = await params;
|
||||||
|
|
||||||
if (!(await canDeleteWebsite(auth, websiteId))) {
|
if (!(await canDeletePixel(auth, pixelId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteWebsite(websiteId);
|
await deletePixel(pixelId);
|
||||||
|
|
||||||
return ok();
|
return ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ export async function GET(request: Request) {
|
||||||
|
|
||||||
const filters = await getQueryFilters(query);
|
const filters = await getQueryFilters(query);
|
||||||
|
|
||||||
const inks = await getUserLinks(auth.user.id, filters);
|
const links = await getUserLinks(auth.user.id, filters);
|
||||||
|
|
||||||
return json(inks);
|
return json(links);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||||
import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from '@/validations';
|
import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from '@/validations';
|
||||||
import { SHARE_ID_REGEX } from '@/lib/constants';
|
import { SHARE_ID_REGEX } from '@/lib/constants';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { ok, json, unauthorized, serverError } from '@/lib/response';
|
import { ok, json, unauthorized, serverError, badRequest } from '@/lib/response';
|
||||||
import { deleteWebsite, getWebsite, updateWebsite } from '@/queries';
|
import { deleteWebsite, getWebsite, updateWebsite } from '@/queries';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
|
|
@ -55,7 +55,7 @@ export async function POST(
|
||||||
return Response.json(website);
|
return Response.json(website);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.message.includes('Unique constraint') && e.message.includes('share_id')) {
|
if (e.message.includes('Unique constraint') && e.message.includes('share_id')) {
|
||||||
return serverError(new Error('That share ID is already taken.'));
|
return badRequest('That share ID is already taken.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverError(e);
|
return serverError(e);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export function useLinkQuery(linkId: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['link', { linkId, modified }],
|
queryKey: ['link', { linkId, modified }],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
return get(`/link/${linkId}`);
|
return get(`/links/${linkId}`);
|
||||||
},
|
},
|
||||||
enabled: !!linkId,
|
enabled: !!linkId,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,7 @@ export const labels = defineMessages({
|
||||||
analysis: { id: 'label.analysis', defaultMessage: 'Analysis' },
|
analysis: { id: 'label.analysis', defaultMessage: 'Analysis' },
|
||||||
destinationUrl: { id: 'label.destination-url', defaultMessage: 'Destination URL' },
|
destinationUrl: { id: 'label.destination-url', defaultMessage: 'Destination URL' },
|
||||||
audience: { id: 'label.audience', defaultMessage: 'Audience' },
|
audience: { id: 'label.audience', defaultMessage: 'Audience' },
|
||||||
|
invalidUrl: { id: 'label.invalid-url', defaultMessage: 'Invalid URL' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ export const REPO_URL = 'https://github.com/umami-software/umami';
|
||||||
export const UPDATES_URL = 'https://api.umami.is/v1/updates';
|
export const UPDATES_URL = 'https://api.umami.is/v1/updates';
|
||||||
export const TELEMETRY_PIXEL = 'https://i.umami.is/a.png';
|
export const TELEMETRY_PIXEL = 'https://i.umami.is/a.png';
|
||||||
export const FAVICON_URL = 'https://icons.duckduckgo.com/ip3/{{domain}}.ico';
|
export const FAVICON_URL = 'https://icons.duckduckgo.com/ip3/{{domain}}.ico';
|
||||||
|
export const LINKS_URL = `${globalThis?.location?.origin}/q`;
|
||||||
|
export const PIXELS_URL = `${globalThis?.location?.origin}/p`;
|
||||||
|
|
||||||
export const DEFAULT_LOCALE = 'en-US';
|
export const DEFAULT_LOCALE = 'en-US';
|
||||||
export const DEFAULT_THEME = 'light';
|
export const DEFAULT_THEME = 'light';
|
||||||
|
|
|
||||||
|
|
@ -38,3 +38,12 @@ export function safeDecodeURIComponent(s: string | undefined | null): string | u
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isValidUrl(url: string) {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue