feat: Introduce course marketing features with dedicated metadata, image generation, and UI in both studio and experience apps.

This commit is contained in:
2026-03-04 15:41:34 -03:00
parent 4458decd22
commit 01c54429a0
25 changed files with 1453 additions and 401 deletions
@@ -12,6 +12,7 @@ import { cmsApi, Course } from "@/lib/api";
type TabKey =
| "outline"
| "marketing"
| "grading"
| "rubrics"
| "calendar"
@@ -73,6 +74,7 @@ export default function CourseEditorLayout({
icon: BookOpen,
tabs: [
{ key: "outline", label: "Outline", icon: Layout, href: `/courses/${id}` },
{ key: "marketing", label: "Marketing", icon: Megaphone, href: `/courses/${id}/marketing` },
{ key: "files", label: "Archivos", icon: Folder, href: `/courses/${id}/files` },
{ key: "sessions", label: "Sesiones en Vivo", icon: Video, href: `/courses/${id}/sessions` },
],
+12
View File
@@ -151,6 +151,18 @@ export default function MediaPlayer({ src, type, transcription, locked, onEnded,
);
}
if (type === "image") {
return (
<div className={`relative glass overflow-hidden border border-white/10 ${locked ? 'blur-xl grayscale' : ''}`}>
<img
src={src}
alt="Lesson Content"
className="w-full aspect-video object-contain"
/>
</div>
);
}
return (
<div className={`relative glass overflow-hidden border border-white/10 ${locked ? 'blur-xl grayscale' : ''}`}>
{type === "video" ? (
@@ -16,7 +16,7 @@ interface MediaBlockProps {
id: string;
title?: string;
url: string;
type: 'video' | 'audio';
type: 'video' | 'audio' | 'image';
config: {
maxPlays?: number;
currentPlays?: number;
@@ -44,7 +44,9 @@ interface MediaBlockProps {
export default function MediaBlock({ title, url, type, config, editMode, onChange, transcription, isGraded }: MediaBlockProps) {
const [localPlays, setLocalPlays] = useState(config.currentPlays || 0);
const [sourceType, setSourceType] = useState<"url" | "upload">(url.startsWith("/assets/") ? "upload" : "url");
const [sourceType, setSourceType] = useState<"url" | "upload">(
(url.startsWith("/assets/") || url.includes("/assets/")) ? "upload" : "url"
);
const maxPlays = config.maxPlays || 0;
const isLocked = maxPlays > 0 && localPlays >= maxPlays;