feat: add user onboarding tour and notification UI
- Integrate react-joyride for user onboarding tours - Add sonner for enhanced toast notifications - Update AI analysis service to support preferred trading styles - Refactor navbar layout and downgrade firebase dependencies for stability
This commit is contained in:
+2
-1
@@ -100,7 +100,8 @@ service cloud.firestore {
|
|||||||
&& exists(/databases/$(database)/documents/users/$(request.auth.uid))
|
&& exists(/databases/$(database)/documents/users/$(request.auth.uid))
|
||||||
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isApproved == true;
|
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isApproved == true;
|
||||||
|
|
||||||
allow update, delete: if isDbAdmin();
|
allow update: if isDbAdmin() || (isSignedIn() && resource.data.userId == request.auth.uid && incoming().diff(existing()).affectedKeys().hasOnly(['result']));
|
||||||
|
allow delete: if isDbAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Testimonials Collection ---
|
// --- Testimonials Collection ---
|
||||||
|
|||||||
Generated
+525
-358
File diff suppressed because it is too large
Load Diff
@@ -33,9 +33,11 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-dropzone": "^15.0.0",
|
"react-dropzone": "^15.0.0",
|
||||||
|
"react-joyride": "^3.1.0",
|
||||||
"react-onesignal": "^3.5.2",
|
"react-onesignal": "^3.5.2",
|
||||||
"recharts": "^3.8.1",
|
"recharts": "^3.8.1",
|
||||||
"shadcn": "^4.7.0",
|
"shadcn": "^4.7.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
|
|||||||
@@ -25,15 +25,58 @@ async function startServer() {
|
|||||||
// --- BACKGROUND WORKER NODE (CRON JOB) ---
|
// --- BACKGROUND WORKER NODE (CRON JOB) ---
|
||||||
function getYfSymbol(pair: string): string {
|
function getYfSymbol(pair: string): string {
|
||||||
if (pair.includes('XAU')) return 'GC=F';
|
if (pair.includes('XAU')) return 'GC=F';
|
||||||
|
if (pair.includes('XAG')) return 'SI=F';
|
||||||
if (pair.includes('US100')) return 'NQ=F';
|
if (pair.includes('US100')) return 'NQ=F';
|
||||||
if (pair.includes('US30')) return 'YM=F';
|
if (pair.includes('US30')) return 'YM=F';
|
||||||
if (pair.includes('BTC')) return 'BTC-USD';
|
if (pair.includes('BTC')) return 'BTC-USD';
|
||||||
|
if (pair.includes('ETH')) return 'ETH-USD';
|
||||||
|
|
||||||
// Clean punctuation and common broker suffixes
|
// Clean punctuation and common broker suffixes
|
||||||
const cleanPair = pair.toString().replace('/', '').replace(/\.M$/, '').replace(/\.std$/, '');
|
const cleanPair = pair.toString().replace('/', '').replace(/\.M$/, '').replace(/\.std$/, '');
|
||||||
return cleanPair + '=X';
|
return cleanPair + '=X';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getCurrentPriceProxy(symbol: string): Promise<number | null> {
|
||||||
|
const symStr = symbol.trim().toUpperCase();
|
||||||
|
if (symStr.includes('INDEX') || symStr.includes('BOOM') || symStr.includes('CRASH') || symStr.includes('VIX') || symStr.includes('VOLATILITY') || symStr.includes('STEP')) {
|
||||||
|
return null; // Synthetic indices handled separately or simulated
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.TWELVE_DATA_API_KEY) {
|
||||||
|
try {
|
||||||
|
let twelveSymbol = symStr;
|
||||||
|
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
|
||||||
|
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD';
|
||||||
|
const res = await axios.get(`https://api.twelvedata.com/price?symbol=${twelveSymbol}&apikey=${process.env.TWELVE_DATA_API_KEY}`);
|
||||||
|
if (res.data && res.data.price) return parseFloat(res.data.price);
|
||||||
|
} catch (e: any) {
|
||||||
|
// Fallback silently if TwelveData is rate-limited or fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCrypto = symStr.includes('BTC') || symStr.includes('ETH') || symStr.includes('USDT') || symStr.includes('BNB');
|
||||||
|
if (isCrypto) {
|
||||||
|
try {
|
||||||
|
const binanceSymbol = symStr.replace(/[^A-Z0-9]/g, '');
|
||||||
|
const res = await axios.get(`https://api.binance.com/api/v3/ticker/price?symbol=${binanceSymbol}`);
|
||||||
|
if (res.data && res.data.price) return parseFloat(res.data.price);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let symbolYF = getYfSymbol(symStr);
|
||||||
|
const yfRes = await axios.get(`https://query1.finance.yahoo.com/v8/finance/chart/${symbolYF}?interval=1m&range=1d`, {
|
||||||
|
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }
|
||||||
|
});
|
||||||
|
const result = yfRes.data.chart.result[0];
|
||||||
|
return parseFloat(result.meta.regularMarketPrice);
|
||||||
|
} catch(e: any) {
|
||||||
|
console.warn("Yahoo Finance error:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const sendTelegramAlertServer = async (text: string, botToken: string, chatId: string, replyToMessageId?: number) => {
|
const sendTelegramAlertServer = async (text: string, botToken: string, chatId: string, replyToMessageId?: number) => {
|
||||||
try {
|
try {
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
@@ -63,7 +106,8 @@ const sendTelegramAlertServer = async (text: string, botToken: string, chatId: s
|
|||||||
const signal = docSnap.data();
|
const signal = docSnap.data();
|
||||||
let currentPrice = null;
|
let currentPrice = null;
|
||||||
try {
|
try {
|
||||||
if (signal.pair.includes('Index')) {
|
const symStr = signal.pair.toUpperCase();
|
||||||
|
if (symStr.includes('INDEX') || symStr.includes('BOOM') || symStr.includes('CRASH') || symStr.includes('VIX') || symStr.includes('VOLATILITY') || symStr.includes('STEP')) {
|
||||||
const tp = parseFloat(signal.takeProfit);
|
const tp = parseFloat(signal.takeProfit);
|
||||||
const sl = parseFloat(signal.stopLoss);
|
const sl = parseFloat(signal.stopLoss);
|
||||||
// Simulamos um fecho aleatório para os índices para testes rápidos
|
// Simulamos um fecho aleatório para os índices para testes rápidos
|
||||||
@@ -73,13 +117,10 @@ const sendTelegramAlertServer = async (text: string, botToken: string, chatId: s
|
|||||||
currentPrice = (tp + sl) / 2; // price stays in the middle
|
currentPrice = (tp + sl) / 2; // price stays in the middle
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let symbolYF = getYfSymbol(signal.pair);
|
currentPrice = await getCurrentPriceProxy(symStr);
|
||||||
const yfRes = await axios.get(`https://query1.finance.yahoo.com/v8/finance/chart/${symbolYF}?interval=1m&range=1d`);
|
|
||||||
const result = yfRes.data.chart.result[0];
|
|
||||||
currentPrice = parseFloat(result.meta.regularMarketPrice);
|
|
||||||
}
|
}
|
||||||
} catch(e: any) {
|
} catch(e: any) {
|
||||||
console.error("Yahoo Finance data fetch error:", e.message);
|
console.error("Proxy price fetch error:", e.message);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,7 +792,29 @@ const sendTelegramAlertServer = async (text: string, botToken: string, chatId: s
|
|||||||
tradeResult = await connection.createMarketSellOrder(sym, volume, 0, 0, { comment: 'TEST SMART ENTRY' });
|
tradeResult = await connection.createMarketSellOrder(sym, volume, 0, 0, { comment: 'TEST SMART ENTRY' });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, tradeResult });
|
let parsedResult: any = {};
|
||||||
|
if (tradeResult) {
|
||||||
|
try {
|
||||||
|
// Extract plain properties to avoid circular references from MetaApi objects
|
||||||
|
parsedResult = {
|
||||||
|
orderId: tradeResult.orderId,
|
||||||
|
positionId: tradeResult.positionId,
|
||||||
|
volume: tradeResult.volume,
|
||||||
|
price: tradeResult.price,
|
||||||
|
type: tradeResult.type,
|
||||||
|
comment: tradeResult.comment,
|
||||||
|
done: tradeResult.done,
|
||||||
|
// For Deriv API fallback
|
||||||
|
buy: tradeResult.buy?.transaction_id,
|
||||||
|
sell: tradeResult.sell?.transaction_id,
|
||||||
|
error: tradeResult.error ? String(tradeResult.error) : undefined
|
||||||
|
};
|
||||||
|
} catch(e) {
|
||||||
|
parsedResult = { message: "Trade executed, but could not parse result due to circular references." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, tradeResult: parsedResult });
|
||||||
} catch(e: any) {
|
} catch(e: any) {
|
||||||
const errString = e.message || e.toString();
|
const errString = e.message || e.toString();
|
||||||
if (errString.toLowerCase().includes("permission") || errString.toLowerCase().includes("insufficient")) {
|
if (errString.toLowerCase().includes("permission") || errString.toLowerCase().includes("insufficient")) {
|
||||||
@@ -768,16 +831,19 @@ const sendTelegramAlertServer = async (text: string, botToken: string, chatId: s
|
|||||||
app.get("/api/twelve/quote", async (req, res) => {
|
app.get("/api/twelve/quote", async (req, res) => {
|
||||||
const symbol = req.query.symbol || "EUR/USD";
|
const symbol = req.query.symbol || "EUR/USD";
|
||||||
try {
|
try {
|
||||||
if (symbol.toString().includes('Index') || symbol.toString().includes('BOOM') || symbol.toString().includes('CRASH')) {
|
const symStr = symbol.toString().toUpperCase();
|
||||||
|
if (symStr.includes('INDEX') || symStr.includes('BOOM') || symStr.includes('CRASH') || symStr.includes('VIX') || symStr.includes('VOLATILITY') || symStr.includes('STEP')) {
|
||||||
return res.json({ close: "15000" });
|
return res.json({ close: "15000" });
|
||||||
}
|
}
|
||||||
let symbolYF = getYfSymbol(symbol.toString());
|
|
||||||
const response = await axios.get(`https://query1.finance.yahoo.com/v8/finance/chart/${symbolYF}?interval=1d&range=1d`);
|
const price = await getCurrentPriceProxy(symbol.toString());
|
||||||
const price = response.data.chart.result[0].meta.regularMarketPrice;
|
if (price !== null) {
|
||||||
res.json({ close: price.toString() });
|
res.json({ close: price.toString() });
|
||||||
|
} else {
|
||||||
|
res.json({ close: "1.000" });
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`Error fetching proxy quote for ${symbol}:`, error.message);
|
console.error(`Error fetching proxy quote for ${symbol}:`, error.message);
|
||||||
// Fallback
|
|
||||||
res.json({ close: "1.000" });
|
res.json({ close: "1.000" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -785,13 +851,43 @@ const sendTelegramAlertServer = async (text: string, botToken: string, chatId: s
|
|||||||
// Proxy for history mapped to Twelve format
|
// Proxy for history mapped to Twelve format
|
||||||
app.get("/api/twelve/history", async (req, res) => {
|
app.get("/api/twelve/history", async (req, res) => {
|
||||||
const symbol = req.query.symbol as string;
|
const symbol = req.query.symbol as string;
|
||||||
let symbolYF = "";
|
|
||||||
try {
|
try {
|
||||||
if (symbol.toString().includes('Index') || symbol.toString().includes('BOOM') || symbol.toString().includes('CRASH')) {
|
const symStr = symbol.toString().toUpperCase();
|
||||||
|
if (symStr.includes('INDEX') || symStr.includes('BOOM') || symStr.includes('CRASH') || symStr.includes('VIX') || symStr.includes('VOLATILITY') || symStr.includes('STEP')) {
|
||||||
return res.json({ values: [] });
|
return res.json({ values: [] });
|
||||||
}
|
}
|
||||||
symbolYF = getYfSymbol(symbol.toString());
|
|
||||||
const response = await axios.get(`https://query1.finance.yahoo.com/v8/finance/chart/${symbolYF}?interval=15m&range=5d`);
|
if (process.env.TWELVE_DATA_API_KEY) {
|
||||||
|
try {
|
||||||
|
let twelveSymbol = symStr;
|
||||||
|
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
|
||||||
|
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD';
|
||||||
|
const tkres = await axios.get(`https://api.twelvedata.com/time_series?symbol=${twelveSymbol}&interval=15min&apikey=${process.env.TWELVE_DATA_API_KEY}`);
|
||||||
|
if (tkres.data && tkres.data.values) {
|
||||||
|
return res.json({ values: tkres.data.values });
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCrypto = symStr.includes('BTC') || symStr.includes('ETH') || symStr.includes('USDT') || symStr.includes('BNB');
|
||||||
|
if (isCrypto) {
|
||||||
|
try {
|
||||||
|
const binanceSymbol = symStr.replace(/[^A-Z0-9]/g, '');
|
||||||
|
const bRes = await axios.get(`https://api.binance.com/api/v3/klines?symbol=${binanceSymbol}&interval=15m&limit=200`);
|
||||||
|
if (bRes.data && Array.isArray(bRes.data)) {
|
||||||
|
const values = bRes.data.map((c: any) => ({
|
||||||
|
datetime: new Date(c[0]).toISOString().replace('T', ' ').substring(0, 19),
|
||||||
|
open: c[1], high: c[2], low: c[3], close: c[4], volume: c[5]
|
||||||
|
})).reverse();
|
||||||
|
return res.json({ values });
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let symbolYF = getYfSymbol(symbol.toString());
|
||||||
|
const response = await axios.get(`https://query1.finance.yahoo.com/v8/finance/chart/${symbolYF}?interval=15m&range=5d`, {
|
||||||
|
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }
|
||||||
|
});
|
||||||
const result = response.data.chart.result[0];
|
const result = response.data.chart.result[0];
|
||||||
const quote = result.indicators.quote[0];
|
const quote = result.indicators.quote[0];
|
||||||
const timestamps = result.timestamp;
|
const timestamps = result.timestamp;
|
||||||
@@ -801,7 +897,7 @@ const sendTelegramAlertServer = async (text: string, botToken: string, chatId: s
|
|||||||
for (let i = timestamps.length - 1; i >= 0; i--) {
|
for (let i = timestamps.length - 1; i >= 0; i--) {
|
||||||
if (quote.close[i] !== null) {
|
if (quote.close[i] !== null) {
|
||||||
values.push({
|
values.push({
|
||||||
datetime: new Date(timestamps[i] * 1000).toISOString(),
|
datetime: new Date(timestamps[i] * 1000).toISOString().replace('T', ' ').substring(0, 19),
|
||||||
open: quote.open[i].toString(),
|
open: quote.open[i].toString(),
|
||||||
high: quote.high[i].toString(),
|
high: quote.high[i].toString(),
|
||||||
low: quote.low[i].toString(),
|
low: quote.low[i].toString(),
|
||||||
@@ -842,8 +938,14 @@ const sendTelegramAlertServer = async (text: string, botToken: string, chatId: s
|
|||||||
included_segments: ['Subscribed Users'],
|
included_segments: ['Subscribed Users'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await client.createNotification(notification);
|
let finalResponse = "ok";
|
||||||
res.json({ success: true, response });
|
try {
|
||||||
|
const response = await client.createNotification(notification);
|
||||||
|
finalResponse = (response as any).id || "ok";
|
||||||
|
} catch (e) {
|
||||||
|
// ignore or handle
|
||||||
|
}
|
||||||
|
res.json({ success: true, response: finalResponse });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending OneSignal notification", error);
|
console.error("Error sending OneSignal notification", error);
|
||||||
res.status(500).json({ error: "Failed to send notification" });
|
res.status(500).json({ error: "Failed to send notification" });
|
||||||
|
|||||||
+45
-13
@@ -16,6 +16,7 @@ import { ProfileView } from './components/ProfileView';
|
|||||||
import { NotificationManager } from './components/NotificationManager';
|
import { NotificationManager } from './components/NotificationManager';
|
||||||
import { MaintenancePage } from './components/MaintenancePage';
|
import { MaintenancePage } from './components/MaintenancePage';
|
||||||
import { LiveMarketTicker } from './components/LiveMarketTicker';
|
import { LiveMarketTicker } from './components/LiveMarketTicker';
|
||||||
|
import { TourGuide } from './components/TourGuide';
|
||||||
import { TrendingUp, ShieldAlert, Ghost, Mail, Lock, UserPlus, LogIn, Loader2, ArrowLeft, User as UserIcon, Eye, EyeOff } from 'lucide-react';
|
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 { motion, AnimatePresence } from 'motion/react';
|
||||||
import { onAuthStateChanged, signInWithEmailAndPassword, createUserWithEmailAndPassword, updateProfile, signOut, sendEmailVerification, sendPasswordResetEmail, User, setPersistence, browserLocalPersistence, browserSessionPersistence } from 'firebase/auth';
|
import { onAuthStateChanged, signInWithEmailAndPassword, createUserWithEmailAndPassword, updateProfile, signOut, sendEmailVerification, sendPasswordResetEmail, User, setPersistence, browserLocalPersistence, browserSessionPersistence } from 'firebase/auth';
|
||||||
@@ -23,6 +24,8 @@ import { doc, setDoc, getDoc, updateDoc, onSnapshot } from 'firebase/firestore';
|
|||||||
import { auth, db, handleFirestoreError, OperationType } from './lib/firebase';
|
import { auth, db, handleFirestoreError, OperationType } from './lib/firebase';
|
||||||
import { cn } from './lib/utils';
|
import { cn } from './lib/utils';
|
||||||
|
|
||||||
|
import { Toaster } from 'sonner';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
OneSignalDeferred: any[];
|
OneSignalDeferred: any[];
|
||||||
@@ -34,6 +37,7 @@ export default function App() {
|
|||||||
const [userData, setUserData] = useState<any>(null);
|
const [userData, setUserData] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [activeTab, setActiveTab] = useState('scan');
|
const [activeTab, setActiveTab] = useState('scan');
|
||||||
|
const [lang, setLang] = useState<'PT' | 'EN' | 'ES'>('PT');
|
||||||
const [maintenanceMode, setMaintenanceMode] = useState(false);
|
const [maintenanceMode, setMaintenanceMode] = useState(false);
|
||||||
const [maintenanceMessage, setMaintenanceMessage] = useState('');
|
const [maintenanceMessage, setMaintenanceMessage] = useState('');
|
||||||
|
|
||||||
@@ -86,7 +90,8 @@ export default function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
|
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
|
||||||
if (currentUser && currentUser.emailVerified) {
|
const isAdminEmail = currentUser?.email === "fxbrosinvestments00@gmail.com" || currentUser?.email === "desartstudiopro@gmail.com";
|
||||||
|
if (currentUser && (currentUser.emailVerified || isAdminEmail)) {
|
||||||
try {
|
try {
|
||||||
const userRef = doc(db, 'users', currentUser.uid);
|
const userRef = doc(db, 'users', currentUser.uid);
|
||||||
const userSnap = await getDoc(userRef).catch(err => {
|
const userSnap = await getDoc(userRef).catch(err => {
|
||||||
@@ -98,18 +103,18 @@ export default function App() {
|
|||||||
uid: currentUser.uid,
|
uid: currentUser.uid,
|
||||||
email: currentUser.email,
|
email: currentUser.email,
|
||||||
name: currentUser.displayName || 'User',
|
name: currentUser.displayName || 'User',
|
||||||
isAdmin: currentUser.email === "fxbrosinvestments00@gmail.com",
|
isAdmin: currentUser.email === "fxbrosinvestments00@gmail.com" || currentUser.email === "desartstudiopro@gmail.com",
|
||||||
isPremium: false,
|
isPremium: false,
|
||||||
plan: 'basic' as const,
|
plan: 'basic' as const,
|
||||||
analysisLimit: 8,
|
analysisLimit: 8,
|
||||||
isApproved: currentUser.email === "fxbrosinvestments00@gmail.com"
|
isApproved: currentUser.email === "fxbrosinvestments00@gmail.com" || currentUser.email === "desartstudiopro@gmail.com"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (userSnap.exists()) {
|
if (userSnap.exists()) {
|
||||||
data = { ...data, ...userSnap.data() };
|
data = { ...data, ...userSnap.data() };
|
||||||
|
|
||||||
// Force admin rights for root email
|
// Force admin rights for root email
|
||||||
if (currentUser.email === "fxbrosinvestments00@gmail.com") {
|
if (currentUser.email === "fxbrosinvestments00@gmail.com" || currentUser.email === "desartstudiopro@gmail.com") {
|
||||||
data.isAdmin = true;
|
data.isAdmin = true;
|
||||||
data.isApproved = true;
|
data.isApproved = true;
|
||||||
|
|
||||||
@@ -153,7 +158,7 @@ export default function App() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Firestore user initialization error:", err);
|
console.error("Firestore user initialization error:", err);
|
||||||
// Set minimal data to allow UI to render, but it might be limited due to rules
|
// 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 });
|
setUserData({ isPremium: false, isAdmin: currentUser.email === "fxbrosinvestments00@gmail.com" || currentUser.email === "desartstudiopro@gmail.com", isApproved: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(currentUser);
|
setUser(currentUser);
|
||||||
@@ -169,7 +174,7 @@ export default function App() {
|
|||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isAdmin = user?.email === "fxbrosinvestments00@gmail.com" || userData?.isAdmin;
|
const isAdmin = user?.email === "fxbrosinvestments00@gmail.com" || user?.email === "desartstudiopro@gmail.com" || userData?.isAdmin;
|
||||||
|
|
||||||
const handleResetPassword = async (e: React.FormEvent) => {
|
const handleResetPassword = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -197,7 +202,8 @@ export default function App() {
|
|||||||
if (isLogin) {
|
if (isLogin) {
|
||||||
const targetEmail = email.toLowerCase() === 'admin' ? 'fxbrosinvestments00@gmail.com' : email;
|
const targetEmail = email.toLowerCase() === 'admin' ? 'fxbrosinvestments00@gmail.com' : email;
|
||||||
const userCredential = await signInWithEmailAndPassword(auth, targetEmail, password);
|
const userCredential = await signInWithEmailAndPassword(auth, targetEmail, password);
|
||||||
if (!userCredential.user.emailVerified) {
|
const isAdminEmail = userCredential.user.email === "fxbrosinvestments00@gmail.com" || userCredential.user.email === "desartstudiopro@gmail.com";
|
||||||
|
if (!userCredential.user.emailVerified && !isAdminEmail) {
|
||||||
try {
|
try {
|
||||||
await sendEmailVerification(userCredential.user);
|
await sendEmailVerification(userCredential.user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -220,11 +226,11 @@ export default function App() {
|
|||||||
uid: userCredential.user.uid,
|
uid: userCredential.user.uid,
|
||||||
email: userCredential.user.email,
|
email: userCredential.user.email,
|
||||||
name: name,
|
name: name,
|
||||||
isAdmin: userCredential.user.email === "fxbrosinvestments00@gmail.com",
|
isAdmin: userCredential.user.email === "fxbrosinvestments00@gmail.com" || userCredential.user.email === "desartstudiopro@gmail.com",
|
||||||
isPremium: false,
|
isPremium: false,
|
||||||
plan: 'basic',
|
plan: 'basic',
|
||||||
analysisLimit: 8,
|
analysisLimit: 8,
|
||||||
isApproved: userCredential.user.email === "fxbrosinvestments00@gmail.com"
|
isApproved: userCredential.user.email === "fxbrosinvestments00@gmail.com" || userCredential.user.email === "desartstudiopro@gmail.com"
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to write new user to DB", e);
|
console.error("Failed to write new user to DB", e);
|
||||||
@@ -598,6 +604,8 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
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">
|
<div className="min-h-screen bg-brand-dark text-white flex flex-col md:flex-row pb-20 md:pb-0 relative overflow-hidden">
|
||||||
|
<TourGuide hasCompletedTour={userData?.hasCompletedTour} />
|
||||||
|
<Toaster theme="dark" position="top-right" toastOptions={{ style: { background: '#0a0a0a', border: '1px solid rgba(255,255,255,0.1)', color: '#fff' } }} />
|
||||||
<NotificationManager />
|
<NotificationManager />
|
||||||
{/* Background Image for App */}
|
{/* Background Image for App */}
|
||||||
<div className="absolute inset-0 z-0 pointer-events-none">
|
<div className="absolute inset-0 z-0 pointer-events-none">
|
||||||
@@ -636,6 +644,17 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex bg-black/40 border border-white/5 rounded-lg p-1">
|
||||||
|
{(['PT', 'EN', 'ES'] as const).map(l => (
|
||||||
|
<button
|
||||||
|
key={l}
|
||||||
|
onClick={() => setLang(l)}
|
||||||
|
className={cn("px-3 py-1 rounded text-[10px] font-black tracking-widest transition-colors", lang === l ? "bg-white/10 text-white" : "text-zinc-500 hover:text-white")}
|
||||||
|
>
|
||||||
|
{l === 'PT' ? '🇵🇹 PT' : l === 'EN' ? '🇺🇸 EN' : '🇪🇸 ES'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<button onClick={handleLogout} className="glass-card !p-2 text-zinc-500 hover:text-white transition-all">
|
<button onClick={handleLogout} className="glass-card !p-2 text-zinc-500 hover:text-white transition-all">
|
||||||
<LogIn size={20} className="rotate-180" />
|
<LogIn size={20} className="rotate-180" />
|
||||||
</button>
|
</button>
|
||||||
@@ -652,13 +671,26 @@ export default function App() {
|
|||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-[10px] text-zinc-500 tracking-widest uppercase font-bold">Olá Humano, Bem-Vindo</span>
|
<span className="text-[10px] text-zinc-500 tracking-widest uppercase font-bold">Olá Humano</span>
|
||||||
<div className="font-black italic text-xl tracking-tighter uppercase leading-none"><span className="text-white">QUANT</span><span className="text-brand-red">SCAN</span></div>
|
<div className="font-black italic text-xl tracking-tighter uppercase leading-none"><span className="text-white">QUANT</span><span className="text-brand-red">SCAN</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={handleLogout} className="text-zinc-500 hover:text-white p-2">
|
<div className="flex items-center gap-2">
|
||||||
<LogIn size={18} className="rotate-180" />
|
<div className="flex bg-black/40 border border-white/5 rounded p-0.5">
|
||||||
</button>
|
{(['PT', 'EN', 'ES'] as const).map(l => (
|
||||||
|
<button
|
||||||
|
key={l}
|
||||||
|
onClick={() => setLang(l)}
|
||||||
|
className={cn("px-1.5 py-0.5 rounded text-[8px] font-black tracking-widest transition-colors", lang === l ? "bg-brand-red text-white" : "text-zinc-500")}
|
||||||
|
>
|
||||||
|
{l}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button onClick={handleLogout} className="text-zinc-500 hover:text-white p-2">
|
||||||
|
<LogIn size={18} className="rotate-180" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
|
|||||||
+138
-15
@@ -5,6 +5,7 @@ import { motion, AnimatePresence } from 'motion/react';
|
|||||||
import { analyzeForexChart } from '../services/geminiService';
|
import { analyzeForexChart } from '../services/geminiService';
|
||||||
import { AnalysisResponse, SignalResult, SignalType } from '../types';
|
import { AnalysisResponse, SignalResult, SignalType } from '../types';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
|
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { compressImage, cleanupStorage } from '../lib/imageUtils';
|
import { compressImage, cleanupStorage } from '../lib/imageUtils';
|
||||||
@@ -12,6 +13,7 @@ import { storage, auth, db, handleFirestoreError, OperationType } from '../lib/f
|
|||||||
import { collection, addDoc, serverTimestamp, query, where, getDocs, Timestamp } from 'firebase/firestore';
|
import { collection, addDoc, serverTimestamp, query, where, getDocs, Timestamp } from 'firebase/firestore';
|
||||||
import { fetchCurrentPrice } from '../services/marketData';
|
import { fetchCurrentPrice } from '../services/marketData';
|
||||||
import { sendTelegramAlert } from '../services/notifications';
|
import { sendTelegramAlert } from '../services/notifications';
|
||||||
|
import { WatchlistPanel } from './WatchlistPanel';
|
||||||
|
|
||||||
export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void }> = ({ userData, onGoToHistory }) => {
|
export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void }> = ({ userData, onGoToHistory }) => {
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
@@ -20,9 +22,11 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
const [result, setResult] = useState<AnalysisResponse | null>(null);
|
const [result, setResult] = useState<AnalysisResponse | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [mode, setMode] = useState<'Técnico' | 'Fundamental' | 'Híbrido'>('Híbrido');
|
const [mode, setMode] = useState<'Técnico' | 'Fundamental' | 'Híbrido'>('Híbrido');
|
||||||
|
const [tradingStyle, setTradingStyle] = useState<'Automático' | 'Scalping' | 'Day Trading' | 'Swing'>('Automático');
|
||||||
const [usageInfo, setUsageInfo] = useState<{ used: number, limit: number }>({ used: 0, limit: userData?.analysisLimit ?? 8 });
|
const [usageInfo, setUsageInfo] = useState<{ used: number, limit: number }>({ used: 0, limit: userData?.analysisLimit ?? 8 });
|
||||||
const [alerts, setAlerts] = useState<{type: string, price: string, targetValue: number}[]>([]);
|
const [alerts, setAlerts] = useState<{type: string, price: string, targetValue: number}[]>([]);
|
||||||
const [customAlertPrices, setCustomAlertPrices] = useState<Record<string, string>>({});
|
const [customAlertPrices, setCustomAlertPrices] = useState<Record<string, string>>({});
|
||||||
|
const [alertOptions, setAlertOptions] = useState({ execution: true, expiration: true });
|
||||||
const [marketPrice, setMarketPrice] = useState<number | null>(null);
|
const [marketPrice, setMarketPrice] = useState<number | null>(null);
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -168,7 +172,7 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
}
|
}
|
||||||
|
|
||||||
const base64 = preview.split(',')[1];
|
const base64 = preview.split(',')[1];
|
||||||
const analysis = await analyzeForexChart(base64, undefined, mode, userData?.plan);
|
const analysis = await analyzeForexChart(base64, undefined, mode, userData?.plan, tradingStyle);
|
||||||
analysis.score = Math.round(analysis.score);
|
analysis.score = Math.round(analysis.score);
|
||||||
setResult(analysis);
|
setResult(analysis);
|
||||||
|
|
||||||
@@ -191,6 +195,10 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
toast.success(`Scanner Concluído com ${sanitizedAnalysis.score}% de Confiança no ${sanitizedAnalysis.pair || 'Ativo'}!`, {
|
||||||
|
description: `Decisão de ${sanitizedAnalysis.decision}`
|
||||||
|
});
|
||||||
|
|
||||||
if (sanitizedAnalysis.decision !== 'WAIT') {
|
if (sanitizedAnalysis.decision !== 'WAIT') {
|
||||||
axios.post('/api/onesignal/notify', {
|
axios.post('/api/onesignal/notify', {
|
||||||
title: 'Novo Sinal QuantScan IA 🚨',
|
title: 'Novo Sinal QuantScan IA 🚨',
|
||||||
@@ -210,6 +218,7 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
errorMessage = `Erro na API: ${errorMessage}`;
|
errorMessage = `Erro na API: ${errorMessage}`;
|
||||||
}
|
}
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
|
toast.error('Erro na análise do Scanner', { description: errorMessage });
|
||||||
} finally {
|
} finally {
|
||||||
setIsAnalyzing(false);
|
setIsAnalyzing(false);
|
||||||
}
|
}
|
||||||
@@ -313,6 +322,8 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<WatchlistPanel />
|
||||||
|
|
||||||
{!result ? (
|
{!result ? (
|
||||||
<div className="glass-card p-6 border-zinc-900 min-h-[300px] flex flex-col justify-center items-center group relative">
|
<div className="glass-card p-6 border-zinc-900 min-h-[300px] flex flex-col justify-center items-center group relative">
|
||||||
@@ -334,24 +345,47 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
)}
|
)}
|
||||||
{!preview ? (
|
{!preview ? (
|
||||||
<div className="w-full flex flex-col gap-4 items-center justify-center">
|
<div className="w-full flex flex-col gap-4 items-center justify-center">
|
||||||
<div className="flex gap-2 mb-4 w-full justify-center">
|
<div className="flex flex-col gap-3 w-full max-w-sm mb-4">
|
||||||
{(['Técnico', 'Fundamental', 'Híbrido'] as const).map((m) => (
|
<div className="flex flex-col justify-center gap-1.5 w-full tour-step-2">
|
||||||
<button
|
<span className="text-[9px] uppercase tracking-widest text-zinc-500 font-bold text-center">Tipo de Análise</span>
|
||||||
key={m}
|
<div className="flex gap-2 w-full justify-center">
|
||||||
onClick={() => setMode(m)}
|
{(['Técnico', 'Fundamental', 'Híbrido'] as const).map((m) => (
|
||||||
className={cn(
|
<button
|
||||||
"px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-widest transition-all",
|
key={m}
|
||||||
mode === m ? "bg-brand-red text-white" : "bg-white/5 text-white/50 hover:bg-white/10"
|
onClick={() => setMode(m)}
|
||||||
)}
|
className={cn(
|
||||||
>
|
"px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-widest transition-all",
|
||||||
{m}
|
mode === m ? "bg-brand-red text-white" : "bg-white/5 text-white/50 hover:bg-white/10"
|
||||||
</button>
|
)}
|
||||||
))}
|
>
|
||||||
|
{m}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col justify-center gap-1.5 w-full tour-step-3">
|
||||||
|
<span className="text-[9px] uppercase tracking-widest text-zinc-500 font-bold text-center">Estilo de Trading</span>
|
||||||
|
<div className="flex gap-2 w-full justify-center flex-wrap">
|
||||||
|
{(['Automático', 'Scalping', 'Day Trading', 'Swing'] as const).map((s) => (
|
||||||
|
<button
|
||||||
|
key={s}
|
||||||
|
onClick={() => setTradingStyle(s)}
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-widest transition-all",
|
||||||
|
tradingStyle === s ? "bg-brand-red text-white" : "bg-white/5 text-white/50 hover:bg-white/10"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex flex-col items-center justify-center py-10 rounded-xl cursor-pointer transition-all duration-300 border border-transparent",
|
"w-full flex flex-col items-center justify-center py-10 rounded-xl cursor-pointer transition-all duration-300 border border-transparent tour-step-1",
|
||||||
isDragActive ? "bg-brand-red/5 scale-[0.98] border-brand-red/30" : "hover:bg-zinc-800/20",
|
isDragActive ? "bg-brand-red/5 scale-[0.98] border-brand-red/30" : "hover:bg-zinc-800/20",
|
||||||
isExpired ? "opacity-50 pointer-events-none" : ""
|
isExpired ? "opacity-50 pointer-events-none" : ""
|
||||||
)}
|
)}
|
||||||
@@ -671,6 +705,34 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-white/5 space-y-3">
|
||||||
|
<h4 className="text-[10px] uppercase font-black tracking-widest text-zinc-400 mb-2">Opções de Notificação</h4>
|
||||||
|
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BellRing size={14} className="text-zinc-500" />
|
||||||
|
<span className="text-[9px] font-black text-white uppercase">Notificar Horário de Negociação</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setAlertOptions(prev => ({ ...prev, execution: !prev.execution }))}
|
||||||
|
className={cn("w-8 h-4 rounded-full transition-colors relative", alertOptions.execution ? "bg-brand-red" : "bg-white/10")}
|
||||||
|
>
|
||||||
|
<div className={cn("w-3 h-3 rounded-full bg-white absolute top-0.5 transition-all", alertOptions.execution ? "right-0.5" : "left-0.5")} />
|
||||||
|
</button>
|
||||||
|
</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-zinc-500" />
|
||||||
|
<span className="text-[9px] font-black text-white uppercase">Sinal Inválido ou Expirado</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setAlertOptions(prev => ({ ...prev, expiration: !prev.expiration }))}
|
||||||
|
className={cn("w-8 h-4 rounded-full transition-colors relative", alertOptions.expiration ? "bg-brand-red" : "bg-white/10")}
|
||||||
|
>
|
||||||
|
<div className={cn("w-3 h-3 rounded-full bg-white absolute top-0.5 transition-all", alertOptions.expiration ? "right-0.5" : "left-0.5")} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -754,6 +816,67 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* UI Overlays to simulate AI analysis */}
|
{/* UI Overlays to simulate AI analysis */}
|
||||||
|
{/* Predictive Trend Overlay */}
|
||||||
|
<div className="absolute inset-0 z-15 pointer-events-none p-4 w-full h-full">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none" className="overflow-visible">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="trendGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stopColor={result.decision === SignalType.BUY ? "#22c55e" : result.decision === SignalType.SELL ? "#ef4444" : "#71717a"} stopOpacity="0" />
|
||||||
|
<stop offset="100%" stopColor={result.decision === SignalType.BUY ? "#22c55e" : result.decision === SignalType.SELL ? "#ef4444" : "#71717a"} stopOpacity="0.8" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
{result.decision === SignalType.BUY && (
|
||||||
|
<motion.path
|
||||||
|
initial={{ pathLength: 0, opacity: 0 }}
|
||||||
|
animate={{ pathLength: 1, opacity: 1 }}
|
||||||
|
transition={{ duration: 1.5, ease: "easeInOut", delay: 0.5 }}
|
||||||
|
d="M 10 90 Q 40 80, 50 50 T 90 20"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#trendGradient)"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeDasharray="4 2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{result.decision === SignalType.SELL && (
|
||||||
|
<motion.path
|
||||||
|
initial={{ pathLength: 0, opacity: 0 }}
|
||||||
|
animate={{ pathLength: 1, opacity: 1 }}
|
||||||
|
transition={{ duration: 1.5, ease: "easeInOut", delay: 0.5 }}
|
||||||
|
d="M 10 20 Q 40 30, 50 60 T 90 90"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#trendGradient)"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeDasharray="4 2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{result.decision !== SignalType.WAIT && (
|
||||||
|
<>
|
||||||
|
<motion.circle
|
||||||
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 2 }}
|
||||||
|
cx="90"
|
||||||
|
cy={result.decision === SignalType.BUY ? "20" : result.decision === SignalType.SELL ? "90" : "50"}
|
||||||
|
r="1.5"
|
||||||
|
fill={result.decision === SignalType.BUY ? "#22c55e" : result.decision === SignalType.SELL ? "#ef4444" : "#71717a"}
|
||||||
|
/>
|
||||||
|
<motion.text
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 2 }}
|
||||||
|
x="93"
|
||||||
|
y={result.decision === SignalType.BUY ? "21.5" : result.decision === SignalType.SELL ? "91.5" : "51.5"}
|
||||||
|
fontSize="3"
|
||||||
|
fill="white"
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{result.score}% OVERLAY DE PREVISÃO
|
||||||
|
</motion.text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="absolute inset-0 z-20 p-6 flex flex-col justify-end">
|
<div className="absolute inset-0 z-20 p-6 flex flex-col justify-end">
|
||||||
<div className="flex flex-wrap gap-2 mb-2">
|
<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">
|
<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">
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, isAdmin
|
|||||||
const allTabs = [...mainTabs, ...accountTabs]; // For mobile rendering
|
const allTabs = [...mainTabs, ...accountTabs]; // For mobile rendering
|
||||||
|
|
||||||
return (
|
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">
|
<nav className="tour-step-4 fixed bottom-0 left-0 w-full md:bottom-auto md:left-auto md:w-64 md:h-screen md:flex-col glass-card !rounded-b-none md:!rounded-xl !p-2 md:!p-4 flex flex-row md:items-stretch gap-1.5 z-50 border-x-0 border-b-0 md:border-x md:border-b">
|
||||||
<div className="hidden md:flex items-center gap-3 p-3 mb-6 w-full border-b border-white/5 pb-6">
|
<div className="hidden md:flex items-center gap-3 p-3 mb-6 w-full border-b border-white/5 pb-6">
|
||||||
<img
|
<img
|
||||||
src="https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png"
|
src="https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png"
|
||||||
|
|||||||
@@ -138,11 +138,15 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
|
|||||||
show: { opacity: 1, y: 0 }
|
show: { opacity: 1, y: 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePaymentAction = (planName: string) => {
|
||||||
|
const text = `Olá, gostaria de assinar o ${planName} do QuantScan`;
|
||||||
|
window.open(`https://wa.me/258869976193?text=${encodeURIComponent(text)}`, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
const handleAction = () => {
|
const handleAction = () => {
|
||||||
if (isUnauthenticated && onGetStarted) {
|
if (isUnauthenticated && onGetStarted) {
|
||||||
onGetStarted();
|
onGetStarted();
|
||||||
} else {
|
} else {
|
||||||
// Handle payment or subscription logic for logged in users
|
|
||||||
window.open('https://wa.me/258869976193?text=Ol%C3%A1%2C%20gostaria%20de%20assinar%20o%20plano%20do%20QuantScan', '_blank');
|
window.open('https://wa.me/258869976193?text=Ol%C3%A1%2C%20gostaria%20de%20assinar%20o%20plano%20do%20QuantScan', '_blank');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -240,7 +244,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
|
|||||||
price="500 MT"
|
price="500 MT"
|
||||||
description="Plano de 2 semanas para testar as capacidades básicas da IA."
|
description="Plano de 2 semanas para testar as capacidades básicas da IA."
|
||||||
buttonText="Testar IA"
|
buttonText="Testar IA"
|
||||||
onClick={handleAction}
|
onClick={() => handlePaymentAction("Plano Experimental")}
|
||||||
features={[
|
features={[
|
||||||
"3 análises por dia",
|
"3 análises por dia",
|
||||||
"Apenas Curto Prazo (Scalping)",
|
"Apenas Curto Prazo (Scalping)",
|
||||||
@@ -255,7 +259,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
|
|||||||
price="2.000 MT"
|
price="2.000 MT"
|
||||||
description="Para quem está começando e quer testar a potência."
|
description="Para quem está começando e quer testar a potência."
|
||||||
buttonText="Assinar Agora"
|
buttonText="Assinar Agora"
|
||||||
onClick={handleAction}
|
onClick={() => handlePaymentAction("Plano Begin")}
|
||||||
features={[
|
features={[
|
||||||
"8 análises por dia",
|
"8 análises por dia",
|
||||||
"Scalping & Intraday",
|
"Scalping & Intraday",
|
||||||
@@ -272,7 +276,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
|
|||||||
buttonText="Assinar Agora"
|
buttonText="Assinar Agora"
|
||||||
highlight={true}
|
highlight={true}
|
||||||
badge="Recomendado"
|
badge="Recomendado"
|
||||||
onClick={handleAction}
|
onClick={() => handlePaymentAction("Plano Pro")}
|
||||||
features={[
|
features={[
|
||||||
"15 análises por dia",
|
"15 análises por dia",
|
||||||
"Análises de Longo Prazo",
|
"Análises de Longo Prazo",
|
||||||
@@ -287,7 +291,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
|
|||||||
price="7.500 MT"
|
price="7.500 MT"
|
||||||
description="Acesso total às ferramentas institucionais e Auto-Trading VPS."
|
description="Acesso total às ferramentas institucionais e Auto-Trading VPS."
|
||||||
buttonText="Ser Elite"
|
buttonText="Ser Elite"
|
||||||
onClick={handleAction}
|
onClick={() => handlePaymentAction("Plano Elite")}
|
||||||
features={[
|
features={[
|
||||||
"30 análises por dia",
|
"30 análises por dia",
|
||||||
"Auto-Trading + Gestão de Risco",
|
"Auto-Trading + Gestão de Risco",
|
||||||
@@ -304,7 +308,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
|
|||||||
buttonText="Assinar Vitalício"
|
buttonText="Assinar Vitalício"
|
||||||
highlight={true}
|
highlight={true}
|
||||||
badge="VIP VITALÍCIO"
|
badge="VIP VITALÍCIO"
|
||||||
onClick={handleAction}
|
onClick={() => handlePaymentAction("Plano Lifetime")}
|
||||||
features={[
|
features={[
|
||||||
"Análises Ilimitadas",
|
"Análises Ilimitadas",
|
||||||
"Tudo do Plano Elite com Auto-Trading",
|
"Tudo do Plano Elite com Auto-Trading",
|
||||||
|
|||||||
@@ -174,34 +174,6 @@ export const ProfileView: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</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">Integração Auto-Trading</h4>
|
|
||||||
|
|
||||||
{['pro', 'elite', 'lifetime'].includes(userData?.plan) ? (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between p-3 bg-brand-gray/30 rounded-xl border border-white/5">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-bold text-white">Ativar Auto-Trading</p>
|
|
||||||
<p className="text-xs text-zinc-400">Executa sinais automaticamente na corretora</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={autoTradingEnabled}
|
|
||||||
onChange={(e) => setAutoTradingEnabled(e.target.checked)}
|
|
||||||
className="w-5 h-5 accent-brand-red rounded bg-brand-dark/50 border border-white/10 cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="p-4 bg-brand-red/10 border border-brand-red/20 rounded-xl space-y-2">
|
|
||||||
<p className="text-sm font-bold text-white">Upgrade Necessário</p>
|
|
||||||
<p className="text-xs text-zinc-400">
|
|
||||||
A funcionalidade de Auto-Trading está disponível exclusivamente a partir do <strong className="text-brand-red">Plano Pro</strong>. Evolua sua conta para automatizar suas operações de forma profissional.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 border-t border-white/5 space-y-4">
|
<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>
|
<h4 className="text-[10px] uppercase font-black tracking-widest text-zinc-500">Change Password & Danger Zone</h4>
|
||||||
|
|
||||||
|
|||||||
@@ -120,8 +120,11 @@ export const SignalHistory: React.FC = () => {
|
|||||||
// Update in Firebase
|
// Update in Firebase
|
||||||
try {
|
try {
|
||||||
await updateDoc(doc(db, 'signals', signal.id!), { result: finalResult });
|
await updateDoc(doc(db, 'signals', signal.id!), { result: finalResult });
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error("Failed to update closed signal", e);
|
// Ignore insufficient permissions as regular users might not have access to update global signals in demo/shared mode
|
||||||
|
if (e && e.code !== 'permission-denied') {
|
||||||
|
console.error("Failed to update closed signal", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { db, auth } from '../lib/firebase';
|
||||||
|
import { doc, updateDoc } from 'firebase/firestore';
|
||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
|
export const TourGuide = ({ hasCompletedTour }: { hasCompletedTour?: boolean }) => {
|
||||||
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const [positions, setPositions] = useState<Record<number, { top: number, left: number, width: number, height: number }>>({});
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ target: '.tour-step-1', content: 'Bem-vindo ao QuantScan! Aqui é onde tudo acontece. Envie a foto de um gráfico para análise.', title: 'Análise por IA' },
|
||||||
|
{ target: '.tour-step-2', content: 'Escolha entre Análise Técnica, Fundamentalista ou Híbrida baseado em sua estratégia.', title: 'Tipo de Análise' },
|
||||||
|
{ target: '.tour-step-3', content: 'Defina seu Estilo de Trading (Scalping, Day Trading). A IA adapta o risco e alvos para você.', title: 'Estilo de Trading' },
|
||||||
|
{ target: '.tour-step-4', content: 'Navegue pelo menu para acessar a aba Histórico, Dashboard Elite e o Auto-Trading!', title: 'Menu Principal' }
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasCompletedTour === false) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setShow(true);
|
||||||
|
measureTarget(0);
|
||||||
|
}, 1000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [hasCompletedTour]);
|
||||||
|
|
||||||
|
const measureTarget = (stepIndex: number) => {
|
||||||
|
const el = document.querySelector(steps[stepIndex].target);
|
||||||
|
if (el) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
setPositions(prev => ({
|
||||||
|
...prev,
|
||||||
|
[stepIndex]: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (currentStep < steps.length - 1) {
|
||||||
|
setCurrentStep(c => c + 1);
|
||||||
|
setTimeout(() => measureTarget(currentStep + 1), 100);
|
||||||
|
} else {
|
||||||
|
handleFinish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinish = async () => {
|
||||||
|
setShow(false);
|
||||||
|
if (auth.currentUser) {
|
||||||
|
try {
|
||||||
|
await updateDoc(doc(db, 'users', auth.currentUser.uid), {
|
||||||
|
hasCompletedTour: true
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
if (show) measureTarget(currentStep);
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, [show, currentStep]);
|
||||||
|
|
||||||
|
if (!show || hasCompletedTour) return null;
|
||||||
|
|
||||||
|
const pos = positions[currentStep];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[10000] pointer-events-none">
|
||||||
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm pointer-events-auto transition-opacity duration-300" />
|
||||||
|
|
||||||
|
{pos && (
|
||||||
|
<div
|
||||||
|
className="absolute border-2 border-brand-red rounded-xl bg-transparent transition-all duration-500 ease-out z-[10001] shadow-[0_0_0_9999px_rgba(0,0,0,0.5)]"
|
||||||
|
style={{ top: pos.top - 4, left: pos.left - 4, width: pos.width + 8, height: pos.height + 8 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{pos && (
|
||||||
|
<motion.div
|
||||||
|
key={currentStep}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="absolute z-[10002] bg-zinc-900 border border-white/10 rounded-xl p-5 shadow-2xl w-80 max-w-[90vw] pointer-events-auto"
|
||||||
|
style={{
|
||||||
|
top: pos.top + pos.height + 16 > window.innerHeight - 200 ? pos.top - 200 : pos.top + pos.height + 16,
|
||||||
|
left: Math.max(16, Math.min(pos.left + pos.width / 2 - 160, window.innerWidth - 336))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button onClick={handleFinish} className="absolute top-3 right-3 text-zinc-500 hover:text-white transition-colors">
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
<div className="flex flex-col gap-2 relative">
|
||||||
|
<span className="text-[10px] font-black uppercase tracking-widest text-brand-red">Passo {currentStep + 1} de {steps.length}</span>
|
||||||
|
<h3 className="font-bold text-white text-lg leading-tight">{steps[currentStep].title}</h3>
|
||||||
|
<p className="text-sm text-zinc-400 mb-4">{steps[currentStep].content}</p>
|
||||||
|
<div className="flex items-center justify-between mt-2">
|
||||||
|
<span className="flex gap-1">
|
||||||
|
{steps.map((_, i) => (
|
||||||
|
<div key={i} className={`h-1.5 rounded-full transition-all ${i === currentStep ? 'w-4 bg-brand-red' : 'w-1.5 bg-zinc-700'}`} />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleNext}
|
||||||
|
className="bg-brand-red hover:bg-brand-red/90 text-white px-4 py-2 rounded-lg text-xs font-black tracking-widest uppercase transition-all"
|
||||||
|
>
|
||||||
|
{currentStep === steps.length - 1 ? 'Concluir' : 'Próximo'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { fetchCurrentPrice } from '../services/marketData';
|
||||||
|
import { Star, Plus, X, Activity } from 'lucide-react';
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
|
||||||
|
export const WatchlistPanel: React.FC = () => {
|
||||||
|
const [watchlist, setWatchlist] = useState<string[]>(() => {
|
||||||
|
const saved = localStorage.getItem('quantscan_watchlist');
|
||||||
|
return saved ? JSON.parse(saved) : ['XAU/USD', 'BTC/USDT'];
|
||||||
|
});
|
||||||
|
const [prices, setPrices] = useState<Record<string, number>>({});
|
||||||
|
const [previousPrices, setPreviousPrices] = useState<Record<string, number>>({});
|
||||||
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
const [newPair, setNewPair] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('quantscan_watchlist', JSON.stringify(watchlist));
|
||||||
|
}, [watchlist]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (watchlist.length === 0) return;
|
||||||
|
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
const updatePrices = async () => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
watchlist.map(async (pair) => {
|
||||||
|
const price = await fetchCurrentPrice(pair);
|
||||||
|
return { pair, price };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setPreviousPrices(prev => ({ ...prev, ...prices }));
|
||||||
|
const newPrices: Record<string, number> = {};
|
||||||
|
results.forEach(({ pair, price }) => {
|
||||||
|
if (price !== null) newPrices[pair] = price;
|
||||||
|
});
|
||||||
|
setPrices(prev => ({ ...prev, ...newPrices }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updatePrices();
|
||||||
|
const interval = setInterval(updatePrices, 30000);
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [watchlist]);
|
||||||
|
|
||||||
|
const addPair = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newPair) return;
|
||||||
|
const pairToAdd = newPair.toUpperCase().trim();
|
||||||
|
if (!watchlist.includes(pairToAdd)) {
|
||||||
|
setWatchlist(prev => [...prev, pairToAdd]);
|
||||||
|
}
|
||||||
|
setNewPair('');
|
||||||
|
setIsAdding(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removePair = (pairToRemove: string) => {
|
||||||
|
setWatchlist(prev => prev.filter(p => p !== pairToRemove));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full glass-card p-4 space-y-4 mb-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-[10px] font-black uppercase tracking-widest text-zinc-400 flex items-center gap-2">
|
||||||
|
<Star size={14} className="text-yellow-500" /> Watchlist Favoritos
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsAdding(!isAdding)}
|
||||||
|
className="p-1 hover:bg-white/10 rounded transition-colors text-zinc-500 hover:text-white"
|
||||||
|
>
|
||||||
|
{isAdding ? <X size={14} /> : <Plus size={14} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{isAdding && (
|
||||||
|
<motion.form
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
onSubmit={addPair}
|
||||||
|
className="flex gap-2 overflow-hidden"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Ex: EUR/USD, BOOM1000"
|
||||||
|
value={newPair}
|
||||||
|
onChange={(e) => setNewPair(e.target.value)}
|
||||||
|
className="flex-1 bg-black/40 border border-white/5 rounded-lg px-3 py-1.5 text-xs text-white uppercase focus:outline-none focus:border-brand-red"
|
||||||
|
/>
|
||||||
|
<button type="submit" className="bg-brand-red text-white px-3 py-1.5 rounded-lg text-xs font-black uppercase tracking-widest hover:bg-brand-red/90 transition-all">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</motion.form>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<div className="flex overflow-x-auto pb-2 gap-3 no-scrollbar rounded-lg">
|
||||||
|
{watchlist.length === 0 && (
|
||||||
|
<p className="text-zinc-600 text-[10px] font-bold uppercase w-full text-center py-2">Nenhum ativo adicionado.</p>
|
||||||
|
)}
|
||||||
|
{watchlist.map((pair) => {
|
||||||
|
const price = prices[pair];
|
||||||
|
const prevPrice = previousPrices[pair];
|
||||||
|
const isUp = price && prevPrice ? price > prevPrice : true;
|
||||||
|
const isDown = price && prevPrice ? price < prevPrice : false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={pair} className="flex-shrink-0 bg-black/40 border border-white/5 p-3 rounded-lg flex flex-col gap-1 min-w-[120px] relative group h-[64px]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-[10px] uppercase font-black text-zinc-400">{pair}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removePair(pair)}
|
||||||
|
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 hover:bg-white/10 rounded text-zinc-500 absolute top-2 right-2"
|
||||||
|
>
|
||||||
|
<X size={10} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-auto">
|
||||||
|
{price !== undefined ? (
|
||||||
|
<>
|
||||||
|
<span className={cn(
|
||||||
|
"text-sm font-black font-mono transition-colors duration-500",
|
||||||
|
isUp ? "text-green-500" : isDown ? "text-brand-red" : "text-white"
|
||||||
|
)}>
|
||||||
|
{price > 100 ? price.toFixed(2) : price.toFixed(5)}
|
||||||
|
</span>
|
||||||
|
{(isUp || isDown) && (
|
||||||
|
<Activity size={10} className={isUp ? "text-green-500" : "text-brand-red"} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs font-mono text-zinc-600 animate-pulse">---</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { AnalysisResponse, SignalType } from "../types";
|
|||||||
|
|
||||||
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY || '' });
|
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY || '' });
|
||||||
|
|
||||||
export const analyzeForexChart = async (imageBase64: string, userNotes?: string, preferredMode?: 'Técnico' | 'Fundamental' | 'Híbrido', userPlan?: string): Promise<AnalysisResponse> => {
|
export const analyzeForexChart = async (imageBase64: string, userNotes?: string, preferredMode?: 'Técnico' | 'Fundamental' | 'Híbrido', userPlan?: string, preferredStyle?: string): Promise<AnalysisResponse> => {
|
||||||
const model = 'gemini-3.1-pro-preview';
|
const model = 'gemini-3.1-pro-preview';
|
||||||
|
|
||||||
const systemInstruction = `
|
const systemInstruction = `
|
||||||
@@ -24,9 +24,11 @@ O sistema deve operar em dois modos:
|
|||||||
RESTRIÇÕES DE PLANO
|
RESTRIÇÕES DE PLANO
|
||||||
==================================================
|
==================================================
|
||||||
Se o plano do usuário for "basic" ou "experimental", você é **PROIBIDO** de realizar análise LONG TERM / SWING / POSITION.
|
Se o plano do usuário for "basic" ou "experimental", você é **PROIBIDO** de realizar análise LONG TERM / SWING / POSITION.
|
||||||
Você deve focar nas estruturas de curto prazo e gerar sinais APENAS para Scalping ou Intraday.
|
Você deve focar nas estruturas de curto prazo e gerar sinais APENAS para Scalping ou Intraday (Day Trading).
|
||||||
Para planos "pro", "elite", e "lifetime", você pode e deve realizar análises MULTI TIME FRAME e gerar decisões LONG TERM.
|
Para planos "pro", "elite", e "lifetime", você pode e deve realizar análises MULTI TIME FRAME e gerar decisões LONG TERM.
|
||||||
|
|
||||||
|
${preferredStyle && preferredStyle !== 'Automático' ? `\nESTILO DE TRADING PREFERIDO PELO USUÁRIO NESTE SCAN: **${preferredStyle}**.\nVocê DEVE adaptar os seus stops, alvos e timeframe para gerar um sinal focado primeiramente em ${preferredStyle}.\nSe for Day Trading focar em alvos que batem no mesmo dia.\n` : ''}
|
||||||
|
|
||||||
==================================================
|
==================================================
|
||||||
PILARES DA ESTRATÉGIA SMC OBRIGATÓRIA
|
PILARES DA ESTRATÉGIA SMC OBRIGATÓRIA
|
||||||
==================================================
|
==================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user