@@ -35,6 +35,41 @@ export default function Index() {
3535 const [ isDragging , setIsDragging ] = useState ( false ) ;
3636 const [ activeTab , setActiveTab ] = useState < "binary" | "image" > ( "binary" ) ;
3737 const [ error , setError ] = useState < string | null > ( null ) ;
38+ const [ isEditing , setIsEditing ] = useState ( false ) ;
39+ const [ editText , setEditText ] = useState ( "" ) ;
40+
41+ const uuidsToBytes = ( uuidList : string [ ] ) : Uint8Array => {
42+ const bytes = new Uint8Array ( uuidList . length * 16 ) ;
43+ uuidList . forEach ( ( uuid , i ) => {
44+ const hex = uuid . replace ( / - / g, "" ) ;
45+ for ( let j = 0 ; j < 16 ; j ++ ) {
46+ bytes [ i * 16 + j ] = parseInt ( hex . slice ( j * 2 , j * 2 + 2 ) , 16 ) ;
47+ }
48+ } ) ;
49+ return bytes ;
50+ } ;
51+
52+ const handleEdit = ( ) => {
53+ setEditText ( uuids . join ( "\n" ) ) ;
54+ setIsEditing ( true ) ;
55+ } ;
56+
57+ const handleSave = ( ) => {
58+ const lines = editText . split ( "\n" ) . map ( l => l . trim ( ) ) . filter ( l => l . length > 0 ) ;
59+
60+ const bytes = uuidsToBytes ( lines ) ;
61+ const blob = new Blob ( [ bytes . buffer as ArrayBuffer ] , { type : "application/octet-stream" } ) ;
62+ const url = URL . createObjectURL ( blob ) ;
63+ const a = document . createElement ( "a" ) ;
64+ a . href = url ;
65+ a . download = "uuids.bin" ;
66+ a . click ( ) ;
67+ URL . revokeObjectURL ( url ) ;
68+
69+ setUuids ( lines ) ;
70+ setIsEditing ( false ) ;
71+ setError ( null ) ;
72+ } ;
3873
3974 const bytesToHex = ( bytes : Uint8Array ) : string =>
4075 Array . from ( bytes ) . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) ) . join ( "" ) ;
@@ -206,17 +241,68 @@ export default function Index() {
206241 { uuids . length > 0 && (
207242 < div className = "mt-6 border border-border rounded-lg p-4" >
208243 < div className = "mb-3 flex items-center justify-between" >
209- < span className = "text-sm text-muted-foreground" > { uuids . length } UUID{ uuids . length !== 1 && "s" } </ span >
210- < button onClick = { copyToClipboard } className = "px-3 py-1 text-sm rounded hover:bg-muted transition-colors" >
211- { copied ? "✓ Copied" : "Copy All" }
212- </ button >
244+ < span className = "text-sm text-muted-foreground" >
245+ { ( ( ) => {
246+ const uuidRegex = / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } $ / i;
247+ if ( ! isEditing ) {
248+ const uniqueCount = new Set ( uuids . map ( u => u . toLowerCase ( ) ) ) . size ;
249+ return `${ uniqueCount } UUID${ uniqueCount !== 1 ? "s" : "" } ` ;
250+ }
251+ const lines = editText . split ( "\n" ) . map ( l => l . trim ( ) . toLowerCase ( ) ) . filter ( l => l . length > 0 ) ;
252+ const invalidCount = lines . filter ( l => ! uuidRegex . test ( l ) ) . length ;
253+ if ( invalidCount > 0 ) return < span className = "text-red-500" > { invalidCount } invalid UUID{ invalidCount !== 1 ? "s" : "" } </ span > ;
254+ const uniqueCount = new Set ( lines ) . size ;
255+ return `${ uniqueCount } UUID${ uniqueCount !== 1 ? "s" : "" } ` ;
256+ } ) ( ) }
257+ </ span >
258+ < div className = "flex gap-2" >
259+ < button onClick = { copyToClipboard } className = "px-3 py-1 text-sm rounded hover:bg-muted transition-colors" >
260+ { copied ? "✓ Copied" : "Copy All" }
261+ </ button >
262+ { isEditing ? (
263+ ( ( ) => {
264+ const lines = editText . split ( "\n" ) . map ( l => l . trim ( ) . toLowerCase ( ) ) . filter ( l => l . length > 0 ) ;
265+ const uuidRegex = / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } $ / i;
266+ const hasInvalid = lines . some ( l => ! uuidRegex . test ( l ) ) ;
267+ const editSet = new Set ( lines ) ;
268+ const originalSet = new Set ( uuids . map ( u => u . toLowerCase ( ) ) ) ;
269+ const setsEqual = editSet . size === originalSet . size && [ ...editSet ] . every ( u => originalSet . has ( u ) ) ;
270+ const isDisabled = setsEqual || hasInvalid ;
271+ return (
272+ < button
273+ onClick = { handleSave }
274+ disabled = { isDisabled }
275+ className = { `px-3 py-1 text-sm rounded transition-colors ${
276+ isDisabled
277+ ? "bg-muted text-muted-foreground cursor-not-allowed"
278+ : "bg-accent text-white hover:bg-accent/80"
279+ } `}
280+ >
281+ Save
282+ </ button >
283+ ) ;
284+ } ) ( )
285+ ) : (
286+ < button onClick = { handleEdit } className = "px-3 py-1 text-sm rounded hover:bg-muted transition-colors" >
287+ Edit
288+ </ button >
289+ ) }
290+ </ div >
213291 </ div >
214292 < div className = "max-h-96 overflow-auto rounded bg-muted p-4" >
215- < pre className = "font-mono text-sm whitespace-pre-wrap break-all" > { uuids . join ( "\n" ) } </ pre >
293+ { isEditing ? (
294+ < textarea
295+ value = { editText }
296+ onChange = { ( e ) => setEditText ( e . target . value ) }
297+ className = "w-full h-64 font-mono text-sm bg-transparent border-none outline-none resize-none"
298+ />
299+ ) : (
300+ < pre className = "font-mono text-sm whitespace-pre-wrap break-all" > { uuids . join ( "\n" ) } </ pre >
301+ ) }
216302 </ div >
217303 </ div >
218304 ) }
219305 </ div >
220306 </ div >
221307 ) ;
222- }
308+ }
0 commit comments