Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ node_modules/
dist/
build/
coverage/node_modules/
.env.local
package-lock.json
1 change: 1 addition & 0 deletions src/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ jest-coverage/

# Misc
*.log
package-lock.json
122 changes: 122 additions & 0 deletions src/app/admin/cms/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
'use client';

import React, { useState, useEffect } from 'react';
import { CourseStructureBuilder } from '@/components/cms/CourseStructureBuilder';
import { ContentEditor } from '@/components/cms/ContentEditor';
import { VersionControl } from '@/components/cms/VersionControl';
import { MediaManager } from '@/components/cms/MediaManager';
import { ContentTemplates } from '@/components/cms/ContentTemplates';
import { useCMS } from '@/hooks/useCMS';
import { Save, Eye, Settings, Share2, AlertTriangle } from 'lucide-react';
import { Toaster } from 'react-hot-toast';

export default function CMSDashboard() {
const { course, setCourse } = useCMS();
const [activeLesson, setActiveLesson] = useState<{ moduleId: string; lessonId: string } | null>(
null,
);

// Initialize with a dummy course if empty
useEffect(() => {
if (course.title === '') {
setCourse({
id: 'course-1',
title: 'Mastering Advanced Web Development',
description: 'A comprehensive guide to modern web technologies.',
modules: [
{
id: 'm1',
title: 'Getting Started',
order: 0,
lessons: [
{
id: 'l1',
title: 'Introduction',
type: 'video',
content: '<h1>Welcome!</h1>',
order: 0,
},
{
id: 'l2',
title: 'Prerequisites',
type: 'article',
content: '<p>You need node.js</p>',
order: 1,
},
],
},
],
});
}
}, [course.title, setCourse]);

return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 p-4 lg:p-8">
<Toaster position="top-right" />

{/* Top Header */}
<header className="mb-8 flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<nav className="flex items-center gap-2 text-xs text-gray-500 mb-1">
<span>Admin</span>
<span>/</span>
<span className="text-blue-500">CMS</span>
</nav>
<h1 className="text-3xl font-bold tracking-tight">Advanced CMS Dashboard</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">
Manage your course content, media, and versions in one place.
</p>
</div>

<div className="flex items-center gap-3">
<button className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-all font-medium text-sm">
<Eye className="w-4 h-4" />
Preview
</button>
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-all font-bold text-sm shadow-lg shadow-blue-500/20">
<Save className="w-4 h-4" />
Publish Changes
</button>
</div>
</header>

{/* Main Layout Grid */}
<div className="grid grid-cols-12 gap-6 h-[calc(100vh-200px)]">
{/* Left Column: Structure Builder */}
<div className="col-span-12 lg:col-span-3 space-y-6 overflow-hidden flex flex-col">
<div className="flex-1 overflow-hidden">
<CourseStructureBuilder />
</div>
<div className="h-1/3 overflow-hidden">
<ContentTemplates />
</div>
</div>

{/* Center Column: Editor */}
<div className="col-span-12 lg:col-span-6 flex flex-col overflow-hidden bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700">
{course.modules.length > 0 && course.modules[0].lessons.length > 0 ? (
<ContentEditor
moduleId={activeLesson?.moduleId || course.modules[0].id}
lessonId={activeLesson?.lessonId || course.modules[0].lessons[0].id}
/>
) : (
<div className="flex flex-col items-center justify-center h-full space-y-4">
<AlertTriangle className="w-12 h-12 text-yellow-500" />
<p className="text-gray-500">Create a module and lesson to start editing.</p>
</div>
)}
</div>

{/* Right Column: Sidebar (Version Control & Media) */}
<div className="col-span-12 lg:col-span-3 flex flex-col gap-6 overflow-hidden">
<div className="flex-1 overflow-hidden">
<VersionControl />
</div>
<div className="flex-1 overflow-hidden">
<MediaManager />
</div>
</div>
</div>
</div>
);
}
54 changes: 54 additions & 0 deletions src/components/cms/ContentEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client';

