import { useState, useEffect, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Card } from '../../components/ui/card'; import { Button } from '../../components/ui/button'; import { Input } from '../../components/ui/input'; import { Label } from '../../components/ui/label'; import { Textarea } from '../../components/ui/textarea'; import { Switch } from '../../components/ui/switch'; import { Separator } from '../../components/ui/separator'; import { toast } from 'sonner'; import { HiOutlineArrowLeft, HiOutlineBookOpen, HiOutlineChevronRight, HiOutlinePhoto, HiOutlineTrash, } from 'react-icons/hi2'; import courseService from '@/services/courseService'; import fileService from '@/services/fileService'; const MAX_IMAGE_BYTES = 5 * 1024 * 1024; function CourseFormPage() { const { courseId } = useParams(); const navigate = useNavigate(); const isEditing = Boolean(courseId); const fileInputRef = useRef(null); const [form, setForm] = useState({ courseCode: '', title: '', slug: '', description: '', image: '', isPublished: false, }); const [imageUrl, setImageUrl] = useState(''); const [uploading, setUploading] = useState(false); const [loading, setLoading] = useState(isEditing); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!isEditing) return; const fetchCourse = async () => { try { const data = await courseService.getById(courseId); setForm({ courseCode: data.courseCode || '', title: data.title || '', slug: data.slug || '', description: data.description || '', image: data.image || '', isPublished: data.isPublished || false, }); setImageUrl(data.imageUrl || data.image || ''); } catch (err) { setError(err.message || 'Failed to load course'); } finally { setLoading(false); } }; fetchCourse(); }, [courseId, isEditing]); const handleChange = (field, value) => { setForm((prev) => ({ ...prev, [field]: value })); }; const handleImagePick = async (event) => { const file = event.target.files?.[0]; event.target.value = ''; if (!file) return; if (!file.type.startsWith('image/')) { toast.error('Only image files are allowed'); return; } if (file.size > MAX_IMAGE_BYTES) { toast.error('Image must not exceed 5MB'); return; } setUploading(true); try { const uploaded = await fileService.upload(file, 'courses'); handleChange('image', uploaded.objectKey); setImageUrl(uploaded.url); } catch { // global API interceptor already surfaced the error toast } finally { setUploading(false); } }; const handleRemoveImage = () => { handleChange('image', ''); setImageUrl(''); }; const generateSlug = () => { const slug = form.title .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/đ/g, 'd') .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .trim(); handleChange('slug', slug); }; const handleSave = async (publish) => { if (!form.courseCode.trim() || !form.title.trim()) { setError('Course code and title are required'); return; } setSaving(true); setError(null); try { const payload = { ...form, isPublished: publish }; if (isEditing) { await courseService.update(courseId, payload); } else { const created = await courseService.create(payload); navigate(`/courses/detail/${created.id}`); return; } navigate(`/courses/detail/${courseId}`); } catch (err) { setError(err.message || 'Failed to save course'); } finally { setSaving(false); } }; if (loading) { return ( <>
Loading course...
); } return ( <>
{/* Breadcrumb */}
{isEditing ? 'Edit Course' : 'New Course'}
{/* Header */}

{isEditing ? 'Edit Course' : 'Create New Course'}

{error && (
{error}
)}

Course Details

handleChange('courseCode', e.target.value)} className="rounded-xl bg-app-surface-muted border-0 focus:bg-white focus:ring-2 focus:ring-app-primary" />
handleChange('title', e.target.value)} className="rounded-xl bg-app-surface-muted border-0 focus:bg-white focus:ring-2 focus:ring-app-primary" />
/courses/ handleChange('slug', e.target.value)} className="border-0 bg-transparent font-mono focus:ring-0 focus:bg-transparent" />