microtecflix/src/App.tsx

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">
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>
);
}