babce35231
Adds OneSignal for push notifications and new AI strategy parameters like trailing stop, winrate learning, and multi-time frame analysis. Also updates build scripts and dependencies.
683 lines
31 KiB
TypeScript
683 lines
31 KiB
TypeScript
/**
|
|
* @license
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Navbar } from './components/Navbar';
|
|
import { AnalysisView } from './components/AnalysisView';
|
|
import { SignalHistory } from './components/SignalHistory';
|
|
import { DashboardStats } from './components/DashboardStats';
|
|
import { PlansView } from './components/PlansView';
|
|
import { AdminDashboard } from './components/AdminDashboard';
|
|
import { LandingPage } from './components/LandingPage';
|
|
import { ProfileView } from './components/ProfileView';
|
|
import { NotificationManager } from './components/NotificationManager';
|
|
import { MaintenancePage } from './components/MaintenancePage';
|
|
import { LiveMarketTicker } from './components/LiveMarketTicker';
|
|
import { TrendingUp, ShieldAlert, Ghost, Mail, Lock, UserPlus, LogIn, Loader2, ArrowLeft, User as UserIcon, Eye, EyeOff } from 'lucide-react';
|
|
import { motion, AnimatePresence } from 'motion/react';
|
|
import { onAuthStateChanged, signInWithEmailAndPassword, createUserWithEmailAndPassword, updateProfile, signOut, sendEmailVerification, sendPasswordResetEmail, User, setPersistence, browserLocalPersistence, browserSessionPersistence } from 'firebase/auth';
|
|
import { doc, setDoc, getDoc, updateDoc, onSnapshot } from 'firebase/firestore';
|
|
import { auth, db, handleFirestoreError, OperationType } from './lib/firebase';
|
|
import { cn } from './lib/utils';
|
|
|
|
declare global {
|
|
interface Window {
|
|
OneSignalDeferred: any[];
|
|
}
|
|
}
|
|
|
|
export default function App() {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [userData, setUserData] = useState<any>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [activeTab, setActiveTab] = useState('scan');
|
|
const [maintenanceMode, setMaintenanceMode] = useState(false);
|
|
const [maintenanceMessage, setMaintenanceMessage] = useState('');
|
|
|
|
// Auth & View State
|
|
const [isSecretAdminRoute, setIsSecretAdminRoute] = useState(() => window.location.pathname === '/fxbros-admin' || window.location.hash === '#/fxbros-admin');
|
|
const [showAuth, setShowAuth] = useState(isSecretAdminRoute);
|
|
const [isLogin, setIsLogin] = useState(true);
|
|
const [showVerification, setShowVerification] = useState(false);
|
|
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
|
const [resetEmailSent, setResetEmailSent] = useState(false);
|
|
const [verificationEmail, setVerificationEmail] = useState('');
|
|
const [name, setName] = useState('');
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [repeatPassword, setRepeatPassword] = useState('');
|
|
const [authError, setAuthError] = useState<string | null>(null);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [rememberMe, setRememberMe] = useState(true);
|
|
|
|
const [unauthView, setUnauthView] = useState<'hero' | 'plans'>('hero');
|
|
|
|
useEffect(() => {
|
|
// Listen for settings changes
|
|
const settingsRef = doc(db, 'settings', 'app');
|
|
const unsubSettings = onSnapshot(settingsRef, (docSnap) => {
|
|
if (docSnap.exists()) {
|
|
setMaintenanceMode(docSnap.data().maintenanceMode || false);
|
|
setMaintenanceMessage(docSnap.data().maintenanceMessage || '');
|
|
}
|
|
}, (error) => {
|
|
console.warn("Failed to listen to settings:", error);
|
|
});
|
|
|
|
// Initialize OneSignal via window object
|
|
if (import.meta.env.VITE_ONESIGNAL_APP_ID) {
|
|
window.OneSignalDeferred = window.OneSignalDeferred || [];
|
|
window.OneSignalDeferred.push(async function(OneSignal: any) {
|
|
await OneSignal.init({
|
|
appId: import.meta.env.VITE_ONESIGNAL_APP_ID,
|
|
allowLocalhostAsSecureOrigin: true,
|
|
});
|
|
OneSignal.Slidedown.promptPush();
|
|
});
|
|
}
|
|
|
|
return () => unsubSettings();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
|
|
if (currentUser && currentUser.emailVerified) {
|
|
try {
|
|
const userRef = doc(db, 'users', currentUser.uid);
|
|
const userSnap = await getDoc(userRef).catch(err => {
|
|
handleFirestoreError(err, OperationType.GET, 'users/' + currentUser.uid);
|
|
throw err;
|
|
});
|
|
|
|
let data = {
|
|
uid: currentUser.uid,
|
|
email: currentUser.email,
|
|
name: currentUser.displayName || 'User',
|
|
isAdmin: currentUser.email === "fxbrosinvestments00@gmail.com",
|
|
isPremium: false,
|
|
plan: 'basic' as const,
|
|
analysisLimit: 8,
|
|
isApproved: currentUser.email === "fxbrosinvestments00@gmail.com"
|
|
};
|
|
|
|
if (userSnap.exists()) {
|
|
data = { ...data, ...userSnap.data() };
|
|
|
|
// Force admin rights for root email
|
|
if (currentUser.email === "fxbrosinvestments00@gmail.com") {
|
|
data.isAdmin = true;
|
|
data.isApproved = true;
|
|
|
|
if (userSnap.data().isAdmin !== true || userSnap.data().isApproved !== true) {
|
|
try {
|
|
await updateDoc(userRef, { isAdmin: true, isApproved: true });
|
|
} catch (e) {
|
|
console.error("Failed to update root admin privileges:", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!data.isApproved && !data.isAdmin) {
|
|
await signOut(auth);
|
|
setAuthError('Sua conta foi criada. Aguarde aprovação manual do administrador.');
|
|
setShowAuth(true);
|
|
setIsLogin(true);
|
|
setUser(null);
|
|
setUserData(null);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
setUserData(data);
|
|
} else {
|
|
await setDoc(userRef, data).catch(err => {
|
|
handleFirestoreError(err, OperationType.WRITE, 'users/' + currentUser.uid);
|
|
throw err;
|
|
});
|
|
if (!data.isApproved && !data.isAdmin) {
|
|
await signOut(auth);
|
|
setAuthError('Sua conta foi criada. Aguarde aprovação manual do administrador.');
|
|
setShowAuth(true);
|
|
setIsLogin(true);
|
|
setUser(null);
|
|
setUserData(null);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
setUserData(data);
|
|
}
|
|
} catch (err) {
|
|
console.error("Firestore user initialization error:", err);
|
|
// Set minimal data to allow UI to render, but it might be limited due to rules
|
|
setUserData({ isPremium: false, isAdmin: currentUser.email === "fxbrosinvestments00@gmail.com", isApproved: false });
|
|
}
|
|
|
|
setUser(currentUser);
|
|
setUnauthView('hero');
|
|
setShowAuth(false);
|
|
} else {
|
|
setUser(null);
|
|
setUserData(null);
|
|
}
|
|
setLoading(false);
|
|
});
|
|
|
|
return () => unsubscribe();
|
|
}, []);
|
|
|
|
const isAdmin = user?.email === "fxbrosinvestments00@gmail.com" || userData?.isAdmin;
|
|
|
|
const handleResetPassword = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setIsSubmitting(true);
|
|
setAuthError(null);
|
|
try {
|
|
const targetEmail = email.toLowerCase() === 'admin' ? 'fxbrosinvestments00@gmail.com' : email;
|
|
await sendPasswordResetEmail(auth, targetEmail);
|
|
setResetEmailSent(true);
|
|
} catch (error: any) {
|
|
console.error(error);
|
|
setAuthError('Falha ao enviar e-mail de recuperação.');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleAuth = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setIsSubmitting(true);
|
|
setAuthError(null);
|
|
|
|
try {
|
|
await setPersistence(auth, rememberMe ? browserLocalPersistence : browserSessionPersistence);
|
|
if (isLogin) {
|
|
const targetEmail = email.toLowerCase() === 'admin' ? 'fxbrosinvestments00@gmail.com' : email;
|
|
const userCredential = await signInWithEmailAndPassword(auth, targetEmail, password);
|
|
if (!userCredential.user.emailVerified) {
|
|
try {
|
|
await sendEmailVerification(userCredential.user);
|
|
} catch (e) {
|
|
console.error("Verification email send error:", e);
|
|
}
|
|
await signOut(auth);
|
|
setVerificationEmail(email);
|
|
setShowVerification(true);
|
|
}
|
|
} else {
|
|
if (password !== repeatPassword) {
|
|
setAuthError('Passwords do not match.');
|
|
setIsSubmitting(false);
|
|
return;
|
|
}
|
|
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
|
|
await updateProfile(userCredential.user, { displayName: name });
|
|
try {
|
|
await setDoc(doc(db, 'users', userCredential.user.uid), {
|
|
uid: userCredential.user.uid,
|
|
email: userCredential.user.email,
|
|
name: name,
|
|
isAdmin: userCredential.user.email === "fxbrosinvestments00@gmail.com",
|
|
isPremium: false,
|
|
plan: 'basic',
|
|
analysisLimit: 8,
|
|
isApproved: userCredential.user.email === "fxbrosinvestments00@gmail.com"
|
|
});
|
|
} catch (e) {
|
|
console.error("Failed to write new user to DB", e);
|
|
}
|
|
try {
|
|
await sendEmailVerification(userCredential.user);
|
|
} catch (e) {
|
|
console.error("Verification email send error:", e);
|
|
}
|
|
await signOut(auth);
|
|
setVerificationEmail(email);
|
|
setShowVerification(true);
|
|
}
|
|
} catch (error: any) {
|
|
console.error("Auth Error:", error);
|
|
if (isLogin) {
|
|
setAuthError('Email or Password Incorrect');
|
|
} else {
|
|
if (error.code === 'auth/email-already-in-use') {
|
|
setAuthError('User already exists. Sign in?');
|
|
setIsLogin(true);
|
|
} else {
|
|
setAuthError('Failed to register.');
|
|
}
|
|
}
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
signOut(auth);
|
|
setShowAuth(false);
|
|
setUnauthView('hero');
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="h-screen w-full flex flex-col items-center justify-center bg-brand-dark gap-6">
|
|
<img
|
|
src="https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png"
|
|
alt="Logo"
|
|
className="w-24 h-24 object-contain animate-pulse"
|
|
/>
|
|
<div className="w-10 h-10 border-4 border-brand-red border-t-transparent rounded-full animate-spin"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Se estiver em manutenção e o usuário não for admin, mostra a página de manutenção
|
|
if (maintenanceMode && !isAdmin) {
|
|
return <MaintenancePage message={maintenanceMessage} />;
|
|
}
|
|
|
|
if (!user) {
|
|
if (!showAuth) {
|
|
if (unauthView === 'hero') {
|
|
return <LandingPage onGetStarted={() => setShowAuth(true)} onViewPlans={() => setUnauthView('plans')} />;
|
|
}
|
|
return (
|
|
<div className="min-h-screen">
|
|
<PlansView
|
|
isUnauthenticated={true}
|
|
onGetStarted={() => setShowAuth(true)}
|
|
onBack={() => setUnauthView('hero')}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<main className="min-h-screen bg-brand-dark flex flex-col items-center justify-center p-6 selection:bg-brand-red selection:text-white relative overflow-hidden">
|
|
{/* Background Image for Login */}
|
|
<div className="absolute inset-0 z-0 pointer-events-none">
|
|
<img
|
|
src="https://i.ibb.co/BRvFhtp/Chat-GPT-Image-28-de-abr-de-2026-12-49-43.png"
|
|
alt="Login Background"
|
|
className="w-full h-full object-cover opacity-80"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-b from-brand-dark/40 via-brand-dark/20 to-brand-dark" />
|
|
</div>
|
|
|
|
<div className="max-w-[400px] w-full space-y-6 relative z-10">
|
|
<button
|
|
onClick={() => setShowAuth(false)}
|
|
className="flex items-center gap-2 text-zinc-500 hover:text-white text-xs font-bold transition-colors mb-4"
|
|
>
|
|
<ArrowLeft size={16} /> VOLTAR PARA O INÍCIO
|
|
</button>
|
|
|
|
<div className="text-center space-y-2">
|
|
<div className="mx-auto w-24 h-24 flex items-center justify-center mb-2">
|
|
<img
|
|
src="https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png"
|
|
alt="Logo"
|
|
className="w-full h-full object-contain"
|
|
referrerPolicy="no-referrer"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-0.5">
|
|
<h1 className="text-2xl font-black italic tracking-tighter uppercase leading-none">
|
|
{isSecretAdminRoute ? <>FXBROS <span className="text-brand-red">ADMIN</span></> : <><span className="text-white">QUANT</span><span className="text-brand-red">SCAN</span></>}
|
|
</h1>
|
|
<p className="text-zinc-500 font-medium text-[10px] leading-tight">
|
|
{isSecretAdminRoute ? "Acesso Restrito" : "Acesse a inteligência institucional Pro."}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<motion.div
|
|
key={showVerification ? 'verify' : showForgotPassword ? 'forgot' : isLogin ? 'login' : 'register'}
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
className="glass-card p-5 md:p-6 space-y-5 border-zinc-800/50"
|
|
>
|
|
{showVerification ? (
|
|
<div className="text-center space-y-6">
|
|
<div className="w-20 h-20 bg-brand-red/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
<Mail size={32} className="text-brand-red" />
|
|
</div>
|
|
<p className="text-sm text-zinc-300 leading-relaxed font-medium">
|
|
We have sent you a verification email to <span className="font-bold text-white">{verificationEmail}</span>. Verify it and log in.
|
|
</p>
|
|
<button
|
|
onClick={() => {
|
|
setShowVerification(false);
|
|
setIsLogin(true);
|
|
setEmail('');
|
|
setPassword('');
|
|
}}
|
|
className="w-full bg-brand-red text-white py-3.5 rounded-xl font-black flex items-center justify-center gap-3 hover:bg-brand-red/90 transition-all active:scale-95 text-base red-glow"
|
|
>
|
|
LOGIN
|
|
</button>
|
|
</div>
|
|
) : showForgotPassword ? (
|
|
resetEmailSent ? (
|
|
<div className="text-center space-y-6">
|
|
<div className="w-20 h-20 bg-brand-red/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
<Mail size={32} className="text-brand-red" />
|
|
</div>
|
|
<p className="text-sm text-zinc-300 leading-relaxed font-medium">
|
|
We sent you a password change link to <span className="font-bold text-white">{email}</span>.
|
|
</p>
|
|
<button
|
|
onClick={() => {
|
|
setShowForgotPassword(false);
|
|
setResetEmailSent(false);
|
|
setIsLogin(true);
|
|
setAuthError(null);
|
|
}}
|
|
className="w-full bg-brand-red text-white py-3.5 rounded-xl font-black flex items-center justify-center gap-3 hover:bg-brand-red/90 transition-all active:scale-95 text-base red-glow"
|
|
>
|
|
Sign In
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="text-center space-y-2 mb-6">
|
|
<h3 className="text-xl font-black italic uppercase text-white tracking-tighter">Recuperar Senha</h3>
|
|
<p className="text-xs text-zinc-400 font-medium">Digite seu e-mail para receber o link de redefinição.</p>
|
|
</div>
|
|
<form onSubmit={handleResetPassword} className="space-y-4">
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">E-mail</label>
|
|
<div className="relative">
|
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-600" size={16} />
|
|
<input
|
|
type="text"
|
|
required
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="seu@email.com"
|
|
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 pl-10 pr-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
|
|
autoCapitalize="none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{authError && (
|
|
<p className="text-brand-red text-[10px] font-bold text-center">{authError}</p>
|
|
)}
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="w-full bg-brand-red text-white py-3.5 rounded-xl font-black flex items-center justify-center gap-3 hover:bg-brand-red/90 transition-all active:scale-95 text-base disabled:opacity-50"
|
|
>
|
|
{isSubmitting ? <Loader2 className="animate-spin" size={18} /> : 'Get Reset Link'}
|
|
</button>
|
|
</form>
|
|
<button
|
|
onClick={() => {
|
|
setShowForgotPassword(false);
|
|
setAuthError(null);
|
|
}}
|
|
className="w-full py-3.5 text-zinc-400 hover:text-white text-xs font-black uppercase tracking-widest transition-colors flex items-center justify-center gap-2"
|
|
>
|
|
<ArrowLeft size={14} /> Voltar
|
|
</button>
|
|
</div>
|
|
)
|
|
) : (
|
|
<>
|
|
{!isSecretAdminRoute && (
|
|
<div className="flex bg-brand-dark p-1 rounded-lg border border-white/5">
|
|
<button
|
|
onClick={() => { setIsLogin(true); setAuthError(null); }}
|
|
className={`flex-1 py-1.5 rounded-md font-bold text-[10px] uppercase tracking-tighter transition-all ${isLogin ? 'bg-brand-red text-white shadow-lg' : 'text-zinc-500 hover:text-zinc-300'}`}
|
|
>
|
|
Login
|
|
</button>
|
|
<button
|
|
onClick={() => { setIsLogin(false); setAuthError(null); }}
|
|
className={`flex-1 py-1.5 rounded-md font-bold text-[10px] uppercase tracking-tighter transition-all ${!isLogin ? 'bg-brand-red text-white shadow-lg' : 'text-zinc-500 hover:text-zinc-300'}`}
|
|
>
|
|
Cadastro
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleAuth} className="space-y-4">
|
|
{!isLogin && (
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Nome Completo</label>
|
|
<div className="relative">
|
|
<UserIcon className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-600" size={16} />
|
|
<input
|
|
type="text"
|
|
required={!isLogin}
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Seu Nome"
|
|
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 pl-10 pr-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">E-mail</label>
|
|
<div className="relative">
|
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-600" size={16} />
|
|
<input
|
|
type="text"
|
|
required
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="seu@email.com"
|
|
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 pl-10 pr-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
|
|
autoCapitalize="none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<div className="flex items-center justify-between ml-1">
|
|
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500">Senha</label>
|
|
{isLogin && (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowForgotPassword(true);
|
|
setAuthError(null);
|
|
setResetEmailSent(false);
|
|
}}
|
|
className="text-[10px] uppercase font-black tracking-widest text-zinc-400 hover:text-brand-red transition-colors"
|
|
>
|
|
Forgot password?
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-600" size={16} />
|
|
<input
|
|
type={showPassword ? "text" : "password"}
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="••••••••"
|
|
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 pl-10 pr-10 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-white transition-colors"
|
|
>
|
|
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{!isLogin && (
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Repetir Senha</label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-600" size={16} />
|
|
<input
|
|
type={showConfirmPassword ? "text" : "password"}
|
|
required={!isLogin}
|
|
value={repeatPassword}
|
|
onChange={(e) => setRepeatPassword(e.target.value)}
|
|
placeholder="••••••••"
|
|
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 pl-10 pr-10 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-white transition-colors"
|
|
>
|
|
{showConfirmPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isLogin && (
|
|
<div className="flex items-center gap-2 mt-2 ml-1">
|
|
<input
|
|
type="checkbox"
|
|
id="remember"
|
|
checked={rememberMe}
|
|
onChange={(e) => setRememberMe(e.target.checked)}
|
|
className="w-3.5 h-3.5 rounded bg-brand-dark/50 border border-white/10 accent-brand-red cursor-pointer"
|
|
/>
|
|
<label htmlFor="remember" className="text-[10px] text-zinc-400 font-bold uppercase tracking-widest cursor-pointer hover:text-zinc-300">
|
|
Mantenha-me logado
|
|
</label>
|
|
</div>
|
|
)}
|
|
|
|
{authError && (
|
|
<p className="text-brand-red text-[10px] font-bold text-center">{authError}</p>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="w-full bg-brand-red text-white py-3.5 rounded-xl font-black flex items-center justify-center gap-3 hover:bg-brand-red/90 transition-all active:scale-95 text-base disabled:opacity-50"
|
|
>
|
|
{isSubmitting ? (
|
|
<Loader2 className="animate-spin" size={18} />
|
|
) : (
|
|
<>
|
|
{isLogin ? 'ENTRAR' : 'CADASTRAR'}
|
|
{isLogin ? <LogIn size={18} /> : <UserPlus size={18} />}
|
|
</>
|
|
)}
|
|
</button>
|
|
{authError && !isLogin && authError.includes('aprov') && (
|
|
<p className="text-zinc-400 text-[10px] text-center mt-2">Após o cadastro, aguarde a aprovação manual.</p>
|
|
)}
|
|
</form>
|
|
</>
|
|
)}
|
|
</motion.div>
|
|
|
|
<div className="flex items-center justify-center gap-6 pt-4 opacity-25">
|
|
<div className="flex items-center gap-2 text-[8px] uppercase font-black tracking-widest text-zinc-600">
|
|
<ShieldAlert size={10} />
|
|
Secure Auth
|
|
</div>
|
|
<div className="flex items-center gap-2 text-[8px] uppercase font-black tracking-widest text-zinc-600">
|
|
<Ghost size={10} />
|
|
IA Logic
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-brand-dark text-white flex flex-col md:flex-row pb-20 md:pb-0 relative overflow-hidden">
|
|
<NotificationManager />
|
|
{/* Background Image for App */}
|
|
<div className="absolute inset-0 z-0 pointer-events-none">
|
|
<img
|
|
src="https://i.ibb.co/YFQNMsjX/7eaead9a-0cd4-48d7-8f10-b32d98256e6f.png"
|
|
alt="App Background"
|
|
className={cn(
|
|
"w-full h-full object-cover transition-opacity duration-500",
|
|
activeTab === 'history' ? "opacity-85" : "opacity-80"
|
|
)}
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-b from-brand-dark/40 via-brand-dark/20 to-brand-dark" />
|
|
</div>
|
|
|
|
<Navbar activeTab={activeTab} setActiveTab={setActiveTab} isAdmin={isAdmin} user={user} />
|
|
|
|
<main className="flex-1 overflow-y-auto overflow-x-hidden relative z-10 flex flex-col">
|
|
<LiveMarketTicker />
|
|
|
|
{/* Desktop Header */}
|
|
<header className="hidden md:flex items-center justify-between p-8 pb-4 max-w-6xl w-full mx-auto">
|
|
<div className="space-y-1">
|
|
<span className="block text-[11px] font-black uppercase tracking-widest text-zinc-500 mb-1">Olá Humano, Bem-Vindo</span>
|
|
<h2 className="text-2xl font-black italic uppercase tracking-tighter text-white">
|
|
{activeTab === 'scan' && 'Scanner de IA'}
|
|
{activeTab === 'history' && 'Histórico de Sinais'}
|
|
{activeTab === 'stats' && 'Performance & Dados'}
|
|
{activeTab === 'profile' && 'Perfil do Usuário'}
|
|
{activeTab === 'admin' && 'Painel Administrativo'}
|
|
</h2>
|
|
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-zinc-500">
|
|
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
|
Servidor Live • Latência 24ms
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<button onClick={handleLogout} className="glass-card !p-2 text-zinc-500 hover:text-white transition-all">
|
|
<LogIn size={20} className="rotate-180" />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="max-w-6xl mx-auto p-4 md:p-8 lg:p-12 md:pt-4">
|
|
<header className="flex justify-between items-center mb-6 md:hidden">
|
|
<div className="flex items-center gap-2">
|
|
<img
|
|
src="https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png"
|
|
alt="Logo"
|
|
className="w-12 h-12 object-contain"
|
|
referrerPolicy="no-referrer"
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="text-[10px] text-zinc-500 tracking-widest uppercase font-bold">Olá Humano, Bem-Vindo</span>
|
|
<div className="font-black italic text-xl tracking-tighter uppercase leading-none"><span className="text-white">QUANT</span><span className="text-brand-red">SCAN</span></div>
|
|
</div>
|
|
</div>
|
|
<button onClick={handleLogout} className="text-zinc-500 hover:text-white p-2">
|
|
<LogIn size={18} className="rotate-180" />
|
|
</button>
|
|
</header>
|
|
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={activeTab}
|
|
initial={{ opacity: 0, x: 15 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
exit={{ opacity: 0, x: -15 }}
|
|
transition={{ duration: 0.25, ease: 'easeOut' }}
|
|
>
|
|
{activeTab === 'scan' && <AnalysisView userData={userData} onGoToHistory={() => setActiveTab('history')} />}
|
|
{activeTab === 'history' && <SignalHistory />}
|
|
{activeTab === 'stats' && <DashboardStats />}
|
|
{activeTab === 'profile' && <ProfileView user={user} userData={userData} onUpdate={setUserData} onDeleted={handleLogout} />}
|
|
{activeTab === 'admin' && isAdmin && <AdminDashboard />}
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|