feat: Add options to delete videos with a collection
This commit is contained in:
@@ -95,6 +95,17 @@ const updateCollection = (req, res) => {
|
||||
const deleteCollection = (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { deleteVideos } = req.query;
|
||||
|
||||
// If deleteVideos is true, delete all videos in the collection first
|
||||
if (deleteVideos === 'true') {
|
||||
const collection = storageService.getCollectionById(id);
|
||||
if (collection && collection.videos && collection.videos.length > 0) {
|
||||
collection.videos.forEach(videoId => {
|
||||
storageService.deleteVideo(videoId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const success = storageService.deleteCollection(id);
|
||||
|
||||
|
||||
@@ -1597,8 +1597,71 @@ body {
|
||||
background-color: var(--hover-color);
|
||||
}
|
||||
|
||||
|
||||
/* Unified Modal Button Styles */
|
||||
.modal-btn {
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.modal-btn.primary-btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-btn.primary-btn:hover:not(:disabled) {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.modal-btn.secondary-btn {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-btn.secondary-btn:hover:not(:disabled) {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-color: var(--text-color);
|
||||
}
|
||||
|
||||
.modal-btn.danger-btn {
|
||||
background-color: rgba(255, 62, 62, 0.15);
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.modal-btn.danger-btn:hover:not(:disabled) {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-btn.cancel-btn {
|
||||
background-color: transparent;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-btn.cancel-btn:hover:not(:disabled) {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-color: var(--text-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Video collections info */
|
||||
.video-collections {}
|
||||
|
||||
.video-collections-title {
|
||||
font-weight: bold;
|
||||
@@ -2152,3 +2215,56 @@ body {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Manage Page Sections */
|
||||
.manage-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.manage-section h2 {
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-color);
|
||||
font-size: 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Modal Actions */
|
||||
.column-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.column-actions button {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: transparent;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
color: var(--text-color);
|
||||
border-color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.delete-btn.danger {
|
||||
background-color: rgba(255, 62, 62, 0.1);
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.delete-btn.danger:hover {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
@@ -520,28 +520,21 @@ function App() {
|
||||
// Delete a collection
|
||||
const handleDeleteCollection = async (collectionId, deleteVideos = false) => {
|
||||
try {
|
||||
// Get the collection before deleting it
|
||||
const collection = collections.find(c => c.id === collectionId);
|
||||
|
||||
if (!collection) {
|
||||
return { success: false, error: 'Collection not found' };
|
||||
}
|
||||
|
||||
// If deleteVideos is true, delete all videos in the collection
|
||||
if (deleteVideos && collection.videos.length > 0) {
|
||||
for (const videoId of collection.videos) {
|
||||
await handleDeleteVideo(videoId);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the collection
|
||||
await axios.delete(`${API_URL}/collections/${collectionId}`);
|
||||
// Delete the collection with optional video deletion
|
||||
await axios.delete(`${API_URL}/collections/${collectionId}`, {
|
||||
params: { deleteVideos: deleteVideos ? 'true' : 'false' }
|
||||
});
|
||||
|
||||
// Update the collections state
|
||||
setCollections(prevCollections =>
|
||||
prevCollections.filter(collection => collection.id !== collectionId)
|
||||
);
|
||||
|
||||
// If videos were deleted, refresh the videos list
|
||||
if (deleteVideos) {
|
||||
await fetchVideos();
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error deleting collection:', error);
|
||||
@@ -688,6 +681,8 @@ function App() {
|
||||
<ManagePage
|
||||
videos={videos}
|
||||
onDeleteVideo={handleDeleteVideo}
|
||||
collections={collections}
|
||||
onDeleteCollection={handleDeleteCollection}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -50,16 +50,7 @@ const AuthorsList = ({ videos }) => {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="manage-videos-link-container" style={{ marginTop: '1rem', borderTop: '1px solid var(--border-color)', paddingTop: '0.5rem' }}>
|
||||
<Link
|
||||
to="/manage"
|
||||
className="author-link manage-link"
|
||||
onClick={() => setIsOpen(false)}
|
||||
style={{ fontWeight: 'bold', color: 'var(--primary-color)' }}
|
||||
>
|
||||
Manage Videos
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -79,9 +79,6 @@ const CollectionPage = ({ collections, videos, onDeleteVideo, onDeleteCollection
|
||||
<h2 className="collection-title">Collection: {collection.name}</h2>
|
||||
<span className="video-count">{collectionVideos.length} video{collectionVideos.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<button className="delete-collection-button" onClick={handleShowDeleteModal}>
|
||||
Delete Collection
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{collectionVideos.length === 0 ? (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import AuthorsList from '../components/AuthorsList';
|
||||
import Collections from '../components/Collections';
|
||||
import VideoCard from '../components/VideoCard';
|
||||
@@ -156,6 +157,16 @@ const Home = ({
|
||||
|
||||
{/* Authors list */}
|
||||
<AuthorsList videos={videoArray} />
|
||||
|
||||
<div className="manage-videos-link-container" style={{ marginTop: '1rem', paddingTop: '0.5rem' }}>
|
||||
<Link
|
||||
to="/manage"
|
||||
className="author-link manage-link"
|
||||
style={{ fontWeight: 'bold', color: 'var(--primary-color)', display: 'block', textAlign: 'center' }}
|
||||
>
|
||||
Manage Videos
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Videos grid */}
|
||||
|
||||
@@ -3,9 +3,11 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
const ManagePage = ({ videos, onDeleteVideo }) => {
|
||||
const ManagePage = ({ videos, onDeleteVideo, collections = [], onDeleteCollection }) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [deletingId, setDeletingId] = useState(null);
|
||||
const [collectionToDelete, setCollectionToDelete] = useState(null);
|
||||
const [isDeletingCollection, setIsDeletingCollection] = useState(false);
|
||||
|
||||
const filteredVideos = videos.filter(video =>
|
||||
video.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
@@ -20,6 +22,19 @@ const ManagePage = ({ videos, onDeleteVideo }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeleteCollection = (collection) => {
|
||||
setCollectionToDelete(collection);
|
||||
};
|
||||
|
||||
const handleCollectionDelete = async (deleteVideos) => {
|
||||
if (!collectionToDelete) return;
|
||||
|
||||
setIsDeletingCollection(true);
|
||||
await onDeleteCollection(collectionToDelete.id, deleteVideos);
|
||||
setIsDeletingCollection(false);
|
||||
setCollectionToDelete(null);
|
||||
};
|
||||
|
||||
const getThumbnailSrc = (video) => {
|
||||
if (video.thumbnailPath) {
|
||||
return `${BACKEND_URL}${video.thumbnailPath}`;
|
||||
@@ -30,10 +45,89 @@ const ManagePage = ({ videos, onDeleteVideo }) => {
|
||||
return (
|
||||
<div className="manage-page">
|
||||
<div className="manage-header">
|
||||
<h1>Manage Videos</h1>
|
||||
<h1>Manage Content</h1>
|
||||
<Link to="/" className="back-link">← Back to Home</Link>
|
||||
</div>
|
||||
|
||||
{/* Delete Collection Modal */}
|
||||
{collectionToDelete && (
|
||||
<div className="modal-overlay" onClick={() => !isDeletingCollection && setCollectionToDelete(null)}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<h2>Delete Collection</h2>
|
||||
<p style={{ marginBottom: '0.5rem' }}>
|
||||
You are about to delete the collection <strong>"{collectionToDelete.name}"</strong>.
|
||||
</p>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginBottom: '1.5rem' }}>
|
||||
This collection contains {collectionToDelete.videos.length} video{collectionToDelete.videos.length !== 1 ? 's' : ''}.
|
||||
</p>
|
||||
<div className="modal-actions" style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
<button
|
||||
className="modal-btn secondary-btn"
|
||||
onClick={() => handleCollectionDelete(false)}
|
||||
disabled={isDeletingCollection}
|
||||
>
|
||||
{isDeletingCollection ? 'Deleting...' : 'Delete Collection Only'}
|
||||
</button>
|
||||
<button
|
||||
className="modal-btn danger-btn"
|
||||
onClick={() => handleCollectionDelete(true)}
|
||||
disabled={isDeletingCollection}
|
||||
>
|
||||
{isDeletingCollection ? 'Deleting...' : 'Delete Collection & Videos'}
|
||||
</button>
|
||||
<button
|
||||
className="modal-btn cancel-btn"
|
||||
onClick={() => setCollectionToDelete(null)}
|
||||
disabled={isDeletingCollection}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="manage-section">
|
||||
<h2>Collections ({collections.length})</h2>
|
||||
<div className="manage-list">
|
||||
{collections.length > 0 ? (
|
||||
<table className="manage-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Videos</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{collections.map(collection => (
|
||||
<tr key={collection.id}>
|
||||
<td className="col-title">{collection.name}</td>
|
||||
<td>{collection.videos.length} videos</td>
|
||||
<td>{new Date(collection.createdAt).toLocaleDateString()}</td>
|
||||
<td className="col-actions">
|
||||
<button
|
||||
className="delete-btn-small"
|
||||
onClick={() => confirmDeleteCollection(collection)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="no-videos-found">
|
||||
No collections found.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="manage-section">
|
||||
<h2>Videos ({filteredVideos.length})</h2>
|
||||
<div className="manage-controls">
|
||||
<input
|
||||
type="text"
|
||||
@@ -42,9 +136,6 @@ const ManagePage = ({ videos, onDeleteVideo }) => {
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="manage-search"
|
||||
/>
|
||||
<div className="video-count">
|
||||
{filteredVideos.length} videos found
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="manage-list">
|
||||
@@ -90,6 +181,7 @@ const ManagePage = ({ videos, onDeleteVideo }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -300,20 +300,27 @@ const VideoPlayer = ({ videos, onDeleteVideo, collections, onAddToCollection, on
|
||||
|
||||
{/* Collection Modal */}
|
||||
{showCollectionModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
<div className="modal-overlay" onClick={handleCloseModal}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<h2>Add to Collection</h2>
|
||||
|
||||
{videoCollections.length > 0 && (
|
||||
<div className="current-collection">
|
||||
<p className="collection-note">
|
||||
This video is currently in the collection: <strong>{videoCollections[0].name}</strong>
|
||||
<div className="current-collection" style={{
|
||||
marginBottom: '1.5rem',
|
||||
padding: '1rem',
|
||||
backgroundColor: 'rgba(62, 166, 255, 0.1)',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(62, 166, 255, 0.3)'
|
||||
}}>
|
||||
<p style={{ margin: '0 0 0.5rem 0', color: 'var(--text-color)' }}>
|
||||
Currently in: <strong>{videoCollections[0].name}</strong>
|
||||
</p>
|
||||
<p className="collection-warning">
|
||||
<p style={{ margin: '0 0 1rem 0', fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
|
||||
Adding to a different collection will remove it from the current one.
|
||||
</p>
|
||||
<button
|
||||
className="remove-from-collection"
|
||||
className="modal-btn danger-btn"
|
||||
style={{ width: '100%' }}
|
||||
onClick={handleRemoveFromCollection}
|
||||
>
|
||||
Remove from Collection
|
||||
@@ -322,7 +329,7 @@ const VideoPlayer = ({ videos, onDeleteVideo, collections, onAddToCollection, on
|
||||
)}
|
||||
|
||||
{collections && collections.length > 0 && (
|
||||
<div className="existing-collections">
|
||||
<div className="existing-collections" style={{ marginBottom: '1.5rem' }}>
|
||||
<h3>Add to existing collection:</h3>
|
||||
<select
|
||||
value={selectedCollection}
|
||||
@@ -340,6 +347,8 @@ const VideoPlayer = ({ videos, onDeleteVideo, collections, onAddToCollection, on
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="modal-btn primary-btn"
|
||||
style={{ width: '100%', marginTop: '0.5rem' }}
|
||||
onClick={handleAddToExistingCollection}
|
||||
disabled={!selectedCollection}
|
||||
>
|
||||
@@ -348,15 +357,18 @@ const VideoPlayer = ({ videos, onDeleteVideo, collections, onAddToCollection, on
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="new-collection">
|
||||
<div className="new-collection" style={{ marginBottom: '1.5rem' }}>
|
||||
<h3>Create new collection:</h3>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Collection name"
|
||||
value={newCollectionName}
|
||||
onChange={(e) => setNewCollectionName(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && newCollectionName.trim() && handleCreateCollection()}
|
||||
/>
|
||||
<button
|
||||
className="modal-btn primary-btn"
|
||||
style={{ width: '100%', marginTop: '0.5rem' }}
|
||||
onClick={handleCreateCollection}
|
||||
disabled={!newCollectionName.trim()}
|
||||
>
|
||||
@@ -364,7 +376,7 @@ const VideoPlayer = ({ videos, onDeleteVideo, collections, onAddToCollection, on
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button className="close-modal" onClick={handleCloseModal}>
|
||||
<button className="modal-btn cancel-btn" style={{ width: '100%' }} onClick={handleCloseModal}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user