Files
aplp.frontend.lms/src/pages/Course/CourseFormPage.jsx
T

268 lines
9.4 KiB
React

import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import PageHeader from '../../components/PageHeader';
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 {
HiOutlineArrowLeft,
HiOutlineBookOpen,
HiOutlineChevronRight,
} from 'react-icons/hi2';
import courseService from '@/services/courseService';
function CourseFormPage() {
const { courseId } = useParams();
const navigate = useNavigate();
const isEditing = Boolean(courseId);
const [form, setForm] = useState({
courseCode: '',
title: '',
slug: '',
description: '',
image: '',
isPublished: 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,
});
} 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 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/${created.id}`);
return;
}
navigate(`/courses/${courseId}`);
} catch (err) {
setError(err.message || 'Failed to save course');
} finally {
setSaving(false);
}
};
if (loading) {
return (
<>
<PageHeader />
<div className="flex items-center justify-center py-20 text-app-text-muted">Loading course...</div>
</>
);
}
return (
<>
<PageHeader />
<div className="max-w-4xl mx-auto px-6 py-8">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-app-text-muted mb-4">
<button
onClick={() => navigate('/courses')}
className="hover:text-app-primary transition-colors flex items-center gap-1"
>
<HiOutlineBookOpen className="w-4 h-4" />
Courses
</button>
<HiOutlineChevronRight className="w-3.5 h-3.5" />
<span className="font-medium text-app-text">
{isEditing ? 'Edit Course' : 'New Course'}
</span>
</div>
{/* Header */}
<div className="flex flex-row items-center justify-between mb-6">
<h2 className="text-xl font-bold text-app-text">
{isEditing ? 'Edit Course' : 'Create New Course'}
</h2>
<div className="flex items-center gap-2">
<Button
variant="outline"
className="rounded-xl bg-app-surface-muted text-app-text hover:bg-app-interactive-hover"
onClick={() => navigate(isEditing ? `/courses/${courseId}` : '/courses')}
disabled={saving}
>
<HiOutlineArrowLeft className="w-4 h-4 mr-2" />
Cancel
</Button>
<Button
variant="outline"
className="rounded-xl bg-app-surface-muted text-app-text hover:bg-app-interactive-hover"
onClick={() => handleSave(false)}
disabled={saving}
>
{saving ? 'Saving...' : 'Save Draft'}
</Button>
<Button
className="rounded-xl bg-app-primary text-white hover:bg-app-primary-dark"
onClick={() => handleSave(true)}
disabled={saving}
>
{saving ? 'Saving...' : isEditing ? 'Update & Publish' : 'Save & Publish'}
</Button>
</div>
</div>
<Separator className="mb-6" />
{error && (
<div className="mb-4 p-3 rounded-xl bg-red-50 border border-red-200 text-red-700 text-sm">
{error}
</div>
)}
<Card
className="rounded-xl border border-app-border bg-white overflow-hidden"
style={{ boxShadow: 'rgba(0,0,0,0.03) 0px 4px 24px' }}
>
<div className="p-5 border-b border-app-border">
<h4 className="font-semibold text-app-text">Course Details</h4>
</div>
<div className="p-5 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="mb-1.5 block text-app-text text-xs font-semibold uppercase tracking-wider">
Course Code <span className="text-red-500">*</span>
</Label>
<Input
placeholder="e.g. LDR-101"
value={form.courseCode}
onChange={(e) => handleChange('courseCode', e.target.value)}
className="rounded-xl bg-app-surface-muted border-0 focus:bg-white focus:ring-2 focus:ring-app-primary"
/>
</div>
<div>
<Label className="mb-1.5 block text-app-text text-xs font-semibold uppercase tracking-wider">
Title <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Course title"
value={form.title}
onChange={(e) => handleChange('title', e.target.value)}
className="rounded-xl bg-app-surface-muted border-0 focus:bg-white focus:ring-2 focus:ring-app-primary"
/>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-1.5">
<Label className="block text-app-text text-xs font-semibold uppercase tracking-wider">
Slug
</Label>
<button
type="button"
onClick={generateSlug}
className="text-app-primary hover:underline text-xs font-medium"
>
Auto-generate from title
</button>
</div>
<div className="flex rounded-xl overflow-hidden bg-app-surface-muted">
<span className="px-4 py-2 text-app-text-muted font-mono text-sm bg-app-border/30 flex items-center select-none">
/courses/
</span>
<Input
value={form.slug}
onChange={(e) => handleChange('slug', e.target.value)}
className="border-0 bg-transparent font-mono focus:ring-0 focus:bg-transparent"
/>
</div>
</div>
<div>
<Label className="mb-1.5 block text-app-text text-xs font-semibold uppercase tracking-wider">
Description
</Label>
<Textarea
placeholder="Course description..."
rows={4}
value={form.description}
onChange={(e) => handleChange('description', e.target.value)}
className="rounded-xl bg-app-surface-muted border-0 focus:bg-white focus:ring-2 focus:ring-app-primary resize-none"
/>
</div>
<div>
<Label className="mb-1.5 block text-app-text text-xs font-semibold uppercase tracking-wider">
Image URL
</Label>
<Input
placeholder="https://..."
value={form.image}
onChange={(e) => handleChange('image', e.target.value)}
className="rounded-xl bg-app-surface-muted border-0 focus:bg-white focus:ring-2 focus:ring-app-primary"
/>
</div>
<div className="flex items-center justify-between p-4 rounded-xl bg-app-surface-muted">
<div className="flex flex-col">
<span className="font-medium text-sm text-app-text">Published</span>
<span className="text-xs text-app-text-muted">Make this course visible to students</span>
</div>
<Switch
checked={form.isPublished}
onCheckedChange={(checked) => handleChange('isPublished', checked)}
/>
</div>
</div>
</Card>
</div>
</>
);
}
export default CourseFormPage;