Skip to content

Commit b7e4d07

Browse files
committed
fix(lint): restore UniversalTransformer from main + catch (_e) + drop unused FileJson/FileType
1 parent 85c938d commit b7e4d07

1 file changed

Lines changed: 293 additions & 1 deletion

File tree

widgets/UniversalTransformer.tsx

Lines changed: 293 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,293 @@
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(/\.000$/, '')}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

Comments
 (0)