494 lines
19 KiB
TypeScript
494 lines
19 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Play, Shield, User, GraduationCap, ArrowRight, Lock, Mail,
|
|
Plus, CheckCircle, ChevronRight, HelpCircle, Film, Laptop
|
|
} from 'lucide-react';
|
|
import Navbar from './components/Navbar';
|
|
import HeroBanner from './components/HeroBanner';
|
|
import CourseCarousel from './components/CourseCarousel';
|
|
import CourseView from './components/CourseView';
|
|
import SubscriptionView from './components/SubscriptionView';
|
|
import CourseUnlockModal from './components/CourseUnlockModal';
|
|
import CertificatesTab from './components/CertificatesTab';
|
|
import ProfileView from './components/ProfileView';
|
|
import HelpView from './components/HelpView';
|
|
import { User as UserType, Course as CourseType, SubscriptionStatus } from './types';
|
|
|
|
export default function App() {
|
|
const [token, setToken] = useState<string | null>(localStorage.getItem('devflix_token'));
|
|
const [user, setUser] = useState<{
|
|
name: string;
|
|
email: string;
|
|
role: 'admin' | 'student';
|
|
subscriptionStatus: SubscriptionStatus;
|
|
} | null>(null);
|
|
|
|
// App routing
|
|
const [activeView, setView] = useState<string>('home'); // home, course-view, subscription, admin-dashboard, admin-content
|
|
const [selectedCourseId, setSelectedCourseId] = useState<string | null>(null);
|
|
const [unlockingCourse, setUnlockingCourse] = useState<CourseType | null>(null);
|
|
|
|
// Authentication forms
|
|
const [isRegister, setIsRegister] = useState(false);
|
|
const [loginEmail, setLoginEmail] = useState('');
|
|
const [loginPassword, setLoginPassword] = useState('');
|
|
const [regName, setRegName] = useState('');
|
|
const [regEmail, setRegEmail] = useState('');
|
|
const [regPassword, setRegPassword] = useState('');
|
|
const [regBirthDate, setRegBirthDate] = useState('');
|
|
const [regWhatsapp, setRegWhatsapp] = useState('');
|
|
const [authError, setAuthError] = useState<string | null>(null);
|
|
const [authSuccess, setAuthSuccess] = useState<string | null>(null);
|
|
const [authLoading, setAuthLoading] = useState(false);
|
|
|
|
// Course catalogue
|
|
const [courses, setCourses] = useState<(CourseType & { progress?: number; totalLessons?: number; completedLessons?: number })[]>([]);
|
|
const [isLoadingCourses, setIsLoadingCourses] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (token) {
|
|
fetchUserProfile();
|
|
}
|
|
}, [token]);
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
fetchCourses();
|
|
}
|
|
}, [user]);
|
|
|
|
const fetchUserProfile = async () => {
|
|
try {
|
|
const res = await fetch('/api/auth/me', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setUser(data);
|
|
} else {
|
|
// Stale token
|
|
handleLogout();
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load profile:', err);
|
|
}
|
|
};
|
|
|
|
const fetchCourses = async () => {
|
|
setIsLoadingCourses(true);
|
|
try {
|
|
const res = await fetch('/api/courses', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setCourses(data);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch courses:', err);
|
|
} finally {
|
|
setIsLoadingCourses(false);
|
|
}
|
|
};
|
|
|
|
const handleLogin = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setAuthError(null);
|
|
setAuthLoading(true);
|
|
|
|
try {
|
|
const res = await fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: loginEmail, password: loginPassword })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data.error || 'Erro ao efetuar login');
|
|
}
|
|
|
|
localStorage.setItem('devflix_token', data.token);
|
|
setToken(data.token);
|
|
setUser(data.user);
|
|
setLoginEmail('');
|
|
setLoginPassword('');
|
|
} catch (err: any) {
|
|
setAuthError(err.message);
|
|
} finally {
|
|
setAuthLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRegister = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setAuthError(null);
|
|
setAuthSuccess(null);
|
|
setAuthLoading(true);
|
|
|
|
try {
|
|
const res = await fetch('/api/auth/register', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: regName, email: regEmail, password: regPassword, birthDate: regBirthDate, whatsapp: regWhatsapp })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data.error || 'Erro ao efetuar cadastro');
|
|
}
|
|
|
|
localStorage.setItem('devflix_token', data.token);
|
|
setToken(data.token);
|
|
setUser(data.user);
|
|
setRegName('');
|
|
setRegEmail('');
|
|
setRegPassword('');
|
|
} catch (err: any) {
|
|
setAuthError(err.message);
|
|
} finally {
|
|
setAuthLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem('devflix_token');
|
|
setToken(null);
|
|
setUser(null);
|
|
setView('home');
|
|
setSelectedCourseId(null);
|
|
};
|
|
|
|
const handleSelectCourse = (courseId: string) => {
|
|
const selected = courses.find(c => c.id === courseId);
|
|
if (selected && (selected as any).isUnlocked === false) {
|
|
setUnlockingCourse(selected);
|
|
return;
|
|
}
|
|
setSelectedCourseId(courseId);
|
|
setView('course-view');
|
|
};
|
|
|
|
// Group courses by Category
|
|
const categories = Array.from(new Set(courses.map(c => c.category))) as string[];
|
|
|
|
// Preset quick-fill logins for easy testing
|
|
const quickFill = (email: string, pass: string) => {
|
|
setLoginEmail(email);
|
|
setLoginPassword(pass);
|
|
setAuthError(null);
|
|
};
|
|
|
|
// Check if active student is blocked (overdue or cancelled)
|
|
const isBlocked = user?.subscriptionStatus === 'OVERDUE' || user?.subscriptionStatus === 'CANCELED';
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#141414] text-white flex flex-col selection:bg-[#E50914] selection:text-white">
|
|
{token && user ? (
|
|
/* --- AUTHENTICATED AREA --- */
|
|
<>
|
|
<Navbar
|
|
user={user}
|
|
activeView={activeView}
|
|
setView={(v) => { setView(v); setSelectedCourseId(null); }}
|
|
onLogout={handleLogout}
|
|
/>
|
|
|
|
<main className="flex-grow p-4 md:p-8 max-w-7xl mx-auto w-full space-y-8">
|
|
|
|
{/* RENDER VIEW: HOME CATALOGUE */}
|
|
{activeView === 'home' && (
|
|
<div className="space-y-12">
|
|
{/* Hero Banner (Featured last course or first catalogue course) */}
|
|
<HeroBanner
|
|
course={courses.length > 0 ? courses[0] : null}
|
|
onPlay={handleSelectCourse}
|
|
subscriptionStatus={user.subscriptionStatus}
|
|
setView={setView}
|
|
/>
|
|
|
|
{/* Categories Carousels */}
|
|
<div className="space-y-10">
|
|
{isLoadingCourses ? (
|
|
<div className="flex justify-center py-12">
|
|
<div className="w-8 h-8 border-4 border-[#E50914] border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
) : courses.length === 0 ? (
|
|
<div className="text-center text-zinc-500 py-12 border border-zinc-800 rounded-xl bg-[#1c1c1c]">
|
|
Nenhum curso disponível no momento.
|
|
</div>
|
|
) : (
|
|
categories.map(cat => (
|
|
<CourseCarousel
|
|
key={cat}
|
|
title={cat}
|
|
courses={courses.filter(c => c.category === cat)}
|
|
onSelectCourse={handleSelectCourse}
|
|
onUnlockCourse={(course) => setUnlockingCourse(course)}
|
|
subscriptionStatus={user.subscriptionStatus}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* RENDER VIEW: COURSE STUDY ROOM */}
|
|
{activeView === 'course-view' && selectedCourseId && (
|
|
<CourseView
|
|
courseId={selectedCourseId}
|
|
token={token}
|
|
onBack={() => { setView('home'); setSelectedCourseId(null); fetchCourses(); }}
|
|
/>
|
|
)}
|
|
|
|
{/* RENDER VIEW: ACCOUNT & BILLING / SUBSCRIPTION */}
|
|
{activeView === 'subscription' && (
|
|
<SubscriptionView
|
|
user={user}
|
|
token={token}
|
|
onStatusUpdate={(newStatus) => setUser(prev => prev ? { ...prev, subscriptionStatus: newStatus } : null)}
|
|
/>
|
|
)}
|
|
|
|
{/* RENDER VIEW: CERTIFICATES */}
|
|
{activeView === 'certificates' && (
|
|
<CertificatesTab
|
|
token={token}
|
|
studentName={user.name}
|
|
/>
|
|
)}
|
|
|
|
{/* RENDER VIEW: PROFILE */}
|
|
{activeView === 'profile' && (
|
|
<ProfileView
|
|
user={user as any}
|
|
onBack={() => setView('home')}
|
|
/>
|
|
)}
|
|
|
|
{/* RENDER VIEW: HELP */}
|
|
{activeView === 'help' && (
|
|
<HelpView
|
|
token={token}
|
|
onBack={() => setView('home')}
|
|
/>
|
|
)}
|
|
|
|
</main>
|
|
|
|
{/* Unlock Modal */}
|
|
{unlockingCourse && (
|
|
<CourseUnlockModal
|
|
course={unlockingCourse}
|
|
onClose={() => setUnlockingCourse(null)}
|
|
onSuccess={() => {
|
|
setUnlockingCourse(null);
|
|
fetchCourses();
|
|
fetchUserProfile();
|
|
}}
|
|
token={token || ''}
|
|
/>
|
|
)}
|
|
|
|
{/* Footer */}
|
|
<footer className="border-t border-zinc-900 bg-zinc-950 py-8 px-4 text-center text-xs text-zinc-500 space-y-2 mt-12">
|
|
<p className="font-semibold text-zinc-400">MICROTECFLIX - Plataforma de Cursos Profissionalizantes de Informática</p>
|
|
<p className="max-w-md mx-auto">Desenvolvido com foco em alta experiência de estudo para Windows, Pacote Office (Word, Excel, PowerPoint) e informática profissionalizante.</p>
|
|
<p className="text-zinc-600">© 2026 MicrotecFlix Inc. Todos os direitos reservados. Integrado oficialmente com a API Asaas.</p>
|
|
</footer>
|
|
</>
|
|
) : (
|
|
/* --- UNAUTHENTICATED PORTAL (LOGIN / REGISTER) --- */
|
|
<div className="flex-grow flex items-center justify-center p-4 md:p-8 bg-cover bg-center bg-no-repeat relative" style={{ backgroundImage: `linear-gradient(rgba(20,20,20,0.85), rgba(20,20,20,0.85)), url('https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1200&auto=format&fit=crop')` }}>
|
|
<div className="absolute inset-0 bg-[#141414]/90 z-0" />
|
|
|
|
{/* Login Container */}
|
|
<div className="relative z-10 w-full max-w-md glass-card rounded-2xl p-6 md:p-10 space-y-6 shadow-2xl">
|
|
|
|
{/* Logo */}
|
|
<div className="flex flex-col items-center space-y-2 text-center">
|
|
<div className="bg-[#E50914] text-white p-2 rounded flex items-center justify-center shadow-lg">
|
|
<Play className="fill-white w-6 h-6" />
|
|
</div>
|
|
<h1 className="font-display font-black text-2xl tracking-tighter text-[#E50914]">
|
|
MICROTEC<span className="text-white">FLIX</span>
|
|
</h1>
|
|
<p className="text-xs text-zinc-400 font-medium">Acelere seus estudos em informática e pacote Office no estilo Netflix.</p>
|
|
</div>
|
|
|
|
{/* Error indicators */}
|
|
{authError && (
|
|
<div className="bg-red-950/40 border border-red-900/50 text-red-400 p-3 rounded-lg text-xs leading-relaxed text-center">
|
|
{authError}
|
|
</div>
|
|
)}
|
|
|
|
{isRegister ? (
|
|
/* REGISTER FORM */
|
|
<form onSubmit={handleRegister} className="space-y-4">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">Seu Nome Completo</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={regName}
|
|
onChange={(e) => setRegName(e.target.value)}
|
|
placeholder="Seu nome"
|
|
className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">E-mail para Acesso</label>
|
|
<input
|
|
type="email"
|
|
required
|
|
value={regEmail}
|
|
onChange={(e) => setRegEmail(e.target.value)}
|
|
placeholder="email@exemplo.com"
|
|
className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">Data de Nascimento</label>
|
|
<input
|
|
type="date"
|
|
required
|
|
value={regBirthDate}
|
|
onChange={(e) => setRegBirthDate(e.target.value)}
|
|
className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">WhatsApp</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={regWhatsapp}
|
|
onChange={(e) => setRegWhatsapp(e.target.value)}
|
|
placeholder="(11) 99999-9999"
|
|
className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">Crie uma Senha</label>
|
|
<input
|
|
type="password"
|
|
required
|
|
value={regPassword}
|
|
onChange={(e) => setRegPassword(e.target.value)}
|
|
placeholder="Mínimo 6 caracteres"
|
|
className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={authLoading}
|
|
className="w-full glass-btn-primary text-white text-xs font-bold py-3 px-4 rounded-lg flex items-center justify-center space-x-1.5 cursor-pointer disabled:opacity-50"
|
|
>
|
|
<span>{authLoading ? 'Registrando...' : 'Criar Conta & Iniciar Teste'}</span>
|
|
<ArrowRight className="w-4 h-4" />
|
|
</button>
|
|
|
|
<p className="text-center text-xs text-zinc-500">
|
|
Já possui cadastro?{' '}
|
|
<button
|
|
type="button"
|
|
onClick={() => { setIsRegister(false); setAuthError(null); }}
|
|
className="text-white hover:underline font-bold"
|
|
>
|
|
Fazer Login
|
|
</button>
|
|
</p>
|
|
</form>
|
|
) : (
|
|
/* LOGIN FORM */
|
|
<form onSubmit={handleLogin} className="space-y-4">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">E-mail</label>
|
|
<div className="relative">
|
|
<Mail className="absolute left-3 top-3 w-4 h-4 text-zinc-500" />
|
|
<input
|
|
type="email"
|
|
required
|
|
value={loginEmail}
|
|
onChange={(e) => setLoginEmail(e.target.value)}
|
|
placeholder="email@exemplo.com"
|
|
className="w-full glass-input rounded-lg p-3 pl-10 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">Senha</label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-3 w-4 h-4 text-zinc-500" />
|
|
<input
|
|
type="password"
|
|
required
|
|
value={loginPassword}
|
|
onChange={(e) => setLoginPassword(e.target.value)}
|
|
placeholder="Digite sua senha"
|
|
className="w-full glass-input rounded-lg p-3 pl-10 text-xs text-white focus:outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={authLoading}
|
|
className="w-full glass-btn-primary text-white text-xs font-bold py-3 px-4 rounded-lg flex items-center justify-center space-x-1.5 cursor-pointer disabled:opacity-50"
|
|
>
|
|
<span>{authLoading ? 'Autenticando...' : 'Entrar na Plataforma'}</span>
|
|
<ArrowRight className="w-4 h-4" />
|
|
</button>
|
|
|
|
<p className="text-center text-xs text-zinc-500">
|
|
Novo por aqui?{' '}
|
|
<button
|
|
type="button"
|
|
onClick={() => { setIsRegister(true); setAuthError(null); }}
|
|
className="text-white hover:underline font-bold"
|
|
>
|
|
Registre-se gratuitamente
|
|
</button>
|
|
</p>
|
|
|
|
{/* QUICK PRESETS FOR SANDBOX DEMO */}
|
|
<div className="border-t border-white/10 pt-4 space-y-3">
|
|
<p className="text-center text-[10px] text-zinc-500 font-bold uppercase tracking-wider">Acesso de Teste Rápido (Iframe Sandbox)</p>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => quickFill('aluno@microtecflix.com', 'aluno123')}
|
|
className="glass-btn-secondary text-zinc-300 font-semibold p-2 rounded text-[10px] transition-colors"
|
|
>
|
|
Logar como Aluno
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => quickFill('sidney.gomes1989@gmail.com', 'admin123')}
|
|
className="glass-btn-secondary text-zinc-300 font-semibold p-2 rounded text-[10px] transition-colors"
|
|
>
|
|
Logar como Admin
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|