tubewatch/playlist-monitor/UI_INTEGRATION.md

33 KiB

Playlist Monitor Service - UI Integration Guide

📋 Table of Contents

  1. UI Options Overview
  2. Option 1: Standalone React App
  3. Option 2: Extend MeTube Angular
  4. Option 3: Simple HTML Dashboard
  5. API Integration Examples
  6. UI Components Design
  7. Styling Guidelines
  8. Responsive Design
  9. Testing UI
  10. Deployment Options

🎨 UI Options Overview

Since the Playlist Monitor Service is a separate microservice, you have several UI integration options:

Option Comparison

Option Complexity Maintenance Features Best For
React App Medium Low Full-featured New projects, standalone
Extend MeTube High High Integrated Existing MeTube users
Simple HTML Low Low Basic Quick setup, minimal needs

Quick Setup (5 minutes)

# Create React app with TypeScript
cd /root/workspace/tubewatch
npx create-react-app playlist-monitor-ui --template typescript
cd playlist-monitor-ui

# Install UI dependencies
npm install @mui/material @emotion/react @emotion/styled axios react-query
npm install @mui/icons-material @mui/x-data-grid date-fns
npm install recharts  # For charts
npm install react-router-dom  # For routing

# Start development server
npm start

Project Structure

playlist-monitor-ui/
├── src/
│   ├── components/          # Reusable components
│   │   ├── PlaylistCard.tsx
│   │   ├── VideoTable.tsx
│   │   └── StatusChart.tsx
│   ├── pages/              # Main pages
│   │   ├── Dashboard.tsx
│   │   ├── Playlists.tsx
│   │   ├── PlaylistDetail.tsx
│   │   └── Settings.tsx
│   ├── services/           # API services
│   │   └── api.ts
│   ├── hooks/              # Custom hooks
│   │   └── usePlaylists.ts
│   ├── utils/              # Utilities
│   │   └── formatters.ts
│   └── App.tsx
├── public/
└── package.json

Core API Service

// src/services/api.ts
import axios from 'axios';

const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8082/api';

