From 684f929f28e0ceb93f9254ecdb3aa80a227cf5bc Mon Sep 17 00:00:00 2001 From: MMrp89 Date: Thu, 27 Aug 2026 12:34:12 -0300 Subject: [PATCH] feat: implement core course platform UI and logic - Add project configuration and git ignore rules - Implement App router with protected routes - Develop course player, grid, and card components - Add state management for course progress and navigation - Initialize database services for course and comment data --- .gitignore | 24 + App.tsx | 123 ++++ README.md | 11 - components/CourseCard.tsx | 104 ++++ components/CourseGrid.tsx | 228 ++++++++ components/CoursePlayerModal.tsx | 571 +++++++++++++++++++ components/CourseRow.tsx | 176 ++++++ components/Hero.tsx | 75 +++ components/LoginModal.tsx | 166 ++++++ components/Logo.tsx | 88 +++ components/MaterialCard.tsx | 194 +++++++ components/MaterialsHubModal.tsx | 218 +++++++ components/Navbar.tsx | 143 +++++ components/SearchModal.tsx | 170 ++++++ constants.ts | 561 ++++++++++++++++++ context/AuthContext.tsx | 69 +++ context/ToastContext.tsx | 124 ++++ index.css | 84 +++ index.html | 89 +++ index.tsx | 15 + metadata.json | 8 + package.json | 25 + pages/ManageCourses.tsx | 905 +++++++++++++++++++++++++++++ pages/MaterialsPage.tsx | 948 +++++++++++++++++++++++++++++++ public/Compor Hub-negativo.svg | 30 + public/Compor Hub.svg | 30 + public/favicon.svg | 12 + public/logo-dark-bg.svg | 19 + public/logo-light-bg.svg | 19 + services/ai.ts | 33 ++ services/db.ts | 81 +++ tsconfig.json | 29 + types.ts | 66 +++ vite.config.ts | 23 + 34 files changed, 5450 insertions(+), 11 deletions(-) create mode 100644 .gitignore create mode 100644 App.tsx delete mode 100644 README.md create mode 100644 components/CourseCard.tsx create mode 100644 components/CourseGrid.tsx create mode 100644 components/CoursePlayerModal.tsx create mode 100644 components/CourseRow.tsx create mode 100644 components/Hero.tsx create mode 100644 components/LoginModal.tsx create mode 100644 components/Logo.tsx create mode 100644 components/MaterialCard.tsx create mode 100644 components/MaterialsHubModal.tsx create mode 100644 components/Navbar.tsx create mode 100644 components/SearchModal.tsx create mode 100644 constants.ts create mode 100644 context/AuthContext.tsx create mode 100644 context/ToastContext.tsx create mode 100644 index.css create mode 100644 index.html create mode 100644 index.tsx create mode 100644 metadata.json create mode 100644 package.json create mode 100644 pages/ManageCourses.tsx create mode 100644 pages/MaterialsPage.tsx create mode 100644 public/Compor Hub-negativo.svg create mode 100644 public/Compor Hub.svg create mode 100644 public/favicon.svg create mode 100644 public/logo-dark-bg.svg create mode 100644 public/logo-light-bg.svg create mode 100644 services/ai.ts create mode 100644 services/db.ts create mode 100644 tsconfig.json create mode 100644 types.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..1b26505 --- /dev/null +++ b/App.tsx @@ -0,0 +1,123 @@ +import React, { useState } from 'react'; +import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { Navbar } from './components/Navbar'; +import { Hero } from './components/Hero'; +import { CourseGrid } from './components/CourseGrid'; +import { ManageCourses } from './pages/ManageCourses'; +import { MaterialsPage } from './pages/MaterialsPage'; +import { CoursePlayerModal } from './components/CoursePlayerModal'; +import { AuthProvider, useAuth } from './context/AuthContext'; +import { ToastProvider } from './context/ToastContext'; +import { Course } from './types'; + +// Protected Route Component +const ProtectedRoute: React.FC<{ children: React.ReactNode; allowedRole?: 'professor' | 'student' }> = ({ children, allowedRole }) => { + const { user, isLoading } = useAuth(); + + if (isLoading) return null; + + if (!user) { + return ; + } + + if (allowedRole && user.role !== allowedRole) { + return ; + } + + return <>{children}; +}; + +// Home component +interface HomeProps { + onPlay: (course: Course) => void; +} + +const Home: React.FC = ({ onPlay }) => { + return ( +
+ + +
+ ); +}; + +function AppContent() { + const [selectedCourse, setSelectedCourse] = useState(null); + + return ( +
+ {/* Navbar with Direct Materials Hub Trigger */} + + + + + } + /> + + {/* Full Dedicated Materials & Downloads Page */} + + } + /> + + {/* Professor Route - Protected */} + + + + } + /> + + {/* Catch all redirect */} + } /> + + + {/* Global Player Modal */} + setSelectedCourse(null)} + /> + + {/* Footer */} +
+
+
+ Compor HUB + | + Portal de Cursos & Central de Downloads +
+
+ © {new Date().getFullYear()} Compor HUB. Todos os direitos reservados. +
+
+
+
+ ); +} + +function App() { + return ( + + + + + + + + ); +} + +export default App; diff --git a/README.md b/README.md deleted file mode 100644 index 2241000..0000000 --- a/README.md +++ /dev/null @@ -1,11 +0,0 @@ -
- -GHBanner - -

Built with AI Studio

- -

The fastest path from prompt to production with Gemini.

- - Start building - -
diff --git a/components/CourseCard.tsx b/components/CourseCard.tsx new file mode 100644 index 0000000..bd3c39f --- /dev/null +++ b/components/CourseCard.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { PlayCircle, Download, FileText, CheckCircle2 } from 'lucide-react'; +import { Course, SectionVariant } from '../types'; + +interface CourseCardProps { + course: Course; + variant: SectionVariant; + onClick?: (course: Course) => void; + onOpenMaterials?: (course: Course) => void; +} + +export const CourseCard: React.FC = ({ + course, + variant, + onClick, + onOpenMaterials +}) => { + const widthClass = variant === 'landscape' + ? 'w-[320px] md:w-[420px]' + : 'w-[200px] md:w-[260px]'; + + const aspectRatioClass = variant === 'landscape' + ? 'aspect-video' + : 'aspect-[3/4]'; + + // Count total materials (global + lesson specific) + const globalAttachmentsCount = course.attachments?.length || 0; + const lessonAttachmentsCount = course.lessons?.reduce((acc, l) => acc + (l.attachments?.length || 0), 0) || 0; + const totalMaterials = globalAttachmentsCount + lessonAttachmentsCount; + + return ( +
+ {/* Thumbnail Container */} +
onClick?.(course)} + className={`relative ${aspectRatioClass} w-full overflow-hidden rounded-[8px] bg-zinc-900 shadow-xl transition-all duration-300 ease-out group-hover:scale-[1.02] group-hover:shadow-2xl group-hover:shadow-orange-500/10 group-hover:z-10 ring-0 ring-white/0 group-hover:ring-2 group-hover:ring-orange-500/40`} + > + {course.title} e.preventDefault()} + className="h-full w-full object-cover transition-opacity duration-300 opacity-90 group-hover:opacity-100 rounded-[8px] select-none pointer-events-none" + /> + + {/* Badges on Thumbnail */} +
+ + {course.category.split('&')[0].trim()} + + + {totalMaterials > 0 && ( + + + {totalMaterials} {totalMaterials === 1 ? 'Material' : 'Materiais'} + + )} +
+ + {/* Hover Play Button Overlay */} +
+
+ +
+
+ + {/* Progress Bar */} + {course.progress !== undefined && course.progress > 0 && ( +
+
+
+ )} +
+ + {/* Metadata */} +
+
+

onClick?.(course)} + className="text-body font-semibold text-white/95 line-clamp-1 group-hover:text-orange-400 transition-colors leading-body tracking-body" + > + {course.title} +

