|
217
|
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 }
|