Skip to content

Commit

Permalink
feat: edit urls
Browse files Browse the repository at this point in the history
  • Loading branch information
diced committed Nov 13, 2024
1 parent 0deeb0f commit 129ba0c
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 16 deletions.
9 changes: 5 additions & 4 deletions src/components/file/DashboardFile/EditFileDetailsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default function EditFileDetailsModal({
mutateFiles();
}
};

const handleSave = async () => {
const data: {
maxViews?: number;
Expand Down Expand Up @@ -95,7 +96,7 @@ export default function EditFileDetailsModal({
<NumberInput
label='Max Views'
placeholder='Unlimited'
description='The maximum number of views the files can have before they are deleted. Leave blank to allow as many views as you want.'
description='The maximum number of views this file can have before it is deleted. Leave blank to allow as many views as you want.'
min={0}
value={maxViews || ''}
onChange={(value) => setMaxViews(value === '' ? null : Number(value))}
Expand All @@ -104,7 +105,7 @@ export default function EditFileDetailsModal({

<TextInput
label='Original Name'
description='Add an original name. When downloading this file instead of using the generated file name (if chosen), it will download with this "original name" instead.'
description='Add an original name. When downloading this file, instead of using the generated file name (if chosen), it will download with this "original name" instead.'
value={originalName ?? ''}
onChange={(event) =>
setOriginalName(event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim())
Expand Down Expand Up @@ -140,7 +141,7 @@ export default function EditFileDetailsModal({
) : (
<PasswordInput
label='Password'
description='Set a password for these files. Leave blank to disable password protection.'
description='Set a password for this file. Leave blank to disable password protection.'
value={password ?? ''}
autoComplete='off'
onChange={(event) =>
Expand All @@ -153,7 +154,7 @@ export default function EditFileDetailsModal({
<Divider />

<Button onClick={handleSave} leftSection={<IconPencil size='1rem' />}>
Save Changes
Save changes
</Button>
</Stack>
</Modal>
Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/files/views/FileTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ export default function FileTable({ id }: { id?: string }) {
{
accessor: 'actions',
textAlign: 'right',
width: 180,
width: 45 * 4,
render: (file) => (
<Group gap='sm'>
<Tooltip label='More details'>
Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/folders/views/FolderTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default function FolderTableView() {
},
{
accessor: 'actions',
width: 170,
width: 45 * 4,
render: (folder) => (
<Group gap='sm'>
<Tooltip label='View files'>
Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/invites/views/InviteTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export default function InviteTableView() {
},
{
accessor: 'actions',
width: 100,
width: 45 * 2,
render: (invite) => (
<Group gap='sm'>
<Tooltip label='Copy invite link'>
Expand Down
157 changes: 157 additions & 0 deletions src/components/pages/urls/EditUrlModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Url } from '@/lib/db/models/url';
import { fetchApi } from '@/lib/fetchApi';
import { Button, Divider, Modal, NumberInput, PasswordInput, Stack, TextInput, Title } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconEye, IconKey, IconPencil, IconPencilOff, IconTrashFilled } from '@tabler/icons-react';
import { useState } from 'react';
import { mutate } from 'swr';

export default function EditUrlModal({
url,
onClose,
open,
}: {
open: boolean;
url: Url | null;
onClose: () => void;
}) {
if (!url) return null;

const [maxViews, setMaxViews] = useState<number | null>(url?.maxViews ?? null);
const [password, setPassword] = useState<string | null>('');
const [vanity, setVanity] = useState<string | null>(url?.vanity ?? null);
const [destination, setDestination] = useState<string | null>(url?.destination ?? null);

const handleRemovePassword = async () => {
if (!url.password) return;

const { error } = await fetchApi(`/api/user/urls/${url.id}`, 'PATCH', {
password: null,
});

if (error) {
showNotification({
title: 'Failed to remove password...',
message: error.error,
color: 'red',
icon: <IconPencilOff size='1rem' />,
});
} else {
showNotification({
title: 'Password removed!',
message: 'The password has been removed from the URL.',
color: 'green',
icon: <IconPencil size='1rem' />,
});

onClose();
mutate('/api/user/urls');
mutate({ key: '/api/user/urls' });
}
};

const handleSave = async () => {
const data: {
maxViews?: number;
password?: string;
vanity?: string;
destination?: string;
} = {};

if (maxViews !== null) data['maxViews'] = maxViews;
if (password !== null) data['password'] = password?.trim();
if (vanity !== null && vanity !== url.vanity) data['vanity'] = vanity?.trim();
if (destination !== null && destination !== url.destination) data['destination'] = destination?.trim();

const { error } = await fetchApi(`/api/user/urls/${url.id}`, 'PATCH', data);

if (error) {
showNotification({
title: 'Failed to save changes...',
message: error.error,
color: 'red',
icon: <IconPencilOff size='1rem' />,
});
} else {
showNotification({
title: 'Changes saved!',
message: 'The changes have been saved successfully.',
color: 'green',
icon: <IconPencil size='1rem' />,
});

onClose();
mutate('/api/user/urls');
mutate({ key: '/api/user/urls' });
}
};

return (
<Modal
title={<Title>Editing &quot;{url.vanity ?? url.code}&quot;</Title>}
opened={open}
onClose={onClose}
>
<Stack gap='xs' my='sm'>
<NumberInput
label='Max Views'
placeholder='Unlimited'
description='The maximum number of clicks this URL can have before it is automatically deleted. Leave blank to allow as many views as you want.'
value={maxViews || ''}
onChange={(value) => setMaxViews(value === '' ? null : Number(value))}
min={0}
leftSection={<IconEye size='1rem' />}
/>

<TextInput
label='Vanity'
placeholder='Optional'
description='A custom alias for your URL. Leave blank to use the randomly generated code.'
value={vanity || ''}
onChange={(event) =>
setVanity(event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim())
}
/>

<TextInput
label='Destination'
placeholder='https://example.com'
value={destination || ''}
onChange={(event) =>
setDestination(event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim())
}
/>

<Divider />

{url.password ? (
<Button
variant='light'
color='red'
leftSection={<IconTrashFilled size='1rem' />}
onClick={handleRemovePassword}
>
Remove password
</Button>
) : (
<PasswordInput
label='Password'
description='Set a password for this URL. Leave blank to disable password protection.'
value={password ?? ''}
autoComplete='off'
onChange={(event) =>
setPassword(event.currentTarget.value.trim() === '' ? null : event.currentTarget.value.trim())
}
leftSection={<IconKey size='1rem' />}
/>
)}

<Divider />

<Button onClick={handleSave} leftSection={<IconPencil size='1rem' />}>
Save changes
</Button>
</Stack>
</Modal>
);
}
7 changes: 5 additions & 2 deletions src/components/pages/urls/UrlCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { Url } from '@/lib/db/models/url';
import { formatRootUrl } from '@/lib/url';
import { ActionIcon, Anchor, Card, Group, Menu, Stack, Text, Tooltip } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { IconCopy, IconDots, IconTrashFilled } from '@tabler/icons-react';
import { IconCopy, IconDots, IconPencil, IconTrashFilled } from '@tabler/icons-react';
import { copyUrl, deleteUrl } from './actions';
import { useSettingsStore } from '@/lib/store/settings';

export default function UserCard({ url }: { url: Url }) {
export default function UserCard({ url, setSelectedUrl }: { url: Url; setSelectedUrl: (url: Url) => void }) {
const config = useConfig();
const clipboard = useClipboard();

Expand Down Expand Up @@ -51,6 +51,9 @@ export default function UserCard({ url }: { url: Url }) {
>
Copy destination
</Menu.Item>
<Menu.Item leftSection={<IconPencil size='1rem' />} onClick={() => setSelectedUrl(url)}>
Edit
</Menu.Item>
<Menu.Item
leftSection={<IconTrashFilled size='1rem' />}
color='red'
Expand Down
7 changes: 6 additions & 1 deletion src/components/pages/urls/views/UrlGridView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import { Response } from '@/lib/api/response';
import type { Url } from '@/lib/db/models/url';
import { Center, Group, LoadingOverlay, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { IconLink } from '@tabler/icons-react';
import { useState } from 'react';
import useSWR from 'swr';
import EditUrlModal from '../EditUrlModal';
import UrlCard from '../UrlCard';

export default function UrlGridView() {
const { data: urls, isLoading } = useSWR<Extract<Response['/api/user/urls'], Url[]>>('/api/user/urls');
const [selectedUrl, setSelectedUrl] = useState<Url | null>(null);

return (
<>
<EditUrlModal url={selectedUrl} onClose={() => setSelectedUrl(null)} open={!!selectedUrl} />

{isLoading ? (
<Paper withBorder h={200}>
<LoadingOverlay visible />
Expand All @@ -25,7 +30,7 @@ export default function UrlGridView() {
}}
pos='relative'
>
{urls?.map((url) => <UrlCard key={url.id} url={url} />)}
{urls?.map((url) => <UrlCard setSelectedUrl={setSelectedUrl} key={url.id} url={url} />)}
</SimpleGrid>
) : (
<Paper withBorder p='sm' my='sm'>
Expand Down
19 changes: 17 additions & 2 deletions src/components/pages/urls/views/UrlTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import { useEffect, useReducer, useState } from 'react';
import useSWR from 'swr';
import { copyUrl, deleteUrl } from '../actions';
import { IconCopy, IconTrashFilled } from '@tabler/icons-react';
import { IconCopy, IconPencil, IconTrashFilled } from '@tabler/icons-react';
import { useConfig } from '@/components/ConfigProvider';
import { useClipboard } from '@mantine/hooks';
import { useSettingsStore } from '@/lib/store/settings';
import { formatRootUrl } from '@/lib/url';
import EditUrlModal from '../EditUrlModal';

const NAMES = {
code: 'Code',
Expand Down Expand Up @@ -131,6 +132,8 @@ export default function UrlTableView() {
searchQuery.vanity.trim() !== '' ||
searchQuery.destination.trim() !== '';

const [selectedUrl, setSelectedUrl] = useState<Url | null>(null);

useEffect(() => {
if (data) {
const sorted = data.sort((a, b) => {
Expand Down Expand Up @@ -162,6 +165,8 @@ export default function UrlTableView() {

return (
<>
<EditUrlModal url={selectedUrl} onClose={() => setSelectedUrl(null)} open={!!selectedUrl} />

<Box my='sm'>
<DataTable
borderRadius='sm'
Expand Down Expand Up @@ -250,7 +255,7 @@ export default function UrlTableView() {
},
{
accessor: 'actions',
width: 100,
width: 45 * 3,
render: (url) => (
<Group gap='sm'>
<Tooltip label='Copy URL'>
Expand All @@ -263,6 +268,16 @@ export default function UrlTableView() {
<IconCopy size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Edit URL'>
<ActionIcon
onClick={(e) => {
e.stopPropagation();
setSelectedUrl(url);
}}
>
<IconPencil size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Delete URL'>
<ActionIcon
color='red'
Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/users/views/UserTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default function UserTableView() {
},
{
accessor: 'actions',
width: 150,
width: 45 * 3,
render: (user) => (
<Group gap='sm'>
<Tooltip label="View user's files">
Expand Down
2 changes: 1 addition & 1 deletion src/lib/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ParseValueMetrics } from './metrics';

export type ParseValue = {
file?: File;
url?: Url;
url?: Partial<Url>;
user?: User | Omit<User, 'oauthProviders' | 'passkeys'>;

link?: {
Expand Down
12 changes: 10 additions & 2 deletions src/lib/webhooks/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function parseContent(
export function buildResponse(
content: ReturnType<typeof parseContent>,
file?: File,
url?: Url,
url?: Partial<Url>,
): WebhooksExecuteBody | null {
if (!content) return null;
if (!file && !url) return null;
Expand Down Expand Up @@ -138,7 +138,15 @@ export async function onUpload({ user, file, link }: { user: User; file: File; l
return;
}

export async function onShorten({ user, url, link }: { user: User; url: Url; link: ParseValue['link'] }) {
export async function onShorten({
user,
url,
link,
}: {
user: User;
url: Partial<Url>;
link: ParseValue['link'];
}) {
if (!config.discord?.onShorten) return logger.debug('no onShorten config, no webhook executed');
if (!config.discord?.webhookUrl || !config.discord?.onShorten?.webhookUrl)
return logger.debug('no webhookUrl config, no webhook executed');
Expand Down
Loading

0 comments on commit 129ba0c

Please sign in to comment.