1+ import { useCodeMap } from '@/components/ConfigProvider' ;
12import Render from '@/components/render/Render' ;
23import { 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' ;
45import { 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' ;
715import { Link } from 'react-router-dom' ;
816import { useShallow } from 'zustand/shallow' ;
9- import UploadOptionsButton from '../UploadOptionsButton' ;
1017import { renderMode } from '../renderMode' ;
1118import { uploadFiles } from '../uploadFiles' ;
12-
13- import { useCodeMap } from '@/components/ConfigProvider ' ;
19+ import UploadOptionsButton from '../UploadOptionsButton' ;
20+ import useMultiTextFiles from '../useMultiTextFiles ' ;
1421import styles from './index.module.css' ;
1522
1623export 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
0 commit comments