feat: Add i18n support, new content block types, course export, and lesson interaction tracking.

This commit is contained in:
2026-01-17 02:19:39 -03:00
parent b166387a48
commit 05faa20993
50 changed files with 3368 additions and 388 deletions
+154 -12
View File
@@ -4,11 +4,13 @@ import { useEffect, useState } from "react";
import { cmsApi, Course, Organization } from "@/lib/api";
import Link from "next/link";
import { useAuth } from "@/context/AuthContext";
import { Plus, BookOpen } from "lucide-react";
import { useTranslation } from "@/context/I18nContext";
import { Plus, BookOpen, Download, Upload, Sparkles, Wand2 } from "lucide-react";
import OrganizationSelector from "@/components/OrganizationSelector";
import Modal from "@/components/Modal";
export default function StudioDashboard() {
const { t } = useTranslation();
const [courses, setCourses] = useState<Course[]>([]);
const [loading, setLoading] = useState(true);
const { user } = useAuth();
@@ -34,7 +36,10 @@ export default function StudioDashboard() {
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [isOrgModalOpen, setIsOrgModalOpen] = useState(false);
const [isTitleModalOpen, setIsTitleModalOpen] = useState(false);
const [isAIModalOpen, setIsAIModalOpen] = useState(false);
const [newCourseTitle, setNewCourseTitle] = useState("");
const [aiPrompt, setAiPrompt] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
useEffect(() => {
const loadOrgs = async () => {
@@ -78,6 +83,63 @@ export default function StudioDashboard() {
}
};
const handleAIGenerate = async (e: React.FormEvent) => {
e.preventDefault();
if (!aiPrompt) return;
setIsGenerating(true);
try {
const newCourse = await cmsApi.generateCourse(aiPrompt);
setCourses((prev: Course[]) => [...prev, newCourse]);
setAiPrompt("");
setIsAIModalOpen(false);
alert("Course generated successfully with AI!");
} catch (err) {
console.error("AI generation failed", err);
alert("Failed to generate course with AI. Check backend logs and AI provider status.");
} finally {
setIsGenerating(false);
}
};
const handleExport = async (e: React.MouseEvent, courseId: string, title: string) => {
e.preventDefault();
e.stopPropagation();
try {
const data = await cmsApi.exportCourse(courseId);
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `course_${title.replace(/\s+/g, '_')}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (err) {
console.error("Export failed", err);
alert("Failed to export course");
}
};
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (event) => {
try {
const json = JSON.parse(event.target?.result as string);
const newCourse = await cmsApi.importCourse(json);
setCourses((prev: Course[]) => [...prev, newCourse]);
alert("Course imported successfully!");
} catch (err) {
console.error("Import failed", err);
alert("Failed to import course. Ensure the file is a valid OpenCCB course export.");
}
};
reader.readAsText(file);
};
return (
<div className="min-h-screen bg-[#0f1115] text-white p-8">
<div className="max-w-7xl mx-auto">
@@ -85,17 +147,31 @@ export default function StudioDashboard() {
<div className="flex justify-between items-center mb-12">
<div>
<h1 className="text-4xl font-black bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent tracking-tight">
My Courses
{t('nav.courses')}
</h1>
<p className="text-gray-400 mt-2">Manage and monitor your educational content</p>
<p className="text-gray-400 mt-2">{t('dashboard.title')}</p>
</div>
<div className="flex items-center gap-3">
<label className="flex items-center gap-2 px-6 py-3 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl font-bold transition-all cursor-pointer active:scale-95">
<Upload size={18} />
Import
<input type="file" accept=".json" onChange={handleImport} className="hidden" />
</label>
<button
onClick={() => setIsAIModalOpen(true)}
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 rounded-xl font-bold shadow-lg shadow-indigo-500/20 transition-all active:scale-95 group"
>
<Sparkles size={18} className="text-amber-300 group-hover:rotate-12 transition-transform" />
AI Wizard
</button>
<button
onClick={handleCreateCourse}
className="flex items-center gap-2 px-6 py-3 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl font-bold transition-all active:scale-95"
>
<Plus size={20} />
Manual
</button>
</div>
<button
onClick={handleCreateCourse}
className="flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-xl font-bold shadow-lg shadow-blue-500/20 transition-all active:scale-95"
>
<Plus size={20} />
New Course
</button>
</div>
{loading ? (
@@ -114,8 +190,17 @@ export default function StudioDashboard() {
<Link href={`/courses/${course.id}`} key={course.id}>
<div className="glass-card h-full flex flex-col group hover:border-blue-500/50 transition-all">
<div className="flex-1">
<div className="w-12 h-12 rounded-xl bg-blue-500/10 flex items-center justify-center mb-4">
<BookOpen className="text-blue-400" />
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 rounded-xl bg-blue-500/10 flex items-center justify-center">
<BookOpen className="text-blue-400" />
</div>
<button
onClick={(e) => handleExport(e, course.id, course.title)}
className="p-2 hover:bg-white/10 rounded-lg text-gray-500 hover:text-white transition-all"
title="Export Course"
>
<Download size={18} />
</button>
</div>
<h3 className="font-bold text-lg mb-2 group-hover:text-blue-400 transition-colors">{course.title}</h3>
<p className="text-sm text-gray-400 line-clamp-2">{course.description || "No description provided."}</p>
@@ -170,6 +255,63 @@ export default function StudioDashboard() {
</form>
</Modal>
{/* AI Wizard Modal */}
<Modal
isOpen={isAIModalOpen}
onClose={() => !isGenerating && setIsAIModalOpen(false)}
title="AI Course Wizard"
>
<form onSubmit={handleAIGenerate} className="space-y-6">
<div className="p-4 rounded-xl bg-indigo-500/5 border border-indigo-500/10 mb-2">
<p className="text-xs text-indigo-300 leading-relaxed font-medium">
Describe the course topic and target audience. Our AI will structure the modules and lessons for you in seconds.
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-2">
Course Topic or Description
</label>
<textarea
autoFocus
required
rows={4}
value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)}
placeholder="e.g. A comprehensive guide to building distributed systems with Rust and Axum for intermediate developers."
className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-purple-500/50 transition-all text-white resize-none"
disabled={isGenerating}
/>
</div>
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={() => setIsAIModalOpen(false)}
disabled={isGenerating}
className="flex-1 px-4 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg transition-all text-sm font-medium"
>
Cancel
</button>
<button
type="submit"
disabled={isGenerating}
className="flex-[2] px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg transition-all shadow-lg shadow-indigo-500/20 font-bold text-sm flex items-center justify-center gap-2"
>
{isGenerating ? (
<>
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
Generating...
</>
) : (
<>
<Wand2 size={16} />
Magic Create
</>
)}
</button>
</div>
</form>
</Modal>
{/* Organization Selector Modal */}
<OrganizationSelector
isOpen={isOrgModalOpen}