import React from 'react';
import { RichContentEditor } from '../editor/RichContentEditor';
import { useCMS } from '@/hooks/useCMS';

interface ContentEditorProps {
moduleId: string;
lessonId: string;
}

/**
* Advanced Content Editor for the CMS.
* Wraps the RichContentEditor and integrates it with the CMS store for auto-saving.
*/
export const ContentEditor: React.FC<ContentEditorProps> = ({ moduleId, lessonId }) => {
const { course, updateLessonContent } = useCMS();

// Find the lesson in the course structure
const currentModule = course.modules.find((m) => m.id === moduleId);
const lesson = currentModule?.lessons.find((l) => l.id === lessonId);

const handleUpdate = (content: string) => {
updateLessonContent(moduleId, lessonId, content);
};

if (!lesson) {
return (
<div className="flex items-center justify-center h-full text-gray-500">
Select a lesson to start editing.
</div>
);
}

return (
<div className="flex flex-col h-full space-y-4">
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-800 dark:text-white">
Editing: <span className="text-blue-500">{lesson.title}</span>
</h2>
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-400">
Last saved: {new Date().toLocaleTimeString()}
</span>
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
</div>
</div>

<div className="flex-1 overflow-hidden">
<RichContentEditor initialContent={lesson.content} onUpdate={handleUpdate} />
</div>
</div>
);
};
92 changes: 92 additions & 0 deletions src/components/cms/ContentTemplates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'use client';

import React from 'react';
import { useCMS } from '@/hooks/useCMS';
import { Layout, Video, FileText, HelpCircle, ArrowRight } from 'lucide-react';

const DUMMY_TEMPLATES = [
{
id: 't1',
name: 'Video Tutorial',
description: 'Perfect for video-first lessons with supporting notes.',
icon: Video,
color: 'bg-red-100 text-red-600',
},
{
id: 't2',
name: 'In-depth Article',
description: 'Long-form content with images and pull quotes.',
icon: FileText,
color: 'bg-blue-100 text-blue-600',
},
{
id: 't3',
name: 'Interactive Quiz',
description: 'Engage students with multiple choice and essay questions.',
icon: HelpCircle,
color: 'bg-green-100 text-green-600',
},
{
id: 't4',
name: 'Mixed Content',
description: 'A combination of text, media, and quick checks.',
icon: Layout,
color: 'bg-purple-100 text-purple-600',
},
];

export const ContentTemplates: React.FC = () => {
const { templates } = useCMS();

return (
<div className="flex flex-col h-full bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center gap-2 bg-gray-50 dark:bg-gray-900">
<Layout className="w-5 h-5 text-purple-500" />
<h2 className="text-xl font-bold text-gray-800 dark:text-white">Content Templates</h2>
</div>

<div className="flex-1 overflow-y-auto p-4">
<div className="grid grid-cols-1 gap-4">
{DUMMY_TEMPLATES.map((template) => (
<div
key={template.id}
className="group p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:border-purple-400 dark:hover:border-purple-600 hover:shadow-md transition-all cursor-pointer"
>
<div className="flex items-start gap-4">
<div className={`p-3 rounded-lg ${template.color}`}>
<template.icon className="w-6 h-6" />
</div>
<div className="flex-1">
<h3 className="font-bold text-gray-800 dark:text-white group-hover:text-purple-600 transition-colors">
{template.name}
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{template.description}
</p>
</div>
<div className="self-center opacity-0 group-hover:opacity-100 transition-opacity">
<ArrowRight className="w-4 h-4 text-purple-500" />
</div>
</div>

<div className="mt-4 flex gap-2">
<span className="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-[10px] text-gray-500">
Reusable
</span>
<span className="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-[10px] text-gray-500">
Multimodal
</span>
</div>
</div>
))}
</div>
</div>

<div className="p-4 bg-purple-50 dark:bg-purple-900/10 border-t border-purple-100 dark:border-purple-900/30">
<p className="text-xs text-purple-700 dark:text-purple-300 text-center">
Drag and drop templates directly into your course structure or editor.
</p>
</div>
</div>
);
};
114 changes: 114 additions & 0 deletions src/components/cms/CourseStructureBuilder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use client';