+
+ +
+ {course.duration} + {course.instructor && ( + <> + + {course.instructor} + + )} +
+
+
+ ); +}; diff --git a/components/CourseGrid.tsx b/components/CourseGrid.tsx new file mode 100644 index 0000000..29c21b2 --- /dev/null +++ b/components/CourseGrid.tsx @@ -0,0 +1,228 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { CourseRow } from './CourseRow'; +import { Section, Course } from '../types'; +import { initDB, getCourses } from '../services/db'; +import { Loader2, ChevronLeft, ChevronRight } from 'lucide-react'; +import { CATEGORIES } from '../constants'; + +interface CourseGridProps { + onCourseSelect?: (course: Course) => void; + selectedCategoryFilter?: string; +} + +export const CourseGrid: React.FC = ({ + onCourseSelect, + selectedCategoryFilter +}) => { + const [courses, setCourses] = useState([]); + const [loading, setLoading] = useState(true); + const [activeCategory, setActiveCategory] = useState('all'); + + // Slider State & Refs for Categories + const categoryScrollRef = useRef(null); + const isMouseDownRef = useRef(false); + const startXRef = useRef(0); + const scrollLeftRef = useRef(0); + const draggedDistanceRef = useRef(0); + const [isDraggingCategory, setIsDraggingCategory] = useState(false); + const [canScrollCatLeft, setCanScrollCatLeft] = useState(false); + const [canScrollCatRight, setCanScrollCatRight] = useState(false); + + const checkCategoryBoundaries = useCallback(() => { + if (!categoryScrollRef.current) return; + const { scrollLeft, scrollWidth, clientWidth } = categoryScrollRef.current; + setCanScrollCatLeft(scrollLeft > 10); + setCanScrollCatRight(scrollLeft + clientWidth < scrollWidth - 10); + }, []); + + useEffect(() => { + const loadData = async () => { + await initDB(); + const allCourses = await getCourses(); + setCourses(allCourses); + setLoading(false); + }; + + loadData(); + }, []); + + useEffect(() => { + checkCategoryBoundaries(); + window.addEventListener('resize', checkCategoryBoundaries); + return () => window.removeEventListener('resize', checkCategoryBoundaries); + }, [checkCategoryBoundaries, courses]); + + const handleCategoryScroll = (direction: 'left' | 'right') => { + if (!categoryScrollRef.current) return; + const scrollAmount = 300; + categoryScrollRef.current.scrollBy({ + left: direction === 'left' ? -scrollAmount : scrollAmount, + behavior: 'smooth' + }); + }; + + const handleCatMouseDown = (e: React.MouseEvent) => { + if (!categoryScrollRef.current) return; + isMouseDownRef.current = true; + startXRef.current = e.pageX - categoryScrollRef.current.offsetLeft; + scrollLeftRef.current = categoryScrollRef.current.scrollLeft; + draggedDistanceRef.current = 0; + setIsDraggingCategory(false); + }; + + const handleCatMouseMove = (e: React.MouseEvent) => { + if (!isMouseDownRef.current || !categoryScrollRef.current) return; + e.preventDefault(); + const x = e.pageX - categoryScrollRef.current.offsetLeft; + const walk = (x - startXRef.current) * 1.4; + draggedDistanceRef.current = Math.abs(walk); + + if (draggedDistanceRef.current > 5) { + setIsDraggingCategory(true); + categoryScrollRef.current.scrollLeft = scrollLeftRef.current - walk; + } + }; + + const handleCatMouseUp = () => { + isMouseDownRef.current = false; + setTimeout(() => { + setIsDraggingCategory(false); + draggedDistanceRef.current = 0; + }, 50); + }; + + const handleCategoryClick = (cat: string) => { + if (draggedDistanceRef.current < 8) { + setActiveCategory(cat); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + // Filter courses if a specific category is active + const filteredCourses = activeCategory === 'all' + ? courses + : courses.filter(c => c.category === activeCategory); + + // Group into curated sections for Netflix/Apple TV style layout + const sections: Section[] = [ + { + title: activeCategory === 'all' ? 'Cursos em Destaque' : activeCategory, + variant: 'landscape', + courses: filteredCourses + }, + { + title: 'Com Materiais & Planilhas para Download', + variant: 'landscape', + courses: courses.filter(c => (c.attachments && c.attachments.length > 0) || c.lessons.some(l => l.attachments && l.attachments.length > 0)) + }, + { + title: 'Formações Recomendadas', + variant: 'portrait', + courses: courses + } + ]; + + return ( +
+ + {/* Category Pills Slider Container */} +
+
+ + {/* Scroll Button Left */} + {canScrollCatLeft && ( + + )} + + {/* Left Gradient Fade */} + {canScrollCatLeft && ( +
+ )} + + {/* Scrollable Category Track */} +
+ + + {CATEGORIES.map(cat => { + const count = courses.filter(c => c.category === cat).length; + return ( + + ); + })} +
+ + {/* Right Gradient Fade */} + {canScrollCatRight && ( +
+ )} + + {/* Scroll Button Right */} + {canScrollCatRight && ( + + )} + +
+
+ + {/* Sections Rows */} + {sections.map((section, index) => ( + section.courses.length > 0 && ( + + ) + ))} +
+ ); +}; + diff --git a/components/CoursePlayerModal.tsx b/components/CoursePlayerModal.tsx new file mode 100644 index 0000000..42a07f3 --- /dev/null +++ b/components/CoursePlayerModal.tsx @@ -0,0 +1,571 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { + X, + Send, + User as UserIcon, + Lightbulb, + Paperclip, + Lock, + Unlock, + ListVideo, + Play, + Download, + Sparkles, + MessageSquare, + CheckCircle2, + Check, + Share2, + FileText +} from 'lucide-react'; +import { Course, Comment, Lesson, Attachment } from '../types'; +import { useAuth } from '../context/AuthContext'; +import { getComments, saveComment, saveProgress, incrementViews } from '../services/db'; +import { LoginModal } from './LoginModal'; +import { MaterialCard } from './MaterialCard'; + +interface CoursePlayerModalProps { + course: Course | null; + onClose: () => void; +} + +export const CoursePlayerModal: React.FC = ({ course, onClose }) => { + const { user } = useAuth(); + const [comments, setComments] = useState([]); + const [newComment, setNewComment] = useState(''); + const [currentProgress, setCurrentProgress] = useState(0); + const [currentLesson, setCurrentLesson] = useState(null); + const [completedLessonIds, setCompletedLessonIds] = useState([]); + const [showLoginPrompt, setShowLoginPrompt] = useState(false); + const [activeTab, setActiveTab] = useState<'playlist' | 'materials' | 'discussion'>('playlist'); + const [copiedLink, setCopiedLink] = useState(false); + + const videoRef = useRef(null); + + // Load course data & completed lessons from storage + useEffect(() => { + if (course) { + incrementViews(course.id); + getComments(course.id).then(setComments); + setCurrentProgress(course.progress || 0); + + // Load completed lessons from localStorage + const storageKey = `course_${course.id}_completed_lessons`; + const savedCompleted = localStorage.getItem(storageKey); + if (savedCompleted) { + try { + setCompletedLessonIds(JSON.parse(savedCompleted)); + } catch (e) { + setCompletedLessonIds([]); + } + } else if (course.progress && course.progress > 0 && course.lessons && course.lessons.length > 0) { + // Initial demo completion based on progress + const count = Math.ceil((course.progress / 100) * course.lessons.length); + const initialCompleted = course.lessons.slice(0, count).map(l => l.id); + setCompletedLessonIds(initialCompleted); + localStorage.setItem(storageKey, JSON.stringify(initialCompleted)); + } + + // Select first lesson by default + if (course.lessons && course.lessons.length > 0) { + setCurrentLesson(course.lessons[0]); + } + } + }, [course]); + + // Persist completed lessons and recalculate progress + const updateCompletedLessons = (newCompleted: string[]) => { + setCompletedLessonIds(newCompleted); + if (course) { + localStorage.setItem(`course_${course.id}_completed_lessons`, JSON.stringify(newCompleted)); + if (course.lessons && course.lessons.length > 0) { + const percent = Math.round((newCompleted.length / course.lessons.length) * 100); + setCurrentProgress(percent); + saveProgress(course.id, percent); + } + } + }; + + const toggleLessonCompleted = (lessonId: string) => { + if (completedLessonIds.includes(lessonId)) { + const updated = completedLessonIds.filter(id => id !== lessonId); + updateCompletedLessons(updated); + } else { + const updated = [...completedLessonIds, lessonId]; + updateCompletedLessons(updated); + } + }; + + // Video finished automatically -> mark lesson as completed + const handleVideoEnded = () => { + if (currentLesson && !completedLessonIds.includes(currentLesson.id)) { + const updated = [...completedLessonIds, currentLesson.id]; + updateCompletedLessons(updated); + } + + // Auto-advance to next lesson if available + if (course?.lessons && currentLesson) { + const currentIndex = course.lessons.findIndex(l => l.id === currentLesson.id); + if (currentIndex >= 0 && currentIndex < course.lessons.length - 1) { + const nextLesson = course.lessons[currentIndex + 1]; + if (nextLesson.isFree || user) { + setTimeout(() => { + setCurrentLesson(nextLesson); + }, 1000); + } + } + } + }; + + // Track video progress + const handleTimeUpdate = () => { + if (videoRef.current) { + const { currentTime, duration } = videoRef.current; + if (duration > 0) { + const percent = Math.round((currentTime / duration) * 100); + // Automatically mark as complete when reaching 95%+ + if (percent >= 95 && currentLesson && !completedLessonIds.includes(currentLesson.id)) { + const updated = [...completedLessonIds, currentLesson.id]; + updateCompletedLessons(updated); + } + } + } + }; + + const handleAddComment = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newComment.trim() || !course) return; + + const comment: Comment = { + id: Date.now().toString(), + courseId: course.id, + lessonId: currentLesson?.id, + user: user?.name || 'Aluno Convidado', + text: newComment, + date: new Date().toLocaleDateString('pt-BR') + }; + + await saveComment(comment); + setComments([comment, ...comments]); + setNewComment(''); + }; + + const changeLesson = (lesson: Lesson) => { + setCurrentLesson(lesson); + }; + + const handleShare = () => { + navigator.clipboard.writeText(window.location.href); + setCopiedLink(true); + setTimeout(() => setCopiedLink(false), 2500); + }; + + // Collect all downloadable materials for this course (Global + All Lessons) + const allCourseAttachments: Array<{ attachment: Attachment; source: string }> = []; + + if (course?.attachments) { + course.attachments.forEach(att => { + allCourseAttachments.push({ attachment: att, source: 'Material Geral do Curso' }); + }); + } + + if (course?.lessons) { + course.lessons.forEach(l => { + if (l.attachments) { + l.attachments.forEach(att => { + allCourseAttachments.push({ attachment: att, source: l.title }); + }); + } + }); + } + + const currentLessonAttachments = currentLesson?.attachments || []; + + // Access Control Logic + const canWatch = currentLesson && (currentLesson.isFree || user); + + if (!course) return null; + + return ( +
+ {/* Top Bar Floating Controls */} +
+
+ + + {course.title} + +
+ +
+ + + +
+
+ +
+
+ + {/* Main Left Column: Video & Lesson Details */} +
+ + {/* Video Player Section */} +
+ {canWatch ? ( + + ) : ( + // Locked State +
+
+ +
+

Aula Exclusiva (Pro)

+

+ Esta aula e seus materiais de download complementares estão disponíveis para membros. Faça login para continuar assistindo. +

+ +
+ )} +
+ + {/* Current Lesson Header Info */} +
+
+
+ {course.category} + + {currentLesson?.duration || '15m'} +
+ +
+ {/* Mark as completed toggle button */} + {currentLesson && ( + + )} + + {/* Direct Button to Materials Tab if Available */} + {allCourseAttachments.length > 0 && ( + + )} +
+
+ +

+ {currentLesson?.title || course.title} +

+ +

+ {currentLesson?.description || course.description} +

+ + {/* Lesson Specific Quick Downloads (if present) */} + {currentLessonAttachments.length > 0 && ( +
+
+

+ + Downloads Desta Aula +

+ + {currentLessonAttachments.length} {currentLessonAttachments.length === 1 ? 'arquivo' : 'arquivos'} + +
+ +
+ {currentLessonAttachments.map(att => ( + + ))} +
+
+ )} + + {/* Instructor Key Takeaways / Tips */} + {course.tips && course.tips.length > 0 && ( +
+

+ + Recomendações Práticas do Instrutor +

+
    + {course.tips.map((tip, i) => ( +
  • + + {tip} +
  • + ))} +
+
+ )} +
+
+ + {/* Right Sidebar: Tabs for Playlist, Materials, and Discussion */} +
+ + {/* Tab Navigation */} +
+ + + + + +
+ + {/* TAB CONTENT: Playlist */} + {activeTab === 'playlist' && ( +
+
+ Trilha de Aulas + {course.duration} +
+ +
+ {course.lessons.map((lesson, index) => { + const isActive = currentLesson?.id === lesson.id; + const isCompleted = completedLessonIds.includes(lesson.id); + const isLocked = !lesson.isFree && !user; + const hasAttachments = lesson.attachments && lesson.attachments.length > 0; + + return ( +
changeLesson(lesson)} + className={` + group flex items-start gap-3.5 p-3.5 rounded-xl cursor-pointer transition-all border + ${isActive + ? 'bg-orange-500/15 border-orange-500/40 text-orange-400 shadow-md shadow-orange-500/10' + : isCompleted + ? 'bg-emerald-950/40 hover:bg-emerald-900/50 border-emerald-500/30 text-emerald-300 shadow-sm' + : 'bg-zinc-900/80 hover:bg-zinc-800/90 border-transparent text-gray-200' + } + ${isLocked ? 'opacity-70' : 'opacity-100'} + `} + > + {/* Status Icon */} +
+ {isActive ? ( +
+ +
+ ) : isCompleted ? ( +
+ +
+ ) : isLocked ? ( +
+ +
+ ) : ( +
+ {index + 1} +
+ )} +
+ +
+

+ {lesson.title} +

+
+ {lesson.duration} + + {isCompleted && ( + + Concluída + + )} + + {lesson.isFree ? ( + Grátis + ) : ( + Exclusiva + )} + {hasAttachments && ( + + + {lesson.attachments!.length} anexo(s) + + )} +
+
+
+ ); + })} +
+
+ )} + + {/* TAB CONTENT: Materials & Downloads */} + {activeTab === 'materials' && ( +
+
+
+

Materiais para Download

+

Arquivos anexados a este curso

+
+ + {allCourseAttachments.length} Total + +
+ + {allCourseAttachments.length === 0 ? ( +
+ +

Nenhum material cadastrado para este curso ainda.

+
+ ) : ( +
+ {allCourseAttachments.map((item, idx) => ( +
+ + + {item.source} + + +
+ ))} +
+ )} +
+ )} + + {/* TAB CONTENT: Discussion */} + {activeTab === 'discussion' && ( +
+

Dúvidas & Comentários

+ + {/* Comment Form */} +
+ setNewComment(e.target.value)} + placeholder={user ? "Faça uma pergunta sobre a aula..." : "Faça login para comentar"} + disabled={!user} + className="flex-1 bg-zinc-900/90 rounded-xl px-3.5 py-2.5 text-xs text-white placeholder:text-gray-500 focus:outline-none focus:ring-1 focus:ring-orange-500" + /> + +
+ + {/* Comments List */} +
+ {comments.length === 0 ? ( +

Nenhum comentário ainda. Seja o primeiro a perguntar!

+ ) : ( + comments.map(c => ( +
+
+ {c.user} + {c.date} +
+

{c.text}

+ + {c.reply && ( +
+ Instrutor: +

{c.reply}

+
+ )} +
+ )) + )} +
+
+ )} + +
+ +
+
+ + setShowLoginPrompt(false)} /> +
+ ); +}; diff --git a/components/CourseRow.tsx b/components/CourseRow.tsx new file mode 100644 index 0000000..efb99a2 --- /dev/null +++ b/components/CourseRow.tsx @@ -0,0 +1,176 @@ +import React, { useRef, useState, useEffect, useCallback } from 'react'; +import { CourseCard } from './CourseCard'; +import { Section, Course } from '../types'; +import { ChevronRight, ChevronLeft } from 'lucide-react'; + +interface CourseRowProps { + section: Section; + onCourseClick?: (course: Course) => void; +} + +export const CourseRow: React.FC = ({ section, onCourseClick }) => { + const containerRef = useRef(null); + const isMouseDownRef = useRef(false); + const startXRef = useRef(0); + const scrollLeftRef = useRef(0); + const draggedDistanceRef = useRef(0); + + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(true); + const [isDragging, setIsDragging] = useState(false); + + const checkScrollBoundaries = useCallback(() => { + if (!containerRef.current) return; + const { scrollLeft, scrollWidth, clientWidth } = containerRef.current; + setCanScrollLeft(scrollLeft > 20); + setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 20); + }, []); + + useEffect(() => { + checkScrollBoundaries(); + window.addEventListener('resize', checkScrollBoundaries); + return () => window.removeEventListener('resize', checkScrollBoundaries); + }, [checkScrollBoundaries, section.courses]); + + // Arrow button handlers + const handleScroll = (direction: 'left' | 'right') => { + if (!containerRef.current) return; + const scrollAmount = containerRef.current.clientWidth * 0.75; + containerRef.current.scrollBy({ + left: direction === 'left' ? -scrollAmount : scrollAmount, + behavior: 'smooth' + }); + }; + + // Mouse Drag-to-Scroll Handlers + const handleMouseDown = (e: React.MouseEvent) => { + if (!containerRef.current) return; + // Don't drag if clicking buttons or specific interactive items + isMouseDownRef.current = true; + startXRef.current = e.pageX - containerRef.current.offsetLeft; + scrollLeftRef.current = containerRef.current.scrollLeft; + draggedDistanceRef.current = 0; + setIsDragging(false); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isMouseDownRef.current || !containerRef.current) return; + e.preventDefault(); + const x = e.pageX - containerRef.current.offsetLeft; + const walk = (x - startXRef.current) * 1.5; // Drag sensitivity multiplier + draggedDistanceRef.current = Math.abs(walk); + + if (draggedDistanceRef.current > 6) { + setIsDragging(true); + containerRef.current.scrollLeft = scrollLeftRef.current - walk; + } + }; + + const handleMouseUp = () => { + isMouseDownRef.current = false; + setTimeout(() => { + setIsDragging(false); + draggedDistanceRef.current = 0; + }, 50); + }; + + const handleCardClick = (course: Course) => { + // Only trigger play/open if user wasn't dragging + if (draggedDistanceRef.current < 10) { + onCourseClick?.(course); + } + }; + + return ( +
+ {/* Title Header */} +
+
+

