changemaker.lite/admin/src/components/media/BulkAddToPlaylistModal.tsx
bunker-admin 39d74e7b85 Add guided tour, media enhancements, error handling, and DevOps improvements
Major additions: onboarding tour system, correlation-id middleware, media
error handler, restore script, env validation script, Dockerignore files.
Updates across 70+ admin components for improved UX and error handling.

Bunker Admin
2026-03-26 10:31:51 -06:00

190 lines
5.4 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Modal, Select, Button, Input, Space, Typography, Spin, Divider, message } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { mediaApi } from '@/lib/media-api';
import type { PlaylistSummary } from '@/types/media';
import axios from 'axios';
const { Text } = Typography;
interface BulkAddToPlaylistModalProps {
videoIds: number[];
open: boolean;
onClose: () => void;
onSuccess?: () => void;
}
export default function BulkAddToPlaylistModal({
videoIds,
open,
onClose,
onSuccess,
}: BulkAddToPlaylistModalProps) {
const [playlists, setPlaylists] = useState<PlaylistSummary[]>([]);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [selectedPlaylistId, setSelectedPlaylistId] = useState<number | null>(null);
// Inline create state
const [showCreate, setShowCreate] = useState(false);
const [newName, setNewName] = useState('');
const [creating, setCreating] = useState(false);
useEffect(() => {
if (!open) return;
const fetchPlaylists = async () => {
try {
setLoading(true);
const { data } = await mediaApi.get('/playlists/my');
setPlaylists(data.data || []);
} catch {
message.error('Failed to load playlists');
} finally {
setLoading(false);
}
};
fetchPlaylists();
setSelectedPlaylistId(null);
setShowCreate(false);
setNewName('');
}, [open]);
const handleAdd = async () => {
if (!selectedPlaylistId || videoIds.length === 0) return;
try {
setSaving(true);
let added = 0;
let skipped = 0;
for (const mediaId of videoIds) {
try {
await mediaApi.post(`/playlists/${selectedPlaylistId}/videos`, { mediaId });
added++;
} catch (error: unknown) {
if (axios.isAxiosError(error) && error.response?.status === 409) {
skipped++;
} else {
throw error;
}
}
}
const parts: string[] = [];
if (added > 0) parts.push(`${added} video${added > 1 ? 's' : ''} added`);
if (skipped > 0) parts.push(`${skipped} already in playlist`);
message.success(parts.join(', '));
onSuccess?.();
} catch {
message.error('Failed to add videos to playlist');
} finally {
setSaving(false);
}
};
const handleCreateNew = async () => {
if (!newName.trim()) return;
try {
setCreating(true);
const { data } = await mediaApi.post('/playlists/', {
name: newName.trim(),
isPublic: false,
});
setPlaylists((prev) => [...prev, data]);
setSelectedPlaylistId(data.id);
setNewName('');
setShowCreate(false);
message.success(`Created "${data.name}"`);
} catch (error: unknown) {
if (axios.isAxiosError(error) && error.response?.status === 409) {
message.error('You already have a playlist with this name');
} else {
message.error('Failed to create playlist');
}
} finally {
setCreating(false);
}
};
const selectedPlaylist = playlists.find((p) => p.id === selectedPlaylistId);
return (
<Modal
title={`Add ${videoIds.length} video${videoIds.length > 1 ? 's' : ''} to playlist`}
open={open}
onOk={handleAdd}
onCancel={onClose}
confirmLoading={saving}
okText="Add"
okButtonProps={{ disabled: !selectedPlaylistId }}
>
{loading ? (
<div style={{ textAlign: 'center', padding: 32 }}>
<Spin />
</div>
) : (
<>
<Select
placeholder="Select a playlist"
value={selectedPlaylistId}
onChange={setSelectedPlaylistId}
style={{ width: '100%', marginBottom: 12 }}
options={playlists.map((p) => ({
value: p.id,
label: `${p.name} (${p.videoCount} videos)`,
}))}
showSearch
filterOption={(input, option) =>
(option?.label as string ?? '').toLowerCase().includes(input.toLowerCase())
}
/>
{selectedPlaylist && (
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 12 }}>
{selectedPlaylist.isPublic ? 'Public' : 'Private'} playlist
{selectedPlaylist.videoCount > 0 && ` with ${selectedPlaylist.videoCount} videos`}
</Text>
)}
<Divider style={{ margin: '12px 0' }} />
{showCreate ? (
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="New playlist name"
value={newName}
onChange={(e) => setNewName(e.target.value)}
onPressEnter={handleCreateNew}
maxLength={100}
autoFocus
/>
<Button
type="primary"
onClick={handleCreateNew}
loading={creating}
disabled={!newName.trim()}
>
Create
</Button>
<Button onClick={() => setShowCreate(false)}>Cancel</Button>
</Space.Compact>
) : (
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => setShowCreate(true)}
block
>
Create New Playlist
</Button>
)}
</>
)}
</Modal>
);
}