- 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
387 lines
14 KiB
JavaScript
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
|
|
};
|
|
};
|