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