858 lines
39 KiB
TypeScript
858 lines
39 KiB
TypeScript
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import {
|
|
Play, Shield, User, GraduationCap, ArrowRight, Lock, Mail,
|
|
Plus, CheckCircle, ChevronRight, HelpCircle, Film, Laptop, Eye, EyeOff
|
|
} 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 CustomModal from './components/CustomModal';
|
|
import { User as UserType, Course as CourseType, SubscriptionStatus } from './types';
|
|
|
|
export default function App() {
|
|
const [token, setToken] = useState<string | null>(localStorage.getItem('app_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);
|
|
const [expandedCourseId, setExpandedCourseId] = useState<string | null>(null);
|
|
const [selectedLessonId, setSelectedLessonId] = useState<string | null>(null);
|
|
const lessonShelfRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
|
|
// Authentication forms
|
|
const [isRegister, setIsRegister] = useState(false);
|
|
const [isForgotPassword, setIsForgotPassword] = useState(false);
|
|
const [resetToken, setResetToken] = useState<string | null>(null);
|
|
const [loginEmail, setLoginEmail] = useState('');
|
|
const [loginPassword, setLoginPassword] = useState('');
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showRegPassword, setShowRegPassword] = useState(false);
|
|
const [regName, setRegName] = useState('');
|
|
const [regEmail, setRegEmail] = useState('');
|
|
const [regPassword, setRegPassword] = useState('');
|
|
const [regCpf, setRegCpf] = 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);
|
|
const [settings, setSettings] = useState<{ brandName?: string; brandSlogan?: string; cnpj?: string; address?: string; termsOfUse?: string; privacyPolicy?: string; featuredCourseId?: string | null; heroImageUrl?: string | null; heroTitle?: string | null; heroDescription?: string | null } | null>(null);
|
|
const [infoModal, setInfoModal] = useState<{isOpen: boolean, title: string, content: string}>({isOpen: false, title: '', content: ''});
|
|
|
|
useEffect(() => {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const tokenParam = urlParams.get('token');
|
|
if (window.location.pathname === '/reset-password' && tokenParam) {
|
|
setResetToken(tokenParam);
|
|
setIsForgotPassword(true);
|
|
}
|
|
|
|
fetch('/api/settings')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
setSettings(data);
|
|
if (data.brandName) {
|
|
document.title = `${data.brandName}FLIX - Plataforma de Estudos`;
|
|
}
|
|
})
|
|
.catch(err => console.error('Error fetching settings:', err));
|
|
}, []);
|
|
|
|
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 handleForgotPassword = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setAuthError(null);
|
|
setAuthSuccess(null);
|
|
setAuthLoading(true);
|
|
try {
|
|
const res = await fetch('/api/auth/forgot-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ identifier: loginEmail, channel: loginEmail.includes('@') ? 'email' : 'whatsapp' })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Erro ao solicitar recuperação.');
|
|
setAuthSuccess(data.message || 'Link de recuperação enviado.');
|
|
} catch (err: any) {
|
|
setAuthError(err.message);
|
|
} finally {
|
|
setAuthLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleResetPassword = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (regPassword !== regPassword.trim() || regPassword.length < 6) {
|
|
setAuthError('A senha deve ter no mínimo 6 caracteres.');
|
|
return;
|
|
}
|
|
setAuthError(null);
|
|
setAuthSuccess(null);
|
|
setAuthLoading(true);
|
|
try {
|
|
const res = await fetch('/api/auth/reset-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token: resetToken, newPassword: regPassword })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Erro ao redefinir senha.');
|
|
setAuthSuccess(data.message || 'Senha redefinida com sucesso! Redirecionando...');
|
|
setTimeout(() => {
|
|
window.location.href = '/';
|
|
}, 3000);
|
|
} catch (err: any) {
|
|
setAuthError(err.message);
|
|
} finally {
|
|
setAuthLoading(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('app_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, cpf: regCpf, birthDate: regBirthDate, whatsapp: regWhatsapp })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data.error || 'Erro ao efetuar cadastro');
|
|
}
|
|
|
|
localStorage.setItem('app_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('app_token');
|
|
setToken(null);
|
|
setUser(null);
|
|
setView('home');
|
|
setSelectedCourseId(null);
|
|
setExpandedCourseId(null);
|
|
setSelectedLessonId(null);
|
|
};
|
|
|
|
const handleSelectCourse = (courseId: string) => {
|
|
const selected = courses.find(c => c.id === courseId);
|
|
if (selected && (selected as any).isUnlocked === false) {
|
|
setUnlockingCourse(selected);
|
|
return;
|
|
}
|
|
if (expandedCourseId === courseId) {
|
|
setExpandedCourseId(null);
|
|
} else {
|
|
setExpandedCourseId(courseId);
|
|
// Smooth scroll to lesson shelf after brief render delay
|
|
setTimeout(() => {
|
|
const el = lessonShelfRefs.current[courseId];
|
|
if (el) {
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}, 80);
|
|
}
|
|
};
|
|
|
|
// 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}
|
|
settings={settings}
|
|
/>
|
|
|
|
<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={
|
|
(settings?.featuredCourseId && courses.find(c => c.id === settings.featuredCourseId))
|
|
|| (courses.length > 0 ? courses[0] : null)
|
|
}
|
|
customHeroTitle={settings?.heroTitle}
|
|
customHeroDescription={settings?.heroDescription}
|
|
customHeroImage={settings?.heroImageUrl}
|
|
onPlay={(courseId) => {
|
|
const selected = courses.find(c => c.id === courseId);
|
|
if (selected && (selected as any).isUnlocked === false) {
|
|
setUnlockingCourse(selected);
|
|
return;
|
|
}
|
|
setSelectedCourseId(courseId);
|
|
const firstLesson = selected?.lessons && selected.lessons.length > 0 ? selected.lessons[0].id : null;
|
|
setSelectedLessonId(firstLesson);
|
|
setView('course-view');
|
|
}}
|
|
onDetail={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 => {
|
|
const categoryCourses = courses.filter(c => c.category === cat);
|
|
const expandedCourse = categoryCourses.find(c => c.id === expandedCourseId);
|
|
|
|
return (
|
|
<div key={cat} className="space-y-4">
|
|
<CourseCarousel
|
|
title={cat}
|
|
courses={categoryCourses}
|
|
onSelectCourse={handleSelectCourse}
|
|
onUnlockCourse={(course) => setUnlockingCourse(course)}
|
|
subscriptionStatus={user.subscriptionStatus}
|
|
/>
|
|
|
|
{/* Expanded Lessons Shelf */}
|
|
{expandedCourse && expandedCourse.lessons && expandedCourse.lessons.length > 0 && (
|
|
<div
|
|
ref={(el) => { lessonShelfRefs.current[expandedCourse.id] = el; }}
|
|
className="lesson-shelf-enter bg-[#141414]/95 border border-white/5 rounded-2xl p-5 space-y-4 shadow-2xl relative overflow-hidden"
|
|
>
|
|
{/* Red accent left bar */}
|
|
<div className="absolute top-0 left-0 w-1.5 h-full bg-[#E50914] rounded-l-2xl" />
|
|
|
|
{/* Header: course thumbnail + title + close */}
|
|
<div className="flex items-center justify-between pl-3 gap-3">
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
{/* Course miniature */}
|
|
<img
|
|
src={expandedCourse.thumbnail}
|
|
alt={expandedCourse.title}
|
|
className="w-14 h-20 object-cover rounded-lg border border-white/10 flex-shrink-0 shadow-lg"
|
|
referrerPolicy="no-referrer"
|
|
/>
|
|
<div className="min-w-0">
|
|
<h3 className="text-[10px] font-extrabold text-[#E50914] uppercase tracking-widest">
|
|
Aulas do Curso: <span className="text-white">{expandedCourse.title}</span>
|
|
</h3>
|
|
<p className="text-[10px] text-zinc-500 mt-0.5">{expandedCourse.lessons.length} aulas disponíveis</p>
|
|
|
|
{(expandedCourse as any).studyMaterialUrl && (
|
|
<a
|
|
href={(expandedCourse as any).studyMaterialUrl}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="inline-flex items-center gap-1.5 mt-2 bg-[#E50914]/10 hover:bg-[#E50914]/20 border border-[#E50914]/30 text-[#E50914] text-[10px] font-bold px-2.5 py-1 rounded-md transition-colors"
|
|
>
|
|
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
|
Baixar Material (PDF)
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setExpandedCourseId(null)}
|
|
className="text-zinc-400 hover:text-white p-2 rounded-full hover:bg-zinc-800/50 transition-colors text-sm font-bold flex-shrink-0"
|
|
title="Fechar"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
{/* Lessons carousel - portrait cards */}
|
|
<div className="flex overflow-x-auto gap-4 py-2 pl-3 custom-scrollbar snap-x scroll-smooth">
|
|
{expandedCourse.lessons.map(lesson => (
|
|
<div
|
|
key={lesson.id}
|
|
onClick={() => {
|
|
setSelectedCourseId(expandedCourse.id);
|
|
setSelectedLessonId(lesson.id);
|
|
setView('course-view');
|
|
}}
|
|
className="flex-shrink-0 w-44 glass-card glass-card-hover rounded-xl overflow-hidden cursor-pointer snap-start transition-all duration-300 group/lesson flex flex-col"
|
|
>
|
|
{/* Portrait thumbnail */}
|
|
<div className="relative w-full aspect-[3/4] bg-zinc-950 overflow-hidden flex-shrink-0">
|
|
{lesson.thumbnailUrl ? (
|
|
<img
|
|
src={lesson.thumbnailUrl}
|
|
alt={lesson.title}
|
|
className="w-full h-full object-cover transition-transform duration-500 group-hover/lesson:scale-110"
|
|
referrerPolicy="no-referrer"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full bg-gradient-to-br from-zinc-800 via-zinc-900 to-zinc-950 flex flex-col items-center justify-center p-3 text-center">
|
|
<span className="text-[9px] text-[#E50914] font-extrabold tracking-widest uppercase mb-1.5 bg-[#E50914]/10 px-2 py-0.5 rounded-full border border-[#E50914]/20">Aula {lesson.order}</span>
|
|
<span className="text-xs text-white/50 font-semibold line-clamp-3 leading-snug">{lesson.title}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Play overlay */}
|
|
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover/lesson:opacity-100 transition-opacity flex items-center justify-center">
|
|
<div className="bg-[#E50914] text-white p-2.5 rounded-full transform scale-75 group-hover/lesson:scale-100 transition-all duration-300 shadow-lg">
|
|
<svg className="w-5 h-5 fill-white" viewBox="0 0 24 24">
|
|
<path d="M8 5v14l11-7z"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order badge */}
|
|
<span className="absolute top-2 left-2 bg-black/70 text-[#E50914] text-[9px] font-extrabold px-1.5 py-0.5 rounded backdrop-blur-sm border border-white/10">
|
|
#{lesson.order}
|
|
</span>
|
|
|
|
{/* Progress Bar Overlay */}
|
|
{(lesson as any).progress !== undefined && (
|
|
<div className="absolute bottom-0 left-0 w-full h-1.5 bg-black/80">
|
|
<div
|
|
className="h-full bg-[#E50914] transition-all duration-300"
|
|
style={{ width: `${(lesson as any).progress.watchedPercentage}%` }}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Info below */}
|
|
<div className="p-2.5 flex-grow">
|
|
<h4 className="text-[11px] font-bold text-white group-hover/lesson:text-[#E50914] transition-colors line-clamp-2 leading-snug">
|
|
{lesson.title}
|
|
</h4>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* RENDER VIEW: COURSE STUDY ROOM */}
|
|
{activeView === 'course-view' && selectedCourseId && (
|
|
<CourseView
|
|
courseId={selectedCourseId}
|
|
token={token}
|
|
initialLessonId={selectedLessonId}
|
|
onBack={() => { setView('home'); setSelectedCourseId(null); setSelectedLessonId(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')}
|
|
onUpdateUser={(updated) => setUser(updated)}
|
|
/>
|
|
)}
|
|
|
|
{/* RENDER VIEW: HELP */}
|
|
{activeView === 'help' && (
|
|
<HelpView
|
|
token={token}
|
|
onBack={() => setView('home')}
|
|
/>
|
|
)}
|
|
|
|
</main>
|
|
|
|
{/* Unlock Modal */}
|
|
{unlockingCourse && (
|
|
<CourseUnlockModal
|
|
course={unlockingCourse}
|
|
user={user}
|
|
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">{(settings?.brandName || 'Plataforma')}FLIX - Plataforma de Cursos Profissionalizantes</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>
|
|
{settings?.cnpj && (
|
|
<p className="text-[11px] text-zinc-650">CNPJ: {settings.cnpj} • Endereço: {settings.address}</p>
|
|
)}
|
|
<div className="flex items-center justify-center gap-4 mt-2">
|
|
<button
|
|
onClick={() => setInfoModal({isOpen: true, title: 'Termos de Uso', content: settings?.termsOfUse || 'Termos de uso não definidos.'})}
|
|
className="text-[11px] hover:text-white transition-colors underline"
|
|
>
|
|
Termos de Uso
|
|
</button>
|
|
<button
|
|
onClick={() => setInfoModal({isOpen: true, title: 'Política de Privacidade', content: settings?.privacyPolicy || 'Política de privacidade não definida.'})}
|
|
className="text-[11px] hover:text-white transition-colors underline"
|
|
>
|
|
Política de Privacidade
|
|
</button>
|
|
</div>
|
|
<p className="text-zinc-600 mt-2">© 2026 {(settings?.brandName || 'Plataforma')}FLIX Inc. Todos os direitos reservados. Integrado oficialmente com a API Asaas.</p>
|
|
</footer>
|
|
|
|
{/* Info Modal for Terms and Privacy */}
|
|
<CustomModal
|
|
isOpen={infoModal.isOpen}
|
|
onClose={() => setInfoModal({...infoModal, isOpen: false})}
|
|
title={infoModal.title}
|
|
isGlass={true}
|
|
>
|
|
<div className="whitespace-pre-wrap text-sm text-zinc-300">
|
|
{infoModal.content}
|
|
</div>
|
|
</CustomModal>
|
|
</>
|
|
) : (
|
|
/* --- 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]">
|
|
{(settings?.brandName || 'Plataforma')}<span className="text-white">FLIX</span>
|
|
</h1>
|
|
<p className="text-xs text-zinc-400 font-medium">
|
|
{settings?.brandSlogan || 'Acelere seus estudos em informática e pacote Office no estilo Netflix.'}
|
|
</p>
|
|
</div>
|
|
|
|
{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>
|
|
)}
|
|
|
|
{authSuccess && (
|
|
<div className="bg-emerald-950/40 border border-emerald-900/50 text-emerald-400 p-3 rounded-lg text-xs leading-relaxed text-center">
|
|
{authSuccess}
|
|
</div>
|
|
)}
|
|
|
|
{resetToken ? (
|
|
/* RESET PASSWORD FORM */
|
|
<form onSubmit={handleResetPassword} className="space-y-4">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">Nova Senha</label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-3 w-4 h-4 text-zinc-500" />
|
|
<input
|
|
type={showPassword ? "text" : "password"}
|
|
required
|
|
value={regPassword}
|
|
onChange={(e) => setRegPassword(e.target.value)}
|
|
placeholder="Mínimo 6 caracteres"
|
|
className="w-full glass-input rounded-lg p-3 pl-10 pr-10 text-xs text-white focus:outline-none"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-3 text-zinc-500 hover:text-white transition-colors"
|
|
>
|
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</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 ? 'Redefinindo...' : 'Salvar Nova Senha'}</span>
|
|
</button>
|
|
</form>
|
|
) : isForgotPassword ? (
|
|
/* FORGOT PASSWORD FORM */
|
|
<form onSubmit={handleForgotPassword} className="space-y-4">
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">E-mail ou WhatsApp (com DDI ex: 5511999999999)</label>
|
|
<div className="relative">
|
|
<Mail className="absolute left-3 top-3 w-4 h-4 text-zinc-500" />
|
|
<input
|
|
type="text"
|
|
required
|
|
value={loginEmail}
|
|
onChange={(e) => setLoginEmail(e.target.value)}
|
|
placeholder="email@exemplo.com ou 5511999999999"
|
|
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 ? 'Enviando...' : 'Receber Link de Recuperação'}</span>
|
|
</button>
|
|
<p className="text-center text-xs text-zinc-500">
|
|
Lembrou a senha?{' '}
|
|
<button
|
|
type="button"
|
|
onClick={() => { setIsForgotPassword(false); setAuthError(null); setAuthSuccess(null); }}
|
|
className="text-white hover:underline font-bold"
|
|
>
|
|
Fazer Login
|
|
</button>
|
|
</p>
|
|
</form>
|
|
) : 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">CPF</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={regCpf}
|
|
onChange={(e) => setRegCpf(e.target.value)}
|
|
placeholder="000.000.000-00"
|
|
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>
|
|
<div className="relative">
|
|
<input
|
|
type={showRegPassword ? "text" : "password"}
|
|
required
|
|
value={regPassword}
|
|
onChange={(e) => setRegPassword(e.target.value)}
|
|
placeholder="Mínimo 6 caracteres"
|
|
className="w-full glass-input rounded-lg p-3 pr-10 text-xs text-white focus:outline-none"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowRegPassword(!showRegPassword)}
|
|
className="absolute right-3 top-2.5 text-zinc-500 hover:text-white transition-colors"
|
|
>
|
|
{showRegPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</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 ? '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 ou WhatsApp</label>
|
|
<div className="relative">
|
|
<Mail className="absolute left-3 top-3 w-4 h-4 text-zinc-500" />
|
|
<input
|
|
type="text"
|
|
required
|
|
value={loginEmail}
|
|
onChange={(e) => setLoginEmail(e.target.value)}
|
|
placeholder="email@exemplo.com ou 5585981145217"
|
|
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={showPassword ? "text" : "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 pr-10 text-xs text-white focus:outline-none"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-3 text-zinc-500 hover:text-white transition-colors animate-fade-in"
|
|
>
|
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
<button
|
|
type="button"
|
|
onClick={() => { setIsForgotPassword(true); setAuthError(null); setAuthSuccess(null); }}
|
|
className="text-[10px] text-zinc-400 hover:text-white transition-colors"
|
|
>
|
|
Esqueci minha senha
|
|
</button>
|
|
</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="flex justify-center">
|
|
<button
|
|
type="button"
|
|
onClick={() => quickFill('aluno@microtecflix.com', 'aluno123')}
|
|
className="w-full glass-btn-secondary text-zinc-300 font-semibold p-2.5 rounded text-[10px] transition-colors text-center"
|
|
>
|
|
Logar como Aluno
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|