comparison medi/src/app.tsx @ 217:7ef4c9d2a72d hg-web

[DO NOT PUSH] Random values.
author MrJuneJune <me@mrjunejune.com>
date Sun, 25 Jan 2026 10:44:04 -0800
parents
children
comparison
equal deleted inserted replaced
192:b818a4561a3c 217:7ef4c9d2a72d
1 import React, { useState, useEffect, useCallback, useRef } from 'react';
2
3 interface MediaItem {
4 id: string;
5 type: 'image' | 'video';
6 status: 'processing' | 'completed' | 'error';
7 webp?: string;
8 hls?: string;
9 thumbnail?: string;
10 original?: string;
11 error?: string;
12 }
13
14 const UploadIcon = () => (
15 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
16 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
17 <polyline points="17 8 12 3 7 8" />
18 <line x1="12" y1="3" x2="12" y2="15" />
19 </svg>
20 );
21
22 export function App() {
23 const [media, setMedia] = useState<MediaItem[]>([]);
24 const [uploading, setUploading] = useState(false);
25 const [dragover, setDragover] = useState(false);
26 const fileInputRef = useRef<HTMLInputElement>(null);
27
28 // Fetch media list on mount
29 useEffect(() => {
30 fetchMediaList();
31 }, []);
32
33 // Poll for processing status
34 useEffect(() => {
35 const processingItems = media.filter(m => m.status === 'processing');
36 if (processingItems.length === 0) return;
37
38 const interval = setInterval(() => {
39 processingItems.forEach(item => {
40 checkMediaStatus(item.id);
41 });
42 }, 2000);
43
44 return () => clearInterval(interval);
45 }, [media]);
46
47 const fetchMediaList = async () => {
48 try {
49 const res = await fetch('/api/media');
50 const data = await res.json();
51
52 // Fetch details for each item
53 const items = await Promise.all(
54 data.items.map(async (item: { id: string; type: string }) => {
55 const infoRes = await fetch(`/api/media/${item.id}`);
56 return infoRes.json();
57 })
58 );
59
60 setMedia(items);
61 } catch (err) {
62 console.error('Failed to fetch media list:', err);
63 }
64 };
65
66 const checkMediaStatus = async (id: string) => {
67 try {
68 const res = await fetch(`/api/media/${id}`);
69 const data = await res.json();
70
71 setMedia(prev => prev.map(item =>
72 item.id === id ? { ...item, ...data } : item
73 ));
74 } catch (err) {
75 console.error('Failed to check status:', err);
76 }
77 };
78
79 const uploadFile = async (file: File) => {
80 const formData = new FormData();
81 formData.append('file', file);
82
83 setUploading(true);
84
85 try {
86 const res = await fetch('/api/upload', {
87 method: 'POST',
88 body: formData,
89 });
90
91 const data = await res.json();
92
93 if (data.error) {
94 throw new Error(data.error);
95 }
96
97 // Add to list with processing status
98 setMedia(prev => [{
99 id: data.id,
100 type: data.type,
101 status: 'processing',
102 }, ...prev]);
103
104 } catch (err: any) {
105 console.error('Upload failed:', err);
106 alert('Upload failed: ' + err.message);
107 } finally {
108 setUploading(false);
109 }
110 };
111
112 const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
113 const files = e.target.files;
114 if (files && files.length > 0) {
115 Array.from(files).forEach(uploadFile);
116 }
117 e.target.value = '';
118 };
119
120 const handleDrop = useCallback((e: React.DragEvent) => {
121 e.preventDefault();
122 setDragover(false);
123
124 const files = e.dataTransfer.files;
125 if (files && files.length > 0) {
126 Array.from(files).forEach(uploadFile);
127 }
128 }, []);
129
130 const handleDragOver = useCallback((e: React.DragEvent) => {
131 e.preventDefault();
132 setDragover(true);
133 }, []);
134
135 const handleDragLeave = useCallback(() => {
136 setDragover(false);
137 }, []);
138
139 return (
140 <>
141 <h1>Medi</h1>
142 <p className="subtitle">Upload images and videos for processing</p>
143
144 <div
145 className={`upload-area ${dragover ? 'dragover' : ''}`}
146 onClick={() => fileInputRef.current?.click()}
147 onDrop={handleDrop}
148 onDragOver={handleDragOver}
149 onDragLeave={handleDragLeave}
150 >
151 <input
152 ref={fileInputRef}
153 type="file"
154 accept="image/*,video/*"
155 multiple
156 onChange={handleFileSelect}
157 />
158 {uploading ? (
159 <>
160 <div className="loading-spinner" />
161 <h3>Uploading...</h3>
162 </>
163 ) : (
164 <>
165 <UploadIcon />
166 <h3>Drop files here or click to upload</h3>
167 <p>Supports images (JPG, PNG, GIF, WebP) and videos (MP4, MOV, WebM)</p>
168 </>
169 )}
170 </div>
171
172 {media.length === 0 ? (
173 <div className="empty-state">
174 <p>No media uploaded yet</p>
175 </div>
176 ) : (
177 <div className="media-grid">
178 {media.map(item => (
179 <MediaCard key={item.id} item={item} />
180 ))}
181 </div>
182 )}
183 </>
184 );
185 }
186
187 function MediaCard({ item }: { item: MediaItem }) {
188 const [videoError, setVideoError] = useState(false);
189
190 return (
191 <div className="media-card">
192 <div className="media-preview">
193 {item.status === 'processing' ? (
194 <div className="loading-spinner" />
195 ) : item.type === 'image' && item.webp ? (
196 <img src={item.webp} alt={item.id} />
197 ) : item.type === 'video' && item.thumbnail ? (
198 <img src={item.thumbnail} alt={item.id} />
199 ) : (
200 <span style={{ color: '#8b949e' }}>No preview</span>
201 )}
202 </div>
203
204 <div className="media-info">
205 <h4>{item.id}</h4>
206 <span className="type">{item.type}</span>
207 <div>
208 <span className={`status ${item.status}`}>
209 {item.status === 'processing' ? 'Processing...' :
210 item.status === 'completed' ? 'Ready' : 'Error'}
211 </span>
212 </div>
213 </div>
214
215 {item.status === 'completed' && (
216 <div className="media-links">
217 {item.type === 'image' ? (
218 <>
219 <a href={item.webp} target="_blank" rel="noreferrer">WebP</a>
220 <a href={item.original} target="_blank" rel="noreferrer">Original</a>
221 </>
222 ) : (
223 <>
224 <a href={item.hls} target="_blank" rel="noreferrer">HLS Stream</a>
225 <a href={item.thumbnail} target="_blank" rel="noreferrer">Thumbnail</a>
226 </>
227 )}
228 </div>
229 )}
230 </div>
231 );
232 }