+ {section.title} +

+ + {section.courses.length} {section.courses.length === 1 ? 'curso' : 'cursos'} + +
+ + {/* Inline Navigation Arrows for Desktop */} +
+ + +
+
+ + {/* Horizontal Carousel Container */} +
+ + {/* Floating Side Arrow Left (Hover Overlay) */} + {canScrollLeft && ( + + )} + + {/* Scroll Container with drag-to-scroll */} +
+ {section.courses.map((course) => ( +
+ +
+ ))} + {/* Padding at the end */} +
+
+ + {/* Floating Side Arrow Right (Hover Overlay) */} + {canScrollRight && ( + + )} + + {/* Subtle Vignette on the right */} +
+
+
+ ); +}; + + diff --git a/components/Hero.tsx b/components/Hero.tsx new file mode 100644 index 0000000..2a5cb9b --- /dev/null +++ b/components/Hero.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Play, Plus, Sparkles } from 'lucide-react'; +import { FEATURED_COURSE } from '../constants'; +import { Course } from '../types'; + +interface HeroProps { + onPlay?: (course: Course) => void; +} + +export const Hero: React.FC = ({ onPlay }) => { + + return ( +
+ {/* Background Image with Cinematic Gradient Overlay */} +
+ Featured Course + {/* Gradients for text readability and seamless transition */} +
+
+
+ + {/* Content */} +
+
+ + {/* Metadata Badges / Tags */} +
+ + + Destaque + + {FEATURED_COURSE.category} + • + {FEATURED_COURSE.duration} +
+ + {/* Main Display Headline */} +

+ {FEATURED_COURSE.title} +

+ + {/* Subhead / Lead Description */} +

+ {FEATURED_COURSE.description} +

+ + {/* Action Buttons with comfortable width, borders, and spacing */} +
+ {/* Primary CTA */} + + + {/* Add to List */} + +
+
+
+
+ ); +}; + diff --git a/components/LoginModal.tsx b/components/LoginModal.tsx new file mode 100644 index 0000000..8781048 --- /dev/null +++ b/components/LoginModal.tsx @@ -0,0 +1,166 @@ +import React, { useState } from 'react'; +import { X, Loader2, GraduationCap, School, KeyRound } from 'lucide-react'; +import { useAuth } from '../context/AuthContext'; +import { useNavigate } from 'react-router-dom'; +import { UserRole } from '../types'; + +interface LoginModalProps { + isOpen: boolean; + onClose: () => void; +} + +export const LoginModal: React.FC = ({ isOpen, onClose }) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [role, setRole] = useState('student'); // Default role + const [error, setError] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const { login } = useAuth(); + const navigate = useNavigate(); + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsSubmitting(true); + + // Simulate network delay for "premium" feel + setTimeout(() => { + // In a real app, the backend verifies credentials AND role matches. + // Here we simulate checking if the user exists for that specific context. + const success = login(email); + setIsSubmitting(false); + + if (success) { + onClose(); + if (email === 'admin@prof.com') { + // Force check: if they selected student but logged in as admin credentials, redirect correctly or warn. + // For UX simplicity here, we trust the credential mapping in AuthContext + navigate('/gerenciar'); + } else { + navigate('/'); + } + } else { + setError('Credenciais inválidas. Verifique o email ou a função selecionada.'); + } + }, 800); + }; + + const handleDemoLogin = (roleType: UserRole) => { + const demoEmail = roleType === 'professor' ? 'admin@prof.com' : 'aluno@test.com'; + setEmail(demoEmail); + setPassword('demo123'); + setRole(roleType); + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal Content */} +
+ + +
+

