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,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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user