Mercurial
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/medi/src/app.tsx Sun Jan 25 10:44:04 2026 -0800 @@ -0,0 +1,232 @@ +import React, { useState, useEffect, useCallback, useRef } from 'react'; + +interface MediaItem { + id: string; + type: 'image' | 'video'; + status: 'processing' | 'completed' | 'error'; + webp?: string; + hls?: string; + thumbnail?: string; + original?: string; + error?: string; +} + +const UploadIcon = () => ( + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> + <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> + <polyline points="17 8 12 3 7 8" /> + <line x1="12" y1="3" x2="12" y2="15" /> + </svg> +); + +export function App() { + const [media, setMedia] = useState<MediaItem[]>([]); + const [uploading, setUploading] = useState(false); + const [dragover, setDragover] = useState(false); + const fileInputRef = useRef<HTMLInputElement>(null); + + // Fetch media list on mount + useEffect(() => { + fetchMediaList(); + }, []); + + // Poll for processing status + useEffect(() => { + const processingItems = media.filter(m => m.status === 'processing'); + if (processingItems.length === 0) return; + + const interval = setInterval(() => { + processingItems.forEach(item => { + checkMediaStatus(item.id); + }); + }, 2000); + + return () => clearInterval(interval); + }, [media]); + + const fetchMediaList = async () => { + try { + const res = await fetch('/api/media'); + const data = await res.json(); + + // Fetch details for each item + const items = await Promise.all( + data.items.map(async (item: { id: string; type: string }) => { + const infoRes = await fetch(`/api/media/${item.id}`); + return infoRes.json(); + }) + ); + + setMedia(items); + } catch (err) { + console.error('Failed to fetch media list:', err); + } + }; + + const checkMediaStatus = async (id: string) => { + try { + const res = await fetch(`/api/media/${id}`); + const data = await res.json(); + + setMedia(prev => prev.map(item => + item.id === id ? { ...item, ...data } : item + )); + } catch (err) { + console.error('Failed to check status:', err); + } + }; + + const uploadFile = async (file: File) => { + const formData = new FormData(); + formData.append('file', file); + + setUploading(true); + + try { + const res = await fetch('/api/upload', { + method: 'POST', + body: formData, + }); + + const data = await res.json(); + + if (data.error) { + throw new Error(data.error); + } + + // Add to list with processing status + setMedia(prev => [{ + id: data.id, + type: data.type, + status: 'processing', + }, ...prev]); + + } catch (err: any) { + console.error('Upload failed:', err); + alert('Upload failed: ' + err.message); + } finally { + setUploading(false); + } + }; + + const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { + const files = e.target.files; + if (files && files.length > 0) { + Array.from(files).forEach(uploadFile); + } + e.target.value = ''; + }; + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setDragover(false); + + const files = e.dataTransfer.files; + if (files && files.length > 0) { + Array.from(files).forEach(uploadFile); + } + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setDragover(true); + }, []); + + const handleDragLeave = useCallback(() => { + setDragover(false); + }, []); + + return ( + <> + <h1>Medi</h1> + <p className="subtitle">Upload images and videos for processing</p> + + <div + className={`upload-area ${dragover ? 'dragover' : ''}`} + onClick={() => fileInputRef.current?.click()} + onDrop={handleDrop} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + > + <input + ref={fileInputRef} + type="file" + accept="image/*,video/*" + multiple + onChange={handleFileSelect} + /> + {uploading ? ( + <> + <div className="loading-spinner" /> + <h3>Uploading...</h3> + </> + ) : ( + <> + <UploadIcon /> + <h3>Drop files here or click to upload</h3> + <p>Supports images (JPG, PNG, GIF, WebP) and videos (MP4, MOV, WebM)</p> + </> + )} + </div> + + {media.length === 0 ? ( + <div className="empty-state"> + <p>No media uploaded yet</p> + </div> + ) : ( + <div className="media-grid"> + {media.map(item => ( + <MediaCard key={item.id} item={item} /> + ))} + </div> + )} + </> + ); +} + +function MediaCard({ item }: { item: MediaItem }) { + const [videoError, setVideoError] = useState(false); + + return ( + <div className="media-card"> + <div className="media-preview"> + {item.status === 'processing' ? ( + <div className="loading-spinner" /> + ) : item.type === 'image' && item.webp ? ( + <img src={item.webp} alt={item.id} /> + ) : item.type === 'video' && item.thumbnail ? ( + <img src={item.thumbnail} alt={item.id} /> + ) : ( + <span style={{ color: '#8b949e' }}>No preview</span> + )} + </div> + + <div className="media-info"> + <h4>{item.id}</h4> + <span className="type">{item.type}</span> + <div> + <span className={`status ${item.status}`}> + {item.status === 'processing' ? 'Processing...' : + item.status === 'completed' ? 'Ready' : 'Error'} + </span> + </div> + </div> + + {item.status === 'completed' && ( + <div className="media-links"> + {item.type === 'image' ? ( + <> + <a href={item.webp} target="_blank" rel="noreferrer">WebP</a> + <a href={item.original} target="_blank" rel="noreferrer">Original</a> + </> + ) : ( + <> + <a href={item.hls} target="_blank" rel="noreferrer">HLS Stream</a> + <a href={item.thumbnail} target="_blank" rel="noreferrer">Thumbnail</a> + </> + )} + </div> + )} + </div> + ); +}