const api = axios.create({
  baseURL: API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor
api.interceptors.request.use(
  (config) => {
    console.log('API Request:', config.method?.toUpperCase(), config.url);
    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor
api.interceptors.response.use(
  (response) => response,
  (error) => {
    console.error('API Error:', error.response?.data || error.message);
    return Promise.reject(error);
  }
);

// Playlist API calls
export const playlistAPI = {
  getAll: () => api.get('/playlists'),
  getById: (id: string) => api.get(`/playlists/${id}`),
  create: (data: any) => api.post('/playlists', data),
  update: (id: string, data: any) => api.put(`/playlists/${id}`, data),
  delete: (id: string) => api.delete(`/playlists/${id}`),
  check: (id: string) => api.post(`/playlists/${id}/check`),
};

// Video API calls
export const videoAPI = {
  getById: (id: string) => api.get(`/videos/${id}`),
  download: (id: string) => api.post(`/videos/${id}/download`),
  skip: (id: string) => api.post(`/videos/${id}/skip`),
  reset: (id: string) => api.post(`/videos/${id}/reset`),
  markAsMoved: (id: string, locationNote?: string) => 
    api.post(`/videos/${id}/file-moved`, { location_note: locationNote }),
};

// System API calls
export const systemAPI = {
  getStatus: () => api.get('/status'),
  getSchedulerStatus: () => api.get('/scheduler/status'),
  syncWithMeTube: () => api.post('/sync-metube'),
  healthCheck: () => api.get('/health'),
};

Main Dashboard Component

// src/pages/Dashboard.tsx
import React, { useState, useEffect } from 'react';
import { 
  Container, 
  Grid, 
  Card, 
  CardContent, 
  Typography, 
  Box, 
  LinearProgress,
  Alert 
} from '@mui/material';
import { 
  PlaylistPlay, 
  VideoLibrary, 
  Download, 
  Error 
} from '@mui/icons-material';
import { systemAPI } from '../services/api';
import StatusChart from '../components/StatusChart';

interface SystemStatus {
  total_playlists: number;
  active_playlists: number;
  total_videos: number;
  pending_downloads: number;
  active_downloads: number;
  completed_downloads: number;
  failed_downloads: number;
  metube_status: {
    connected: boolean;
    error?: string;
  };
}

const Dashboard: React.FC = () => {
  const [status, setStatus] = useState<SystemStatus | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    loadStatus();
    const interval = setInterval(loadStatus, 30000); // Refresh every 30s
    return () => clearInterval(interval);
  }, []);

  const loadStatus = async () => {
    try {
      const response = await systemAPI.getStatus();
      setStatus(response.data);
      setError(null);
    } catch (err) {
      setError('Failed to load system status');
      console.error('Error loading status:', err);
    } finally {
      setLoading(false);
    }
  };

  if (loading) return <LinearProgress />;
  if (error) return <Alert severity="error">{error}</Alert>;
  if (!status) return <Alert severity="info">No data available</Alert>;

  const stats = [
    { label: 'Total Playlists', value: status.total_playlists, icon: <PlaylistPlay /> },
    { label: 'Active Playlists', value: status.active_playlists, icon: <PlaylistPlay /> },
    { label: 'Total Videos', value: status.total_videos, icon: <VideoLibrary /> },
    { label: 'Pending Downloads', value: status.pending_downloads, icon: <Download /> },
    { label: 'Active Downloads', value: status.active_downloads, icon: <Download /> },
    { label: 'Completed', value: status.completed_downloads, icon: <VideoLibrary /> },
    { label: 'Failed', value: status.failed_downloads, icon: <Error /> },
  ];

  return (
    <Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
      <Grid container spacing={3}>
        {/* Status Alert */}
        {!status.metube_status.connected && (
          <Grid item xs={12}>
            <Alert severity="warning">
              MeTube connection failed: {status.metube_status.error}
            </Alert>
          </Grid>
        )}

        {/* Stats Cards */}
        {stats.map((stat, index) => (
          <Grid item xs={12} sm={6} md={4} lg={3} key={index}>
            <Card>
              <CardContent>
                <Box display="flex" alignItems="center" mb={2}>
                  {stat.icon}
                  <Typography variant="h6" sx={{ ml: 1 }}>
                    {stat.label}
                  </Typography>
                </Box>
                <Typography variant="h3" component="div">
                  {stat.value}
                </Typography>
              </CardContent>
            </Card>
          </Grid>
        ))}

        {/* Status Chart */}
        <Grid item xs={12}>
          <Card>
            <CardContent>
              <Typography variant="h6" gutterBottom>
                Video Status Distribution
              </Typography>
              <StatusChart status={status} />
            </CardContent>
          </Card>
        </Grid>
      </Grid>
    </Container>
  );
};

export default Dashboard;

Playlist Management Component

// src/pages/Playlists.tsx
import React, { useState, useEffect } from 'react';
import {
  Container,
  Grid,
  Card,
  CardContent,
  Typography,
  Button,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  TextField,
  Box,
  Chip,
  IconButton,
  Alert,
} from '@mui/material';
import {
  Add,
  PlayArrow,
  Delete,
  Edit,
  Refresh,
  CheckCircle,
  PauseCircle,
} from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from 'react-query';
import { playlistAPI } from '../services/api';

interface Playlist {
  id: string;
  url: string;
  title?: string;
  check_interval: number;
  enabled: boolean;
  created_at: string;
  stats: {
    total: number;
    pending: number;
    completed: number;
    failed: number;
  };
}

const Playlists: React.FC = () => {
  const queryClient = useQueryClient();
  const [addDialog, setAddDialog] = useState(false);
  const [newPlaylist, setNewPlaylist] = useState({
    url: '',
    title: '',
    check_interval: 60,
    quality: 'best',
    format: 'mp4',
    folder: '',
    enabled: true,
  });

  // Fetch playlists
  const { data: playlists, isLoading, error } = useQuery(
    'playlists',
    () => playlistAPI.getAll().then(res => res.data),
    {
      refetchInterval: 30000, // Refresh every 30s
    }
  );

  // Create playlist mutation
  const createMutation = useMutation(
    (data: any) => playlistAPI.create(data),
    {
      onSuccess: () => {
        queryClient.invalidateQueries('playlists');
        setAddDialog(false);
        setNewPlaylist({
          url: '',
          title: '',
          check_interval: 60,
          quality: 'best',
          format: 'mp4',
          folder: '',
          enabled: true,
        });
      },
    }
  );

  // Check playlist mutation
  const checkMutation = useMutation(
    (id: string) => playlistAPI.check(id),
    {
      onSuccess: () => {
        queryClient.invalidateQueries('playlists');
      },
    }
  );

  // Delete playlist mutation
  const deleteMutation = useMutation(
    (id: string) => playlistAPI.delete(id),
    {
      onSuccess: () => {
        queryClient.invalidateQueries('playlists');
      },
    }
  );

  const handleAddPlaylist = () => {
    createMutation.mutate(newPlaylist);
  };

  const handleCheckPlaylist = (id: string) => {
    checkMutation.mutate(id);
  };

  const handleDeletePlaylist = (id: string) => {
    if (window.confirm('Are you sure you want to delete this playlist?')) {
      deleteMutation.mutate(id);
    }
  };

  if (isLoading) return <div>Loading...</div>;
  if (error) return <Alert severity="error">Failed to load playlists</Alert>;

  return (
    <Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
      <Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
        <Typography variant="h4" component="h1">
          Playlists
        </Typography>
        <Button
          variant="contained"
          startIcon={<Add />}
          onClick={() => setAddDialog(true)}
        >
          Add Playlist
        </Button>
      </Box>

      <Grid container spacing={3}>
        {playlists?.map((playlist: Playlist) => (
          <Grid item xs={12} sm={6} md={4} lg={3} key={playlist.id}>
            <Card>
              <CardContent>
                <Box display="flex" justifyContent="space-between" alignItems="flex-start" mb={2}>
                  <Typography variant="h6" component="h2" noWrap>
                    {playlist.title || 'Untitled Playlist'}
                  </Typography>
                  <Chip
                    icon={playlist.enabled ? <CheckCircle /> : <PauseCircle />}
                    label={playlist.enabled ? 'Enabled' : 'Disabled'}
                    color={playlist.enabled ? 'success' : 'default'}
                    size="small"
                  />
                </Box>

                <Typography variant="body2" color="text.secondary" noWrap mb={1}>
                  {playlist.url}
                </Typography>

                <Box mb={2}>
                  <Typography variant="body2" color="text.secondary">
                    Check Interval: {playlist.check_interval} minutes
                  </Typography>
                  <Typography variant="body2" color="text.secondary">
                    Total Videos: {playlist.stats.total}
                  </Typography>
                  <Typography variant="body2" color="text.secondary">
                    Pending: {playlist.stats.pending} | Completed: {playlist.stats.completed}
                  </Typography>
                </Box>

                <Box display="flex" justifyContent="space-between">
                  <IconButton
                    size="small"
                    onClick={() => handleCheckPlaylist(playlist.id)}
                    disabled={checkMutation.isLoading}
                    title="Check for new videos"
                  >
                    <Refresh />
                  </IconButton>
                  <IconButton
                    size="small"
                    onClick={() => handleDeletePlaylist(playlist.id)}
                    disabled={deleteMutation.isLoading}
                    title="Delete playlist"
                  >
                    <Delete />
                  </IconButton>
                </Box>
              </CardContent>
            </Card>
          </Grid>
        ))}
      </Grid>

      {/* Add Playlist Dialog */}
      <Dialog open={addDialog} onClose={() => setAddDialog(false)} maxWidth="sm" fullWidth>
        <DialogTitle>Add New Playlist</DialogTitle>
        <DialogContent>
          <TextField
            fullWidth
            label="YouTube Playlist URL"
            value={newPlaylist.url}
            onChange={(e) => setNewPlaylist({ ...newPlaylist, url: e.target.value })}
            margin="normal"
            placeholder="https://www.youtube.com/playlist?list=..."
          />
          <TextField
            fullWidth
            label="Title (optional)"
            value={newPlaylist.title}
            onChange={(e) => setNewPlaylist({ ...newPlaylist, title: e.target.value })}
            margin="normal"
          />
          <TextField
            fullWidth
            label="Check Interval (minutes)"
            type="number"
            value={newPlaylist.check_interval}
            onChange={(e) => setNewPlaylist({ ...newPlaylist, check_interval: parseInt(e.target.value) })}
            margin="normal"
          />
          <TextField
            fullWidth
            label="Download Folder (optional)"
            value={newPlaylist.folder}
            onChange={(e) => setNewPlaylist({ ...newPlaylist, folder: e.target.value })}
            margin="normal"
          />
        </DialogContent>
        <DialogActions>
          <Button onClick={() => setAddDialog(false)}>Cancel</Button>
          <Button
            onClick={handleAddPlaylist}
            disabled={!newPlaylist.url || createMutation.isLoading}
            variant="contained"
          >
            Add
          </Button>
        </DialogActions>
      </Dialog>
    </Container>
  );
};

export default Playlists;

🔄 Option 2: Extend MeTube Angular

Integration Approach

Since MeTube uses Angular, you can extend its existing UI:

// Add to MeTube's existing structure
metube/ui/src/app/
├── playlist-monitor/          # New module
   ├── playlist-monitor.module.ts
   ├── components/
      ├── playlist-list/
      ├── playlist-detail/
      └── video-dashboard/
   ├── services/
      └── playlist-monitor.service.ts
   └── models/
       └── playlist.model.ts

Service Integration

// metube/ui/src/app/playlist-monitor/services/playlist-monitor.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class PlaylistMonitorService {
  private apiUrl = 'http://localhost:8082/api';

  constructor(private http: HttpClient) {}

  getPlaylists(): Observable<any> {
    return this.http.get(`${this.apiUrl}/playlists`);
  }

  createPlaylist(playlist: any): Observable<any> {
    return this.http.post(`${this.apiUrl}/playlists`, playlist);
  }

  checkPlaylist(id: string): Observable<any> {
    return this.http.post(`${this.apiUrl}/playlists/${id}/check`, {});
  }
}

Component Integration

// Add to MeTube's navigation
// metube/ui/src/app/app.component.html
<nav class="navbar">
  <!-- Existing navigation -->
  <li class="nav-item">
    <a class="nav-link" routerLink="/playlists" routerLinkActive="active">
      <fa-icon [icon]="['fas', 'list']"></fa-icon> Playlists
    </a>
  </li>
</nav>

📝 Option 3: Simple HTML Dashboard

Basic HTML Dashboard

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Playlist Monitor Dashboard</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
    <style>
        .status-card { transition: transform 0.2s; }
        .status-card:hover { transform: translateY(-2px); }
        .playlist-card { cursor: pointer; }
        .video-status { font-size: 0.8rem; }
    </style>
</head>
<body>
    <nav class="navbar navbar-dark bg-dark">
        <div class="container-fluid">
            <span class="navbar-brand mb-0 h1">
                <i class="fas fa-list"></i> Playlist Monitor
            </span>
            <button class="btn btn-outline-light btn-sm" onclick="refreshData()">
                <i class="fas fa-sync-alt"></i> Refresh
            </button>
        </div>
    </nav>

    <div class="container-fluid mt-4">
        <!-- Status Cards -->
        <div class="row mb-4" id="status-cards">
            <div class="col-md-3">
                <div class="card status-card border-primary">
                    <div class="card-body text-center">
                        <i class="fas fa-list fa-2x text-primary mb-2"></i>
                        <h5 class="card-title">Loading...</h5>
                        <p class="card-text">Total Playlists</p>
                    </div>
                </div>
            </div>
        </div>

        <!-- Playlists Table -->
        <div class="card">
            <div class="card-header d-flex justify-content-between align-items-center">
                <h5 class="mb-0">Playlists</h5>
                <button class="btn btn-primary btn-sm" onclick="showAddPlaylistModal()">
                    <i class="fas fa-plus"></i> Add Playlist
                </button>
            </div>
            <div class="card-body">
                <div class="table-responsive">
                    <table class="table table-hover" id="playlists-table">
                        <thead>
                            <tr>
                                <th>Title</th>
                                <th>URL</th>
                                <th>Status</th>
                                <th>Videos</th>
                                <th>Actions</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr>
                                <td colspan="5" class="text-center">Loading playlists...</td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>

    <!-- Add Playlist Modal -->
    <div class="modal fade" id="addPlaylistModal" tabindex="-1">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Add New Playlist</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <form id="add-playlist-form">
                        <div class="mb-3">
                            <label for="playlist-url" class="form-label">YouTube Playlist URL</label>
                            <input type="url" class="form-control" id="playlist-url" required
                                   placeholder="https://www.youtube.com/playlist?list=...">
                        </div>
                        <div class="mb-3">
                            <label for="playlist-title" class="form-label">Title (optional)</label>
                            <input type="text" class="form-control" id="playlist-title"
                                   placeholder="My Playlist">
                        </div>
                        <div class="mb-3">
                            <label for="check-interval" class="form-label">Check Interval (minutes)</label>
                            <input type="number" class="form-control" id="check-interval" value="60" min="5" max="1440">
                        </div>
                    </form>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                    <button type="button" class="btn btn-primary" onclick="addPlaylist()">Add Playlist</button>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    <script src="dashboard.js"></script>
</body>
</html>

JavaScript for Simple Dashboard

// dashboard.js
const API_BASE_URL = 'http://localhost:8082/api';

// API helper functions
async function apiCall(endpoint, options = {}) {
    try {
        const response = await fetch(`${API_BASE_URL}${endpoint}`, {
            headers: {
                'Content-Type': 'application/json',
            },
            ...options
        });
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        return await response.json();
    } catch (error) {
        console.error('API call failed:', error);
        throw error;
    }
}

// Load system status
async function loadStatus() {
    try {
        const status = await apiCall('/status');
        updateStatusCards(status);
    } catch (error) {
        showError('Failed to load system status');
    }
}

// Update status cards
function updateStatusCards(status) {
    const statusCards = document.getElementById('status-cards');
    statusCards.innerHTML = `
        <div class="col-md-3">
            <div class="card status-card border-primary">
                <div class="card-body text-center">
                    <i class="fas fa-list fa-2x text-primary mb-2"></i>
                    <h3 class="card-title">${status.total_playlists}</h3>
                    <p class="card-text">Total Playlists</p>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card status-card border-success">
                <div class="card-body text-center">
                    <i class="fas fa-play-circle fa-2x text-success mb-2"></i>
                    <h3 class="card-title">${status.active_playlists}</h3>
                    <p class="card-text">Active Playlists</p>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card status-card border-info">
                <div class="card-body text-center">
                    <i class="fas fa-video fa-2x text-info mb-2"></i>
                    <h3 class="card-title">${status.total_videos}</h3>
                    <p class="card-text">Total Videos</p>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card status-card border-warning">
                <div class="card-body text-center">
                    <i class="fas fa-download fa-2x text-warning mb-2"></i>
                    <h3 class="card-title">${status.pending_downloads}</h3>
                    <p class="card-text">Pending Downloads</p>
                </div>
            </div>
        </div>
    `;
}

// Load playlists
async function loadPlaylists() {
    try {
        const playlists = await apiCall('/playlists');
        updatePlaylistsTable(playlists);
    } catch (error) {
        showError('Failed to load playlists');
    }
}

// Update playlists table
function updatePlaylistsTable(playlists) {
    const tbody = document.querySelector('#playlists-table tbody');
    
    if (playlists.length === 0) {
        tbody.innerHTML = '<tr><td colspan="5" class="text-center">No playlists found</td></tr>';
        return;
    }
    
    tbody.innerHTML = playlists.map(playlist => `
        <tr class="playlist-card" onclick="showPlaylistDetails('${playlist.id}')">
            <td>${playlist.title || 'Untitled'}</td>
            <td><small>${playlist.url}</small></td>
            <td>
                <span class="badge ${playlist.enabled ? 'bg-success' : 'bg-secondary'}">
                    ${playlist.enabled ? 'Enabled' : 'Disabled'}
                </span>
            </td>
            <td>
                <span class="badge bg-primary">${playlist.stats.total}</span>
                <small class="text-muted ms-1">
                    (${playlist.stats.pending} pending, ${playlist.stats.completed} completed)
                </small>
            </td>
            <td>
                <button class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation(); checkPlaylist('${playlist.id}')">
                    <i class="fas fa-sync-alt"></i>
                </button>
                <button class="btn btn-sm btn-outline-danger" onclick="event.stopPropagation(); deletePlaylist('${playlist.id}')">
                    <i class="fas fa-trash"></i>
                </button>
            </td>
        </tr>
    `).join('');
}

// Add playlist
async function addPlaylist() {
    const url = document.getElementById('playlist-url').value;
    const title = document.getElementById('playlist-title').value;
    const checkInterval = parseInt(document.getElementById('check-interval').value);
    
    if (!url) {
        alert('Please enter a YouTube playlist URL');
        return;
    }
    
    try {
        await apiCall('/playlists', {
            method: 'POST',
            body: JSON.stringify({
                url: url,
                title: title || undefined,
                check_interval: checkInterval,
                enabled: true
            })
        });
        
        // Close modal and refresh
        bootstrap.Modal.getInstance(document.getElementById('addPlaylistModal')).hide();
        refreshData();
        showSuccess('Playlist added successfully');
    } catch (error) {
        showError('Failed to add playlist');
    }
}

// Check playlist
async function checkPlaylist(id) {
    try {
        await apiCall(`/playlists/${id}/check`, { method: 'POST' });
        showSuccess('Playlist check initiated');
        refreshData();
    } catch (error) {
        showError('Failed to check playlist');
    }
}

// UI helper functions
function showAddPlaylistModal() {
    new bootstrap.Modal(document.getElementById('addPlaylistModal')).show();
}

function showError(message) {
    // Simple error display - you could enhance this with toast notifications
    alert(`Error: ${message}`);
}

function showSuccess(message) {
    alert(`Success: ${message}`);
}

function refreshData() {
    loadStatus();
    loadPlaylists();
}

// Initialize dashboard
document.addEventListener('DOMContentLoaded', function() {
    refreshData();
    // Auto-refresh every 30 seconds
    setInterval(refreshData, 30000);
});

🔗 API Integration Examples

React Hook for Real-time Updates

// src/hooks/useRealtimeUpdates.ts
import { useEffect, useRef } from 'react';
import { systemAPI } from '../services/api';

export const useRealtimeUpdates = (onUpdate: (data: any) => void) => {
  const wsRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    // Connect to WebSocket for real-time updates
    const connectWebSocket = () => {
      const ws = new WebSocket('ws://localhost:8082/ws');
      
      ws.onopen = () => {
        console.log('WebSocket connected');
      };
      
      ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        onUpdate(data);
      };
      
      ws.onclose = () => {
        console.log('WebSocket disconnected');
        // Reconnect after 5 seconds
        setTimeout(connectWebSocket, 5000);
      };
      
      ws.onerror = (error) => {
        console.error('WebSocket error:', error);
      };
      
      wsRef.current = ws;
    };

    connectWebSocket();

    return () => {
      if (wsRef.current) {
        wsRef.current.close();
      }
    };
  }, [onUpdate]);

  return wsRef.current;
};

