1+ import React , { useState , useEffect } from 'react' ;
2+ import { Layers , Scissors , Zap , Moon , Sun , Info , ArrowRight , Download , Trash2 , FileText , CheckCircle } from 'lucide-react' ;
3+ import { ToolCard } from './components/ToolCard' ;
4+ import { mergePDFs , createDownloadLink } from './utils/pdfOps' ;
5+
6+ enum ToolType {
7+ NONE ,
8+ MERGE ,
9+ SPLIT ,
10+ COMPRESS
11+ }
12+
13+ const App : React . FC = ( ) => {
14+ const [ darkMode , setDarkMode ] = useState ( false ) ;
15+ const [ selectedTool , setSelectedTool ] = useState < ToolType > ( ToolType . NONE ) ;
16+ const [ files , setFiles ] = useState < File [ ] > ( [ ] ) ;
17+ const [ isProcessing , setIsProcessing ] = useState ( false ) ;
18+ const [ downloadUrl , setDownloadUrl ] = useState < string | null > ( null ) ;
19+
20+ useEffect ( ( ) => {
21+ if ( darkMode ) {
22+ document . documentElement . classList . add ( 'dark' ) ;
23+ } else {
24+ document . documentElement . classList . remove ( 'dark' ) ;
25+ }
26+ } , [ darkMode ] ) ;
27+
28+ const handleFileChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
29+ if ( e . target . files ) {
30+ setFiles ( Array . from ( e . target . files ) ) ;
31+ setDownloadUrl ( null ) ;
32+ }
33+ } ;
34+
35+ const handleProcess = async ( ) => {
36+ if ( files . length === 0 ) return ;
37+ setIsProcessing ( true ) ;
38+
39+ try {
40+ let resultData : Uint8Array | null = null ;
41+ let filename = 'processed.pdf' ;
42+
43+ // NOTE: For this V1, only Merge is fully implemented in logic to save space.
44+ // Other tools show the UI flow but perform a pass-through or placeholder action.
45+ if ( selectedTool === ToolType . MERGE ) {
46+ resultData = await mergePDFs ( files ) ;
47+ filename = 'paperknife-merged.pdf' ;
48+ } else {
49+ // Placeholder for Split/Compress in V1
50+ alert ( "This Vibecoder is strictly MVP! Only Merge is live. Others coming in V2." ) ;
51+ setIsProcessing ( false ) ;
52+ return ;
53+ }
54+
55+ if ( resultData ) {
56+ const url = await createDownloadLink ( resultData , filename ) ;
57+ setDownloadUrl ( url ) ;
58+ }
59+ } catch ( err ) {
60+ console . error ( err ) ;
61+ alert ( "Failed to process PDF. It might be encrypted." ) ;
62+ } finally {
63+ setIsProcessing ( false ) ;
64+ }
65+ } ;
66+
67+ const resetTool = ( ) => {
68+ setSelectedTool ( ToolType . NONE ) ;
69+ setFiles ( [ ] ) ;
70+ setDownloadUrl ( null ) ;
71+ } ;
72+
73+ return (
74+ < div className = "min-h-screen flex flex-col font-sans selection:bg-swissRed selection:text-white" >
75+ { /* Header */ }
76+ < header className = "sticky top-0 z-50 bg-white/80 dark:bg-black/80 backdrop-blur-md border-b-2 border-black dark:border-white" >
77+ < div className = "max-w-4xl mx-auto px-6 py-4 flex items-center justify-between" >
78+ < div className = "flex items-center gap-3 cursor-pointer" onClick = { resetTool } >
79+ < div className = "bg-swissRed text-white p-2 rounded-md" >
80+ < Zap size = { 24 } strokeWidth = { 3 } />
81+ </ div >
82+ < h1 className = "text-2xl font-extrabold tracking-tighter uppercase" > PaperKnife</ h1 >
83+ </ div >
84+ < button
85+ onClick = { ( ) => setDarkMode ( ! darkMode ) }
86+ className = "p-3 border-2 border-black dark:border-white rounded-full hover:bg-black hover:text-white dark:hover:bg-white dark:hover:text-black transition-colors"
87+ >
88+ { darkMode ? < Sun size = { 20 } /> : < Moon size = { 20 } /> }
89+ </ button >
90+ </ div >
91+ </ header >
92+
93+ { /* Main Content */ }
94+ < main className = "flex-grow px-6 py-8 max-w-4xl mx-auto w-full" >
95+
96+ { /* Intro Text */ }
97+ { selectedTool === ToolType . NONE && (
98+ < div className = "mb-10 text-center space-y-4" >
99+ < h2 className = "text-4xl md:text-5xl font-black leading-tight" >
100+ The Swiss Army Knife< br />
101+ < span className = "text-swissRed" > For Your Documents.</ span >
102+ </ h2 >
103+ < p className = "text-lg font-medium text-gray-600 dark:text-gray-400 max-w-lg mx-auto" >
104+ 100% Client-side. No data leaves your device. Fast, private, and bloat-free.
105+ </ p >
106+ </ div >
107+ ) }
108+
109+ { /* Bento Grid Selection */ }
110+ { selectedTool === ToolType . NONE ? (
111+ < div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
112+ < ToolCard
113+ title = "Merge PDFs"
114+ description = "Combine multiple files into one master document."
115+ icon = { < Layers size = { 28 } /> }
116+ onClick = { ( ) => setSelectedTool ( ToolType . MERGE ) }
117+ />
118+ < ToolCard
119+ title = "Split PDF"
120+ description = "Extract pages or cut a document in half."
121+ icon = { < Scissors size = { 28 } /> }
122+ onClick = { ( ) => setSelectedTool ( ToolType . SPLIT ) }
123+ />
124+ { /* Placeholder Visuals for V2 */ }
125+ < div className = "md:col-span-2 p-6 border-2 border-dashed border-gray-300 dark:border-zinc-700 rounded-xl flex items-center justify-center gap-3 text-gray-400 dark:text-zinc-600" >
126+ < Info size = { 20 } />
127+ < span className = "font-bold" > More tools (Compress, Sign, Convert) coming soon.</ span >
128+ </ div >
129+ </ div >
130+ ) : (
131+ /* Tool Workspace */
132+ < div className = "animate-in fade-in slide-in-from-bottom-4 duration-300" >
133+ < div className = "flex items-center gap-2 mb-6 text-sm font-bold text-gray-500 uppercase tracking-widest" >
134+ < button onClick = { resetTool } className = "hover:text-swissRed" > Tools</ button >
135+ < span > /</ span >
136+ < span className = "text-black dark:text-white" > { selectedTool === ToolType . MERGE ? "Merge" : "Tool" } </ span >
137+ </ div >
138+
139+ < div className = "border-2 border-black dark:border-white rounded-xl p-6 md:p-8 bg-white dark:bg-zinc-900 shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] dark:shadow-[8px_8px_0px_0px_rgba(255,255,255,1)]" >
140+ < h3 className = "text-3xl font-black mb-6 flex items-center gap-3" >
141+ { selectedTool === ToolType . MERGE && < Layers size = { 32 } className = "text-swissRed" /> }
142+ { selectedTool === ToolType . MERGE ? "Merge PDFs" : "Process PDF" }
143+ </ h3 >
144+
145+ { ! downloadUrl ? (
146+ < div className = "space-y-6" >
147+ { /* Upload Area */ }
148+ < label className = "block w-full cursor-pointer group" >
149+ < input
150+ type = "file"
151+ multiple = { selectedTool === ToolType . MERGE }
152+ accept = ".pdf"
153+ onChange = { handleFileChange }
154+ className = "hidden"
155+ />
156+ < div className = "border-4 border-dashed border-gray-200 dark:border-zinc-700 rounded-xl p-10 flex flex-col items-center justify-center gap-4 group-hover:border-swissRed group-hover:bg-red-50 dark:group-hover:bg-red-900/10 transition-colors" >
157+ < div className = "bg-black dark:bg-white text-white dark:text-black p-4 rounded-full" >
158+ < FileText size = { 32 } />
159+ </ div >
160+ < div className = "text-center" >
161+ < p className = "text-xl font-bold" > Tap to select files</ p >
162+ < p className = "text-gray-500 mt-1" > Select { selectedTool === ToolType . MERGE ? "multiple" : "a" } PDF{ selectedTool === ToolType . MERGE && "s" } </ p >
163+ </ div >
164+ </ div >
165+ </ label >
166+
167+ { /* File List */ }
168+ { files . length > 0 && (
169+ < div className = "space-y-2" >
170+ < h4 className = "font-bold text-sm uppercase text-gray-500" > Selected Files:</ h4 >
171+ < div className = "flex flex-wrap gap-2" >
172+ { files . map ( ( f , i ) => (
173+ < div key = { i } className = "flex items-center gap-2 bg-gray-100 dark:bg-zinc-800 px-3 py-2 rounded-md border border-gray-200 dark:border-zinc-700" >
174+ < FileText size = { 16 } />
175+ < span className = "text-sm font-medium truncate max-w-[150px]" > { f . name } </ span >
176+ </ div >
177+ ) ) }
178+ < button onClick = { ( ) => setFiles ( [ ] ) } className = "text-red-500 p-2 hover:bg-red-50 rounded-md" >
179+ < Trash2 size = { 18 } />
180+ </ button >
181+ </ div >
182+ </ div >
183+ ) }
184+
185+ { /* Action Button */ }
186+ < button
187+ disabled = { files . length === 0 || isProcessing }
188+ onClick = { handleProcess }
189+ className = "w-full py-4 bg-black dark:bg-white text-white dark:text-black text-lg font-bold rounded-lg hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-3 transition-all"
190+ >
191+ { isProcessing ? "Processing..." : "Run Tool" }
192+ { ! isProcessing && < ArrowRight size = { 20 } strokeWidth = { 3 } /> }
193+ </ button >
194+ </ div >
195+ ) : (
196+ /* Success State */
197+ < div className = "text-center py-10 space-y-6 animate-in zoom-in-95 duration-200" >
198+ < div className = "mx-auto w-20 h-20 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center text-green-600 dark:text-green-400" >
199+ < CheckCircle size = { 40 } strokeWidth = { 3 } />
200+ </ div >
201+ < h4 className = "text-2xl font-bold" > Ready to go!</ h4 >
202+ < a
203+ href = { downloadUrl }
204+ download = { selectedTool === ToolType . MERGE ? "paperknife-merged.pdf" : "processed.pdf" }
205+ className = "block w-full py-4 bg-swissRed text-white text-lg font-bold rounded-lg hover:bg-red-700 flex items-center justify-center gap-3 shadow-lg"
206+ >
207+ < Download size = { 24 } />
208+ Download File
209+ </ a >
210+ < button onClick = { resetTool } className = "text-sm font-bold text-gray-500 hover:text-black dark:hover:text-white underline" >
211+ Process another file
212+ </ button >
213+ </ div >
214+ ) }
215+ </ div >
216+ </ div >
217+ ) }
218+ </ main >
219+
220+ { /* Footer */ }
221+ < footer className = "py-6 text-center border-t-2 border-black dark:border-white mt-auto" >
222+ < p className = "font-bold text-sm" > PAPERKNIFE © { new Date ( ) . getFullYear ( ) } </ p >
223+ < p className = "text-xs text-gray-500 mt-1" > Built with vibes. Zero Analytics.</ p >
224+ </ footer >
225+ </ div >
226+ ) ;
227+ } ;
228+
229+ export default App ;
0 commit comments