-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathApp.tsx
More file actions
161 lines (146 loc) · 9.84 KB
/
Copy pathApp.tsx
File metadata and controls
161 lines (146 loc) · 9.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import React, { useState, useCallback } from 'react';
import { ImageFormat, ImgJob, ConvState } from './types';
import { convImg } from './utils/imageUtils';
import { DEF_Q } from './constants';
import DropZone from './components/DropZone';
import ImageItem from './components/ImageItem';
import PrivacyPolicy from './components/PrivacyPolicy';
import TermsOfService from './components/TermsOfService';
import ContactUs from './components/ContactUs';
import { Trash2, Loader2 } from 'lucide-react';
type Pg = 'home' | 'priv' | 'term' | 'cont';
var App: React.FC = () => {
var [pg, setPg] = useState<Pg>('home');
var [items, setItems] = useState<ImgJob[]>([]);
var [gfmt, setGFmt] = useState<ImageFormat>(ImageFormat.WEBP);
var [busy, setBusy] = useState(false);
var onAdd = useCallback((fs: File[]) => {
var list: ImgJob[] = fs.map((f) => ({
id: Math.random().toString(36).substring(7),
f, pre: URL.createObjectURL(f), osz: f.size, fmt: gfmt, st: ConvState.IDLE, blob: null, url: null,
}));
setItems((v) => [...v, ...list]);
}, [gfmt]);
var onDel = (id: string) => setItems((v) => {
var x = v.find((i) => i.id === id);
if (x?.pre) URL.revokeObjectURL(x.pre);
if (x?.url) URL.revokeObjectURL(x.url);
return v.filter((i) => i.id !== id);
});
var onClr = () => {
items.forEach(v => { v.pre && URL.revokeObjectURL(v.pre); v.url && URL.revokeObjectURL(v.url); });
setItems([]);
};
var onUpd = (id: string, fmt: ImageFormat) => setItems((v) => v.map((x) => x.id === id ? { ...x, fmt, st: ConvState.IDLE } : x));
var onGlob = (nf: ImageFormat) => { setGFmt(nf); setItems((v) => v.map((x) => x.st === ConvState.IDLE ? { ...x, fmt: nf } : x)); };
var runOne = async (x: ImgJob): Promise<ImgJob> => {
try {
var b = await convImg(x.f, x.fmt, DEF_Q);
return { ...x, st: ConvState.DONE, blob: b, url: URL.createObjectURL(b) };
} catch (e: any) {
return { ...x, st: ConvState.FAIL, err: e?.message || 'Failed' };
}
};
var runAll = async () => {
var todo = items.filter(v => v.st === ConvState.IDLE || v.st === ConvState.FAIL);
if (!todo.length) return;
setBusy(true);
setItems(v => v.map(x => (x.st === ConvState.IDLE || x.st === ConvState.FAIL) ? { ...x, st: ConvState.BUSY } : x));
for (var x of todo) {
var res = await runOne(x);
setItems(v => v.map(i => i.id === x.id ? res : i));
}
setBusy(false);
};
var go = (p: Pg) => { setPg(p); window.scrollTo(0, 0); };
var Main = () => (
<>
<div className="w-full max-w-4xl flex flex-col gap-8 text-center mb-16 animate-fade-in-up">
<div className="flex flex-col gap-4 items-center">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-surface-dark border border-surface-border text-sm text-gray-300 mb-2">
<span className="relative flex h-2 w-2"><span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span><span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span></span>
No account required
</div>
<h1 className="text-4xl sm:text-5xl md:text-6xl font-black tracking-tight text-white leading-tight">Convert Images <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-blue-400">Instantly</span></h1>
<p className="text-gray-400 text-lg sm:text-xl max-w-2xl mx-auto leading-relaxed">Securely convert your images to optimized formats. Drag & drop files or browse to start your high speed conversion.</p>
</div>
<div className="w-full max-w-2xl mx-auto mt-6"><DropZone onAdd={onAdd} gfmt={gfmt} onGlob={onGlob} /></div>
</div>
{items.length > 0 && (
<div className="w-full max-w-3xl space-y-4 mb-16">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 p-4 bg-surface-dark/50 backdrop-blur-md rounded-2xl border border-surface-border sticky top-20 z-20">
<div className="text-sm font-bold text-gray-400 uppercase tracking-widest">{items.length} {items.length === 1 ? 'Image' : 'Images'} in queue</div>
<div className="flex items-center gap-3 w-full sm:w-auto">
<button onClick={onClr} disabled={busy} className="p-2.5 text-gray-400 hover:text-red-500 bg-background-dark border border-surface-border rounded-xl transition-all disabled:opacity-50"><Trash2 size={20} /></button>
<button onClick={runAll} disabled={busy || items.every(x => x.st === ConvState.DONE)} className="flex-1 sm:flex-none flex items-center justify-center gap-2 px-8 py-2.5 bg-primary hover:bg-blue-600 text-white rounded-xl text-sm font-bold transition-all shadow-lg shadow-primary/30 disabled:opacity-50">
{busy ? <><Loader2 size={18} className="animate-spin" />Converting...</> : <>Convert All</>}
</button>
</div>
</div>
<div className="space-y-3">{items.map((v) => <ImageItem key={v.id} item={v} onDel={onDel} onUpd={onUpd} />)}</div>
</div>
)}
<div className="w-full max-w-[960px] mx-auto px-4 mb-16">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[
{ i: 'bolt', t: 'Lightning Fast', d: 'Our optimized engine converts bulk images in seconds, handling heavy workloads with ease.' },
{ i: 'no_accounts', t: 'Anonymous Use', d: 'No registration needed. We process your files locally without storing personal data.' },
{ i: 'inventory_2', t: 'Privacy First', d: 'Your images never leave your computer for most operations. Secure and safe.' }
].map((v, i) => (
<div key={i} className="flex flex-col gap-4 p-6 rounded-xl border border-surface-border bg-surface-dark hover:border-primary/50 transition-colors group">
<div className="size-12 rounded-lg bg-surface-border flex items-center justify-center text-white group-hover:bg-primary transition-colors"><span className="material-symbols-outlined">{v.i}</span></div>
<div className="flex flex-col gap-2"><h3 className="text-white text-lg font-bold">{v.t}</h3><p className="text-gray-400 text-sm leading-relaxed">{v.d}</p></div>
</div>
))}
</div>
</div>
</>
);
var Content = () => {
switch (pg) {
case 'priv': return <PrivacyPolicy />;
case 'term': return <TermsOfService />;
case 'cont': return <ContactUs />;
default: return <Main />;
}
};
return (
<div className="relative z-10 flex min-h-screen w-full flex-col">
<div className="fixed inset-0 z-0 pointer-events-none opacity-[0.15]" style={{ backgroundSize: '40px 40px', backgroundImage: 'linear-gradient(to right, #324467 1px, transparent 1px), linear-gradient(to bottom, #324467 1px, transparent 1px)' }}></div>
<div className="w-full border-b border-surface-border bg-background-dark/80 backdrop-blur-md sticky top-0 z-50">
<div className="max-w-[1200px] mx-auto px-4 sm:px-6 lg:px-8">
<header className="flex h-16 items-center justify-between whitespace-nowrap">
<button onClick={() => go('home')} className="flex items-center gap-3 text-white hover:opacity-80 transition-opacity">
<div className="size-8 flex items-center justify-center text-primary"><span className="material-symbols-outlined text-3xl">transform</span></div>
<h2 className="text-white text-lg font-bold leading-tight tracking-tight">ImageConverter</h2>
<span className="inline-flex items-center rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary ring-1 ring-inset ring-primary/20 ml-2">Fast & Free</span>
</button>
<div className="hidden md:flex flex-1 justify-end gap-8 items-center">
<nav className="flex items-center gap-6">
{['home', 'cont'].map(v => <button key={v} onClick={() => go(v as Pg)} className={`text-sm font-medium transition-colors ${pg === v ? 'text-primary' : 'text-gray-400 hover:text-white'}`}>{v === 'home' ? 'Home' : 'Contact'}</button>)}
</nav>
</div>
</header>
</div>
</div>
<main className="flex-1 flex flex-col items-center py-12 px-4 sm:px-6 lg:px-8 relative z-10"><Content /></main>
<footer className="w-full border-t border-surface-border bg-background-dark py-12 mt-auto relative z-10">
<div className="max-w-[960px] mx-auto px-4 flex flex-col items-center gap-8">
<div className="flex flex-wrap items-center justify-center gap-x-8 gap-y-4">
{[{ k: 'priv', l: 'Privacy Policy' }, { k: 'term', l: 'Terms of Service' }, { k: 'cont', l: 'Contact Us' }].map(v => <button key={v.k} onClick={() => go(v.k as Pg)} className={`text-sm font-medium transition-colors ${pg === v.k ? 'text-primary' : 'text-gray-400 hover:text-primary'}`}>{v.l}</button>)}
</div>
<div className="flex gap-6">
<button onClick={() => {
if (navigator.share) navigator.share({ title: 'ImageConverter', url: window.location.href });
else alert('Copied to clipboard!');
}} className="text-gray-500 hover:text-white transition-colors p-2 rounded-full hover:bg-surface-border"><span className="material-symbols-outlined">share</span></button>
<button onClick={() => go('cont')} className="text-gray-500 hover:text-white transition-colors p-2 rounded-full hover:bg-surface-border"><span className="material-symbols-outlined">chat</span></button>
<a className="text-gray-500 hover:text-white transition-colors p-2 rounded-full hover:bg-surface-border" href="https://github.com/coders908" target="_blank" rel="noopener noreferrer"><span className="material-symbols-outlined">code</span></a>
</div>
<p className="text-gray-600 text-sm font-normal">© 2026 Made with ❤️ by coders908(github)</p>
</div>
</footer>
</div>
);
};
export default App;