🎨 UI Components Design

Key Components Needed

  1. Dashboard - System overview with charts and statistics
  2. Playlist List - Table/card view of all playlists
  3. Playlist Detail - Individual playlist management
  4. Video Table - Video status and management
  5. Add/Edit Forms - Playlist creation and editing
  6. Status Indicators - Real-time status updates
  7. Charts/Graphs - Statistics visualization

Design System Integration

// src/theme.ts
import { createTheme } from '@mui/material/styles';

// Custom theme matching MeTube's style
export const theme = createTheme({
  palette: {
    primary: {
      main: '#1976d2', // Blue matching MeTube
    },
    secondary: {
      main: '#dc004e',
    },
    background: {
      default: '#f5f5f5',
    },
  },
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
  },
  components: {
    MuiCard: {
      styleOverrides: {
        root: {
          boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
          borderRadius: 8,
        },
      },
    },
  },
});

📱 Responsive Design

Mobile-First Approach

// Responsive grid breakpoints
<Grid container spacing={{ xs: 2, md: 3 }}>
  <Grid item xs={12} sm={6} md={4} lg={3}>
    <Card>
      {/* Card content */}
    </Card>
  </Grid>
</Grid>

// Responsive typography
<Typography 
  variant={{ xs: 'h6', md: 'h5' }}
  fontSize={{ xs: '1rem', md: '1.25rem' }}
