courseworx/frontend/src/hooks/useContentManagement.js
mmabdalla c06600f263 v2.0.1 - CRITICAL FIX: Video Upload Bug - Content Creation File Upload Issue
- Fixed parameter shadowing in useContentManagement.js handleAddContent function
- Changed selectedFile parameter to selectedFileParam to avoid state variable shadowing
- Added fallback logic: fileToUpload = selectedFileParam || selectedFile
- Updated all upload logic references to use fileToUpload instead of selectedFile
- Enhanced debugging with useEffect tracking and stack traces
- Fixed React error in LessonDetail.js with null checks for nextSibling
- Fixed media authentication by adding token to query parameters in imageUtils.js
- Updated dependency arrays for proper state management
- Resolved video upload issue during initial content creation

Files modified:
- frontend/src/hooks/useContentManagement.js
- frontend/src/hooks/useFileUpload.js
- frontend/src/pages/CourseContentViewer.js
- frontend/src/pages/LessonDetail.js
- frontend/src/utils/imageUtils.js
- backend/routes/courseContent.js
- version.txt
2025-09-14 04:12:23 +03:00

387 lines
14 KiB
JavaScript

import { useState, useCallback, useEffect } from 'react';
import { useMutation, useQueryClient } from 'react-query';
import { courseContentAPI } from '../services/api';
import toast from 'react-hot-toast';
export const useContentManagement = (courseId) => {
const queryClient = useQueryClient();
// Content form state
const [contentForm, setContentForm] = useState({
title: '',
description: '',
type: 'document',
order: 0,
points: 0,
isRequired: true,
isPublished: true,
articleContent: '',
url: '',
sectionId: null
});
const [editingContent, setEditingContent] = useState(null);
const [selectedFile, setSelectedFile] = useState(null);
// Debug selectedFile changes
useEffect(() => {
console.log('📁 useContentManagement: selectedFile changed:', selectedFile?.name || 'null');
// Log the stack trace to see what's causing the reset
if (selectedFile === null) {
console.trace('🔍 selectedFile reset to null - stack trace:');
}
}, [selectedFile]);
// Mutations
const createContentMutation = useMutation(
(data) => {
console.log('🔄 createContentMutation called with data:', data);
return courseContentAPI.create(courseId, data);
},
{
onSuccess: (response) => {
console.log('✅ Content created successfully:', response);
queryClient.invalidateQueries(['course-content', courseId]);
queryClient.invalidateQueries(['course-sections', courseId]);
resetContentForm();
toast.success('Content added successfully!');
},
onError: (error) => {
console.error('❌ Content creation failed:', error);
console.error('❌ Error response:', error.response);
console.error('❌ Error data:', error.response?.data);
// Show validation errors if available
if (error.response?.data?.errors && Array.isArray(error.response.data.errors)) {
const validationErrors = error.response.data.errors;
console.error('❌ Validation errors:', validationErrors);
// Show first validation error to user
const firstError = validationErrors[0];
toast.error(`Validation failed: ${firstError.msg || firstError.error || 'Invalid data'}`);
} else {
toast.error(error.response?.data?.error || 'Failed to add content');
}
},
}
);
const updateContentMutation = useMutation(
({ contentId, data }) => courseContentAPI.update(courseId, contentId, data),
{
onSuccess: () => {
queryClient.invalidateQueries(['course-content', courseId]);
queryClient.invalidateQueries(['course-sections', courseId]);
setEditingContent(null);
resetContentForm();
toast.success('Content updated successfully!');
},
onError: (error) => {
toast.error(error.response?.data?.error || 'Failed to update content');
},
}
);
const deleteContentMutation = useMutation(
(contentId) => courseContentAPI.delete(courseId, contentId),
{
onSuccess: () => {
queryClient.invalidateQueries(['course-content', courseId]);
queryClient.invalidateQueries(['course-sections', courseId]);
toast.success('Content deleted successfully!');
},
onError: (error) => {
toast.error(error.response?.data?.error || 'Failed to delete content');
},
}
);
const addQuizQuestionsMutation = useMutation(
({ contentId, questions }) => courseContentAPI.addQuizQuestions(courseId, contentId, questions),
{
onSuccess: () => {
queryClient.invalidateQueries(['course-content', courseId]);
toast.success('Quiz questions added successfully!');
},
onError: (error) => {
toast.error(error.response?.data?.error || 'Failed to add quiz questions');
},
}
);
// Handlers
const handleContentFormChange = useCallback((e) => {
const { name, value, type, checked } = e.target;
setContentForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
}, []);
const resetContentForm = useCallback(() => {
setContentForm({
title: '',
description: '',
type: 'document',
order: 0,
points: 0,
isRequired: true,
isPublished: true,
articleContent: '',
url: '',
sectionId: null
});
// Don't reset selectedFile here - it should persist until content is created
console.log('🔄 resetContentForm called - selectedFile preserved:', selectedFile?.name || 'null');
}, [selectedFile]);
const handleAddContent = useCallback(async (e, selectedFileParam = null, uploadFileMutation = null) => {
e.preventDefault();
console.log('🚀 handleAddContent called');
console.log('📋 Form data:', contentForm);
console.log('📁 Selected file parameter:', selectedFileParam);
console.log('📁 Selected file from state:', selectedFile);
console.log('📁 Upload mutation available:', !!uploadFileMutation);
console.log('📁 Content type:', contentForm.type);
console.log('📁 File upload required:', ['document', 'image', 'video'].includes(contentForm.type));
// Use parameter if provided, otherwise use state
const fileToUpload = selectedFileParam || selectedFile;
console.log('📁 File to upload (final decision):', fileToUpload?.name || 'null');
let data = { ...contentForm };
// Clean up sectionId - convert empty string to null
if (data.sectionId === '') {
data.sectionId = null;
}
// Transform articleContent to content field for articles
if (contentForm.type === 'article' && contentForm.articleContent) {
data.content = contentForm.articleContent;
delete data.articleContent; // Remove the form field
}
// If image/video and url is provided, set fileUrl
if ((contentForm.type === 'image' || contentForm.type === 'video') && contentForm.url) {
data.fileUrl = contentForm.url;
}
console.log('📤 Sending data to API:', data);
console.log('🔗 Course ID:', courseId);
console.log('📏 Title length:', data.title?.length || 0);
console.log('🔍 Data validation check:', {
title: data.title,
type: data.type,
order: typeof data.order,
points: typeof data.points,
sectionId: data.sectionId
});
try {
return new Promise((resolve, reject) => {
createContentMutation.mutate(data, {
onSuccess: async (response) => {
console.log('✅ Content created successfully:', response);
// Extract content ID from response (handle different response structures)
console.log('🔍 Response structure debug:', {
response: response,
responseContent: response.content,
responseId: response.id,
responseData: response.data,
responseDataContent: response.data?.content,
responseDataId: response.data?.id
});
const contentId = response.content?.id || response.id || response.data?.id || response.data?.content?.id;
if (!contentId) {
console.error('❌ No content ID found in response:', response);
console.error('❌ Response structure:', JSON.stringify(response, null, 2));
reject(new Error('Server response format error: No content ID found'));
return;
}
// Invalidate queries to refresh the page data
console.log('🔄 Invalidating queries to refresh page data...');
queryClient.invalidateQueries(['course-content', courseId]);
queryClient.invalidateQueries(['course-sections', courseId]);
queryClient.invalidateQueries(['courses', courseId]);
// If there's a file to upload and we have the upload mutation
console.log('🔍 File upload check:', {
hasSelectedFile: !!fileToUpload,
hasUploadMutation: !!uploadFileMutation,
contentType: contentForm.type,
requiresFileUpload: ['document', 'image', 'video'].includes(contentForm.type),
willUploadFile: !!(fileToUpload && uploadFileMutation && ['document', 'image', 'video'].includes(contentForm.type))
});
if (fileToUpload && uploadFileMutation && ['document', 'image', 'video'].includes(contentForm.type)) {
console.log('📤 Uploading file for content:', {
contentId,
fileName: fileToUpload.name,
fileType: fileToUpload.type,
contentType: contentForm.type
});
try {
// Upload file with the newly created content ID
const uploadResponse = await new Promise((uploadResolve, uploadReject) => {
uploadFileMutation.mutate({
file: fileToUpload,
contentType: contentForm.type,
contentId: contentId
}, {
onSuccess: (uploadData) => {
console.log('✅ File uploaded successfully:', uploadData);
uploadResolve(uploadData);
},
onError: (uploadError) => {
console.error('❌ File upload failed:', uploadError);
uploadReject(uploadError);
}
});
});
console.log('✅ Content creation and file upload completed');
// Reset selectedFile after successful upload
setSelectedFile(null);
resolve({ ...response, uploadData: uploadResponse });
} catch (uploadError) {
console.error('❌ File upload failed, but content was created:', uploadError);
// Still resolve with content creation success, but log the upload failure
resolve(response);
}
} else {
console.log('✅ Content created without file upload');
resolve(response);
}
},
onError: (error) => {
console.error('❌ Error in handleAddContent:', error);
reject(error);
}
});
});
} catch (error) {
console.error('❌ Error in handleAddContent:', error);
throw error;
}
}, [contentForm, courseId, createContentMutation, queryClient, setSelectedFile, selectedFile]);
const handleEditContent = useCallback(async (e, selectedFile = null, uploadFileMutation = null) => {
e.preventDefault();
let data = { ...contentForm };
// Transform articleContent to content field for articles
if (contentForm.type === 'article' && contentForm.articleContent) {
data.content = contentForm.articleContent;
}
// Always remove articleContent from the data sent to backend
// since it's only used in the frontend form
delete data.articleContent;
try {
return new Promise((resolve, reject) => {
updateContentMutation.mutate({
contentId: editingContent.id,
data: data
}, {
onSuccess: async (response) => {
console.log('✅ Content updated successfully:', response);
// If there's a selected file and upload mutation, upload the file after updating content
if (selectedFile && uploadFileMutation && ['document', 'image', 'video'].includes(contentForm.type)) {
console.log('📤 Uploading file for updated content:', {
file: selectedFile.name,
contentType: contentForm.type,
contentId: editingContent.id
});
try {
const uploadResponse = await new Promise((uploadResolve, uploadReject) => {
uploadFileMutation.mutate({
file: selectedFile,
contentType: contentForm.type,
contentId: editingContent.id
}, {
onSuccess: (uploadData) => {
console.log('✅ File uploaded successfully:', uploadData);
uploadResolve(uploadData);
},
onError: (uploadError) => {
console.error('❌ File upload failed:', uploadError);
uploadReject(uploadError);
}
});
});
resolve({ ...response, uploadData: uploadResponse });
} catch (uploadError) {
console.error('❌ File upload failed, but content update succeeded:', uploadError);
resolve(response); // Still resolve content update, log upload failure
}
} else {
resolve(response);
}
},
onError: (error) => {
console.error('❌ Error in handleEditContent:', error);
reject(error);
}
});
});
} catch (error) {
console.error('❌ Error in handleEditContent:', error);
throw error;
}
}, [editingContent, contentForm, updateContentMutation]);
const handleDeleteContent = useCallback((contentId) => {
if (window.confirm('Are you sure you want to delete this content?')) {
deleteContentMutation.mutate(contentId);
}
}, [deleteContentMutation]);
const handleEditContentClick = useCallback((content) => {
setEditingContent(content);
setContentForm({
title: content.title,
description: content.description,
type: content.type,
order: content.order,
points: content.points,
isRequired: content.isRequired,
isPublished: content.isPublished,
articleContent: content.content || content.articleContent || '',
url: content.fileUrl || '',
sectionId: content.sectionId
});
}, []);
return {
// State
contentForm,
setContentForm,
editingContent,
setEditingContent,
selectedFile,
setSelectedFile,
// Mutations
createContentMutation,
updateContentMutation,
deleteContentMutation,
addQuizQuestionsMutation,
// Handlers
handleContentFormChange,
resetContentForm,
handleAddContent,
handleEditContent,
handleDeleteContent,
handleEditContentClick
};
};