1- PLACEHOLDER_WILL_REPLACE
1+ import React , { useState , useRef } from 'react' ;
2+ import { ArrowRightLeft , Check , Copy , GripHorizontal , Upload } from 'lucide-react' ;
3+
4+ export const UniversalTransformer : React . FC = ( ) => {
5+ const [ activeTab , setActiveTab ] = useState < 'json-csv' | 'unit' > ( 'json-csv' ) ;
6+ const [ inputText , setInputText ] = useState ( '' ) ;
7+ const [ outputText , setOutputText ] = useState ( '' ) ;
8+ const [ copied , setCopied ] = useState ( false ) ;
9+ const [ unitValue , setUnitValue ] = useState < number > ( 0 ) ;
10+ const [ unitFrom , setUnitFrom ] = useState ( 'px' ) ;
11+ const [ isDragOver , setIsDragOver ] = useState ( false ) ;
12+ const fileInputRef = useRef < HTMLInputElement > ( null ) ;
13+
14+ // JSON <-> CSV Logic
15+ const convertToCSV = ( ) => {
16+ try {
17+ const jsonData = JSON . parse ( inputText ) ;
18+ const array = Array . isArray ( jsonData ) ? jsonData : [ jsonData ] ;
19+ if ( array . length === 0 ) {
20+ setOutputText ( 'Empty Array' ) ;
21+ return ;
22+ }
23+ const header = Object . keys ( array [ 0 ] ) . join ( ',' ) ;
24+ const rows = array . map ( ( obj : any ) => Object . values ( obj ) . join ( ',' ) ) . join ( '\n' ) ;
25+ setOutputText ( `${ header } \n${ rows } ` ) ;
26+ } catch ( _e ) {
27+ setOutputText ( 'Invalid JSON' ) ;
28+ }
29+ } ;
30+
31+ const convertToJSON = ( ) => {
32+ try {
33+ const rows = inputText . trim ( ) . split ( '\n' ) ;
34+ const headers = rows [ 0 ] . split ( ',' ) ;
35+ const json = rows . slice ( 1 ) . map ( row => {
36+ const values = row . split ( ',' ) ;
37+ return headers . reduce ( ( obj : any , header , index ) => {
38+ obj [ header . trim ( ) ] = values [ index ] ?. trim ( ) ;
39+ return obj ;
40+ } , { } ) ;
41+ } ) ;
42+ setOutputText ( JSON . stringify ( json , null , 2 ) ) ;
43+ } catch ( _e ) {
44+ setOutputText ( 'Invalid CSV' ) ;
45+ }
46+ } ;
47+
48+ const handleCopy = ( ) => {
49+ navigator . clipboard . writeText ( outputText ) ;
50+ setCopied ( true ) ;
51+ setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
52+ } ;
53+
54+ // Unit Conversion Logic
55+ const calculateConversion = ( val : number , from : string ) => {
56+ if ( isNaN ( val ) ) return '...' ;
57+ switch ( from ) {
58+ case 'px' :
59+ return `${ ( val / 16 ) . toFixed ( 3 ) . replace ( / \. 0 0 0 $ / , '' ) } rem` ;
60+ case 'rem' :
61+ return `${ ( val * 16 ) . toFixed ( 0 ) } px` ;
62+ case 'epoch' :
63+ return new Date ( val * 1000 ) . toLocaleString ( ) ;
64+ case 'c' :
65+ return `${ ( ( val * 9 ) / 5 + 32 ) . toFixed ( 1 ) } °F` ;
66+ case 'f' :
67+ return `${ ( ( ( val - 32 ) * 5 ) / 9 ) . toFixed ( 1 ) } °C` ;
68+ case 'm' :
69+ return `${ ( val * 3.28084 ) . toFixed ( 2 ) } ft` ;
70+ case 'ft' :
71+ return `${ ( val / 3.28084 ) . toFixed ( 2 ) } m` ;
72+ case 'kg' :
73+ return `${ ( val * 2.20462 ) . toFixed ( 2 ) } lb` ;
74+ case 'lb' :
75+ return `${ ( val / 2.20462 ) . toFixed ( 2 ) } kg` ;
76+ default :
77+ return '' ;
78+ }
79+ } ;
80+
81+ const unitResult = calculateConversion ( unitValue , unitFrom ) ;
82+
83+ const handleUnitCopy = ( ) => {
84+ if ( unitResult && unitResult !== '...' ) {
85+ navigator . clipboard . writeText ( unitResult ) ;
86+ setCopied ( true ) ;
87+ setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
88+ }
89+ } ;
90+
91+ // Drag and Drop Handlers
92+ const handleFileDrop = ( e : React . DragEvent ) => {
93+ e . preventDefault ( ) ;
94+ setIsDragOver ( false ) ;
95+ const file = e . dataTransfer . files [ 0 ] ;
96+ if ( file ) {
97+ readFile ( file ) ;
98+ }
99+ } ;
100+
101+ const handleFileUpload = ( e : React . ChangeEvent < HTMLInputElement > ) => {
102+ const file = e . target . files ?. [ 0 ] ;
103+ if ( file ) {
104+ readFile ( file ) ;
105+ }
106+ // Reset to allow selecting same file again
107+ e . target . value = '' ;
108+ } ;
109+
110+ const readFile = ( file : File ) => {
111+ const reader = new FileReader ( ) ;
112+ reader . onload = event => {
113+ if ( event . target ?. result ) {
114+ setInputText ( event . target . result as string ) ;
115+ }
116+ } ;
117+ reader . readAsText ( file ) ;
118+ } ;
119+
120+ const handleDragStart = ( e : React . DragEvent ) => {
121+ // Allows dragging the output text directly to another widget
122+ e . dataTransfer . setData ( 'text/plain' , outputText ) ;
123+ e . dataTransfer . effectAllowed = 'copy' ;
124+ } ;
125+
126+ return (
127+ < div className = "h-full flex flex-col gap-4" >
128+ { /* Tabs */ }
129+ < div className = "flex gap-2 p-1 bg-slate-900 rounded-lg" >
130+ < button
131+ onClick = { ( ) => {
132+ setActiveTab ( 'json-csv' ) ;
133+ setCopied ( false ) ;
134+ } }
135+ className = { `flex-1 py-1 text-xs font-medium rounded-md transition-colors ${ activeTab === 'json-csv' ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-white' } ` }
136+ >
137+ JSON / CSV
138+ </ button >
139+ < button
140+ onClick = { ( ) => {
141+ setActiveTab ( 'unit' ) ;
142+ setCopied ( false ) ;
143+ } }
144+ className = { `flex-1 py-1 text-xs font-medium rounded-md transition-colors ${ activeTab === 'unit' ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-white' } ` }
145+ >
146+ Units
147+ </ button >
148+ </ div >
149+
150+ { activeTab === 'json-csv' ? (
151+ < div className = "flex-1 flex flex-col gap-2 min-h-0" >
152+ < input
153+ type = "file"
154+ ref = { fileInputRef }
155+ onChange = { handleFileUpload }
156+ className = "hidden"
157+ accept = ".json,.csv,.txt"
158+ />
159+ < div
160+ className = { `flex-1 relative transition-all duration-200 ${ isDragOver ? 'ring-2 ring-indigo-500 bg-slate-800' : '' } ` }
161+ onDragOver = { e => {
162+ e . preventDefault ( ) ;
163+ setIsDragOver ( true ) ;
164+ } }
165+ onDragLeave = { ( ) => setIsDragOver ( false ) }
166+ onDrop = { handleFileDrop }
167+ >
168+ < textarea
169+ className = "w-full h-full bg-slate-950 border border-slate-800 rounded p-2 text-xs font-mono text-slate-300 resize-none focus:outline-none focus:border-indigo-500"
170+ placeholder = "Paste JSON/CSV here, Drag & Drop, or use the Upload button..."
171+ value = { inputText }
172+ onChange = { e => setInputText ( e . target . value ) }
173+ />
174+ { isDragOver && (
175+ < div className = "absolute inset-0 bg-indigo-900/20 flex items-center justify-center pointer-events-none" >
176+ < div className = "bg-slate-900 text-indigo-400 px-3 py-1 rounded-full text-xs font-bold flex items-center gap-2 shadow-xl border border-indigo-500/50" >
177+ < Upload size = { 14 } /> Drop File Here
178+ </ div >
179+ </ div >
180+ ) }
181+ </ div >
182+
183+ < div className = "flex gap-2 justify-center" >
184+ < button
185+ onClick = { ( ) => fileInputRef . current ?. click ( ) }
186+ className = "bg-slate-800 hover:bg-slate-700 px-3 py-1 rounded text-xs text-indigo-400 font-medium flex items-center gap-1 border border-indigo-900/30"
187+ title = "Upload file"
188+ >
189+ < Upload size = { 12 } /> Upload
190+ </ button >
191+ < button
192+ onClick = { convertToCSV }
193+ className = "bg-slate-800 hover:bg-slate-700 px-3 py-1 rounded text-xs text-indigo-400 font-medium"
194+ >
195+ to CSV
196+ </ button >
197+ < button
198+ onClick = { convertToJSON }
199+ className = "bg-slate-800 hover:bg-slate-700 px-3 py-1 rounded text-xs text-indigo-400 font-medium"
200+ >
201+ to JSON
202+ </ button >
203+ </ div >
204+
205+ < div className = "flex-1 relative group" >
206+ < div className = "absolute top-0 right-0 left-0 h-1 cursor-ns-resize z-10" > </ div >
207+ < textarea
208+ readOnly
209+ draggable = { ! ! outputText }
210+ onDragStart = { handleDragStart }
211+ className = { `w-full h-full bg-slate-900 border border-slate-800 rounded p-2 text-xs font-mono text-emerald-400 resize-none focus:outline-none cursor-grab active:cursor-grabbing ${ outputText ? 'hover:border-emerald-500/50' : '' } ` }
212+ placeholder = "Output (Drag me to Scratchpad)..."
213+ value = { outputText }
214+ />
215+ { outputText && (
216+ < >
217+ < div className = "absolute top-2 left-2 p-1 bg-slate-800/80 rounded pointer-events-none opacity-50 group-hover:opacity-100 transition-opacity" >
218+ < GripHorizontal size = { 14 } className = "text-emerald-500" />
219+ </ div >
220+ < button
221+ onClick = { handleCopy }
222+ className = "absolute top-2 right-2 p-1 bg-slate-800 rounded hover:bg-slate-700 text-slate-300 transition-colors z-20"
223+ >
224+ { copied ? < Check size = { 14 } className = "text-emerald-500" /> : < Copy size = { 14 } /> }
225+ </ button >
226+ </ >
227+ ) }
228+ </ div >
229+ </ div >
230+ ) : (
231+ < div className = "flex flex-col gap-4 p-2" >
232+ < div className = "flex gap-2 items-end" >
233+ < div className = "flex-1" >
234+ < label className = "text-[10px] uppercase text-slate-500 font-bold" > Input</ label >
235+ < input
236+ type = "number"
237+ value = { unitValue }
238+ onChange = { e => setUnitValue ( parseFloat ( e . target . value ) ) }
239+ className = "w-full bg-slate-950 border border-slate-800 rounded p-2 text-sm text-slate-200 focus:border-indigo-500 focus:outline-none"
240+ />
241+ </ div >
242+ </ div >
243+
244+ < select
245+ value = { unitFrom }
246+ onChange = { e => setUnitFrom ( e . target . value ) }
247+ className = "w-full bg-slate-800 text-slate-300 text-xs p-2 rounded outline-none focus:ring-1 focus:ring-indigo-500"
248+ >
249+ < optgroup label = "Web / Time" >
250+ < option value = "px" > Pixels → REM (16px base)</ option >
251+ < option value = "rem" > REM → Pixels</ option >
252+ < option value = "epoch" > Epoch → Local Date</ option >
253+ </ optgroup >
254+ < optgroup label = "Temperature" >
255+ < option value = "c" > Celsius → Fahrenheit</ option >
256+ < option value = "f" > Fahrenheit → Celsius</ option >
257+ </ optgroup >
258+ < optgroup label = "Length" >
259+ < option value = "m" > Meters → Feet</ option >
260+ < option value = "ft" > Feet → Meters</ option >
261+ </ optgroup >
262+ < optgroup label = "Weight" >
263+ < option value = "kg" > Kilograms → Pounds</ option >
264+ < option value = "lb" > Pounds → Kilograms</ option >
265+ </ optgroup >
266+ </ select >
267+
268+ < div className = "flex items-center justify-center text-slate-600 my-1" >
269+ < ArrowRightLeft size = { 16 } />
270+ </ div >
271+
272+ < div className = "bg-slate-900 p-4 rounded-lg border border-slate-800 text-center relative overflow-hidden group" >
273+ < div className = "absolute inset-0 bg-indigo-500/5 opacity-0 group-hover:opacity-100 transition-opacity" > </ div >
274+ < label className = "text-[10px] uppercase text-slate-500 font-bold block mb-1" >
275+ Result
276+ </ label >
277+ < div className = "text-xl text-emerald-400 font-mono font-bold truncate select-all" >
278+ { unitResult }
279+ </ div >
280+ { unitResult && unitResult !== '...' && (
281+ < button
282+ onClick = { handleUnitCopy }
283+ className = "absolute top-2 right-2 p-1 bg-slate-800 rounded hover:bg-slate-700 text-slate-300 transition-colors opacity-0 group-hover:opacity-100"
284+ >
285+ { copied ? < Check size = { 14 } className = "text-emerald-500" /> : < Copy size = { 14 } /> }
286+ </ button >
287+ ) }
288+ </ div >
289+ </ div >
290+ ) }
291+ </ div >
292+ ) ;
293+ } ;
0 commit comments