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>
+  );
+}