>
  Playlist Title
</Typography>

🧪 Testing UI

Component Testing with React Testing Library

// src/components/__tests__/PlaylistCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import PlaylistCard from '../PlaylistCard';

describe('PlaylistCard', () => {
  const mockPlaylist = {
    id: '1',
    title: 'Test Playlist',
    url: 'https://www.youtube.com/playlist?list=TEST123',
    enabled: true,
    stats: { total: 10, pending: 2, completed: 8 }
  };

  it('renders playlist information correctly', () => {
    render(<PlaylistCard playlist={mockPlaylist} />);
    
    expect(screen.getByText('Test Playlist')).toBeInTheDocument();
    expect(screen.getByText('10 videos')).toBeInTheDocument();
  });

  it('calls onCheck when check button is clicked', () => {
    const onCheck = jest.fn();
    render(<PlaylistCard playlist={mockPlaylist} onCheck={onCheck} />);
    
    fireEvent.click(screen.getByLabelText('Check playlist'));
    expect(onCheck).toHaveBeenCalledWith('1');
  });
});

🚀 Deployment Options

Option 1: Serve with Backend

// Modify app/main.py to serve static files
from fastapi.staticfiles import StaticFiles

app.mount("/ui", StaticFiles(directory="ui/build", html=True), name="ui")

# Add redirect
@app.get("/")
async def serve_ui():
    return RedirectResponse(url="/ui")

Option 2: Separate Deployment

# Build React app
npm run build

# Deploy to static hosting
# - Netlify
# - Vercel
# - GitHub Pages
# - Nginx

Option 3: Docker Multi-Service

# Dockerfile for React app
FROM node:18-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

📚 Next Steps

  1. Choose UI Option based on your needs and technical requirements
  2. Implement Core Components following the examples above
  3. Add Real-time Updates with WebSocket integration
  4. Test Thoroughly with different screen sizes and browsers
  5. Deploy using one of the suggested deployment options

The UI implementation will provide a complete, user-friendly interface for managing your playlists and monitoring download progress! 🎨