Compor HUB ID

+

Acesse seu espaço de aprendizado e materiais

+
+ + {/* Role Toggle */} +
+ {/* Sliding Background */} +
+ + + +
+ +
+
+
+ setEmail(e.target.value)} + className="w-full bg-white/10 text-white placeholder:text-white/30 px-4 py-3 rounded-[8px] focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:bg-white/15 transition-all text-body tracking-body" + autoFocus + /> +
+
+ setPassword(e.target.value)} + className="w-full bg-white/10 text-white placeholder:text-white/30 px-4 py-3 rounded-[8px] focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:bg-white/15 transition-all text-body tracking-body" + /> +
+
+ + {/* Quick Demo Buttons */} +
+ + +
+ + {error && ( +
+

{error}

+
+ )} + + +
+
+
+ ); +}; \ No newline at end of file diff --git a/components/Logo.tsx b/components/Logo.tsx new file mode 100644 index 0000000..16975fc --- /dev/null +++ b/components/Logo.tsx @@ -0,0 +1,88 @@ +import React from 'react'; + +interface LogoProps { + variant?: 'dark' | 'light'; // 'dark' = fundo escuro (compor branco + HUB laranja) | 'light' = fundo claro (compor escuro + HUB laranja) + className?: string; + size?: 'sm' | 'md' | 'lg' | 'xl'; + showSymbolOnly?: boolean; +} + +export const Logo: React.FC = ({ + variant = 'dark', + className = '', + size = 'md', + showSymbolOnly = false, +}) => { + const isDark = variant === 'dark'; + + // Sizing styles + const textSizes = { + sm: 'text-xl', + md: 'text-2xl', + lg: 'text-3xl', + xl: 'text-4xl', + }; + + const dotSizes = { + sm: { r: 2.5, gap: 'gap-1', w: 'w-4' }, + md: { r: 3.5, gap: 'gap-1', w: 'w-5' }, + lg: { r: 4.5, gap: 'gap-1.5', w: 'w-6' }, + xl: { r: 5.5, gap: 'gap-2', w: 'w-8' }, + }; + + if (showSymbolOnly) { + return ( + + + + + + ); + } + + return ( +
+ {/* "compor" */} + + compor + + + {/* 3 Orange Dots Icon - Floating elevated between compor & HUB */} +
+ + + + + +
+ + {/* "HUB" */} + + HUB + +
+ ); +}; + +export default Logo; diff --git a/components/MaterialCard.tsx b/components/MaterialCard.tsx new file mode 100644 index 0000000..814c222 --- /dev/null +++ b/components/MaterialCard.tsx @@ -0,0 +1,194 @@ +import React from 'react'; +import { + FileText, + FileSpreadsheet, + FileArchive, + FileCode, + Image as ImageIcon, + Link as LinkIcon, + Download, + ExternalLink, + CheckCircle2, + Paperclip +} from 'lucide-react'; +import { Attachment, AttachmentType } from '../types'; +import { useToast } from '../context/ToastContext'; + +interface MaterialCardProps { + attachment: Attachment; + onDownload?: (attachment: Attachment) => void; + compact?: boolean; +} + +export const getAttachmentIcon = (type: AttachmentType) => { + switch (type) { + case 'pdf': + return ; + case 'spreadsheet': + return ; + case 'zip': + return ; + case 'doc': + return ; + case 'image': + return ; + case 'link': + return ; + default: + return ; + } +}; + +export const getAttachmentBadge = (type: AttachmentType) => { + switch (type) { + case 'pdf': + return { label: 'PDF', bg: 'bg-rose-500/15 text-rose-400 font-bold' }; + case 'spreadsheet': + return { label: 'PLANILHA', bg: 'bg-emerald-500/15 text-emerald-400 font-bold' }; + case 'zip': + return { label: 'PACOTE ZIP', bg: 'bg-amber-500/15 text-amber-400 font-bold' }; + case 'doc': + return { label: 'DOCUMENTO', bg: 'bg-blue-500/15 text-blue-400 font-bold' }; + case 'image': + return { label: 'IMAGEM / ASSET', bg: 'bg-purple-500/15 text-purple-400 font-bold' }; + case 'link': + return { label: 'LINK EXTERNO', bg: 'bg-sky-500/15 text-sky-400 font-bold' }; + default: + return { label: 'ARQUIVO', bg: 'bg-gray-500/15 text-gray-400 font-bold' }; + } +}; + +export const MaterialCard: React.FC = ({ + attachment, + onDownload, + compact = false, +}) => { + const [downloaded, setDownloaded] = React.useState(false); + const { showDownloadToast, showToast } = useToast(); + const badge = getAttachmentBadge(attachment.type); + + const handleAction = (e: React.MouseEvent) => { + setDownloaded(true); + if (attachment.type === 'link') { + showToast(`Acessando link "${attachment.name}"`, 'info'); + } else { + showDownloadToast(attachment.name, attachment.size); + } + + if (onDownload) { + onDownload(attachment); + } + // If it has a real URL that is not '#', let the default anchor do its work + if (!attachment.url || attachment.url === '#') { + e.preventDefault(); + // Trigger a synthetic download notification reset + setTimeout(() => setDownloaded(false), 3000); + } + }; + + if (compact) { + return ( + +
+
+ {getAttachmentIcon(attachment.type)} +
+
+

+ {attachment.name} +

+
+ + {badge.label} + + {attachment.size && • {attachment.size}} +
+
+
+ +
+ {downloaded ? ( + + + Baixado + + ) : ( + + )} +
+
+ ); + } + + return ( +
+
+
+
+ {getAttachmentIcon(attachment.type)} +
+ + {badge.label} + +
+ +

+ {attachment.name} +

+ + {attachment.description && ( +

+ {attachment.description} +

+ )} +
+ + +
+ ); +}; diff --git a/components/MaterialsHubModal.tsx b/components/MaterialsHubModal.tsx new file mode 100644 index 0000000..0909df8 --- /dev/null +++ b/components/MaterialsHubModal.tsx @@ -0,0 +1,218 @@ +import React, { useState } from 'react'; +import { + Download, + Search, + Filter, + FileText, + FileSpreadsheet, + FileArchive, + FileCode, + Sparkles, + ExternalLink, + BookOpen, + Layers +} from 'lucide-react'; +import { Course, Attachment, AttachmentType } from '../types'; +import { MaterialCard } from './MaterialCard'; + +interface MaterialsHubModalProps { + isOpen: boolean; + onClose: () => void; + courses: Course[]; + onSelectCourse?: (course: Course) => void; +} + +export const MaterialsHubModal: React.FC = ({ + isOpen, + onClose, + courses, + onSelectCourse, +}) => { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedType, setSelectedType] = useState('all'); + const [selectedCategory, setSelectedCategory] = useState('all'); + + if (!isOpen) return null; + + // Aggregate all materials across all courses and lessons + const allMaterials: Array<{ attachment: Attachment; course: Course; lessonTitle?: string }> = []; + + courses.forEach(course => { + // Global course attachments + if (course.attachments && course.attachments.length > 0) { + course.attachments.forEach(att => { + allMaterials.push({ attachment: att, course }); + }); + } + + // Lesson specific attachments + if (course.lessons) { + course.lessons.forEach(lesson => { + if (lesson.attachments && lesson.attachments.length > 0) { + lesson.attachments.forEach(att => { + allMaterials.push({ attachment: att, course, lessonTitle: lesson.title }); + }); + } + }); + } + }); + + // Filtered materials + const filtered = allMaterials.filter(item => { + const matchesSearch = + item.attachment.name.toLowerCase().includes(searchTerm.toLowerCase()) || + item.course.title.toLowerCase().includes(searchTerm.toLowerCase()) || + (item.attachment.description && item.attachment.description.toLowerCase().includes(searchTerm.toLowerCase())); + + const matchesType = selectedType === 'all' || item.attachment.type === selectedType; + const matchesCategory = selectedCategory === 'all' || item.course.category === selectedCategory; + + return matchesSearch && matchesType && matchesCategory; + }); + + const categories = Array.from(new Set(courses.map(c => c.category))); + + return ( +
+
+ + {/* Header */} +
+
+
+ + Central de Recursos & Downloads +
+

+ Biblioteca de Materiais Complementares +

+