import React, { useState } from 'react';
import { useCMS } from '@/hooks/useCMS';
import { Plus, GripVertical, ChevronDown, ChevronRight, Edit2, Trash2 } from 'lucide-react';

export const CourseStructureBuilder: React.FC = () => {
const { course, addModule, addLesson, updateCourse } = useCMS();
const [expandedModules, setExpandedModules] = useState<Record<string, boolean>>({});

const toggleModule = (id: string) => {
setExpandedModules((prev) => ({ ...prev, [id]: !prev[id] }));
};

const handleAddModule = () => {
addModule();
};

return (
<div className="flex flex-col h-full bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between bg-gray-50 dark:bg-gray-900">
<h2 className="text-xl font-bold text-gray-800 dark:text-white">Course Structure</h2>
<button
onClick={handleAddModule}
className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm font-medium"
>
<Plus className="w-4 h-4" />
Add Module
</button>
</div>

<div className="flex-1 overflow-y-auto p-4 space-y-4">
{course.modules.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-xl text-gray-500">
<p>No modules added yet.</p>
<button onClick={handleAddModule} className="mt-2 text-blue-500 hover:underline">
Click here to add your first module
</button>
</div>
) : (
course.modules
.sort((a, b) => a.order - b.order)
.map((module) => (
<div
key={module.id}
className="border border-gray-200 dark:border-gray-700 rounded-xl overflow-hidden shadow-sm"
>
{/* Module Header */}
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-900/50 group">
<button onClick={() => toggleModule(module.id)}>
{expandedModules[module.id] ? (
<ChevronDown className="w-5 h-5 text-gray-500" />
) : (
<ChevronRight className="w-5 h-5 text-gray-500" />
)}
</button>
<GripVertical className="w-4 h-4 text-gray-400 cursor-grab active:cursor-grabbing" />
<input
value={module.title}
onChange={(e) => {
const updatedModules = course.modules.map((m) =>
m.id === module.id ? { ...m, title: e.target.value } : m,
);
updateCourse({ modules: updatedModules });
}}
className="flex-1 bg-transparent border-none focus:ring-0 font-semibold text-gray-800 dark:text-white p-0"
/>
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded">
<Trash2 className="w-4 h-4 text-red-500" />
</button>
</div>
</div>

{/* Lessons List */}
{expandedModules[module.id] && (
<div className="p-3 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 space-y-2">
{module.lessons
.sort((a, b) => a.order - b.order)
.map((lesson) => (
<div
key={lesson.id}
className="flex items-center gap-3 p-2 rounded-lg bg-gray-50 dark:bg-gray-900/30 border border-transparent hover:border-blue-200 dark:hover:border-blue-800 group"
>
<GripVertical className="w-4 h-4 text-gray-400 cursor-grab active:cursor-grabbing" />
<div className="flex-1">
<div className="font-medium text-gray-700 dark:text-gray-200 text-sm">
{lesson.title}
</div>
<div className="text-[10px] text-gray-500 uppercase tracking-wider">
{lesson.type}
</div>
</div>
<button className="opacity-0 group-hover:opacity-100 p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-opacity">
<Edit2 className="w-4 h-4 text-gray-500" />
</button>
</div>
))}
<button
onClick={() => addLesson(module.id)}
className="w-full py-2 flex items-center justify-center gap-2 border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-lg text-gray-500 hover:border-blue-400 hover:text-blue-500 transition-all text-xs font-medium mt-2"
>
<Plus className="w-3 h-3" />
Add Lesson
</button>
</div>
)}
</div>
))
)}
</div>
</div>
);
};
Loading
Loading