feat: Initialize QuantScan AI frontend project
Set up the basic React project structure, including necessary dependencies, configuration files, and initial styling for the QuantScan AI Forex Pro scanner. This commit establishes the foundation for building the frontend application.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||
# AI Studio automatically injects this at runtime from user secrets.
|
||||
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||
|
||||
# APP_URL: The URL where this applet is hosted.
|
||||
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||
APP_URL="MY_APP_URL"
|
||||
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
@@ -1,11 +1,20 @@
|
||||
<div align="center">
|
||||
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
|
||||
<h1>Built with AI Studio</h2>
|
||||
|
||||
<p>The fastest path from prompt to production with Gemini.</p>
|
||||
|
||||
<a href="https://aistudio.google.com/apps">Start building</a>
|
||||
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/c922d781-23ec-403c-becd-e12f2237b1b0
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"entities": {
|
||||
"User": {
|
||||
"title": "User Profile",
|
||||
"description": "Represents a user profile with their plan and approval status.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uid": { "type": "string" },
|
||||
"email": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"isPremium": { "type": "boolean" },
|
||||
"isAdmin": { "type": "boolean" },
|
||||
"plan": { "type": "string", "enum": ["basic", "pro", "elite"] },
|
||||
"analysisLimit": { "type": "integer" },
|
||||
"isApproved": { "type": "boolean" }
|
||||
},
|
||||
"required": ["uid", "email", "name", "plan"]
|
||||
},
|
||||
"Signal": {
|
||||
"title": "Trading Signal",
|
||||
"description": "Technical analysis signal generated by the AI.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"userId": { "type": "string" },
|
||||
"pair": { "type": "string" },
|
||||
"timeframe": { "type": "string" },
|
||||
"decision": { "type": "string" },
|
||||
"score": { "type": "integer" },
|
||||
"entry": { "type": "string" },
|
||||
"stopLoss": { "type": "string" },
|
||||
"takeProfit": { "type": "string" },
|
||||
"justification": { "type": "string" },
|
||||
"conceptsDetected": { "type": "array", "items": { "type": "string" } },
|
||||
"screenshotUrl": { "type": "string" },
|
||||
"timestamp": { "type": "integer" },
|
||||
"createdAt": { "type": "object" },
|
||||
"result": { "type": "string" }
|
||||
},
|
||||
"required": ["userId", "pair", "decision", "timestamp"]
|
||||
}
|
||||
},
|
||||
"firestore": {
|
||||
"users/{userId}": {
|
||||
"schema": "User",
|
||||
"description": "User profiles by UID."
|
||||
},
|
||||
"signals/{signalId}": {
|
||||
"schema": "Signal",
|
||||
"description": "Generated signals."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
rules_version = '2';
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
// 0. Global Safety Net
|
||||
match /{document=**} {
|
||||
allow read, write: if false;
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
function isSignedIn() {
|
||||
return request.auth != null;
|
||||
}
|
||||
|
||||
// Root Admin Check (Fast, No DB Read)
|
||||
function isRootAdmin() {
|
||||
return isSignedIn() && request.auth.token.email == "fxbrosinvestments00@gmail.com";
|
||||
}
|
||||
|
||||
// Role-based Admin Check (Slower, DB Read)
|
||||
// IMPORTANT: Avoid using this in rules for the 'users' collection itself to prevent recursion.
|
||||
function isDbAdmin() {
|
||||
return isSignedIn() &&
|
||||
(isRootAdmin() || get(/databases/$(database)/documents/users/$(request.auth.uid)).data.get('isAdmin', false) == true);
|
||||
}
|
||||
|
||||
function isOwner(userId) {
|
||||
return isSignedIn() && request.auth.uid == userId;
|
||||
}
|
||||
|
||||
function isValidId(id) {
|
||||
return id is string && id.size() <= 128 && id.matches('^[a-zA-Z0-9_\\-]+$');
|
||||
}
|
||||
|
||||
function incoming() {
|
||||
return request.resource.data;
|
||||
}
|
||||
|
||||
function existing() {
|
||||
return resource.data;
|
||||
}
|
||||
|
||||
// --- Users Collection ---
|
||||
match /users/{userId} {
|
||||
// Use isRootAdmin here instead of isDbAdmin to prevent recursion
|
||||
allow get: if isOwner(userId) || isRootAdmin();
|
||||
allow list: if isRootAdmin() || isDbAdmin();
|
||||
|
||||
function isValidUser(data) {
|
||||
return data.keys().hasAll(['uid', 'email', 'name', 'isPremium', 'isAdmin', 'plan', 'analysisLimit', 'isApproved'])
|
||||
&& data.uid is string && data.uid == request.auth.uid
|
||||
&& data.email is string && data.email == request.auth.token.email
|
||||
&& data.name is string && data.name.size() <= 100
|
||||
&& (data.isPremium is bool || data.isPremium == null)
|
||||
&& (data.isAdmin is bool || data.isAdmin == null)
|
||||
&& data.plan in ['basic', 'pro', 'elite']
|
||||
&& (data.analysisLimit is int || data.analysisLimit == null)
|
||||
&& (data.isApproved is bool || data.isApproved == null);
|
||||
}
|
||||
|
||||
allow create: if isOwner(userId)
|
||||
&& isValidUser(incoming())
|
||||
// Security: Root Admin can create anyone, others created as non-privileged
|
||||
&& (incoming().isAdmin == false || isRootAdmin())
|
||||
&& (incoming().isApproved == false || isRootAdmin());
|
||||
|
||||
allow update: if (
|
||||
isRootAdmin() || isDbAdmin() ||
|
||||
(isOwner(userId) && incoming().diff(existing()).affectedKeys().hasOnly(['name']))
|
||||
);
|
||||
|
||||
allow delete: if isRootAdmin();
|
||||
}
|
||||
|
||||
// --- Signals Collection ---
|
||||
match /signals/{signalId} {
|
||||
allow get, list: if isSignedIn() && (resource.data.userId == request.auth.uid || isDbAdmin());
|
||||
|
||||
function isValidSignal(data) {
|
||||
return data.keys().hasAll(['userId', 'pair', 'timeframe', 'decision', 'score', 'timestamp', 'result'])
|
||||
&& data.userId == request.auth.uid
|
||||
&& data.pair is string && data.pair.size() <= 20
|
||||
&& data.timeframe is string && data.timeframe.size() <= 10
|
||||
&& data.decision is string
|
||||
&& data.score is int && data.score >= 0 && data.score <= 100
|
||||
&& data.timestamp is int
|
||||
&& data.result is string;
|
||||
}
|
||||
|
||||
allow create: if isSignedIn()
|
||||
&& isValidSignal(incoming())
|
||||
// Check approval status (Requires DB read)
|
||||
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isApproved == true;
|
||||
|
||||
allow update, delete: if isDbAdmin();
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>My Google AI Studio App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "QuantScan AI - Scanner Forex Pro",
|
||||
"description": "Scanner de Forex profissional com IA avançada. Analise gráficos, identifique oportunidades de alta probabilidade e acompanhe seu desempenho com inteligência adaptativa e análise institucional.",
|
||||
"requestFramePermissions": [],
|
||||
"majorCapabilities": []
|
||||
}
|
||||
Generated
+6566
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "react-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.29.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"firebase": "^12.12.1",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-dropzone": "^15.0.0",
|
||||
"recharts": "^3.8.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"vite": "^6.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^10.2.1",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
+622
@@ -0,0 +1,622 @@
|
||||
/**
|
||||
* @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 { 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 } from 'firebase/firestore';
|
||||
import { auth, db, handleFirestoreError, OperationType } from './lib/firebase';
|
||||
|
||||
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');
|
||||
|
||||
// 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(() => {
|
||||
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() };
|
||||
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 items-center justify-center bg-brand-dark">
|
||||
<div className="w-10 h-10 border-4 border-brand-red border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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></> : <>QUANT<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">
|
||||
{/* 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="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>
|
||||
|
||||
<Navbar activeTab={activeTab} setActiveTab={setActiveTab} isAdmin={isAdmin} />
|
||||
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden relative z-10">
|
||||
{/* Desktop Header */}
|
||||
<header className="hidden md:flex items-center justify-between p-8 pb-4 max-w-6xl 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 === 'plans' && 'Nossos Planos'}
|
||||
{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">
|
||||
{activeTab !== 'plans' && !userData?.isPremium && (
|
||||
<button
|
||||
onClick={() => setActiveTab('plans')}
|
||||
className="bg-brand-red/10 border border-brand-red/20 text-brand-red px-4 py-2 rounded-lg text-xs font-black uppercase tracking-widest hover:bg-brand-red/20 transition-all"
|
||||
>
|
||||
Upgrade Pro
|
||||
</button>
|
||||
)}
|
||||
<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">QUANT<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} />}
|
||||
{activeTab === 'history' && <SignalHistory />}
|
||||
{activeTab === 'stats' && <DashboardStats />}
|
||||
{activeTab === 'plans' && <PlansView />}
|
||||
{activeTab === 'profile' && <ProfileView user={user} userData={userData} onUpdate={setUserData} onDeleted={handleLogout} />}
|
||||
{activeTab === 'admin' && isAdmin && <AdminDashboard />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Users, ShieldCheck, Search, Loader2 } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import { collection, onSnapshot, doc, updateDoc } from 'firebase/firestore';
|
||||
import { db, handleFirestoreError, OperationType } from '../lib/firebase';
|
||||
|
||||
interface UserProfile {
|
||||
uid: string;
|
||||
email: string;
|
||||
name: string;
|
||||
isPremium: boolean;
|
||||
isAdmin: boolean;
|
||||
analysisLimit?: number;
|
||||
plan?: 'basic' | 'pro' | 'elite';
|
||||
isApproved?: boolean;
|
||||
}
|
||||
|
||||
export const AdminDashboard: React.FC = () => {
|
||||
const [users, setUsers] = useState<UserProfile[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = onSnapshot(collection(db, 'users'), (snapshot) => {
|
||||
const usersList: UserProfile[] = [];
|
||||
snapshot.forEach(doc => {
|
||||
usersList.push(doc.data() as UserProfile);
|
||||
});
|
||||
setUsers(usersList);
|
||||
setLoading(false);
|
||||
}, (error) => {
|
||||
handleFirestoreError(error, OperationType.LIST, 'users');
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => unsub();
|
||||
}, []);
|
||||
|
||||
const handleToggleApproval = async (userId: string, currentStatus: boolean) => {
|
||||
try {
|
||||
await updateDoc(doc(db, 'users', userId), {
|
||||
isApproved: !currentStatus
|
||||
}).catch(err => {
|
||||
handleFirestoreError(err, OperationType.UPDATE, 'users/' + userId);
|
||||
throw err;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to update user approval status", err);
|
||||
alert('Falha ao atualizar status de aprovação. Verifique as permissões de Admin.');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlanChange = async (userId: string, newPlan: 'basic' | 'pro' | 'elite') => {
|
||||
let limit = 8;
|
||||
let isPremium = false;
|
||||
|
||||
if (newPlan === 'pro') {
|
||||
limit = 15;
|
||||
isPremium = true;
|
||||
} else if (newPlan === 'elite') {
|
||||
limit = 999999; // Represents unlimited
|
||||
isPremium = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateDoc(doc(db, 'users', userId), {
|
||||
plan: newPlan,
|
||||
analysisLimit: limit,
|
||||
isPremium
|
||||
}).catch(err => {
|
||||
handleFirestoreError(err, OperationType.UPDATE, 'users/' + userId);
|
||||
throw err;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to update user plan", err);
|
||||
alert('Falha ao atualizar plano do usuário.');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredUsers = users.filter(user =>
|
||||
user.email?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-2xl font-black italic uppercase tracking-tighter text-white flex items-center gap-2">
|
||||
<ShieldCheck size={24} className="text-brand-red" />
|
||||
Painel Admin
|
||||
</h2>
|
||||
<p className="text-zinc-500 text-xs font-medium uppercase tracking-widest">Gerenciamento de Clientes</p>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-brand-red transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar usuário..."
|
||||
className="bg-white/5 border border-white/10 rounded-xl pl-10 pr-4 py-2 text-xs text-white focus:outline-none focus:ring-1 focus:ring-brand-red/50 w-64 transition-all"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 gap-4">
|
||||
<Loader2 size={32} className="text-brand-red animate-spin" />
|
||||
<p className="text-xs font-black uppercase tracking-widest text-zinc-500">Carregando usuários...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="glass-card !p-0 overflow-hidden overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse min-w-[800px]">
|
||||
<thead>
|
||||
<tr className="bg-white/5 border-b border-white/5 text-[10px] font-black uppercase tracking-widest text-zinc-500">
|
||||
<th className="p-4">Usuário</th>
|
||||
<th className="p-4 text-center">Aprovação</th>
|
||||
<th className="p-4 text-center">Plano</th>
|
||||
<th className="p-4 text-center">Limite Diário</th>
|
||||
<th className="p-4 text-center">Admin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredUsers.map((user) => (
|
||||
<tr key={user.uid} className="border-b border-white/5 hover:bg-white/5 transition-colors group">
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-brand-red/10 flex items-center justify-center text-brand-red text-sm font-black italic shrink-0">
|
||||
{user.name?.charAt(0) || user.email?.charAt(0)?.toUpperCase()}
|
||||
</div>
|
||||
<div className="flex flex-col truncate min-w-0">
|
||||
<span className="text-sm font-bold text-white leading-none mb-1 truncate">{user.name}</span>
|
||||
<span className="text-[10px] text-zinc-500 font-medium truncate">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<button
|
||||
onClick={() => handleToggleApproval(user.uid, !!user.isApproved)}
|
||||
className={cn(
|
||||
"px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest transition-all",
|
||||
user.isApproved
|
||||
? "bg-green-500/20 text-green-500 border border-green-500/30"
|
||||
: "bg-orange-500/20 text-orange-500 border border-orange-500/30 hover:bg-orange-500/30"
|
||||
)}
|
||||
>
|
||||
{user.isApproved ? 'Aprovado' : 'Aguardando'}
|
||||
</button>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<select
|
||||
value={user.plan || 'basic'}
|
||||
onChange={(e) => handlePlanChange(user.uid, e.target.value as 'basic' | 'pro' | 'elite')}
|
||||
className="bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white uppercase font-black tracking-widest focus:outline-none focus:border-brand-red/50 cursor-pointer"
|
||||
>
|
||||
<option value="basic">Básico</option>
|
||||
<option value="pro">Premium (Pro)</option>
|
||||
<option value="elite">Elite</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<span className="text-sm font-mono font-bold text-white">
|
||||
{user.analysisLimit === 999999 ? 'Ilimitado' : user.analysisLimit ?? 8}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
{user.isAdmin && (
|
||||
<span className="bg-brand-red/20 text-brand-red px-2 py-1 rounded text-[10px] font-black uppercase tracking-widest border border-brand-red/20">
|
||||
Admin
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{filteredUsers.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-zinc-600">
|
||||
<Users size={48} strokeWidth={1} className="mb-4 opacity-20" />
|
||||
<p className="text-xs font-black uppercase tracking-widest">Nenhum usuário encontrado</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,469 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { Upload, X, ShieldCheck, Target, TrendingUp, AlertTriangle, Loader2, Zap, BrainCircuit, Camera, ScanLine } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { analyzeForexChart } from '../services/geminiService';
|
||||
import { AnalysisResponse, SignalResult, SignalType } from '../types';
|
||||
import { cn } from '../lib/utils';
|
||||
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
|
||||
import { storage, auth, db, handleFirestoreError, OperationType } from '../lib/firebase';
|
||||
import { collection, addDoc, serverTimestamp, query, where, getDocs, Timestamp } from 'firebase/firestore';
|
||||
|
||||
export const AnalysisView: React.FC<{ userData?: any }> = ({ userData }) => {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [result, setResult] = useState<AnalysisResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [usageInfo, setUsageInfo] = useState<{ used: number, limit: number }>({ used: 0, limit: userData?.analysisLimit ?? 8 });
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUsage = async () => {
|
||||
if (!auth.currentUser) return;
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
try {
|
||||
const q = query(
|
||||
collection(db, 'signals'),
|
||||
where('userId', '==', auth.currentUser.uid)
|
||||
);
|
||||
|
||||
const snapshot = await getDocs(q).catch(err => {
|
||||
handleFirestoreError(err, OperationType.LIST, 'signals');
|
||||
throw err;
|
||||
});
|
||||
|
||||
const todaySignals = snapshot.docs.filter((doc) => {
|
||||
const data = doc.data();
|
||||
return data.timestamp >= today.getTime();
|
||||
});
|
||||
|
||||
setUsageInfo({
|
||||
used: todaySignals.length,
|
||||
limit: userData?.analysisLimit ?? 8
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error fetching usage stats", err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUsage();
|
||||
}, [result, userData]);
|
||||
|
||||
const handleStartAnalysis = async () => {
|
||||
if (!preview || !file) return;
|
||||
if (usageInfo.used >= usageInfo.limit) {
|
||||
setError(`Limite diário atingido (${usageInfo.used}/${usageInfo.limit}). Faça upgrade para análises ilimitadas.`);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAnalyzing(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
let downloadUrl = null;
|
||||
if (auth.currentUser) {
|
||||
const fileRef = ref(storage, `files/${Date.now()}_${file.name}`);
|
||||
await uploadBytes(fileRef, file);
|
||||
downloadUrl = await getDownloadURL(fileRef);
|
||||
}
|
||||
|
||||
const base64 = preview.split(',')[1];
|
||||
const analysis = await analyzeForexChart(base64);
|
||||
setResult(analysis);
|
||||
|
||||
if (auth.currentUser) {
|
||||
await addDoc(collection(db, 'signals'), {
|
||||
...analysis,
|
||||
screenshotUrl: downloadUrl,
|
||||
userId: auth.currentUser.uid,
|
||||
createdAt: serverTimestamp(),
|
||||
timestamp: Date.now(),
|
||||
result: SignalResult.PENDING,
|
||||
type: analysis.decision
|
||||
}).catch(err => {
|
||||
handleFirestoreError(err, OperationType.CREATE, 'signals');
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setError(err.message || 'Falha ao analisar imagem. Verifique se o gráfico está claro.');
|
||||
} finally {
|
||||
setIsAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
const selectedFile = acceptedFiles[0];
|
||||
setFile(selectedFile);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setPreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(selectedFile);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: { 'image/*': [] },
|
||||
multiple: false
|
||||
} as any);
|
||||
|
||||
const reset = () => {
|
||||
setFile(null);
|
||||
setPreview(null);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-5">
|
||||
<header className="space-y-1">
|
||||
<h1 className="text-xl font-black italic tracking-tighter text-white flex items-center gap-2.5 uppercase">
|
||||
<div className="w-7 h-7 rounded-lg border-2 border-brand-red flex items-center justify-center red-glow">
|
||||
<TrendingUp size={14} className="text-brand-red" />
|
||||
</div>
|
||||
Novo Scan
|
||||
</h1>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-zinc-500 text-[10px] font-medium leading-none">Envie um gráfico para análise de IA Pro.</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1.5 w-24 bg-white/5 rounded-full overflow-hidden border border-white/5">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full transition-all duration-500",
|
||||
usageInfo.used >= usageInfo.limit ? "bg-brand-red" : "bg-green-500"
|
||||
)}
|
||||
style={{ width: usageInfo.limit === 999999 ? '100%' : `${Math.min(100, (usageInfo.used / usageInfo.limit) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase text-zinc-500">
|
||||
{usageInfo.limit === 999999 ? `${usageInfo.used}/∞ Scans` : `${usageInfo.used}/${usageInfo.limit} Scans`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!result ? (
|
||||
<div className="glass-card p-6 border-zinc-900 min-h-[300px] flex flex-col justify-center items-center group">
|
||||
{!preview ? (
|
||||
<div className="w-full flex flex-col gap-4 items-center justify-center">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cn(
|
||||
"w-full flex flex-col items-center justify-center py-10 rounded-xl cursor-pointer transition-all duration-300 border border-transparent",
|
||||
isDragActive ? "bg-brand-red/5 scale-[0.98] border-brand-red/30" : "hover:bg-zinc-800/20"
|
||||
)}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="w-16 h-16 bg-brand-gray rounded-xl flex items-center justify-center mb-4 border border-white/5 group-hover:border-brand-red/30 transition-colors">
|
||||
<Upload size={24} className="text-zinc-600 group-hover:scale-110 transition-transform" />
|
||||
</div>
|
||||
<p className="text-base font-black uppercase italic tracking-tighter text-white mb-1 text-center px-4">Fazer Upload ou Arrastar</p>
|
||||
<p className="text-zinc-600 text-[10px] font-bold uppercase tracking-widest text-center px-4">TradingView, MT4/MT5, cTrader</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 w-full">
|
||||
<label className="flex-1 bg-brand-dark border-2 border-white/5 text-white py-3.5 rounded-xl font-black text-xs hover:border-brand-red/50 transition-all active:scale-95 flex items-center justify-center gap-2 cursor-pointer relative overflow-hidden group">
|
||||
<Camera size={16} className="text-zinc-400 group-hover:text-brand-red transition-colors" />
|
||||
<span className="whitespace-nowrap pt-0.5">TIRAR FOTO</span>
|
||||
<input type="file" accept="image/*" capture="environment" className="opacity-0 absolute inset-0 cursor-pointer w-full h-full" onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) onDrop([f]);
|
||||
}} />
|
||||
</label>
|
||||
<label className="flex-1 bg-brand-red/10 border-2 border-brand-red/30 text-brand-red py-3.5 rounded-xl font-black text-xs hover:bg-brand-red/20 transition-all active:scale-95 flex items-center justify-center gap-2 cursor-pointer relative overflow-hidden group">
|
||||
<ScanLine size={16} className="group-hover:scale-110 transition-transform" />
|
||||
<span className="whitespace-nowrap pt-0.5">SCAN AO VIVO</span>
|
||||
<input type="file" accept="image/*" capture="environment" className="opacity-0 absolute inset-0 cursor-pointer w-full h-full" onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) onDrop([f]);
|
||||
}} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full space-y-4">
|
||||
<div className="relative rounded-lg overflow-hidden border border-white/5 max-h-[400px]">
|
||||
<img src={preview} alt="Preview" className="w-full h-full object-contain" />
|
||||
<button
|
||||
onClick={reset}
|
||||
className="absolute top-3 right-3 p-1.5 bg-black/60 rounded-full text-white hover:bg-brand-red transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleStartAnalysis}
|
||||
disabled={isAnalyzing}
|
||||
className="flex-1 bg-brand-red text-white py-3.5 rounded-xl font-black text-base hover:bg-brand-red/90 transition-all active:scale-95 flex items-center justify-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
{isAnalyzing ? (
|
||||
<>
|
||||
<Loader2 className="animate-spin" size={20} />
|
||||
ANALISANDO...
|
||||
</>
|
||||
) : (
|
||||
'ANALISAR GRÁFICO'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-brand-red text-center text-xs font-bold">{error}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="space-y-5"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Score & Signal Info */}
|
||||
<div className="glass-card flex flex-col items-center justify-center text-center space-y-4">
|
||||
<div className="relative w-36 h-36 flex items-center justify-center">
|
||||
<svg className="w-full h-full" viewBox="0 0 100 100">
|
||||
<circle
|
||||
cx="50" cy="50" r="45"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.03)"
|
||||
strokeWidth="6"
|
||||
/>
|
||||
<circle
|
||||
cx="50" cy="50" r="45"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="6"
|
||||
strokeDasharray={283}
|
||||
strokeDashoffset={283 - (283 * result.score) / 100}
|
||||
className={cn(
|
||||
"transition-all duration-1000 ease-out",
|
||||
result.decision === SignalType.BUY ? "text-green-500" : "text-brand-red"
|
||||
)}
|
||||
strokeLinecap="round"
|
||||
transform="rotate(-90 50 50)"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className={cn(
|
||||
"text-4xl font-black leading-none",
|
||||
result.decision === SignalType.BUY ? "text-green-500" : "text-brand-red red-text-glow"
|
||||
)}>
|
||||
{result.score}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className={cn(
|
||||
"text-sm font-black uppercase tracking-wider italic",
|
||||
result.score >= 80
|
||||
? (result.decision === SignalType.BUY ? "text-green-500" : "text-brand-red red-text-glow")
|
||||
: result.score >= 60 ? "text-orange-500" : "text-zinc-500"
|
||||
)}>
|
||||
{result.score >= 80 ? '🔥 ALTA PROBABILIDADE' : result.score >= 60 ? '⚖️ MÉDIA PROBABILIDADE' : '❌ EVITAR'}
|
||||
</h3>
|
||||
<p className="text-zinc-500 mt-1 text-[10px] font-medium max-w-[200px]">{result.justification}</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full pt-4 border-t border-white/5 flex gap-3">
|
||||
<div className="flex-1 glass-card p-2">
|
||||
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-0.5">PAR</span>
|
||||
<span className="text-base font-black">{result.pair}</span>
|
||||
</div>
|
||||
<div className="flex-1 glass-card p-2">
|
||||
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-0.5">TF</span>
|
||||
<span className="text-base font-black">{result.timeframe}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trading Decision */}
|
||||
<div className="glass-card space-y-4">
|
||||
<div className={cn(
|
||||
"w-full py-5 rounded-lg flex flex-col items-center justify-center gap-1",
|
||||
result.decision === SignalType.BUY ? "bg-green-500/10 text-green-500" :
|
||||
result.decision === SignalType.SELL ? "bg-brand-red/10 text-brand-red" :
|
||||
"bg-zinc-500/10 text-zinc-500"
|
||||
)}>
|
||||
<span className="text-4xl font-black uppercase italic tracking-tighter">
|
||||
{result.decision === SignalType.BUY ? 'COMPRAR' :
|
||||
result.decision === SignalType.SELL ? 'VENDER' :
|
||||
'AGUARDAR'}
|
||||
</span>
|
||||
<span className="text-[8px] uppercase font-black tracking-widest opacity-60">Status do Signal</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target size={14} className="text-zinc-600" />
|
||||
<span className="text-[9px] font-black text-zinc-500 uppercase">ENTRADA</span>
|
||||
</div>
|
||||
<span className="font-mono font-black text-sm">{result.entry}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle size={14} className="text-brand-red/50" />
|
||||
<span className="text-[9px] font-black text-zinc-500 uppercase">STOP LOSS</span>
|
||||
</div>
|
||||
<span className="font-mono font-black text-sm text-brand-red">{result.stopLoss}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck size={14} className="text-green-500/50" />
|
||||
<span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT</span>
|
||||
</div>
|
||||
<span className="font-mono font-black text-sm text-green-500">{result.takeProfit}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* New Institutional Analysis Section */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="glass-card space-y-4">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-brand-red flex items-center gap-2">
|
||||
<Zap size={14} /> LIQUIDITY SWEEP + TRAP DETECTION
|
||||
</h4>
|
||||
<p className="text-xs text-zinc-400 leading-relaxed font-medium">
|
||||
{result.liquiditySweep || 'Nenhum sweep relevante detectado.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-card space-y-4">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-green-500 flex items-center gap-2">
|
||||
<TrendingUp size={14} /> MOMENTUM + ENTRY TIMING
|
||||
</h4>
|
||||
<p className="text-xs text-zinc-400 leading-relaxed font-medium">
|
||||
{result.momentum || 'Momentum neutro para este timeframe.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-card space-y-4">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-400 flex items-center gap-2">
|
||||
<Target size={14} /> ZONAS IMPORTANTES
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{result.keyZones?.map((zone, i) => (
|
||||
<span key={i} className="px-2 py-1 bg-white/5 rounded text-[10px] font-bold text-zinc-300 border border-white/5 uppercase">
|
||||
{zone}
|
||||
</span>
|
||||
)) || <span className="text-xs text-zinc-500 italic">Identificando zonas...</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="glass-card space-y-4">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-brand-red flex items-center gap-2">
|
||||
<BrainCircuit size={14} /> CONTEXTO INSTITUCIONAL
|
||||
</h4>
|
||||
<div className="flex items-center gap-3">
|
||||
{['Accumulation', 'Manipulation', 'Distribution'].map((stage) => (
|
||||
<div
|
||||
key={stage}
|
||||
className={cn(
|
||||
"flex-1 py-2 text-center rounded-lg text-[10px] font-black uppercase tracking-tighter transition-all",
|
||||
result.institutionalContext === stage
|
||||
? "bg-brand-red text-white shadow-[0_0_15px_rgba(255,0,0,0.3)]"
|
||||
: "bg-white/5 text-zinc-600 grayscale"
|
||||
)}
|
||||
>
|
||||
{stage === 'Accumulation' ? 'Acumulação' : stage === 'Manipulation' ? 'Manipulação' : 'Distribuição'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Analysis Illustration */}
|
||||
<div className="glass-card overflow-hidden !p-0 border-zinc-900/50 relative">
|
||||
<div className="absolute top-4 left-4 z-20">
|
||||
<h3 className="text-xs font-black flex items-center gap-2 uppercase italic tracking-wider text-white">
|
||||
<ShieldCheck size={14} className={cn(result.decision === SignalType.BUY ? "text-green-500" : "text-brand-red")} />
|
||||
Mapeamento Técnico da IA
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="aspect-[21/9] relative bg-zinc-950 flex items-center justify-center">
|
||||
<div className="absolute inset-0 z-10 bg-gradient-to-t from-brand-dark via-brand-dark/20 to-transparent" />
|
||||
<img
|
||||
src={preview || ''}
|
||||
alt="Technical Analysis Map"
|
||||
className="w-full h-full object-cover opacity-60 mix-blend-screen grayscale"
|
||||
/>
|
||||
|
||||
{/* UI Overlays to simulate AI analysis */}
|
||||
<div className="absolute inset-0 z-20 p-6 flex flex-col justify-end">
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
<span className="px-2 py-0.5 bg-brand-red/20 border border-brand-red/30 rounded text-[8px] font-bold text-brand-red uppercase tracking-tighter backdrop-blur-sm">
|
||||
Fluxo Institucional Detectado
|
||||
</span>
|
||||
<span className="px-2 py-0.5 bg-white/5 border border-white/10 rounded text-[8px] font-bold text-zinc-400 uppercase tracking-tighter backdrop-blur-sm">
|
||||
HFT Signature
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xl font-black italic uppercase tracking-tighter text-white">
|
||||
{result.pair} / {result.timeframe}
|
||||
</p>
|
||||
<p className="text-[10px] font-mono text-zinc-500 uppercase tracking-widest">
|
||||
Ref: AIS-BETA-{Math.floor(Math.random() * 10000)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[10px] font-black text-brand-red uppercase">Confirmação IA</p>
|
||||
<p className="text-2xl font-black italic text-white leading-none">{result.score}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative Scanning Line */}
|
||||
<motion.div
|
||||
initial={{ top: '0%' }}
|
||||
animate={{ top: '100%' }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: 'linear' }}
|
||||
className="absolute left-0 right-0 h-px bg-brand-red/50 z-30 shadow-[0_0_10px_rgba(255,0,0,0.5)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detailed IA Analysis */}
|
||||
<div className="glass-card space-y-3">
|
||||
<h3 className="text-xs font-black flex items-center gap-2 uppercase italic tracking-wider">
|
||||
<TrendingUp size={14} className="text-brand-red" />
|
||||
IA Logic Check
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<span className="text-[8px] uppercase text-zinc-600 font-black block mb-1">Estrutura Dominante</span>
|
||||
<p className="text-zinc-400 text-xs leading-relaxed font-medium">{result.structure}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[8px] uppercase text-zinc-600 font-black block mb-1">Confluências</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{result.conceptsDetected.map((c, i) => (
|
||||
<span key={i} className="px-2 py-0.5 bg-white/5 rounded text-[9px] font-black text-zinc-400 border border-white/5 uppercase tracking-tighter">
|
||||
{c}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={reset}
|
||||
className="w-full py-3 text-zinc-600 hover:text-white font-black text-xs uppercase tracking-widest transition-colors"
|
||||
>
|
||||
NOVO SCAN INSTITUCIONAL
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Signal, SignalResult, SignalType } from '../types';
|
||||
import {
|
||||
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
BarChart, Bar, Cell
|
||||
} from 'recharts';
|
||||
import { TrendingUp, Award, Target, Activity } from 'lucide-react';
|
||||
import { collection, query, where, orderBy, onSnapshot } from 'firebase/firestore';
|
||||
import { auth, db, handleFirestoreError, OperationType } from '../lib/firebase';
|
||||
|
||||
export const DashboardStats: React.FC = () => {
|
||||
const [signals, setSignals] = useState<Signal[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!auth.currentUser) return;
|
||||
|
||||
const q = query(
|
||||
collection(db, 'signals'),
|
||||
where('userId', '==', auth.currentUser.uid)
|
||||
);
|
||||
|
||||
const unsubscribe = onSnapshot(q, (snapshot) => {
|
||||
const newSignals: Signal[] = [];
|
||||
snapshot.forEach((doc) => {
|
||||
newSignals.push({ id: doc.id, ...doc.data() } as Signal);
|
||||
});
|
||||
setSignals(newSignals);
|
||||
setLoading(false);
|
||||
}, (error) => {
|
||||
handleFirestoreError(error, OperationType.LIST, 'signals');
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
const totalSignals = signals.length;
|
||||
const gains = signals.filter(s => s.result === SignalResult.GAIN).length;
|
||||
const winRate = totalSignals > 0 ? (gains / totalSignals) * 100 : 0;
|
||||
|
||||
const chartData = signals
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
.reduce((acc: any[], signal, index) => {
|
||||
const prevProfit = index > 0 ? acc[index - 1].profit : 0;
|
||||
const change = signal.result === SignalResult.GAIN ? 50 : signal.result === SignalResult.LOSS ? -30 : 0;
|
||||
acc.push({
|
||||
name: new Date(signal.timestamp).toLocaleDateString(),
|
||||
profit: prevProfit + change
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const timeframeStats = signals.reduce((acc: any[], signal) => {
|
||||
const existing = acc.find(i => i.name === signal.timeframe);
|
||||
if (existing) {
|
||||
existing.count += 1;
|
||||
} else {
|
||||
acc.push({ name: signal.timeframe, count: 1 });
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const pairStats = signals.reduce((acc: any[], signal) => {
|
||||
const existing = acc.find(i => i.name === signal.pair);
|
||||
if (existing) {
|
||||
existing.total += 1;
|
||||
if (signal.result === SignalResult.GAIN) existing.gains += 1;
|
||||
} else {
|
||||
acc.push({ name: signal.pair, total: 1, gains: signal.result === SignalResult.GAIN ? 1 : 0 });
|
||||
}
|
||||
return acc;
|
||||
}, []).map(p => ({ ...p, winRate: Math.round((p.gains / p.total) * 100) }));
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
<header>
|
||||
<h1 className="text-xl font-black italic tracking-tighter text-white flex items-center gap-3 uppercase">
|
||||
<Activity size={20} className="text-brand-red" />
|
||||
Estatísticas da IA
|
||||
</h1>
|
||||
<p className="text-zinc-500 mt-1 text-[10px] font-medium leading-none">Acompanhamento de performance e aprendizado institucional.</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: 'Total Sinais', value: totalSignals, icon: Activity },
|
||||
{ label: 'Taxa de Acerto', value: `${winRate.toFixed(1)}%`, icon: Award },
|
||||
{ label: 'Melhor Ativo', value: [...pairStats].sort((a,b) => b.winRate - a.winRate)[0]?.name || 'N/A', icon: Target },
|
||||
{ label: 'IA Learning', value: '+14%', icon: TrendingUp, positive: true },
|
||||
].map((stat, i) => (
|
||||
<div key={i} className="glass-card p-5 hover:border-white/10 transition-colors">
|
||||
<stat.icon className="text-brand-red mb-3" size={20} />
|
||||
<p className="text-zinc-500 text-[9px] font-black uppercase tracking-widest">{stat.label}</p>
|
||||
<p className="text-2xl font-black mt-0.5 text-white">{stat.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="md:col-span-2 glass-card p-5 min-h-[350px] flex flex-col">
|
||||
<h3 className="font-black italic uppercase tracking-wider text-xs mb-6 text-zinc-400">Curva de Equidade Estimada</h3>
|
||||
<div className="flex-1 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id="colorProfit" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#ff0000" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="#ff0000" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff05" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
stroke="#52525b"
|
||||
fontSize={10}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
minTickGap={20}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#52525b"
|
||||
fontSize={10}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(val) => `${val}p`}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#09090b', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '12px', fontSize: '12px' }}
|
||||
itemStyle={{ color: '#fff', fontWeight: 'bold' }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="profit"
|
||||
stroke="#ff0000"
|
||||
strokeWidth={3}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorProfit)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-5 flex flex-col">
|
||||
<h3 className="font-black italic uppercase tracking-wider text-xs mb-6 text-zinc-400">Volume por Timeframe</h3>
|
||||
<div className="flex-1 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={timeframeStats}>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
stroke="#52525b"
|
||||
fontSize={10}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<Bar dataKey="count" radius={[4, 4, 0, 0]}>
|
||||
{timeframeStats.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={index === 0 ? '#ff0000' : index === 1 ? '#a1a1aa' : '#3f3f46'} />
|
||||
))}
|
||||
</Bar>
|
||||
<Tooltip
|
||||
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
|
||||
contentStyle={{ backgroundColor: '#09090b', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '12px', fontSize: '12px' }}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="glass-card p-5">
|
||||
<h3 className="font-black italic uppercase tracking-wider text-xs mb-6 text-zinc-400 border-b border-white/5 pb-4">Performance por Ativo</h3>
|
||||
<div className="space-y-4">
|
||||
{pairStats.sort((a,b) => b.winRate - a.winRate).map((pair, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded bg-white/5 flex items-center justify-center text-xs font-bold text-white">
|
||||
{pair.name.substring(0,3)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-white">{pair.name}</p>
|
||||
<p className="text-[10px] uppercase text-zinc-500 font-black tracking-widest">{pair.total} Sinais</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-black text-white">{pair.winRate}%</p>
|
||||
<div className="w-24 h-1.5 bg-white/5 rounded-full mt-1 overflow-hidden">
|
||||
<div className="h-full bg-brand-red rounded-full" style={{ width: `${pair.winRate}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="glass-card p-5">
|
||||
<h3 className="font-black italic uppercase tracking-wider text-xs mb-6 text-zinc-400 border-b border-white/5 pb-4">Padrões PRO Logic Mais Eficientes</h3>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ name: "Order Block Mitigado", rate: 94 },
|
||||
{ name: "Liquidity Sweep", rate: 88 },
|
||||
{ name: "Fair Value Gap (FVG)", rate: 82 },
|
||||
{ name: "ChoCh Completo", rate: 76 }
|
||||
].map((logic, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between">
|
||||
<p className="text-sm font-bold text-zinc-300">{logic.name}</p>
|
||||
<div className="px-2 py-1 rounded bg-green-500/10 border border-green-500/20 text-green-500 text-[10px] font-black tracking-widest uppercase">
|
||||
{logic.rate}% Win
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { TrendingUp, ShieldCheck, Zap, BarChart3, Globe, ChevronRight, CheckCircle2 } from 'lucide-react';
|
||||
|
||||
interface LandingPageProps {
|
||||
onGetStarted: () => void;
|
||||
onViewPlans: () => void;
|
||||
}
|
||||
|
||||
export const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted, onViewPlans }) => {
|
||||
return (
|
||||
<div className="min-h-screen bg-brand-dark text-white overflow-x-hidden">
|
||||
{/* Navbar */}
|
||||
<nav className="fixed top-0 w-full z-50 glass-card bg-brand-dark/80 rounded-none border-t-0 border-x-0 border-b border-white/5 py-4">
|
||||
<div className="max-w-7xl mx-auto px-6 flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src="https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png"
|
||||
alt="Logo"
|
||||
className="w-12 h-12 object-contain"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<span className="font-black italic text-2xl tracking-tighter uppercase">
|
||||
QUANT<span className="text-brand-red">SCAN</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex items-center gap-8">
|
||||
<button
|
||||
onClick={onViewPlans}
|
||||
className="text-xs font-black uppercase tracking-widest text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
Planos
|
||||
</button>
|
||||
<button
|
||||
onClick={onGetStarted}
|
||||
className="bg-brand-red hover:bg-brand-red/90 text-white px-5 py-2 rounded-lg font-bold text-sm transition-all active:scale-95 flex items-center gap-2"
|
||||
>
|
||||
Acessar Agora <ChevronRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex md:hidden items-center gap-3">
|
||||
<button
|
||||
onClick={onViewPlans}
|
||||
className="text-[10px] font-black uppercase tracking-widest text-zinc-400"
|
||||
>
|
||||
Planos
|
||||
</button>
|
||||
<button
|
||||
onClick={onGetStarted}
|
||||
className="bg-brand-red hover:bg-brand-red/90 text-white px-4 py-2 rounded-lg font-bold text-xs transition-all flex items-center gap-2"
|
||||
>
|
||||
Acessar <ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-20 px-6 min-h-screen flex items-center">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<img
|
||||
src="https://i.ibb.co/VcJRM0zZ/90a0c129-b771-41d6-ad30-634d1d2546c4.png"
|
||||
alt="Forex Background"
|
||||
className="w-full h-full object-cover opacity-80"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-brand-dark/60 via-brand-dark/40 to-brand-dark" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-12 items-center relative z-10">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-brand-red/10 border border-brand-red/20 text-brand-red text-xs font-black uppercase tracking-widest">
|
||||
<Zap size={14} /> Inteligência Institucional
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-black italic tracking-tighter leading-[0.9] uppercase">
|
||||
REVELE A <span className="text-brand-red">LIQUIDEZ</span> <br />
|
||||
DAS INSTITUIÇÕES.
|
||||
</h1>
|
||||
<p className="text-base md:text-lg max-w-xl font-medium leading-relaxed">
|
||||
Scanner de Forex baseado em IA avançada que identifica padrões de alta probabilidade em segundos. Pare de ser a liquidez do mercado.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 pt-4">
|
||||
<button
|
||||
onClick={onGetStarted}
|
||||
className="bg-white text-brand-dark px-10 py-4 rounded-xl font-black text-lg hover:bg-zinc-200 transition-all flex items-center justify-center gap-3"
|
||||
>
|
||||
TESTAR SCANNER <ChevronRight size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onViewPlans}
|
||||
className="bg-zinc-900 border border-white/10 text-white px-10 py-4 rounded-xl font-black text-lg hover:bg-zinc-800 transition-all flex items-center justify-center gap-3"
|
||||
>
|
||||
VER PLANOS
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="relative"
|
||||
>
|
||||
<div className="absolute -inset-4 bg-brand-red/20 blur-[100px] rounded-full" />
|
||||
<div className="glass-card p-4 border-zinc-800 relative z-10 overflow-hidden">
|
||||
<img
|
||||
src="https://i.ibb.co/TB4yz3GP/Chat-GPT-Image-25-de-abr-de-2026-03-58-37.png"
|
||||
alt="Trading Chart Analysis"
|
||||
className="rounded-xl opacity-80 h-[400px] w-full object-cover transition-all duration-700"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-brand-dark via-transparent to-transparent flex flex-col justify-end p-8">
|
||||
<div className="glass-card p-4 translate-y-4 max-w-xs border-brand-red/30">
|
||||
<p className="text-xs font-bold text-brand-red uppercase mb-1">IA DETECTADA:</p>
|
||||
<p className="text-sm font-black">Bullish Order Block H1</p>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<div className="h-1 flex-1 bg-brand-red rounded-full" />
|
||||
<div className="h-1 flex-1 bg-brand-red/20 rounded-full" />
|
||||
<div className="h-1 flex-1 bg-brand-red/20 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Grid */}
|
||||
<section className="py-20 px-6 bg-zinc-950/50">
|
||||
<div className="max-w-7xl mx-auto space-y-12">
|
||||
<div className="text-center space-y-4">
|
||||
<h2 className="text-2xl font-black uppercase italic tracking-tighter">
|
||||
TECNOLOGIA DE <span className="text-brand-red">PRÓXIMA GERAÇÃO</span>
|
||||
</h2>
|
||||
<div className="w-16 h-1 bg-brand-red mx-auto" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{[
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: "Análise Automática",
|
||||
desc: "Nossa IA identifica quebras de estrutura e mudanças de comportamento do mercado instantaneamente."
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "Probability Score",
|
||||
desc: "Cada setup recebe uma pontuação de 0-100% baseada em confluências institucionais reais."
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
title: "Qualquer Ativo",
|
||||
desc: "Pares de Forex, Índices, Cripto ou Commodities. Se tem gráfico, nossa IA analisa."
|
||||
}
|
||||
].map((feature, i) => (
|
||||
<div key={i} className="glass-card p-6 border-zinc-800 hover:border-brand-red/30 transition-colors group">
|
||||
<div className="w-12 h-12 bg-white/5 rounded-xl flex items-center justify-center mb-6 group-hover:bg-brand-red/10 transition-colors">
|
||||
<feature.icon className="text-zinc-500 group-hover:text-brand-red transition-colors" />
|
||||
</div>
|
||||
<h3 className="text-lg font-black uppercase mb-3 ">{feature.title}</h3>
|
||||
<p className="text-zinc-500 text-sm leading-relaxed">{feature.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Proof Section */}
|
||||
<section className="py-20 px-6 max-w-7xl mx-auto">
|
||||
<div className="glass-card p-12 overflow-hidden relative">
|
||||
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-2xl md:text-3xl font-black italic tracking-tighter uppercase leading-none">
|
||||
PARE DE ADIVINHAR.<br />
|
||||
COMECE A <span className="text-brand-red">QUANTIFICAR</span>.
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{[
|
||||
"Análise em tempo real de fluxos e vácuos",
|
||||
"Detecção de zonas de Oferta & Procura",
|
||||
"Histórico adaptativo para aprendizado de sinais",
|
||||
"Interface otimizada profissional"
|
||||
].map((text, i) => (
|
||||
<li key={i} className="flex items-center gap-3 text-zinc-400 font-medium text-xs md:text-sm">
|
||||
<CheckCircle2 className="text-brand-red shrink-0" size={16} />
|
||||
{text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button
|
||||
onClick={onViewPlans}
|
||||
className="bg-brand-red text-white px-8 py-4 rounded-xl font-black hover:bg-brand-red/90 transition-all flex items-center gap-3"
|
||||
>
|
||||
LIBERAR MEU ACESSO <ChevronRight size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-4">
|
||||
<div className="glass-card p-6 text-center border-white/5">
|
||||
<p className="text-3xl font-black text-brand-red">82%</p>
|
||||
<p className="text-[10px] text-zinc-500 font-bold uppercase tracking-widest">Precisão média</p>
|
||||
</div>
|
||||
<div className="glass-card p-6 text-center border-white/5">
|
||||
<p className="text-3xl font-black">24/7</p>
|
||||
<p className="text-[10px] text-zinc-500 font-bold uppercase tracking-widest">Monitoramento</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4 pt-8">
|
||||
<div className="glass-card p-6 text-center border-white/5">
|
||||
<p className="text-3xl font-black text-white">PRO</p>
|
||||
<p className="text-[10px] text-zinc-500 font-bold uppercase tracking-widest">Estratégia Foco</p>
|
||||
</div>
|
||||
<div className="glass-card p-6 text-center border-white/5">
|
||||
<p className="text-3xl font-black text-white">IA</p>
|
||||
<p className="text-[10px] text-zinc-500 font-bold uppercase tracking-widest">Adaptativa</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="py-12 border-t border-white/5 text-center">
|
||||
<div className="max-w-7xl mx-auto px-6 space-y-6">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="w-8 h-8 bg-brand-red rounded-lg flex items-center justify-center">
|
||||
<TrendingUp size={18} className="text-white" />
|
||||
</div>
|
||||
<span className="font-black italic text-xl tracking-tighter uppercase">
|
||||
QUANT<span className="text-brand-red">SCAN</span>
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-zinc-600 text-xs max-w-lg mx-auto">
|
||||
Aviso Legal: Negociar no mercado Forex envolve riscos elevados. Os sinais gerados pela IA são apenas para fins informativos e não constituem aconselhamento financeiro.
|
||||
</p>
|
||||
<div className="text-zinc-800 text-[10px] font-bold uppercase tracking-[0.2em]">
|
||||
© 2026 QUANT SCAN AI - TODOS OS DIREITOS RESERVADOS
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
import { LayoutDashboard, History, PieChart, Info, LogOut, CreditCard, ShieldCheck, User } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
interface NavbarProps {
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, isAdmin }) => {
|
||||
const mainTabs = [
|
||||
{ id: 'scan', label: 'Scan IA', icon: LayoutDashboard },
|
||||
{ id: 'history', label: 'Histórico', icon: History },
|
||||
{ id: 'stats', label: 'Estatísticas', icon: PieChart },
|
||||
];
|
||||
|
||||
const accountTabs = [
|
||||
{ id: 'plans', label: 'Planos', icon: CreditCard },
|
||||
{ id: 'profile', label: 'Perfil', icon: User },
|
||||
];
|
||||
|
||||
if (isAdmin) {
|
||||
accountTabs.push({ id: 'admin', label: 'Admin', icon: ShieldCheck });
|
||||
}
|
||||
|
||||
const allTabs = [...mainTabs, ...accountTabs]; // For mobile rendering
|
||||
|
||||
return (
|
||||
<nav className="fixed bottom-4 left-1/2 -translate-x-1/2 md:translate-x-0 md:static md:w-64 md:h-screen md:flex-col glass-card !p-4 flex items-center md:items-stretch gap-1.5 z-50">
|
||||
<div className="hidden md:flex items-center gap-3 p-3 mb-6 w-full border-b border-white/5 pb-6">
|
||||
<img
|
||||
src="https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png"
|
||||
alt="Logo"
|
||||
className="w-16 h-16 object-contain drop-shadow-[0_0_15px_rgba(255,0,0,0.2)]"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-black italic text-xl tracking-tighter uppercase leading-none">
|
||||
QUANT<span className="text-brand-red">SCAN</span>
|
||||
</span>
|
||||
<span className="text-[10px] font-black uppercase text-zinc-500 tracking-[0.3em] mt-1">IA TRADER</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile view */}
|
||||
<div className="flex md:hidden gap-2 w-full justify-around">
|
||||
{allTabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={cn(
|
||||
"p-3 rounded-xl transition-all duration-300 flex items-center gap-3 group relative",
|
||||
activeTab === tab.id
|
||||
? "text-brand-red"
|
||||
: "text-zinc-500"
|
||||
)}
|
||||
>
|
||||
{activeTab === tab.id && (
|
||||
<span className="absolute inset-0 bg-brand-red/10 rounded-xl" />
|
||||
)}
|
||||
<tab.icon size={20} className={cn(activeTab === tab.id ? "scale-110" : "group-hover:scale-110 transition-transform")} />
|
||||
<span className="hidden leading-none text-[9px] font-black uppercase tracking-tighter">{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Desktop view */}
|
||||
<div className="hidden md:flex flex-col flex-1 w-full gap-6">
|
||||
{/* Main Section */}
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="px-3 mb-1 text-[10px] font-black uppercase text-zinc-600 tracking-[0.2em]">Menu Principal</div>
|
||||
{mainTabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={cn(
|
||||
"p-3 rounded-xl transition-all duration-300 flex items-center gap-3 w-full group",
|
||||
activeTab === tab.id
|
||||
? "bg-brand-red text-white red-glow"
|
||||
: "text-zinc-500 hover:text-white hover:bg-white/5"
|
||||
)}
|
||||
>
|
||||
<tab.icon size={20} className={cn(activeTab === tab.id ? "scale-110" : "group-hover:scale-110 transition-transform")} />
|
||||
<span className="text-[11px] font-black uppercase tracking-widest transition-all">
|
||||
{tab.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Account Section */}
|
||||
<div className="flex flex-col gap-1 w-full mt-auto mb-2">
|
||||
<div className="px-3 mb-1 text-[10px] font-black uppercase text-zinc-600 tracking-[0.2em]">Conta & Ajuda</div>
|
||||
{accountTabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={cn(
|
||||
"p-3 rounded-xl transition-all duration-300 flex items-center gap-3 w-full group",
|
||||
activeTab === tab.id
|
||||
? "bg-brand-red text-white red-glow"
|
||||
: "text-zinc-500 hover:text-white hover:bg-white/5"
|
||||
)}
|
||||
>
|
||||
<tab.icon size={20} className={cn(activeTab === tab.id ? "scale-110" : "group-hover:scale-110 transition-transform")} />
|
||||
<span className="text-[11px] font-black uppercase tracking-widest transition-all">
|
||||
{tab.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,380 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { motion, animate } from 'motion/react';
|
||||
import {
|
||||
Check,
|
||||
Zap,
|
||||
ShieldCheck,
|
||||
BarChart3,
|
||||
BrainCircuit,
|
||||
History,
|
||||
UserPlus,
|
||||
Sparkles,
|
||||
ArrowRight,
|
||||
Target,
|
||||
Trophy,
|
||||
Users
|
||||
} from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
// Animated Counter Component
|
||||
const AnimatedCounter = ({ from, to, suffix = "", prefix = "", decimal = false, duration = 2 }: { from: number, to: number, suffix?: string, prefix?: string, decimal?: boolean, duration?: number }) => {
|
||||
const nodeRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const node = nodeRef.current;
|
||||
if (node) {
|
||||
const controls = animate(from, to, {
|
||||
duration,
|
||||
ease: "easeOut",
|
||||
onUpdate(value) {
|
||||
const formattedValue = decimal ? value.toFixed(1) : Math.round(value).toString();
|
||||
node.textContent = `${prefix}${formattedValue}${suffix}`;
|
||||
}
|
||||
});
|
||||
return () => controls.stop();
|
||||
}
|
||||
}, [from, to, suffix, prefix, duration, decimal]);
|
||||
|
||||
return <span ref={nodeRef}>{prefix}{from}{suffix}</span>;
|
||||
};
|
||||
|
||||
interface PlansViewProps {
|
||||
isUnauthenticated?: boolean;
|
||||
onGetStarted?: () => void;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
const PlanCard = ({
|
||||
title,
|
||||
price,
|
||||
description,
|
||||
features,
|
||||
buttonText,
|
||||
highlight = false,
|
||||
badge = "",
|
||||
onClick
|
||||
}: {
|
||||
title: string;
|
||||
price: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
buttonText: string;
|
||||
highlight?: boolean;
|
||||
badge?: string;
|
||||
onClick?: () => void;
|
||||
}) => (
|
||||
<motion.div
|
||||
whileHover={{ y: -5 }}
|
||||
className={cn(
|
||||
"relative glass-card flex flex-col h-full overflow-hidden transition-all duration-500",
|
||||
highlight ? "border-brand-red/40 bg-brand-red/5 ring-1 ring-brand-red/20 shadow-[0_0_40px_-15px_rgba(255,0,0,0.2)]" : "border-white/5"
|
||||
)}
|
||||
>
|
||||
{badge && (
|
||||
<div className="absolute top-0 right-0">
|
||||
<div className="bg-brand-red text-white text-[9px] font-black uppercase py-1 px-4 tracking-[0.2em] transform rotate-45 translate-x-[35%] translate-y-[50%] shadow-lg">
|
||||
{badge}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className={cn("text-xs font-black uppercase tracking-widest", highlight ? "text-brand-red" : "text-zinc-500")}>
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-4xl font-black italic tracking-tighter text-white">{price}</span>
|
||||
<span className="text-zinc-500 text-xs font-medium">{title !== "Plano Free" ? "/ mês" : ""}</span>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-400 font-medium leading-relaxed">{description}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 pt-4 border-t border-white/5">
|
||||
{features.map((feature, idx) => (
|
||||
<div key={idx} className="flex items-start gap-3 group">
|
||||
<div className={cn(
|
||||
"mt-0.5 w-4 h-4 rounded-full flex items-center justify-center shrink-0 transition-colors",
|
||||
highlight ? "bg-brand-red/20 text-brand-red" : "bg-white/5 text-zinc-600"
|
||||
)}>
|
||||
<Check size={10} strokeWidth={3} />
|
||||
</div>
|
||||
<span className="text-xs text-zinc-300 font-medium group-hover:text-white transition-colors">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 mt-auto">
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"w-full py-3 rounded-xl font-black uppercase tracking-tighter text-sm flex items-center justify-center gap-2 transition-all active:scale-95",
|
||||
highlight
|
||||
? "bg-brand-red text-white hover:bg-brand-red/90 red-glow"
|
||||
: "bg-white/5 text-white hover:bg-white/10 border border-white/10"
|
||||
)}
|
||||
>
|
||||
{buttonText}
|
||||
<ArrowRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetStarted, onBack }) => {
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
show: { opacity: 1, y: 0 }
|
||||
};
|
||||
|
||||
const handleAction = () => {
|
||||
if (isUnauthenticated && onGetStarted) {
|
||||
onGetStarted();
|
||||
} else {
|
||||
// Handle payment or subscription logic for logged in users
|
||||
window.open('https://wa.me/258840000000?text=Ol%C3%A1%2C%20gostaria%20de%20assinar%20o%20plano%20do%20QuantScan', '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("relative min-h-screen space-y-16 pb-20", isUnauthenticated && "max-w-7xl mx-auto px-6 py-12")}>
|
||||
{/* Background Image requested by user */}
|
||||
<div className="fixed inset-0 -z-30 pointer-events-none">
|
||||
<img
|
||||
src="https://i.ibb.co/M5DzNvR1/Chat-GPT-Image-28-de-abr-de-2026-12-48-30.png"
|
||||
alt="Background"
|
||||
className="w-full h-full object-cover opacity-80"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-brand-dark/20" /> {/* Slight overlay for readability */}
|
||||
</div>
|
||||
|
||||
{isUnauthenticated && (
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-zinc-500 hover:text-white transition-colors text-xs font-black uppercase tracking-widest relative z-10"
|
||||
>
|
||||
<ArrowRight size={16} className="rotate-180" /> Voltar
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 1. HERO SECTION */}
|
||||
<section className="relative overflow-hidden rounded-[2.5rem] py-20 px-8 text-center space-y-6">
|
||||
{/* Background Image for this specific section */}
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<img
|
||||
src="https://i.ibb.co/M5DzNvR1/Chat-GPT-Image-28-de-abr-de-2026-12-48-30.png"
|
||||
alt="Hero Background"
|
||||
className="w-full h-full object-cover opacity-80"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-brand-dark via-transparent to-brand-dark" />
|
||||
<div className="absolute inset-0 bg-brand-red/5" />
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="space-y-4 relative z-10"
|
||||
>
|
||||
<div className="flex justify-center mb-6">
|
||||
<img
|
||||
src="https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png"
|
||||
alt="Logo"
|
||||
className="w-24 h-24 object-contain drop-shadow-[0_0_15px_rgba(255,0,0,0.3)] shadow-red-500/20"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
<span className="relative inline-block px-4 py-1.5 rounded-full overflow-hidden border border-brand-red/30 text-[10px] font-black text-white uppercase tracking-widest mb-4 shadow-lg">
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<img
|
||||
src="https://i.ibb.co/M5DzNvR1/Chat-GPT-Image-28-de-abr-de-2026-12-48-30.png"
|
||||
alt=""
|
||||
className="w-full h-full object-cover opacity-80"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-brand-red/20" />
|
||||
</div>
|
||||
Acesso Premium Pro Logic
|
||||
</span>
|
||||
<h1 className="text-4xl md:text-6xl font-black italic uppercase tracking-tighter text-white leading-[0.9]">
|
||||
Transforme análise em decisão.<br />
|
||||
<span className="text-brand-red">Pro Logic em lucro.</span>
|
||||
</h1>
|
||||
<p className="text-zinc-400 text-sm md:text-lg max-w-xl mx-auto font-medium leading-relaxed">
|
||||
IA que analisa gráficos e entrega sinais com probabilidade real. Pare de adivinhar cada trade.
|
||||
</p>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* 2. BENEFÍCIOS PRINCIPAIS - Re-added for the plan page context */}
|
||||
<section className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
{[
|
||||
{ icon: Target, title: "Score real", desc: "0-100% precisão" },
|
||||
{ icon: BrainCircuit, title: "PRO Logic", desc: "Fluxo Pro" },
|
||||
{ icon: Sparkles, title: "Scan Visual", desc: "Por Imagem" },
|
||||
{ icon: Zap, title: "Adaptativa", desc: "IA que aprende" },
|
||||
{ icon: History, title: "Backtest", desc: "Histórico Full" },
|
||||
{ icon: ShieldCheck, title: "High Prob", desc: "Sinais Elite" },
|
||||
].map((benefit, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
className="glass-card flex flex-col items-center text-center p-4 group hover:border-brand-red/30 transition-colors"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl bg-white/5 flex items-center justify-center text-zinc-500 group-hover:text-brand-red group-hover:bg-brand-red/10 transition-all mb-3">
|
||||
<benefit.icon size={20} />
|
||||
</div>
|
||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-white mb-1">{benefit.title}</h4>
|
||||
<p className="text-[9px] font-medium text-zinc-500 uppercase">{benefit.desc}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* 3. PLANOS */}
|
||||
<section id="planos" className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<PlanCard
|
||||
title="Plano Begin"
|
||||
price="500 MZN"
|
||||
description="Para quem está começando e quer testar a potência da nossa IA."
|
||||
buttonText="Assinar Agora"
|
||||
onClick={handleAction}
|
||||
features={[
|
||||
"8 análises por dia",
|
||||
"Score básico de probabilidade",
|
||||
"Suporte a Timeframes maiores",
|
||||
"Sem histórico completo",
|
||||
"Sem aprendizado personalizado"
|
||||
]}
|
||||
/>
|
||||
<PlanCard
|
||||
title="Plano Pro"
|
||||
price="1.500 MZN"
|
||||
description="O plano mais popular para traders que buscam consistência real."
|
||||
buttonText="Assinar Agora"
|
||||
highlight={true}
|
||||
badge="Recomendado"
|
||||
onClick={handleAction}
|
||||
features={[
|
||||
"15 análises por dia",
|
||||
"Score avançado de alta precisão",
|
||||
"Detecção automática de timeframe",
|
||||
"Histórico completo de sinais",
|
||||
"IA adaptativa (aprende com resultados)",
|
||||
"Sinais com alta probabilidade",
|
||||
"Suporte priorizado"
|
||||
]}
|
||||
/>
|
||||
<PlanCard
|
||||
title="Plano Elite"
|
||||
price="3.000 MZN"
|
||||
description="Acesso total às ferramentas de nível institucional."
|
||||
buttonText="Ser Elite"
|
||||
onClick={handleAction}
|
||||
features={[
|
||||
"Análises ilimitadas",
|
||||
"Tudo do plano PRO",
|
||||
"Prioridade total de processamento",
|
||||
"Insights institucionais avançados",
|
||||
"Estratégias exclusivas (PRO Logic)",
|
||||
"Gerenciamento de risco acoplado",
|
||||
"Suporte VIP via WhatsApp"
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* 4. COMPARAÇÃO DE PLANOS */}
|
||||
<section className="space-y-8">
|
||||
<h2 className="text-2xl font-black italic uppercase tracking-tighter text-white text-center">Tabela Comparativa</h2>
|
||||
<div className="glass-card overflow-hidden !p-0 overflow-x-auto">
|
||||
<table className="w-full min-w-[600px] text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-white/5 font-black uppercase italic text-[10px] tracking-widest text-zinc-400">
|
||||
<th className="p-4 border-b border-white/5">Funcionalidade</th>
|
||||
<th className="p-4 border-b border-white/5 text-center">Begin</th>
|
||||
<th className="p-4 border-b border-white/5 text-center text-brand-red">Pro</th>
|
||||
<th className="p-4 border-b border-white/5 text-center">Elite</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-xs font-medium text-zinc-300">
|
||||
{[
|
||||
{ label: "Nº de Análises", free: "8 / dia", pro: "15 / dia", elite: "Ilimitado" },
|
||||
{ label: "Score de Probabilidade", free: "Básico", pro: "Avançado", elite: "Avançado+" },
|
||||
{ label: "IA Adaptativa", free: "Não", pro: "Sim", elite: "Sim" },
|
||||
{ label: "Histórico de Sinais", free: "Limitado", pro: "Completo", elite: "Completo" },
|
||||
{ label: "Suporte", free: "Comunidade", pro: "Prioritário", elite: "VIP Individual" },
|
||||
{ label: "Processamento", free: "Normal", pro: "Rápido", elite: "Ultra Prioridade" },
|
||||
].map((row, idx) => (
|
||||
<tr key={idx} className="border-b border-white/5 hover:bg-white/5 transition-colors">
|
||||
<td className="p-4 font-bold text-white">{row.label}</td>
|
||||
<td className="p-4 text-center text-zinc-500">{row.free}</td>
|
||||
<td className="p-4 text-center font-bold text-white">{row.pro}</td>
|
||||
<td className="p-4 text-center text-zinc-300">{row.elite}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 5. PROVA SOCIAL */}
|
||||
<section className="grid grid-cols-1 md:grid-cols-3 gap-8 py-10 border-y border-white/5 relative z-10">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="text-4xl font-black text-white italic tracking-tighter">
|
||||
<AnimatedCounter from={0} to={1000} prefix="+" duration={2.5} />
|
||||
</div>
|
||||
<div className="text-[10px] font-black uppercase tracking-[0.2em] text-brand-red">Análises realizadas</div>
|
||||
</div>
|
||||
<div className="text-center space-y-2">
|
||||
<div className="text-4xl font-black text-white italic tracking-tighter">
|
||||
<AnimatedCounter from={0} to={90} suffix="%" duration={2.5} />
|
||||
</div>
|
||||
<div className="text-[10px] font-black uppercase tracking-[0.2em] text-brand-red">Taxa média de acerto</div>
|
||||
</div>
|
||||
<div className="text-center space-y-2">
|
||||
<div className="text-4xl font-black text-white italic tracking-tighter">
|
||||
<AnimatedCounter from={10} to={1.2} suffix="s" decimal={true} duration={2.5} />
|
||||
</div>
|
||||
<div className="text-[10px] font-black uppercase tracking-[0.2em] text-brand-red">Tempo de resposta IA</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 6. FINAL CTA */}
|
||||
<section className="relative overflow-hidden rounded-[2rem] bg-brand-red p-8 md:p-16 text-center text-white space-y-6">
|
||||
<div className="absolute top-0 right-0 -mr-20 -mt-20 w-80 h-80 bg-white/10 blur-[80px] rounded-full" />
|
||||
<div className="absolute bottom-0 left-0 -ml-20 -mb-20 w-60 h-60 bg-black/10 blur-[60px] rounded-full" />
|
||||
|
||||
<h2 className="text-3xl md:text-5xl font-black italic uppercase tracking-tighter leading-tight relative z-10">
|
||||
Pare de adivinhar.<br />
|
||||
Comece a decidir com dados.
|
||||
</h2>
|
||||
|
||||
<div className="pt-4 relative z-10">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={handleAction}
|
||||
className="bg-white text-brand-red px-12 py-4 rounded-2xl font-black text-lg uppercase tracking-tighter shadow-2xl hover:shadow-white/20 transition-all flex items-center gap-3 mx-auto"
|
||||
>
|
||||
Começar Agora
|
||||
<ArrowRight size={24} />
|
||||
</motion.button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
import React, { useState } from 'react';
|
||||
import { doc, updateDoc, deleteDoc } from 'firebase/firestore';
|
||||
import { updatePassword, updateProfile, deleteUser, EmailAuthProvider, reauthenticateWithCredential } from 'firebase/auth';
|
||||
import { ref, listAll, deleteObject } from 'firebase/storage';
|
||||
import { auth, db, storage } from '../lib/firebase';
|
||||
import { Loader2, User, Mail, Save, AlertTriangle, ShieldCheck } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
|
||||
export const ProfileView: React.FC<{
|
||||
user: any,
|
||||
userData: any,
|
||||
onUpdate: (data: any) => void,
|
||||
onDeleted: () => void
|
||||
}> = ({ user, userData, onUpdate, onDeleted }) => {
|
||||
const [name, setName] = useState(userData?.name || user?.displayName || '');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [messages, setMessages] = useState<{ error?: string, success?: string }>({});
|
||||
|
||||
const handleUpdate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!auth.currentUser) return;
|
||||
setIsSubmitting(true);
|
||||
setMessages({});
|
||||
|
||||
try {
|
||||
if (name !== user.displayName) {
|
||||
await updateProfile(auth.currentUser, { displayName: name });
|
||||
await updateDoc(doc(db, 'users', auth.currentUser.uid), { name });
|
||||
onUpdate({ ...userData, name });
|
||||
}
|
||||
|
||||
if (newPassword && currentPassword) {
|
||||
const credential = EmailAuthProvider.credential(auth.currentUser.email!, currentPassword);
|
||||
await reauthenticateWithCredential(auth.currentUser, credential);
|
||||
await updatePassword(auth.currentUser, newPassword);
|
||||
} else if (newPassword && !currentPassword) {
|
||||
throw new Error('Please enter your current password to set a new one.');
|
||||
}
|
||||
|
||||
setMessages({ success: 'Profile updated successfully!' });
|
||||
setNewPassword('');
|
||||
setCurrentPassword('');
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setMessages({ error: err.message || 'Failed to update profile.' });
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!auth.currentUser || !currentPassword) {
|
||||
setMessages({ error: 'Please enter your current password to delete your account.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm('Are you sure you want to delete your account? This action cannot be undone.')) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
setMessages({});
|
||||
try {
|
||||
const credential = EmailAuthProvider.credential(auth.currentUser.email!, currentPassword);
|
||||
await reauthenticateWithCredential(auth.currentUser, credential);
|
||||
|
||||
const uid = auth.currentUser.uid;
|
||||
|
||||
// Delete storage folder
|
||||
try {
|
||||
const folderRef = ref(storage, `user_uploads/${uid}`);
|
||||
const listRes = await listAll(folderRef);
|
||||
const deletePromises = listRes.items.map((itemRef) => deleteObject(itemRef));
|
||||
await Promise.all(deletePromises);
|
||||
} catch (storageErr) {
|
||||
console.error('Error deleting user storage files:', storageErr);
|
||||
}
|
||||
|
||||
// Delete document from firestore first
|
||||
await deleteDoc(doc(db, 'users', uid));
|
||||
|
||||
// Then delete from auth
|
||||
await deleteUser(auth.currentUser);
|
||||
onDeleted();
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
setMessages({ error: err.message || 'Failed to delete account. Make sure your password is correct.' });
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-2xl mx-auto space-y-6 pb-20">
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
<div className="w-full md:w-1/3 space-y-6">
|
||||
<div className="glass-card p-6 border-zinc-800/50 flex flex-col items-center text-center space-y-4">
|
||||
<div className="w-20 h-20 bg-brand-red/10 rounded-full flex items-center justify-center">
|
||||
<User size={32} className="text-brand-red" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-black text-white">{userData?.name || user?.displayName || 'User'}</h3>
|
||||
<p className="text-xs text-zinc-400 font-medium break-all">{user?.email}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{userData?.isPremium && (
|
||||
<div className="bg-gradient-to-r from-amber-500/20 to-yellow-500/20 text-amber-500 px-3 py-1 rounded-md text-[10px] font-black uppercase tracking-widest border border-amber-500/20">
|
||||
Pro
|
||||
</div>
|
||||
)}
|
||||
{userData?.isAdmin && (
|
||||
<div className="bg-brand-red/20 text-brand-red px-3 py-1 rounded-md text-[10px] font-black uppercase tracking-widest border border-brand-red/20 flex items-center gap-1">
|
||||
<ShieldCheck size={12} /> Admin
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-2/3 space-y-6">
|
||||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="glass-card p-6 border-zinc-800/50">
|
||||
<h3 className="text-sm font-black italic uppercase text-white tracking-widest mb-6">Profile Settings</h3>
|
||||
|
||||
<form onSubmit={handleUpdate} className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Name</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-600" size={16} />
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Your Name"
|
||||
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">Email</label>
|
||||
<div className="relative opacity-60 pointer-events-none">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-600" size={16} />
|
||||
<input
|
||||
type="email"
|
||||
value={user?.email || ''}
|
||||
readOnly
|
||||
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 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-white/5 space-y-4">
|
||||
<h4 className="text-[10px] uppercase font-black tracking-widest text-zinc-500">Change Password & Danger Zone</h4>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Current Password (Required for deleting or changing password)</label>
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
placeholder="Current Password"
|
||||
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">New Password (Optional)</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="New Password"
|
||||
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{messages.error && <p className="text-brand-red text-xs font-bold">{messages.error}</p>}
|
||||
{messages.success && <p className="text-green-500 text-xs font-bold">{messages.success}</p>}
|
||||
|
||||
<div className="flex gap-4 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="flex-1 bg-white text-black py-3 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-zinc-200 transition-all text-sm disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? <Loader2 className="animate-spin" size={16} /> : <><Save size={16} /> Update Profile</>}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className="flex-1 bg-red-500/10 border border-red-500/20 text-red-500 py-3 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-red-500/20 transition-all text-sm disabled:opacity-50"
|
||||
>
|
||||
{isDeleting ? <Loader2 className="animate-spin" size={16} /> : <><AlertTriangle size={16} /> Delete Account</>}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Signal, SignalResult, SignalType } from '../types';
|
||||
import { cn } from '../lib/utils';
|
||||
import { Clock, TrendingUp, TrendingDown } from 'lucide-react';
|
||||
import { collection, query, where, orderBy, onSnapshot } from 'firebase/firestore';
|
||||
import { auth, db, handleFirestoreError, OperationType } from '../lib/firebase';
|
||||
|
||||
export const SignalHistory: React.FC = () => {
|
||||
const [signals, setSignals] = useState<Signal[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!auth.currentUser) return;
|
||||
|
||||
const q = query(
|
||||
collection(db, 'signals'),
|
||||
where('userId', '==', auth.currentUser.uid)
|
||||
);
|
||||
|
||||
const unsubscribe = onSnapshot(q, (snapshot) => {
|
||||
let newSignals: Signal[] = [];
|
||||
snapshot.forEach((doc) => {
|
||||
newSignals.push({ id: doc.id, ...doc.data() } as Signal);
|
||||
});
|
||||
newSignals.sort((a, b) => b.timestamp - a.timestamp);
|
||||
setSignals(newSignals);
|
||||
setLoading(false);
|
||||
}, (error) => {
|
||||
handleFirestoreError(error, OperationType.GET, 'signals');
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="w-8 h-8 border-4 border-brand-red border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<header className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-black italic tracking-tighter text-white flex items-center gap-3 uppercase">
|
||||
<Clock size={20} className="text-brand-red" />
|
||||
Histórico de Sinais
|
||||
</h1>
|
||||
<span className="bg-brand-gray px-4 py-2 rounded-full text-zinc-400 text-sm font-bold border border-white/5">
|
||||
{signals.length} Sinais
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{signals.length === 0 ? (
|
||||
<div className="glass-card p-12 text-center text-zinc-500">
|
||||
Nenhum sinal encontrado. Comece realizando um novo scan.
|
||||
</div>
|
||||
) : (
|
||||
signals.map((signal) => (
|
||||
<div key={signal.id} className="glass-card p-4 flex flex-col md:flex-row md:items-center gap-6 group hover:border-white/20 transition-all">
|
||||
<div className={cn(
|
||||
"w-12 h-12 rounded-xl flex items-center justify-center shrink-0",
|
||||
signal.type === SignalType.BUY ? "bg-green-500/10 text-green-500" : "bg-brand-red/10 text-brand-red"
|
||||
)}>
|
||||
{signal.type === SignalType.BUY ? <TrendingUp size={24} /> : <TrendingDown size={24} />}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-7 flex-1 gap-4 items-center">
|
||||
<div>
|
||||
<span className="text-[10px] text-zinc-500 font-black uppercase">Ativo / TF</span>
|
||||
<p className="font-bold text-white text-sm">{signal.pair} <span className="text-zinc-500">· {signal.timeframe}</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-zinc-500 font-black uppercase">Tipo</span>
|
||||
<p className={cn(
|
||||
"font-bold italic uppercase text-sm",
|
||||
signal.type === SignalType.BUY ? "text-green-500" : "text-brand-red"
|
||||
)}>{signal.type}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-zinc-500 font-black uppercase">Score</span>
|
||||
<p className="font-bold text-white text-sm">{signal.score}%</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-zinc-500 font-black uppercase">SL / TP</span>
|
||||
<p className="font-bold text-white text-xs">{signal.stopLoss} <span className="text-zinc-600">/</span> <span className="text-zinc-400">{signal.takeProfit}</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-zinc-500 font-black uppercase">Data e Hora</span>
|
||||
<p className="font-bold text-zinc-300 text-xs whitespace-nowrap">{new Date(signal.timestamp).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" })}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-zinc-500 font-black uppercase">Status</span>
|
||||
<div className={cn(
|
||||
"inline-flex px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest",
|
||||
signal.result === SignalResult.PENDING ? "bg-blue-500/10 text-blue-500 border border-blue-500/20" : "bg-zinc-800 text-zinc-400 border border-zinc-700"
|
||||
)}>
|
||||
{signal.result === SignalResult.PENDING ? "Válido" : "Expirado"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-[10px] text-zinc-500 font-black uppercase">Resultado</span>
|
||||
<div className={cn(
|
||||
"flex items-center justify-end gap-1 font-black italic uppercase text-sm",
|
||||
signal.result === SignalResult.GAIN ? "text-green-500" :
|
||||
signal.result === SignalResult.LOSS ? "text-brand-red" :
|
||||
"text-zinc-500"
|
||||
)}>
|
||||
{signal.result}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px w-full md:h-12 md:w-px bg-white/5" />
|
||||
|
||||
<div className="flex md:flex-col justify-between text-right gap-1 min-w-[80px]">
|
||||
<span className="text-[10px] text-zinc-500 font-black uppercase">Data</span>
|
||||
<span className="text-[10px] text-zinc-400 font-medium">
|
||||
{new Date(signal.timestamp).toLocaleDateString()}
|
||||
</span>
|
||||
<span className="text-[10px] text-zinc-600 font-medium hidden md:block">
|
||||
{new Date(signal.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-brand-red: #ff0000;
|
||||
--color-brand-dark: #0a0a0a;
|
||||
--color-brand-gray: #151515;
|
||||
--color-brand-light: #f5f5f5;
|
||||
|
||||
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
|
||||
|
||||
--radius-xl: 0.75rem;
|
||||
--radius-2xl: 1rem;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
font-size: 12px;
|
||||
@media (min-width: 768px) {
|
||||
font-size: 13px;
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
body {
|
||||
@apply bg-brand-dark text-brand-light font-sans antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
@apply bg-brand-gray/15 backdrop-blur-md border border-white/5 rounded-xl p-4 md:p-5;
|
||||
}
|
||||
|
||||
.red-glow {
|
||||
box-shadow: 0 0 15px rgba(255, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.red-text-glow {
|
||||
text-shadow: 0 0 8px rgba(255, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
input, button {
|
||||
@apply focus:outline-none transition-all duration-200;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-white/10 rounded-full;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { initializeApp } from "firebase/app";
|
||||
import { getAuth } from "firebase/auth";
|
||||
import { getFirestore } from "firebase/firestore";
|
||||
import { getStorage } from "firebase/storage";
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDVUxpAmWf57dGrj0dltwg-2d3jytGpJCY",
|
||||
authDomain: "quantscan-pro.firebaseapp.com",
|
||||
projectId: "quantscan-pro",
|
||||
storageBucket: "quantscan-pro.firebasestorage.app",
|
||||
messagingSenderId: "580184623755",
|
||||
appId: "1:580184623755:web:4c5fcf6fd4a8f7bc13628e",
|
||||
measurementId: "G-4WZHMKD7Y6"
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
export const db = getFirestore(app);
|
||||
export const auth = getAuth(app);
|
||||
export const storage = getStorage(app);
|
||||
|
||||
export enum OperationType {
|
||||
CREATE = 'create',
|
||||
UPDATE = 'update',
|
||||
DELETE = 'delete',
|
||||
LIST = 'list',
|
||||
GET = 'get',
|
||||
WRITE = 'write',
|
||||
}
|
||||
|
||||
interface FirestoreErrorInfo {
|
||||
error: string;
|
||||
operationType: OperationType;
|
||||
path: string | null;
|
||||
authInfo: {
|
||||
userId?: string | null;
|
||||
email?: string | null;
|
||||
emailVerified?: boolean | null;
|
||||
isAnonymous?: boolean | null;
|
||||
tenantId?: string | null;
|
||||
providerInfo?: {
|
||||
providerId?: string | null;
|
||||
email?: string | null;
|
||||
}[];
|
||||
}
|
||||
}
|
||||
|
||||
export function handleFirestoreError(error: unknown, operationType: OperationType, path: string | null) {
|
||||
const errInfo: FirestoreErrorInfo = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
authInfo: {
|
||||
userId: auth.currentUser?.uid,
|
||||
email: auth.currentUser?.email,
|
||||
emailVerified: auth.currentUser?.emailVerified,
|
||||
isAnonymous: auth.currentUser?.isAnonymous,
|
||||
tenantId: auth.currentUser?.tenantId,
|
||||
providerInfo: auth.currentUser?.providerData?.map(provider => ({
|
||||
providerId: provider.providerId,
|
||||
email: provider.email,
|
||||
})) || []
|
||||
},
|
||||
operationType,
|
||||
path
|
||||
}
|
||||
console.error('Firestore Error: ', JSON.stringify(errInfo));
|
||||
throw new Error(JSON.stringify(errInfo));
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,95 @@
|
||||
import { GoogleGenAI, Type } from "@google/genai";
|
||||
import { AnalysisResponse, SignalType } from "../types";
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY || '' });
|
||||
|
||||
export const analyzeForexChart = async (imageBase64: string, userNotes?: string): Promise<AnalysisResponse> => {
|
||||
const model = "gemini-3-flash-preview";
|
||||
|
||||
const systemInstruction = `
|
||||
Você é um Analista Institucional de Forex com IA adaptativa, especializado em Fluxo de Ordens, Liquidez e Comportamento Institucional (PRO Logic).
|
||||
Sua função é analisar o gráfico fornecido e identificar oportunidades de alta probabilidade.
|
||||
|
||||
Analise a imagem detalhadamente em busca de:
|
||||
1. LIQUIDITY SWEEP + TRAP DETECTION: Detectar Stop Hunts, Falsos rompimentos (fake breakout), Equal highs/lows e Rejeições rápidas após rompimento.
|
||||
2. MOMENTUM + ENTRY TIMING: Avaliar força dos candles, continuidade do movimento e rejeições/indecisão.
|
||||
3. ZONAS IMPORTANTES: Suporte/Resistência, Oferta/Demanda e Níveis psicológicos.
|
||||
4. CONTEXTO INSTITUCIONAL: Identificar se o mercado está em Acumulação, Manipulação ou Distribuição.
|
||||
|
||||
Detecte automaticamente o Timeframe e o Par de Moedas se visíveis.
|
||||
|
||||
Calcule um Score de Probabilidade (0-100%):
|
||||
- Estrutura clara e sweep de liquidez confirmado: +30
|
||||
- Contexto institucional favorável (Manipulação para expansão): +20
|
||||
- Momentum forte na direção da entrada: +15
|
||||
- Zonas de oferta/demanda frescas: +15
|
||||
- Confluência técnica: +20
|
||||
- Ruído ou indecisão: -10 a -30
|
||||
`;
|
||||
|
||||
const prompt = `
|
||||
Analise este gráfico de Forex sob a óptica institucional. ${userNotes ? `Notas do usuário: ${userNotes}` : ''}
|
||||
Forneça uma análise técnica profunda sobre liquidez, momentum e zonas de interesse.
|
||||
|
||||
Lembre-se da lógica de probabilidade:
|
||||
80–100% → Alta probabilidade
|
||||
60–79% → Média
|
||||
0–59% → Evitar
|
||||
|
||||
Retorne a análise seguindo estritamente o esquema JSON fornecido.
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model,
|
||||
contents: [
|
||||
{
|
||||
parts: [
|
||||
{ text: prompt },
|
||||
{ inlineData: { mimeType: "image/png", data: imageBase64 } }
|
||||
]
|
||||
}
|
||||
],
|
||||
config: {
|
||||
systemInstruction,
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
pair: { type: Type.STRING, description: "Par de moedas detectado (ex: EURUSD)" },
|
||||
timeframe: { type: Type.STRING, description: "Timeframe detectado (ex: H1)" },
|
||||
structure: { type: Type.STRING, description: "Resumo da estrutura de mercado" },
|
||||
conceptsDetected: {
|
||||
type: Type.ARRAY,
|
||||
items: { type: Type.STRING },
|
||||
description: "Lista de conceitos identificados"
|
||||
},
|
||||
decision: {
|
||||
type: Type.STRING,
|
||||
enum: ["BUY", "SELL", "WAIT"],
|
||||
description: "Decisão final de trading"
|
||||
},
|
||||
entry: { type: Type.STRING, description: "Preço ou zona de entrada" },
|
||||
stopLoss: { type: Type.STRING, description: "Preço de Stop Loss" },
|
||||
takeProfit: { type: Type.STRING, description: "Preço de Take Profit" },
|
||||
score: { type: Type.NUMBER, description: "Score de probabilidade de 0 a 100" },
|
||||
justification: { type: Type.STRING, description: "Explicação técnica para o score" },
|
||||
liquiditySweep: { type: Type.STRING, description: "Descrição do sweep de liquidez detectado ou armadilhas" },
|
||||
momentum: { type: Type.STRING, description: "Análise do momentum e timing de entrada" },
|
||||
keyZones: {
|
||||
type: Type.ARRAY,
|
||||
items: { type: Type.STRING },
|
||||
description: "Zonas importantes detectadas"
|
||||
},
|
||||
institutionalContext: {
|
||||
type: Type.STRING,
|
||||
enum: ["Accumulation", "Manipulation", "Distribution", "None"],
|
||||
description: "Fase do ciclo institucional"
|
||||
}
|
||||
},
|
||||
required: ["pair", "timeframe", "structure", "conceptsDetected", "decision", "entry", "stopLoss", "takeProfit", "score", "justification", "liquiditySweep", "momentum", "keyZones", "institutionalContext"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.parse(response.text || '{}') as AnalysisResponse;
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @license
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export enum SignalType {
|
||||
BUY = 'BUY',
|
||||
SELL = 'SELL',
|
||||
WAIT = 'WAIT'
|
||||
}
|
||||
|
||||
export enum SignalResult {
|
||||
GAIN = 'GAIN',
|
||||
LOSS = 'LOSS',
|
||||
BE = 'BE',
|
||||
PENDING = 'PENDING'
|
||||
}
|
||||
|
||||
export interface Signal {
|
||||
id: string;
|
||||
pair: string;
|
||||
timeframe: string;
|
||||
timestamp: number;
|
||||
type: SignalType;
|
||||
entry: string;
|
||||
stopLoss: string;
|
||||
takeProfit: string;
|
||||
score: number;
|
||||
justification: string;
|
||||
logicElements: string[];
|
||||
result: SignalResult;
|
||||
screenshotUrl?: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface UserStats {
|
||||
userId: string;
|
||||
totalSignals: number;
|
||||
winRate: number;
|
||||
totalProfitPips: number;
|
||||
bestTimeframe: string;
|
||||
bestPattern: string;
|
||||
performanceByTimeframe: Record<string, number>;
|
||||
performanceByPattern: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface AnalysisResponse {
|
||||
pair: string;
|
||||
timeframe: string;
|
||||
structure: string;
|
||||
conceptsDetected: string[];
|
||||
decision: SignalType;
|
||||
entry: string;
|
||||
stopLoss: string;
|
||||
takeProfit: string;
|
||||
score: number;
|
||||
justification: string;
|
||||
liquiditySweep?: string;
|
||||
momentum?: string;
|
||||
keyZones?: string[];
|
||||
institutionalContext?: 'Accumulation' | 'Manipulation' | 'Distribution' | 'None';
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import {defineConfig, loadEnv} from 'vite';
|
||||
|
||||
export default defineConfig(({mode}) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
define: {
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||
hmr: process.env.DISABLE_HMR !== 'true',
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user