Skip to content

Commit 62d21c5

Browse files
authored
Merge pull request #5 from CodePapi/upload-file
ft: added file upload feature
2 parents 98cd559 + d1161f3 commit 62d21c5

File tree

3 files changed

+257
-26
lines changed

3 files changed

+257
-26
lines changed

frontend/src/App.tsx

Lines changed: 171 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Editor, { DiffEditor } from '@monaco-editor/react';
22
import {
33
Bug,
44
Check,
5+
Clipboard,
56
Code2,
67
Copy,
78
RotateCcw,
@@ -11,6 +12,7 @@ import {
1112
} from 'lucide-react';
1213
import { useEffect, useState } from 'react';
1314
import { fixBugs, reviewCode, translateCode } from './api/client';
15+
import FileUploader from './components/FileUploader';
1416
import { LanguageSelector } from './components/LanguageSelector';
1517

1618
type Mode = 'translate' | 'review' | 'fix';
@@ -24,6 +26,7 @@ function App() {
2426
const [mode, setMode] = useState<Mode>('translate');
2527
const [isProcessing, setIsProcessing] = useState(false);
2628
const [copied, setCopied] = useState(false);
29+
const [mobileNavOpen, setMobileNavOpen] = useState(false);
2730

2831
// --- Logic: Syncing & Placeholders ---
2932
useEffect(() => {
@@ -37,7 +40,6 @@ function App() {
3740
else setTargetLang('typescript'); // Default for most modern migrations
3841
} else {
3942
// If user switches back to a plain language, force them to pick a new target
40-
setTargetLang('');
4143
setOutputCode('');
4244
}
4345
}
@@ -73,6 +75,31 @@ function App() {
7375
.trim();
7476
};
7577

78+
// --- Input helpers (upload / paste / clear) ---
79+
const handleFileRead = (content: string, _filename?: string) => {
80+
setSourceCode(content);
81+
setOutputCode('');
82+
};
83+
84+
const handlePasteFromClipboard = async () => {
85+
try {
86+
const text = await navigator.clipboard.readText();
87+
if (!text) {
88+
alert('Clipboard is empty');
89+
return;
90+
}
91+
setSourceCode(text);
92+
setOutputCode('');
93+
} catch (err) {
94+
alert('Unable to read clipboard');
95+
}
96+
};
97+
98+
const handleClearInput = () => {
99+
setSourceCode('// Your code here...');
100+
setOutputCode('');
101+
};
102+
76103
const handleAction = async () => {
77104
if (!sourceCode.trim() || sourceCode === '// Your code here...') return;
78105
if (mode === 'translate' && !targetLang) {
@@ -112,7 +139,7 @@ function App() {
112139
className={`min-h-screen bg-[#0b0e14] text-slate-200 font-sans antialiased ${isProcessing ? 'cursor-wait' : ''}`}
113140
>
114141
{/* Navbar */}
115-
<nav className="border-b border-slate-800 px-6 py-4 flex justify-between items-center bg-[#0b0e14]/80 backdrop-blur-md sticky top-0 z-50">
142+
<nav className="border-b border-slate-800 px-4 sm:px-6 py-3 sm:py-4 flex justify-between items-center bg-[#0b0e14]/80 backdrop-blur-md sticky top-0 z-50">
116143
<div className="flex items-center gap-2">
117144
<div className="bg-blue-600 p-1.5 rounded-lg">
118145
<Zap size={20} className="fill-white text-white" />
@@ -123,31 +150,71 @@ function App() {
123150
</div>
124151

125152
{/* Mode Switcher */}
126-
<div
127-
className={`flex bg-slate-900 border border-slate-800 p-1 rounded-xl transition-all ${isProcessing ? 'opacity-40 pointer-events-none scale-95' : ''}`}
128-
>
129-
{[
130-
{ id: 'translate', label: 'Translate', icon: <Code2 size={16} /> },
131-
{ id: 'review', label: 'Review', icon: <Search size={16} /> },
132-
{ id: 'fix', label: 'Check Bugs', icon: <Bug size={16} /> },
133-
].map((m) => (
134-
<button
135-
type="button"
136-
key={m.id}
137-
disabled={isProcessing}
138-
onClick={() => setMode(m.id as Mode)}
139-
className={`flex items-center gap-2 px-4 py-1.5 rounded-lg text-sm font-medium transition-all ${
140-
mode === m.id
141-
? 'bg-slate-800 text-blue-400 shadow-sm'
142-
: 'text-slate-400 hover:text-slate-200'
143-
}`}
144-
>
145-
{m.icon} {m.label}
146-
</button>
147-
))}
153+
<div className="hidden sm:flex">
154+
<div
155+
className={`flex bg-slate-900 border border-slate-800 p-1 rounded-xl transition-all ${isProcessing ? 'opacity-40 pointer-events-none scale-95' : ''}`}
156+
>
157+
{[
158+
{
159+
id: 'translate',
160+
label: 'Translate',
161+
icon: <Code2 size={16} />,
162+
},
163+
{ id: 'review', label: 'Review', icon: <Search size={16} /> },
164+
{ id: 'fix', label: 'Check Bugs', icon: <Bug size={16} /> },
165+
].map((m) => (
166+
<button
167+
type="button"
168+
key={m.id}
169+
disabled={isProcessing}
170+
onClick={() => setMode(m.id as Mode)}
171+
className={`flex items-center gap-2 px-4 py-1.5 rounded-lg text-sm font-medium transition-all ${
172+
mode === m.id
173+
? 'bg-slate-800 text-blue-400 shadow-sm'
174+
: 'text-slate-400 hover:text-slate-200'
175+
}`}
176+
>
177+
{m.icon} {m.label}
178+
</button>
179+
))}
180+
</div>
148181
</div>
149182

150-
<div className="flex gap-4">
183+
{/* Mobile menu toggle */}
184+
<button
185+
type="button"
186+
className="sm:hidden p-2 rounded-md text-slate-300 hover:bg-slate-800"
187+
onClick={() => setMobileNavOpen((v) => !v)}
188+
aria-label="Toggle menu"
189+
>
190+
<svg
191+
xmlns="http://www.w3.org/2000/svg"
192+
className="h-6 w-6"
193+
fill="none"
194+
viewBox="0 0 24 24"
195+
stroke="currentColor"
196+
role="img"
197+
>
198+
<title>{mobileNavOpen ? 'Close menu' : 'Open menu'}</title>
199+
{mobileNavOpen ? (
200+
<path
201+
strokeLinecap="round"
202+
strokeLinejoin="round"
203+
strokeWidth={2}
204+
d="M6 18L18 6M6 6l12 12"
205+
/>
206+
) : (
207+
<path
208+
strokeLinecap="round"
209+
strokeLinejoin="round"
210+
strokeWidth={2}
211+
d="M4 6h16M4 12h16M4 18h16"
212+
/>
213+
)}
214+
</svg>
215+
</button>
216+
217+
<div className="hidden sm:flex gap-4">
151218
<button
152219
type="button"
153220
disabled={isProcessing}
@@ -182,6 +249,58 @@ function App() {
182249
</div>
183250
</nav>
184251

252+
{/* Mobile nav dropdown */}
253+
{mobileNavOpen && (
254+
<div className="sm:hidden bg-[#0b0e14]/95 border-b border-slate-800 px-4 py-3">
255+
<div className="flex flex-col gap-3">
256+
<div className="flex bg-slate-900 border border-slate-800 p-1 rounded-xl">
257+
{[
258+
{ id: 'translate', label: 'Translate' },
259+
{ id: 'review', label: 'Review' },
260+
{ id: 'fix', label: 'Check Bugs' },
261+
].map((m) => (
262+
<button
263+
key={m.id}
264+
type="button"
265+
onClick={() => {
266+
setMode(m.id as Mode);
267+
setMobileNavOpen(false);
268+
}}
269+
className={`flex-1 text-sm py-2 rounded-md ${mode === m.id ? 'bg-slate-800 text-blue-400' : 'text-slate-300'}`}
270+
>
271+
{m.label}
272+
</button>
273+
))}
274+
</div>
275+
276+
<div className="flex items-center gap-2">
277+
<button
278+
type="button"
279+
onClick={() => {
280+
setSourceCode('// Your code here...');
281+
setOutputCode('');
282+
setMobileNavOpen(false);
283+
}}
284+
className="flex-1 py-2 rounded-md text-sm text-slate-300 hover:bg-slate-800"
285+
>
286+
Reset
287+
</button>
288+
<button
289+
type="button"
290+
onClick={() => {
291+
handleAction();
292+
setMobileNavOpen(false);
293+
}}
294+
disabled={isProcessing || (mode === 'translate' && !targetLang)}
295+
className={`flex-1 py-2 rounded-md text-sm font-bold ${isProcessing || (mode === 'translate' && !targetLang) ? 'bg-slate-700 opacity-50' : 'bg-blue-600'}`}
296+
>
297+
Run AI
298+
</button>
299+
</div>
300+
</div>
301+
</div>
302+
)}
303+
185304
<main className="p-6 max-w-[1600px] mx-auto">
186305
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-[calc(100vh-140px)]">
187306
{/* Input Panel */}
@@ -195,6 +314,33 @@ function App() {
195314
onChange={setSourceLang}
196315
/>
197316
</div>
317+
<div className="flex items-center gap-2">
318+
<div className="flex-1" />
319+
<div className="flex items-center gap-2">
320+
<FileUploader
321+
onFileRead={handleFileRead}
322+
disabled={isProcessing}
323+
/>
324+
<button
325+
type="button"
326+
onClick={handlePasteFromClipboard}
327+
disabled={isProcessing}
328+
className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-800 text-sm"
329+
title="Paste from clipboard"
330+
>
331+
<Clipboard size={14} /> Paste
332+
</button>
333+
<button
334+
type="button"
335+
onClick={handleClearInput}
336+
disabled={isProcessing}
337+
className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-800 text-sm"
338+
title="Clear input"
339+
>
340+
Clear
341+
</button>
342+
</div>
343+
</div>
198344
<div className="flex-1 rounded-xl overflow-hidden border border-slate-800 bg-[#1e1e1e]">
199345
<Editor
200346
height="100%"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React, { useRef, useState } from 'react';
2+
3+
interface Props {
4+
onFileRead: (content: string, filename?: string) => void;
5+
disabled?: boolean;
6+
}
7+
8+
const FileUploader = ({ onFileRead, disabled }: Props) => {
9+
const inputRef = useRef<HTMLInputElement | null>(null);
10+
const [dragOver, setDragOver] = useState(false);
11+
12+
const readFile = (file: File) => {
13+
const reader = new FileReader();
14+
reader.onload = () => {
15+
const result = String(reader.result || '');
16+
onFileRead(result, file.name);
17+
};
18+
reader.readAsText(file);
19+
};
20+
21+
const handleFiles = (files: FileList | null) => {
22+
if (!files || files.length === 0) return;
23+
readFile(files[0]);
24+
};
25+
26+
return (
27+
<div>
28+
<input
29+
ref={inputRef}
30+
type="file"
31+
accept=".js,.ts,.jsx,.tsx,.py,.java,.cs,.cpp,.txt"
32+
className="hidden"
33+
onChange={(e) => handleFiles(e.target.files)}
34+
disabled={disabled}
35+
/>
36+
37+
<button
38+
type="button"
39+
onClick={() => inputRef.current?.click()}
40+
onKeyDown={(e) => {
41+
if (e.key === 'Enter' || e.key === ' ') {
42+
e.preventDefault();
43+
inputRef.current?.click();
44+
}
45+
}}
46+
onDragOver={(e) => {
47+
e.preventDefault();
48+
if (!disabled) setDragOver(true);
49+
}}
50+
onDragLeave={() => setDragOver(false)}
51+
onDrop={(e) => {
52+
e.preventDefault();
53+
setDragOver(false);
54+
if (disabled) return;
55+
handleFiles(e.dataTransfer.files);
56+
}}
57+
disabled={disabled}
58+
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg transition-colors text-sm font-medium ${
59+
disabled ? 'opacity-50 pointer-events-none' : 'hover:bg-slate-800'
60+
} ${dragOver ? 'bg-slate-800' : ''}`}
61+
>
62+
<svg
63+
xmlns="http://www.w3.org/2000/svg"
64+
width="16"
65+
height="16"
66+
viewBox="0 0 24 24"
67+
fill="none"
68+
stroke="currentColor"
69+
strokeWidth={2}
70+
strokeLinecap="round"
71+
strokeLinejoin="round"
72+
className="text-slate-300"
73+
role="img"
74+
>
75+
<title>Upload file</title>
76+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
77+
<polyline points="7 10 12 5 17 10" />
78+
<line x1="12" y1="5" x2="12" y2="19" />
79+
</svg>
80+
<span>{disabled ? 'Upload (disabled)' : 'Upload / Drop'}</span>
81+
</button>
82+
</div>
83+
);
84+
};
85+
86+
export default FileUploader;

frontend/src/constants/languages.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,4 @@ export const MIGRATIONS = [
1717
{ id: 'react-ts', name: 'React JS to React TS' },
1818
{ id: 'react-class-functional', name: 'React Class to Functional' },
1919
{ id: 'angular-react', name: 'Angular to React' },
20-
{ id: 'jquery-vanilla', name: 'jQuery to Vanilla JS' },
2120
];

0 commit comments

Comments
 (0)