+ Baixe planilhas, modelos, e-books, checklists e PDFs de todos os cursos do Compor HUB. +

+
+ + +
+ + {/* Filters and Search Bar */} +
+
+ {/* Search Input */} +
+ + setSearchTerm(e.target.value)} + placeholder="Pesquisar material por nome, curso ou palavra-chave..." + className="w-full bg-black/60 border border-white/10 rounded-[8px] pl-12 pr-4 py-3 text-body-sm text-white placeholder:text-gray-500 focus:outline-none focus:border-orange-500/50 focus:ring-1 focus:ring-orange-500/50 transition-all" + /> +
+ + {/* Type Filters Pills */} +
+ {[ + { id: 'all', label: 'Todos' }, + { id: 'pdf', label: 'PDFs' }, + { id: 'spreadsheet', label: 'Planilhas' }, + { id: 'zip', label: 'Pacotes ZIP' }, + { id: 'doc', label: 'Documentos' }, + { id: 'link', label: 'Links' }, + ].map(t => ( + + ))} +
+
+ + {/* Category Filter */} +
+ + Categoria: + + + {categories.map(cat => { + const count = allMaterials.filter(m => m.course.category === cat).length; + return ( + + ); + })} +
+
+ + {/* Material Cards Grid */} +
+ {filtered.length === 0 ? ( +
+ +

Nenhum material encontrado

+

Tente ajustar seus termos de pesquisa ou filtros.

+
+ ) : ( +
+ {filtered.map((item, index) => ( +
+ {/* Context Badge: Which Course / Lesson */} +
+ { + onClose(); + onSelectCourse?.(item.course); + }} + className="hover:text-orange-400 cursor-pointer truncate flex items-center gap-1.5" + > + + {item.course.title} + + {item.lessonTitle && ( + + {item.lessonTitle} + + )} +
+ + +
+ ))} +
+ )} +
+ + {/* Footer info */} +
+ {filtered.length} materiais disponíveis para download + Compor HUB • Materiais Exclusivos +
+
+
+ ); +}; diff --git a/components/Navbar.tsx b/components/Navbar.tsx new file mode 100644 index 0000000..df889ef --- /dev/null +++ b/components/Navbar.tsx @@ -0,0 +1,143 @@ +import React, { useState, useEffect } from 'react'; +import { User as UserIcon, LogOut, Search, Folder } from 'lucide-react'; +import { useAuth } from '../context/AuthContext'; +import { LoginModal } from './LoginModal'; +import { SearchModal } from './SearchModal'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { Course } from '../types'; +import { Logo } from './Logo'; + +interface NavbarProps { + onPlay: (course: Course) => void; +} + +export const Navbar: React.FC = ({ onPlay }) => { + const [scrolled, setScrolled] = useState(false); + const [showLoginModal, setShowLoginModal] = useState(false); + const [showSearchModal, setShowSearchModal] = useState(false); + const { user, logout } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + + useEffect(() => { + const handleScroll = () => { + setScrolled(window.scrollY > 20); + }; + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + const handleLogout = () => { + logout(); + navigate('/'); + }; + + const isMaterialsPage = location.pathname === '/materiais'; + const isHomePage = location.pathname === '/'; + + return ( + <> + + + setShowLoginModal(false)} /> + setShowSearchModal(false)} onPlay={onPlay} /> + + ); +}; + + diff --git a/components/SearchModal.tsx b/components/SearchModal.tsx new file mode 100644 index 0000000..403f39d --- /dev/null +++ b/components/SearchModal.tsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Search, X, Loader2, PlayCircle, Download } from 'lucide-react'; +import { Course } from '../types'; +import { getCourses } from '../services/db'; + +interface SearchModalProps { + isOpen: boolean; + onClose: () => void; + onPlay: (course: Course) => void; +} + +export const SearchModal: React.FC = ({ isOpen, onClose, onPlay }) => { + const [query, setQuery] = useState(''); + const [allCourses, setAllCourses] = useState([]); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(true); + const inputRef = useRef(null); + + // Load courses on mount + useEffect(() => { + const loadData = async () => { + const courses = await getCourses(); + setAllCourses(courses); + setLoading(false); + }; + loadData(); + }, []); + + // Focus input when opened + useEffect(() => { + if (isOpen) { + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'unset'; + } + return () => { document.body.style.overflow = 'unset'; }; + }, [isOpen]); + + // Filter logic across Title, Category, Instructor and Materials + useEffect(() => { + if (!query.trim()) { + setResults([]); + return; + } + + const lowerQuery = query.toLowerCase(); + const filtered = allCourses.filter(c => { + const matchesTitle = c.title.toLowerCase().includes(lowerQuery); + const matchesCat = c.category.toLowerCase().includes(lowerQuery); + const matchesInstructor = c.instructor?.toLowerCase().includes(lowerQuery); + const matchesLessons = c.lessons.some(l => l.title.toLowerCase().includes(lowerQuery)); + const matchesAttachments = c.attachments?.some(a => a.name.toLowerCase().includes(lowerQuery)); + + return matchesTitle || matchesCat || matchesInstructor || matchesLessons || matchesAttachments; + }); + setResults(filtered); + }, [query, allCourses]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + + if (!isOpen) return null; + + return ( +
+ {/* Header / Search Input */} +
+ + +
+
+ + setQuery(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Buscar por cursos, aulas, instrutor ou materiais..." + className="w-full bg-transparent font-display text-2xl md:text-heading font-bold text-white placeholder:text-gray-600 focus:outline-none tracking-heading leading-heading" + /> +
+
+
+ + {/* Results Area */} +
+
+ {loading ? ( +
+ +
+ ) : ( + <> + {!query && ( +
+

Digite um termo para pesquisar cursos ou arquivos

+

Ex: Meta Ads, ROAS, Planilha, Criativos, Funil

+
+ )} + + {query && results.length === 0 && ( +
+

Nenhum curso ou material encontrado para "{query}"

+
+ )} + + {results.length > 0 && ( +
+

+ + {results.length} Curso{results.length !== 1 ? 's' : ''} Encontrado{results.length !== 1 ? 's' : ''} +

+ +
+ {results.map(course => { + const matCount = (course.attachments?.length || 0) + course.lessons.reduce((acc, l) => acc + (l.attachments?.length || 0), 0); + + return ( +
{ onPlay(course); onClose(); }} + className="cursor-pointer group bg-zinc-900/60 rounded-[8px] p-3 border border-white/5 hover:border-orange-500/40 transition-all duration-300 shadow-xl" + > +
+ {course.title} +
+ +
+ {matCount > 0 && ( +
+ + {matCount} +
+ )} +
+ +

+ {course.title} +

+
+ {course.category} + {course.duration} +
+
+ ); + })} +
+
+ )} + + )} +
+
+
+ ); +}; diff --git a/constants.ts b/constants.ts new file mode 100644 index 0000000..df4344a --- /dev/null +++ b/constants.ts @@ -0,0 +1,561 @@ +import { Course, Section, SectionVariant, Lesson, Attachment } from './types'; + +export const CATEGORIES = [ + 'Tráfego Pago & Meta Ads', + 'Google Ads & YouTube', + 'Funis de Vendas & ROI', + 'Copywriting & Criativos', + 'Gestão & Processos de Agência', + 'Automação & CRM', +]; + +const SAMPLE_VIDEO = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'; + +export const FEATURED_COURSE: Course = { + id: 'featured-masterclass-roas', + title: 'Masterclass: Escala de Tráfego Pago & ROAS 5x+', + category: 'Tráfego Pago & Meta Ads', + thumbnail: 'https://images.unsplash.com/photo-1551836022-d5d88e9218df?auto=format&fit=crop&w=1920&q=80', + duration: '6 Aulas • 3h 45m', + description: 'O método completo e validado para escalar campanhas de Meta Ads e Google Ads com previsibilidade, gestão de criativos de alta retenção e controle estrito de CAC e Blended ROAS.', + instructor: 'Mário Morgado', + instructorRole: 'Head de Performance & Growth', + lessons: [ + { + id: 'l-feat-1', + title: '01. Arquitetura de Conta & Estratégia de Escala CBO/ABO', + duration: '22m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'Como montar uma estrutura robusta à prova de bloqueios e pronta para receber orçamentos 5x maiores sem estourar o CPA.', + attachments: [ + { + id: 'att-feat-1', + name: 'Planilha_Calculadora_ROAS_e_CAC.xlsx', + type: 'spreadsheet', + url: '#', + size: '1.4 MB', + description: 'Calculadora automática para encontrar o ponto de equilíbrio (breakeven) e margem de lucro por campanha.' + }, + { + id: 'att-feat-2', + name: 'Guia_Estrutura_de_Contas_Meta_2026.pdf', + type: 'pdf', + url: '#', + size: '3.8 MB', + description: 'Manual visual com esquemas de públicos abertos, lookalikes e segmentações vantajosas.' + } + ] + }, + { + id: 'l-feat-2', + title: '02. Laboratório de Criativos: Análise de Hook Rate & Hold Rate', + duration: '35m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'A anatomia dos criativos vencedores. Métricas de retenção nos primeiros 3 segundos e copies que convertem em escala.', + attachments: [ + { + id: 'att-feat-3', + name: 'Templates_Criativos_Photoshop_Figma.zip', + type: 'zip', + url: '#', + size: '48.2 MB', + description: 'Pack com 15 layouts de anúncios estáticos e scripts de vídeos curtos prontos para edição.' + }, + { + id: 'att-feat-4', + name: 'Checklist_Auditoria_de_Criativos.pdf', + type: 'pdf', + url: '#', + size: '890 KB', + description: 'Checklist de 12 pontos para aprovação de novos criativos antes de subir na conta.' + } + ] + }, + { + id: 'l-feat-3', + title: '03. Google Ads para E-commerce: Performance Max & Search Alpha', + duration: '40m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Dominando feeds do Google Merchant Center, segmentações avançadas e termos negativos para maximizar a conversão.', + attachments: [ + { + id: 'att-feat-5', + name: 'Lista_Palavras_Chave_Negativas_Universal.xlsx', + type: 'spreadsheet', + url: '#', + size: '640 KB', + description: 'Mais de 2.000 palavras-chave negativas essenciais para economizar orçamento no Google Ads.' + } + ] + }, + { + id: 'l-feat-4', + title: '04. Funil de Vendas de Alta Conversão & Otimização de Checkout', + duration: '38m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Reduzindo o abandono de carrinho e aumentando o ticket médio com upsells, order bumps e automações de CRM.', + attachments: [ + { + id: 'att-feat-6', + name: 'Blueprint_Funil_Vendas_Infoproduto_Ecommerce.pdf', + type: 'pdf', + url: '#', + size: '5.1 MB', + description: 'Mapeamento visual do fluxo de tráfego, página de vendas e checkout.' + } + ] + }, + { + id: 'l-feat-5', + title: '05. Dashboards White-Label & Gestão de Relatórios para Clientes', + duration: '28m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Como apresentar dados e ROI de forma cristalina para clientes sem gastar horas montando relatórios manuais.', + attachments: [ + { + id: 'att-feat-7', + name: 'Modelo_Relatorio_Executivo_Mensal.pdf', + type: 'pdf', + url: '#', + size: '2.3 MB', + description: 'Template de relatório executivo que comprova o valor do gestor de tráfego.' + } + ] + }, + { + id: 'l-feat-6', + title: '06. Plano de Ação: Escalando de R$ 10k para R$ 100k/mês', + duration: '42m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'O passo a passo estratégico para gerenciar grandes volumes de verba sem perder a margem de contribuição.', + attachments: [ + { + id: 'att-feat-8', + name: 'Roadmap_Escala_Orcamento_Planilha.xlsx', + type: 'spreadsheet', + url: '#', + size: '1.1 MB', + description: 'Cronograma e matriz de decisão para aumentos graduais de orçamento diário.' + } + ] + } + ], + tips: [ + "Monitore o Blended ROAS diariamente para ter clareza do retorno global sobre o investimento.", + "Criativos representam mais de 70% do sucesso no Meta Ads atual. Teste pelo menos 5 novos ângulos semanais.", + "Nunca aumente o orçamento em mais de 20% a cada 48h em conjuntos ABO para não reiniciar o aprendizado da máquina.", + "Analise os materiais complementares em PDF e Planilhas anexados em cada aula para acelerar sua implementação." + ], + attachments: [ + { + id: 'att-master-pack', + name: 'Kit_Completo_Materiais_Escala_Compor_HUB.zip', + type: 'zip', + url: '#', + size: '62.4 MB', + description: 'Todos os PDFs, Planilhas, Modelos de Relatório e Scripts do curso compactados em um único arquivo.' + }, + { + id: 'att-master-pdf', + name: 'Apostila_Oficial_Gestao_Trafego_Pago_2026.pdf', + type: 'pdf', + url: '#', + size: '12.8 MB', + description: 'Apostila completa de 94 páginas com resumos teóricos e práticos.' + } + ] +}; + +// Realistic library of courses with attached downloadable assets +export const INITIAL_COURSES: Course[] = [ + FEATURED_COURSE, + { + id: 'course-meta-ads-pro', + title: 'Meta Ads Pro: Segmentações, Públicos & Pixel CAPI', + category: 'Tráfego Pago & Meta Ads', + thumbnail: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=800&q=80', + duration: '5 Aulas • 2h 50m', + description: 'Aprenda a configurar a API de Conversões do Meta, gerenciar eventos prioritários e explorar públicos de alta conversão.', + instructor: 'Lucas Farias', + instructorRole: 'Especialista em Tracking & Meta Ads', + lessons: [ + { + id: 'c-meta-1', + title: '01. Configuração do Pixel e API de Conversões (CAPI)', + duration: '32m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'Tracking avançado com Google Tag Manager (GTM) e servidor para 100% de precisão nos eventos.', + attachments: [ + { + id: 'meta-att-1', + name: 'Container_GTM_Pre_Configurado_Meta_CAPI.json', + type: 'doc', + url: '#', + size: '220 KB', + description: 'Arquivo JSON pronto para importar no Google Tag Manager com todos os eventos essenciais.' + } + ] + }, + { + id: 'c-meta-2', + title: '02. Criação de Públicos Personalizados e Lookalikes', + duration: '28m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'Construindo públicos de compradores, visitantes de carrinho e engajamento no Instagram.', + attachments: [ + { + id: 'meta-att-2', + name: 'Guia_Estrategico_Publicos_Meta.pdf', + type: 'pdf', + url: '#', + size: '2.1 MB', + description: 'Matriz de segmentação por temperatura de público (Frio, Morno, Quente).' + } + ] + }, + { + id: 'c-meta-3', + title: '03. Testes A/B Científicos de Criativos', + duration: '35m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Metodologia para testar manchetes, ganchos e CTAs sem desperdiçar verba.' + }, + { + id: 'c-meta-4', + title: '04. Estratégias de Remarketing Dinâmico', + duration: '30m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Recuperando vendas com catálogos de produtos e mensagens personalizadas de urgência.' + }, + { + id: 'c-meta-5', + title: '05. Contingência & Aquecimento de Ativos', + duration: '45m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Proteja suas contas de anúncios com uma estrutura de contingência profissional.' + } + ], + tips: [ + "A API de Conversões (CAPI) recupera em média 25% mais dados de eventos perdidos pelo iOS.", + "Teste criativos sempre com público amplo (Broad) para validar a força do anúncio por si só." + ], + attachments: [ + { + id: 'meta-att-global-1', + name: 'Checklist_Contingencia_Meta_Ads.pdf', + type: 'pdf', + url: '#', + size: '1.8 MB', + description: 'Passo a passo para blindar seus BMs, perfis e contas de anúncios.' + }, + { + id: 'meta-att-global-2', + name: 'Planilha_Teste_AB_Criativos.xlsx', + type: 'spreadsheet', + url: '#', + size: '950 KB', + description: 'Acompanhamento do CTR e CPA por variação de criativo testado.' + } + ] + }, + { + id: 'course-google-ads-expert', + title: 'Google Ads Expert: Rede de Pesquisa & Performance Max', + category: 'Google Ads & YouTube', + thumbnail: 'https://images.unsplash.com/photo-1572021335469-31706a17aaef?auto=format&fit=crop&w=800&q=80', + duration: '4 Aulas • 2h 15m', + description: 'Domine a intenção de busca no Google. Como estruturar campanhas de Pesquisa de alto índice de qualidade e campanhas PMax focadas em ROAS.', + instructor: 'Renato Silveira', + instructorRole: 'Google Premier Partner', + lessons: [ + { + id: 'c-goog-1', + title: '01. Índice de Qualidade & Correspondência de Palavras', + duration: '30m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'Como pagar menos por clique e aparecer nas primeiras posições do Google.', + attachments: [ + { + id: 'goog-att-1', + name: 'Guia_Indice_Qualidade_10_de_10.pdf', + type: 'pdf', + url: '#', + size: '3.2 MB', + description: 'As 3 regras para alcançar nota 10/10 no índice de qualidade do Google Ads.' + } + ] + }, + { + id: 'c-goog-2', + title: '02. Configuração Avançada de Performance Max (PMax)', + duration: '42m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'Alimentando o algoritmo com sinais de público precisos e exclusões estratégicas.' + }, + { + id: 'c-goog-3', + title: '03. Google Shopping & Otimização de Títulos no Feed', + duration: '33m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Como ranquear no topo do Google Shopping reestruturando os títulos dos seus produtos.' + }, + { + id: 'c-goog-4', + title: '04. Estratégias de Lances: Maximizar Conversões vs tROAS', + duration: '30m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'O momento exato de transicionar lances manuais para lances automatizados inteligentes.' + } + ], + tips: [ + "Adicione palavras-chave negativas semanalmente na aba de Termos de Pesquisa.", + "No Google Ads, o CTR do anúncio dita mais de 50% do seu custo por clique real." + ], + attachments: [ + { + id: 'goog-att-all', + name: 'Kit_Templates_Anuncios_Google_Search.zip', + type: 'zip', + url: '#', + size: '14.5 MB', + description: 'Modelos de textos, títulos e extensões de anúncio de alta taxa de clique.' + } + ] + }, + { + id: 'course-creative-lab', + title: 'Design & Copywriting de Criativos que Vendem', + category: 'Copywriting & Criativos', + thumbnail: 'https://images.unsplash.com/photo-1542744094-3a31f272c490?auto=format&fit=crop&w=800&q=80', + duration: '4 Aulas • 2h 20m', + description: 'Transforme ideias em criativos de alta conversão. Estrutura de roteiros para UGC, ganchos magnéticos e psicologia de compra.', + instructor: 'Camila Rossi', + instructorRole: 'Diretora de Criação & Copywriter', + lessons: [ + { + id: 'c-creat-1', + title: '01. Os 10 Ganchos (Hooks) Mais Poderosos para Vídeos Curtos', + duration: '35m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'Como prender a atenção do usuário nos primeiros 3 segundos do Reels e TikTok.', + attachments: [ + { + id: 'creat-att-1', + name: '50_Ganchos_Validados_para_Reels_e_TikTok.pdf', + type: 'pdf', + url: '#', + size: '4.7 MB', + description: '50 modelos prontos de frases de impacto para abertura de vídeos de tráfego pago.' + } + ] + }, + { + id: 'c-creat-2', + title: '02. Roteirização de Anúncios UGC (User Generated Content)', + duration: '40m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'Como orientar criadores de conteúdo e clientes para gravarem depoimentos autênticos.' + }, + { + id: 'c-creat-3', + title: '03. Anúncios Estáticos: Tipografia, Contraste e Quebra de Padrão', + duration: '32m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Design estratégico no Feed do Instagram para aumentar a taxa de cliques (CTR).' + }, + { + id: 'c-creat-4', + title: '04. Framework de Análise de Fadiga de Criativo', + duration: '33m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Como identificar quando um anúncio saturou antes de perder dinheiro.' + } + ], + tips: [ + "O gancho do anúncio (primeiros 3 segundos) define mais de 80% do CPA final.", + "Criativos simples com estética nativa (parecidos com stories de amigos) costumam converter melhor que vídeos ultra-produzidos." + ], + attachments: [ + { + id: 'creat-att-all', + name: 'Pack_Figma_Templates_Anuncios_Feed_Stories.zip', + type: 'zip', + url: '#', + size: '32.1 MB', + description: 'Mais de 30 templates editáveis no Figma prontos para exportação.' + } + ] + }, + { + id: 'course-agency-scale', + title: 'Gestão de Agência & Relatórios para Clientes de Tráfego', + category: 'Gestão & Processos de Agência', + thumbnail: 'https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&w=800&q=80', + duration: '4 Aulas • 2h 05m', + description: 'Como precificar seus serviços, reter clientes com relatórios claros de ROI e organizar o fluxo de trabalho da sua equipe.', + instructor: 'Mário Morgado', + instructorRole: 'Fundador & Consultor de Performance', + lessons: [ + { + id: 'c-ag-1', + title: '01. Modelos de Precificação: Fixo + Variável sobre ROAS', + duration: '28m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'Como cobrar comissões sobre resultados sem assumir riscos desnecessários.', + attachments: [ + { + id: 'ag-att-1', + name: 'Contrato_Prestacao_Servicos_Trafego_Pago.docx', + type: 'doc', + url: '#', + size: '145 KB', + description: 'Modelo jurídico completo de contrato de prestação de serviços de gestão de tráfego.' + } + ] + }, + { + id: 'c-ag-2', + title: '02. Onboarding Perfeito de Novos Clientes', + duration: '32m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'O formulário de briefing e alinhamento de expectativas que evita cancelamentos prematuros.' + }, + { + id: 'c-ag-3', + title: '03. Reunião Mensal de Resultados: A Apresentação Ideal', + duration: '35m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Mostrando faturamento real e lucro líquido sem afogar o cliente em termos técnicos.' + }, + { + id: 'c-ag-4', + title: '04. Delegando Tarefas e Contratando Designers/Copywriters', + duration: '30m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Processos de esteira de produção para gerenciar mais de 20 clientes simultâneos.' + } + ], + tips: [ + "Clientes não compram cliques ou impressões; eles compram faturamento previsível e tranquilidade.", + "Envie micro-atualizações semanais pelo WhatsApp com o faturamento gerado para manter o cliente seguro." + ], + attachments: [ + { + id: 'ag-att-pack', + name: 'Kit_Processos_e_Documentos_Agencia.zip', + type: 'zip', + url: '#', + size: '18.3 MB', + description: 'Contratos, briefings de onboarding, planilhas de precificação e apresentações em PowerPoint.' + } + ] + }, + { + id: 'course-funnel-cro', + title: 'Funis de Vendas, CRO & Otimização de Taxa de Conversão', + category: 'Funis de Vendas & ROI', + thumbnail: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=800&q=80', + duration: '3 Aulas • 1h 45m', + description: 'Dobre o resultado do seu tráfego sem gastar 1 real a mais em anúncios otimizando as páginas de destino e o checkout.', + instructor: 'Lucas Farias', + instructorRole: 'Especialista em CRO & Funis', + lessons: [ + { + id: 'c-cro-1', + title: '01. Diagnóstico de Gargalos no Funil de Vendas', + duration: '35m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'Onde seus visitantes estão travando: Página, Carrinho ou Checkout?', + attachments: [ + { + id: 'cro-att-1', + name: 'Planilha_Diagnostico_Funil_Vendas.xlsx', + type: 'spreadsheet', + url: '#', + size: '820 KB', + description: 'Insira suas métricas e descubra automaticamente onde está a maior perda financeira do seu funil.' + } + ] + }, + { + id: 'c-cro-2', + title: '02. Otimização de Landing Pages de Alta Conversão', + duration: '40m', + videoUrl: SAMPLE_VIDEO, + isFree: true, + description: 'Estrutura visual, velocidade de carregamento e provas sociais que convertem.' + }, + { + id: 'c-cro-3', + title: '03. Recuperação de Vendas via WhatsApp & E-mail', + duration: '30m', + videoUrl: SAMPLE_VIDEO, + isFree: false, + description: 'Sequências de mensagens automáticas que recuperam até 35% dos checkouts abandonados.' + } + ], + tips: [ + "Uma página que demora mais de 3 segundos para carregar perde em média 40% dos visitantes do anúncio.", + "Order bumps no checkout com ticket baixo aumentam o faturamento total em até 18% imediatamente." + ], + attachments: [ + { + id: 'cro-att-all', + name: 'Scripts_Recuperacao_Carrinho_WhatsApp.pdf', + type: 'pdf', + url: '#', + size: '1.9 MB', + description: 'Mensagens prontas para copiar e colar na sua automação de recuperação.' + } + ] + } +]; + +export const generateMockSections = (): Section[] => { + return [ + { + title: 'Em Destaque • Tráfego & Performance', + variant: 'landscape', + courses: INITIAL_COURSES + }, + { + title: 'Cursos com Pacotes de Materiais & Planilhas', + variant: 'landscape', + courses: INITIAL_COURSES.filter(c => c.attachments && c.attachments.length > 0) + }, + { + title: 'Formações Recomendadas Compor HUB', + variant: 'portrait', + courses: INITIAL_COURSES + }, + { + title: 'Criativos, Copywriting & Design', + variant: 'landscape', + courses: INITIAL_COURSES.filter(c => c.category.includes('Criativos') || c.category.includes('Funis')) + } + ]; +}; diff --git a/context/AuthContext.tsx b/context/AuthContext.tsx new file mode 100644 index 0000000..e2164b4 --- /dev/null +++ b/context/AuthContext.tsx @@ -0,0 +1,69 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { User, UserRole } from '../types'; + +interface AuthContextType { + user: User | null; + login: (email: string) => boolean; + logout: () => void; + isLoading: boolean; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // Check localStorage on mount with error handling + const storedUser = localStorage.getItem('educlean_user'); + if (storedUser) { + try { + const parsedUser = JSON.parse(storedUser); + if (parsedUser && parsedUser.email) { + setUser(parsedUser); + } + } catch (error) { + console.error("Failed to parse user session, clearing storage:", error); + localStorage.removeItem('educlean_user'); + } + } + setIsLoading(false); + }, []); + + const login = (email: string): boolean => { + let newUser: User | null = null; + + if (email === 'admin@prof.com') { + newUser = { email, name: 'Prof. Anderson', role: 'professor' }; + } else if (email === 'aluno@test.com') { + newUser = { email, name: 'Aluno Demo', role: 'student' }; + } + + if (newUser) { + setUser(newUser); + localStorage.setItem('educlean_user', JSON.stringify(newUser)); + return true; + } + return false; + }; + + const logout = () => { + setUser(null); + localStorage.removeItem('educlean_user'); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; \ No newline at end of file diff --git a/context/ToastContext.tsx b/context/ToastContext.tsx new file mode 100644 index 0000000..f518d8a --- /dev/null +++ b/context/ToastContext.tsx @@ -0,0 +1,124 @@ +import React, { createContext, useContext, useState, useCallback } from 'react'; +import { CheckCircle2, XCircle, Info, Download, X, FileText, Check } from 'lucide-react'; + +export type ToastType = 'success' | 'error' | 'info' | 'download'; + +export interface Toast { + id: string; + message: string; + type: ToastType; + fileName?: string; + fileSize?: string; +} + +export interface ToastContextType { + showToast: (message: string, type?: ToastType, fileName?: string, fileSize?: string) => void; + showDownloadToast: (fileName: string, fileSize?: string) => void; +} + +const ToastContext = createContext(undefined); + +export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [toasts, setToasts] = useState([]); + + const showToast = useCallback((message: string, type: ToastType = 'success', fileName?: string, fileSize?: string) => { + const id = Date.now().toString() + Math.random().toString(36).substring(2, 5); + setToasts((prev) => [...prev, { id, message, type, fileName, fileSize }]); + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, 4000); + }, []); + + const showDownloadToast = useCallback((fileName: string, fileSize?: string) => { + const id = Date.now().toString() + Math.random().toString(36).substring(2, 5); + setToasts((prev) => [...prev, { + id, + message: `O material "${fileName}" foi baixado com sucesso.`, + type: 'download', + fileName, + fileSize + }]); + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, 4500); + }, []); + + const removeToast = (id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }; + + return ( + + {children} +
+ {toasts.map((toast) => { + if (toast.type === 'download') { + return ( +
+ {/* Emerald vertical bar */} +
+ +
+ +
+ +
+
+ + + Download Concluído + +
+

+ {toast.fileName || toast.message} +

+
+ Salvo no seu dispositivo + {toast.fileSize && ( + <> + • + {toast.fileSize} + + )} +
+
+ + +
+ ); + } + + return ( +
+ {toast.type === 'success' && } + {toast.type === 'error' && } + {toast.type === 'info' && } +

{toast.message}

+ +
+ ); + })} +
+
+ ); +}; + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) throw new Error('useToast must be used within ToastProvider'); + return context; +}; + diff --git a/index.css b/index.css new file mode 100644 index 0000000..dbcc999 --- /dev/null +++ b/index.css @@ -0,0 +1,84 @@ +:root { + /* Typography Tokens */ + --font-display: "Manrope", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-body: "Manrope", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + /* Border Radii */ + --radius-tag: 980px; + --radius-button: 980px; + --radius-card: 8px; + --radius-image: 8px; + --radius-input: 8px; + + /* Shadows */ + --shadow-xl: 0 10px 30px -5px rgba(0, 0, 0, 0.3); + + /* Layout */ + --page-max-width: 1440px; +} + +body { + font-family: var(--font-body); + background-color: #000000; + color: #f4f4f5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +h1, h2, h3, h4, h5, h6, .font-display, .font-heading { + font-family: var(--font-display); +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +.hide-scrollbar::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.hide-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +/* Custom scrollbar for modals & panels */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: #09090b; +} + +::-webkit-scrollbar-thumb { + background: #27272a; + border-radius: 980px; +} + +::-webkit-scrollbar-thumb:hover { + background: #3f3f46; +} + +/* Smooth drag cursor */ +.cursor-grab { + cursor: grab; +} +.cursor-grabbing { + cursor: grabbing; + user-select: none; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in-up { + animation: fadeInUp 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..0360fab --- /dev/null +++ b/index.html @@ -0,0 +1,89 @@ + + + + + + Compor HUB | Cursos & Central de Materiais + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/index.tsx b/index.tsx new file mode 100644 index 0000000..39e1012 --- /dev/null +++ b/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..683f7a6 --- /dev/null +++ b/metadata.json @@ -0,0 +1,8 @@ +{ + "name": "Compor HUB", + "description": "Portal de Cursos e Central de Download de Materiais (PDFs, Planilhas, Modelos e Checklists) para Gestão de Tráfego Pago e Performance.", + "requestFramePermissions": [], + "majorCapabilities": [ + "MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API" + ] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..ab05e8a --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "educlean", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "react-router-dom": "6.22.3", + "lucide-react": "0.344.0", + "localforage": "1.10.0", + "@google/genai": "latest" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/pages/ManageCourses.tsx b/pages/ManageCourses.tsx new file mode 100644 index 0000000..cc4732a --- /dev/null +++ b/pages/ManageCourses.tsx @@ -0,0 +1,905 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { + Plus, + Settings, + Users, + BarChart, + Trash2, + Edit2, + X, + MessageSquare, + Send, + Image as ImageIcon, + Video, + Sparkles, + Loader2, + FileText, + FileSpreadsheet, + FileArchive, + Link as LinkIcon, + Paperclip, + Lightbulb, + ListVideo, + Check, + Lock, + Unlock, + UploadCloud, + Globe, + Download +} from 'lucide-react'; +import { Course, Comment, Attachment, Lesson, AttachmentType } from '../types'; +import { useToast } from '../context/ToastContext'; +import { getCourses, saveCourse, deleteCourse, getComments, saveComment } from '../services/db'; +import { generateStudentReply } from '../services/ai'; +import { CATEGORIES } from '../constants'; +import { getAttachmentIcon, getAttachmentBadge } from '../components/MaterialCard'; + +// 1. Create/Edit Course Modal +const CourseModal: React.FC<{ + isOpen: boolean; + onClose: () => void; + onSave: (course: Course) => void; + initialData?: Course; +}> = ({ isOpen, onClose, onSave, initialData }) => { + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [category, setCategory] = useState(''); + const [thumbnailPreview, setThumbnailPreview] = useState(''); + const [instructor, setInstructor] = useState('Mário Morgado'); + const [instructorRole, setInstructorRole] = useState('Especialista em Performance'); + + // Lesson Management + const [lessons, setLessons] = useState([]); + + // New Lesson State + const [newLessonTitle, setNewLessonTitle] = useState(''); + const [newLessonDuration, setNewLessonDuration] = useState(''); + const [newLessonVideoUrl, setNewLessonVideoUrl] = useState(''); + const [newLessonIsFree, setNewLessonIsFree] = useState(false); + const [videoSourceType, setVideoSourceType] = useState<'upload' | 'link'>('upload'); + const [selectedFileName, setSelectedFileName] = useState(''); + const lessonFileInputRef = useRef(null); + + // Tips & Global Attachments + const [tips, setTips] = useState([]); + const [newTip, setNewTip] = useState(''); + const [attachments, setAttachments] = useState([]); + + // New Attachment State + const [newAttName, setNewAttName] = useState(''); + const [newAttType, setNewAttType] = useState('pdf'); + const [newAttUrl, setNewAttUrl] = useState(''); + const [newAttSize, setNewAttSize] = useState(''); + const [newAttDesc, setNewAttDesc] = useState(''); + + const [isSubmitting, setIsSubmitting] = useState(false); + const fileInputRef = useRef(null); + + useEffect(() => { + if (isOpen) { + if (initialData) { + setTitle(initialData.title); + setDescription(initialData.description); + setCategory(initialData.category); + setThumbnailPreview(initialData.thumbnail); + setInstructor(initialData.instructor || 'Mário Morgado'); + setInstructorRole(initialData.instructorRole || 'Especialista em Performance'); + setTips(initialData.tips || []); + setAttachments(initialData.attachments || []); + setLessons(initialData.lessons || []); + } else { + setTitle(''); + setDescription(''); + setCategory(CATEGORIES[0]); + setThumbnailPreview(''); + setInstructor('Mário Morgado'); + setInstructorRole('Especialista em Performance'); + setTips([]); + setAttachments([]); + setLessons([]); + } + } + }, [isOpen, initialData]); + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const objectUrl = URL.createObjectURL(file); + setThumbnailPreview(objectUrl); + } + }; + + const handleLessonVideoFileSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const objectUrl = URL.createObjectURL(file); + setNewLessonVideoUrl(objectUrl); + setSelectedFileName(file.name); + if (!newLessonDuration) setNewLessonDuration('15m'); + } + }; + + const handleAddLesson = () => { + if (!newLessonTitle.trim()) { + alert("Digite um título para a aula."); + return; + } + if (!newLessonVideoUrl.trim()) { + alert("Selecione um vídeo ou insira um link."); + return; + } + + const newLesson: Lesson = { + id: `lesson-${Date.now()}`, + title: newLessonTitle, + duration: newLessonDuration || '15m', + videoUrl: newLessonVideoUrl, + isFree: newLessonIsFree, + description: '' + }; + + setLessons([...lessons, newLesson]); + + setNewLessonTitle(''); + setNewLessonDuration(''); + setNewLessonVideoUrl(''); + setSelectedFileName(''); + setNewLessonIsFree(false); + if (lessonFileInputRef.current) lessonFileInputRef.current.value = ''; + }; + + const removeLesson = (id: string) => { + setLessons(lessons.filter(l => l.id !== id)); + }; + + const toggleLessonAccess = (id: string) => { + setLessons(lessons.map(l => l.id === id ? { ...l, isFree: !l.isFree } : l)); + }; + + const handleAddTip = () => { + if (newTip.trim()) { + setTips([...tips, newTip.trim()]); + setNewTip(''); + } + }; + + const removeTip = (idx: number) => { + setTips(tips.filter((_, i) => i !== idx)); + }; + + const handleAddAttachment = () => { + if (!newAttName.trim()) { + alert("Informe o nome do arquivo para download."); + return; + } + + const att: Attachment = { + id: `att-${Date.now()}`, + name: newAttName.trim(), + type: newAttType, + url: newAttUrl.trim() || '#', + size: newAttSize.trim() || '1.5 MB', + description: newAttDesc.trim() + }; + + setAttachments([...attachments, att]); + setNewAttName(''); + setNewAttSize(''); + setNewAttUrl(''); + setNewAttDesc(''); + }; + + const removeAttachment = (id: string) => { + setAttachments(attachments.filter(a => a.id !== id)); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (lessons.length === 0) { + alert("Adicione pelo menos uma aula à trilha."); + return; + } + + setIsSubmitting(true); + + const durationStr = `${lessons.length} Aulas`; + + onSave({ + id: initialData?.id || `course-${Date.now()}`, + title, + description, + category: category || CATEGORIES[0], + thumbnail: thumbnailPreview || 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=800&q=80', + duration: durationStr, + instructor, + instructorRole, + views: initialData?.views || 0, + progress: initialData?.progress || 0, + lessons: lessons, + tips: tips, + attachments: attachments + }); + + setIsSubmitting(false); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+
+
+ + {/* Left Column: Course Details, Tips & Downloads */} +
+
+

{initialData ? 'Editar Curso' : 'Criar Curso'}

+ +
+ + {/* Thumbnail */} +
+ +
fileInputRef.current?.click()} + > + {thumbnailPreview ? ( + <> + Preview +
+ Alterar Capa +
+ + ) : ( +
+ +

Clique para selecionar imagem

+
+ )} + +
+
+ +
+
+ + setTitle(e.target.value)} + placeholder="Ex: Masterclass: Escala de Tráfego Pago" + className="w-full bg-black/50 text-white text-base font-bold border border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:border-orange-500/50" + required + /> +
+ +
+
+ + +
+ +
+ + setInstructor(e.target.value)} + placeholder="Nome do Instrutor" + className="w-full bg-black/50 text-white border border-white/10 rounded-xl px-3 py-2.5 text-xs focus:outline-none focus:border-orange-500/50" + /> +
+
+ +
+ +