Skip to content

Commit 40fd0b1

Browse files
committed
feat: add multiple files for text uploads
1 parent 41240b7 commit 40fd0b1

File tree

8 files changed

+234
-88
lines changed

8 files changed

+234
-88
lines changed

src/components/pages/upload/File/ToUploadFile.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default function ToUploadFile({
3939
return (
4040
<HoverCard shadow='md' position='top'>
4141
<HoverCard.Target>
42-
<Paper withBorder p='md' radius='md' pos='relative'>
42+
<Paper withBorder p='md' radius='md' pos='relative' h='100%'>
4343
<Center h='100%'>
4444
<Group justify='center' gap='xl'>
4545
<IconFileUpload size={48} />

src/components/pages/upload/File/index.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
import { Dropzone } from '@mantine/dropzone';
1919
import { useClipboard, useColorScheme } from '@mantine/hooks';
2020
import { notifications, showNotification } from '@mantine/notifications';
21-
import { IconDeviceSdCard, IconFiles, IconUpload, IconX } from '@tabler/icons-react';
21+
import { IconDeviceSdCard, IconFiles, IconTrashFilled, IconUpload, IconX } from '@tabler/icons-react';
2222
import { useCallback, useEffect, useState } from 'react';
2323
import { Link } from 'react-router-dom';
2424
import { useShallow } from 'zustand/shallow';
@@ -223,10 +223,19 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
223223
</Grid>
224224

225225
<Group justify='right' gap='sm' my='md'>
226+
<Button
227+
variant='outline'
228+
color='red'
229+
leftSection={<IconTrashFilled size='1rem' />}
230+
disabled={files.length === 0 || dropLoading}
231+
onClick={() => setFiles([])}
232+
>
233+
Clear all
234+
</Button>
226235
<UploadOptionsButton folder={folder} numFiles={files.length} />
227236
<Button
228237
variant='outline'
229-
leftSection={<IconUpload size={18} />}
238+
leftSection={<IconUpload size='1rem' />}
230239
disabled={files.length === 0 || dropLoading}
231240
onClick={upload}
232241
>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
.textarea textarea {
22
font-family: monospace;
3-
height: 50vh;
3+
height: 25vh;
44
}

src/components/pages/upload/Text/index.tsx

Lines changed: 104 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,78 @@
1+
import { useCodeMap } from '@/components/ConfigProvider';
12
import Render from '@/components/render/Render';
23
import { useUploadOptionsStore } from '@/lib/store/uploadOptions';
3-
import { Button, Center, Group, Select, Tabs, Text, Textarea, Title } from '@mantine/core';
4+
import { ActionIcon, Button, Group, Select, Tabs, Textarea, Title } from '@mantine/core';
45
import { useClipboard } from '@mantine/hooks';
5-
import { IconCursorText, IconEyeFilled, IconFiles, IconUpload } from '@tabler/icons-react';
6-
import { useEffect, useState } from 'react';
6+
import {
7+
IconCursorText,
8+
IconEyeFilled,
9+
IconFiles,
10+
IconPlus,
11+
IconTrashFilled,
12+
IconUpload,
13+
} from '@tabler/icons-react';
14+
import { useCallback, useEffect, useState } from 'react';
715
import { Link } from 'react-router-dom';
816
import { useShallow } from 'zustand/shallow';
9-
import UploadOptionsButton from '../UploadOptionsButton';
1017
import { renderMode } from '../renderMode';
1118
import { uploadFiles } from '../uploadFiles';
12-
13-
import { useCodeMap } from '@/components/ConfigProvider';
19+
import UploadOptionsButton from '../UploadOptionsButton';
20+
import useMultiTextFiles from '../useMultiTextFiles';
1421
import styles from './index.module.css';
1522

1623
export default function UploadText() {
1724
const clipboard = useClipboard();
1825
const [options, ephemeral, clearEphemeral] = useUploadOptionsStore(
1926
useShallow((state) => [state.options, state.ephemeral, state.clearEphemeral]),
2027
);
21-
const [selectedLanguage, setSelectedLanguage] = useState('txt');
22-
const [text, setText] = useState('');
28+
2329
const [loading, setLoading] = useState(false);
30+
const [files, selected, { setFile, addFile, removeFile }] = useMultiTextFiles();
2431

2532
const codeMap = useCodeMap();
2633

27-
useEffect(() => {
28-
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
29-
if (text.length > 0) {
30-
e.preventDefault();
34+
const handleBeforeUnload = useCallback(
35+
(e: BeforeUnloadEvent) => {
36+
for (const file of files) {
37+
if (file.text.length > 0) e.preventDefault();
3138
}
32-
};
39+
},
40+
[files],
41+
);
3342

43+
useEffect(() => {
3444
window.addEventListener('beforeunload', handleBeforeUnload);
3545
return () => {
3646
window.removeEventListener('beforeunload', handleBeforeUnload);
3747
};
38-
}, [text]);
48+
}, [files]);
3949

40-
const renderIn = renderMode(selectedLanguage);
50+
const handleTab = useCallback(
51+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
52+
if (e.key === 'Tab') {
53+
e.preventDefault();
54+
const { selectionStart, selectionEnd, value } = e.currentTarget;
55+
const newValue = `${value.substring(0, selectionStart)} ${value.substring(selectionEnd)}`;
4156

42-
const handleTab = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
43-
if (e.key === 'Tab') {
44-
e.preventDefault();
45-
const { selectionStart, selectionEnd, value } = e.currentTarget;
46-
const newValue = `${value.substring(0, selectionStart)} ${value.substring(selectionEnd)}`;
47-
setText(newValue);
48-
}
49-
};
57+
setFile(selected, 'text', newValue);
58+
}
59+
},
60+
[selected, setFile],
61+
);
5062

5163
const upload = () => {
52-
const blob = new Blob([text]);
53-
const file = new File([blob], `text.${selectedLanguage}`, {
54-
type: codeMap.find((meta) => meta.ext === selectedLanguage)?.mime,
55-
lastModified: Date.now(),
64+
const fileBlobs = files.map((file) => {
65+
const blob = new Blob([file.text], {
66+
type: codeMap.find((meta) => meta.ext === file.lang)?.mime,
67+
});
68+
69+
return new File([blob], `text.${file.lang}`, {
70+
type: blob.type,
71+
lastModified: Date.now(),
72+
});
5673
});
5774

58-
uploadFiles([file], {
75+
uploadFiles(fileBlobs, {
5976
clipboard,
6077
setFiles: () => {},
6178
setLoading,
@@ -82,52 +99,84 @@ export default function UploadText() {
8299
</Button>
83100
</Group>
84101

85-
<Tabs defaultValue='textarea' variant='pills' my='sm'>
102+
<Tabs defaultValue='textareas' variant='pills' my='sm'>
86103
<Tabs.List my='sm'>
87-
<Tabs.Tab value='textarea' leftSection={<IconCursorText size='1rem' />}>
104+
<Tabs.Tab value='textareas' leftSection={<IconCursorText size='1rem' />}>
88105
Text
89106
</Tabs.Tab>
90107
<Tabs.Tab value='preview' leftSection={<IconEyeFilled size='1rem' />}>
91108
Preview
92109
</Tabs.Tab>
93110
</Tabs.List>
94111

95-
<Tabs.Panel value='textarea'>
96-
<Textarea
97-
my='md'
98-
value={text}
99-
onChange={(e) => setText(e.currentTarget.value)}
100-
onKeyDown={handleTab}
101-
disabled={loading}
102-
className={styles.textarea}
103-
/>
112+
<Tabs.Panel value='textareas'>
113+
{files.map((file, index) => (
114+
<div key={index} style={{ position: 'relative' }}>
115+
<Textarea
116+
value={file.text}
117+
onChange={(e) => setFile(index, 'text', e.currentTarget.value)}
118+
onKeyDown={handleTab}
119+
disabled={loading}
120+
className={styles.textarea}
121+
my='sm'
122+
/>
123+
124+
<Group style={{ position: 'absolute', bottom: 10, right: 10 }} gap='xs'>
125+
<Select
126+
size='xs'
127+
data={codeMap.map((meta) => ({ value: meta.ext, label: meta.name }))}
128+
value={file.lang}
129+
onChange={(value) => setFile(index, 'lang', value as string)}
130+
searchable
131+
/>
132+
133+
{files.length > 1 && (
134+
<ActionIcon onClick={() => removeFile(index)} variant='outline' color='red' size='md'>
135+
<IconTrashFilled size='1rem' />
136+
</ActionIcon>
137+
)}
138+
</Group>
139+
</div>
140+
))}
141+
<Group my='sm' justify='center'>
142+
<Button
143+
onClick={() => addFile(selected)}
144+
variant='outline'
145+
size='compact-sm'
146+
leftSection={<IconPlus size='1rem' />}
147+
>
148+
Add text file
149+
</Button>
150+
151+
{files.some((file) => file.text.length > 0) && (
152+
<Button
153+
variant='outline'
154+
size='compact-sm'
155+
leftSection={<IconTrashFilled size='1rem' />}
156+
onClick={() => removeFile(true)}
157+
>
158+
Clear all
159+
</Button>
160+
)}
161+
</Group>
104162
</Tabs.Panel>
105163

106164
<Tabs.Panel value='preview'>
107-
{text.length === 0 ? (
108-
<Center h='100%'>
109-
<Text size='md' c='red'>
110-
No text to preview!
111-
</Text>
112-
</Center>
113-
) : (
114-
<Render mode={renderIn} code={text} language={selectedLanguage} />
115-
)}
165+
{files.map((file, index) => (
166+
<div key={index}>
167+
<Title order={4}>File {index + 1}</Title>
168+
<Render mode={renderMode(file.lang)} code={file.text} language={file.lang} />
169+
</div>
170+
))}
116171
</Tabs.Panel>
117172
</Tabs>
118173

119174
<Group justify='right' gap='sm' my='md'>
120-
<Select
121-
searchable
122-
defaultValue='txt'
123-
data={codeMap.map((meta) => ({ value: meta.ext, label: meta.name }))}
124-
onChange={(value) => setSelectedLanguage(value as string)}
125-
/>
126175
<UploadOptionsButton numFiles={1} />
127176
<Button
128177
variant='outline'
129178
leftSection={<IconUpload size='1rem' />}
130-
disabled={text.length === 0 || loading}
179+
disabled={files.some((file) => file.text.length === 0) || loading}
131180
onClick={upload}
132181
>
133182
Upload

src/components/pages/upload/UploadOptionsButton.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
IconFolderPlus,
3030
IconKey,
3131
IconPercentage,
32+
IconSettings,
3233
IconTrashFilled,
3334
IconWriting,
3435
} from '@tabler/icons-react';
@@ -466,6 +467,7 @@ export default function UploadOptionsButton({ folder, numFiles }: { folder?: str
466467
variant={changes() !== 0 ? 'light' : 'outline'}
467468
rightSection={changes() !== 0 ? <Badge variant='outline'>{changes()}</Badge> : null}
468469
onClick={() => setOpen(true)}
470+
leftSection={<IconSettings size='1rem' />}
469471
>
470472
Options
471473
</Button>

src/components/pages/upload/uploadFiles.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Response } from '@/lib/api/response';
22
import { ErrorBody } from '@/lib/response';
33
import { UploadOptionsStore } from '@/lib/store/uploadOptions';
4-
import { ActionIcon, Anchor, Button, Group, Stack, Table, Tooltip } from '@mantine/core';
4+
import { ActionIcon, Anchor, Button, Group, Stack, Tooltip } from '@mantine/core';
55
import { useClipboard } from '@mantine/hooks';
66
import { modals } from '@mantine/modals';
77
import { notifications } from '@mantine/notifications';
@@ -105,7 +105,7 @@ export function filesModal(
105105
title: 'Uploaded files',
106106
size: 'auto',
107107
children: (
108-
<Table withTableBorder={false} withColumnBorders={false} highlightOnHover horizontalSpacing={'sm'}>
108+
<>
109109
<Stack>
110110
{files.map((file, idx) => (
111111
<Group key={idx} justify='space-between'>
@@ -131,7 +131,7 @@ export function filesModal(
131131
</Stack>
132132
{files.length > 1 && (
133133
<Group justify='right'>
134-
<Tooltip label='Copy all links to clipboard'>
134+
<Tooltip label='Copy all links to clipboard (seperated by a new line)'>
135135
<Button
136136
onClick={() => {
137137
clipboard.copy(files.map((file) => file.url).join('\n'));
@@ -154,7 +154,7 @@ export function filesModal(
154154
</Tooltip>
155155
</Group>
156156
)}
157-
</Table>
157+
</>
158158
),
159159
});
160160

src/components/pages/upload/uploadPartialFiles.tsx

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useConfig } from '@/components/ConfigProvider';
22
import { Response } from '@/lib/api/response';
33
import { bytes } from '@/lib/bytes';
44
import { UploadOptionsStore } from '@/lib/store/uploadOptions';
5-
import { ActionIcon, Anchor, Group, Stack, Table, Text, Tooltip } from '@mantine/core';
5+
import { ActionIcon, Anchor, Group, Stack, Text, Tooltip } from '@mantine/core';
66
import { useClipboard } from '@mantine/hooks';
77
import { modals } from '@mantine/modals';
88
import { hideNotification, notifications } from '@mantine/notifications';
@@ -78,31 +78,29 @@ export function filesModal(
7878
title: 'Uploaded files',
7979
size: 'auto',
8080
children: (
81-
<Table withTableBorder={false} withColumnBorders={false} highlightOnHover horizontalSpacing={'sm'}>
82-
<Stack>
83-
{files.map((file, idx) => (
84-
<Group key={idx} justify='space-between'>
85-
<Group justify='left'>
86-
<Anchor component={Link} to={file.url}>
87-
{file.url}
88-
</Anchor>
89-
</Group>
90-
<Group justify='right'>
91-
<Tooltip label='Open link in a new tab'>
92-
<ActionIcon onClick={() => open(idx)} variant='filled' color='primary'>
93-
<IconExternalLink size='1rem' />
94-
</ActionIcon>
95-
</Tooltip>
96-
<Tooltip label='Copy link to clipboard'>
97-
<ActionIcon onClick={() => copy(idx)} variant='filled' color='primary'>
98-
<IconClipboardCopy size='1rem' />
99-
</ActionIcon>
100-
</Tooltip>
101-
</Group>
81+
<Stack>
82+
{files.map((file, idx) => (
83+
<Group key={idx} justify='space-between'>
84+
<Group justify='left'>
85+
<Anchor component={Link} to={file.url}>
86+
{file.url}
87+
</Anchor>
10288
</Group>
103-
))}
104-
</Stack>
105-
</Table>
89+
<Group justify='right'>
90+
<Tooltip label='Open link in a new tab'>
91+
<ActionIcon onClick={() => open(idx)} variant='filled' color='primary'>
92+
<IconExternalLink size='1rem' />
93+
</ActionIcon>
94+
</Tooltip>
95+
<Tooltip label='Copy link to clipboard'>
96+
<ActionIcon onClick={() => copy(idx)} variant='filled' color='primary'>
97+
<IconClipboardCopy size='1rem' />
98+
</ActionIcon>
99+
</Tooltip>
100+
</Group>
101+
</Group>
102+
))}
103+
</Stack>
106104
),
107105
});
108106

0 commit comments

Comments
 (0)