Mercurial
view 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 source
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> ); }