feat: Add testimonials and update Gemini model
Integrates a testimonial system to display user results and feedback. This includes: - Adding a new `testimonials` collection in Firestore. - Implementing frontend components for displaying and submitting testimonials. - Modifying the Gemini service to use the `gemini-3.1-pro-preview` model for enhanced analysis. - Updating the `PlansView` with a new WhatsApp contact number. - Enhancing the `AdminDashboard` to include testimonial creation capabilities.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import React, { useState } from 'react';
|
||||
import { collection, addDoc } from 'firebase/firestore';
|
||||
import { db, auth } from '../lib/firebase';
|
||||
import { ImageUploader } from './ImageUploader';
|
||||
|
||||
export const AddTestimonialForm: React.FC = () => {
|
||||
const [text, setText] = useState('');
|
||||
const [imageUrls, setImageUrls] = useState<string[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!auth.currentUser || !text.trim()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await addDoc(collection(db, 'testimonials'), {
|
||||
userId: auth.currentUser.uid,
|
||||
userName: auth.currentUser.email || 'Usuário',
|
||||
text: text,
|
||||
timestamp: Date.now(),
|
||||
imageUrls: imageUrls
|
||||
});
|
||||
setText('');
|
||||
setImageUrls([]);
|
||||
alert('Resultado enviado com sucesso!');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Erro ao enviar resultado.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!auth.currentUser) return null;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="glass-card p-6 space-y-4 mt-8 max-w-2xl mx-auto">
|
||||
<h3 className="font-black uppercase tracking-widest text-brand-red">Deixe o seu resultado</h3>
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-white focus:outline-none focus:ring-1 focus:ring-brand-red"
|
||||
placeholder="O que achou do QuantScanner?"
|
||||
maxLength={500}
|
||||
/>
|
||||
<ImageUploader onUpload={setImageUrls} />
|
||||
{imageUrls.length > 0 && <p className="text-xs text-green-500">{imageUrls.length} imagem(ns) carregada(s) com sucesso!</p>}
|
||||
<button
|
||||
disabled={submitting}
|
||||
type="submit"
|
||||
className="bg-brand-red text-white py-2 px-4 rounded-lg font-bold hover:bg-opacity-90 disabled:opacity-50"
|
||||
>
|
||||
{submitting ? 'Enviando...' : 'Enviar Resultado'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
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';
|
||||
import { collection, onSnapshot, doc, updateDoc, addDoc } from 'firebase/firestore';
|
||||
import { db, handleFirestoreError, OperationType, auth } from '../lib/firebase';
|
||||
import { ImageUploader } from './ImageUploader';
|
||||
|
||||
interface UserProfile {
|
||||
uid: string;
|
||||
@@ -19,6 +20,34 @@ export const AdminDashboard: React.FC = () => {
|
||||
const [users, setUsers] = useState<UserProfile[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [testUser, setTestUser] = useState('');
|
||||
const [testText, setTestText] = useState('');
|
||||
const [testImageUrls, setTestImageUrls] = useState<string[]>([]);
|
||||
const [submittingTestimonial, setSubmittingTestimonial] = useState(false);
|
||||
|
||||
const handleCreateTestimonial = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!testUser.trim() || !testText.trim() || !auth.currentUser) return;
|
||||
setSubmittingTestimonial(true);
|
||||
try {
|
||||
await addDoc(collection(db, 'testimonials'), {
|
||||
userId: auth.currentUser.uid,
|
||||
userName: testUser,
|
||||
text: testText,
|
||||
timestamp: Date.now(),
|
||||
imageUrls: testImageUrls
|
||||
});
|
||||
setTestUser('');
|
||||
setTestText('');
|
||||
setTestImageUrls([]);
|
||||
alert('Resultado postado com sucesso!');
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
alert('Erro ao postar');
|
||||
} finally {
|
||||
setSubmittingTestimonial(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = onSnapshot(collection(db, 'users'), (snapshot) => {
|
||||
@@ -105,6 +134,36 @@ export const AdminDashboard: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleCreateTestimonial} className="glass-card p-6 space-y-4">
|
||||
<h3 className="font-black uppercase tracking-widest text-brand-red text-sm">Postar Resultado como Admin</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nome do Usuário"
|
||||
className="bg-white/5 border border-white/10 rounded-lg p-3 text-white focus:outline-none focus:ring-1 focus:ring-brand-red"
|
||||
value={testUser}
|
||||
onChange={(e) => setTestUser(e.target.value)}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<ImageUploader onUpload={setTestImageUrls} />
|
||||
{testImageUrls.length > 0 && <p className="text-xs text-green-500">{testImageUrls.length} imagem(ns) carregada(s)!</p>}
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
placeholder="Texto do Resultado"
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-white focus:outline-none focus:ring-1 focus:ring-brand-red"
|
||||
value={testText}
|
||||
onChange={(e) => setTestText(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
disabled={submittingTestimonial}
|
||||
type="submit"
|
||||
className="bg-brand-red text-white py-2 px-4 rounded-lg font-bold hover:bg-opacity-90 disabled:opacity-50 w-full"
|
||||
>
|
||||
{submittingTestimonial ? 'Postando...' : 'Postar Resultado'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 gap-4">
|
||||
<Loader2 size={32} className="text-brand-red animate-spin" />
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
|
||||
import { storage, auth } from '../lib/firebase';
|
||||
|
||||
interface ImageUploaderProps {
|
||||
onUpload: (urls: string[]) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ImageUploader: React.FC<ImageUploaderProps> = ({ onUpload, className }) => {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || e.target.files.length === 0 || !auth.currentUser) return;
|
||||
|
||||
const files = Array.from(e.target.files) as File[];
|
||||
setUploading(true);
|
||||
|
||||
try {
|
||||
const uploadPromises = files.map(async (file) => {
|
||||
const storageRef = ref(storage, `testimonials/${auth.currentUser!.uid}/${Date.now()}_${file.name}`);
|
||||
await uploadBytes(storageRef, file);
|
||||
return getDownloadURL(storageRef);
|
||||
});
|
||||
const urls = await Promise.all(uploadPromises);
|
||||
onUpload(urls);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Erro ao fazer upload das imagens.');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
disabled={uploading}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-white focus:outline-none focus:ring-1 focus:ring-brand-red text-sm"
|
||||
/>
|
||||
{uploading && <p className="text-xs text-brand-red mt-1">Enviando imagens...</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { TrendingUp, ShieldCheck, Zap, BarChart3, Globe, ChevronRight, CheckCircle2 } from 'lucide-react';
|
||||
import { collection, query, onSnapshot, orderBy } from 'firebase/firestore';
|
||||
import { db } from '../lib/firebase';
|
||||
import { MarketTicker } from './MarketTicker';
|
||||
import { AddTestimonialForm } from './AddTestimonialForm';
|
||||
|
||||
interface LandingPageProps {
|
||||
onGetStarted: () => void;
|
||||
@@ -9,6 +12,16 @@ interface LandingPageProps {
|
||||
}
|
||||
|
||||
export const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted, onViewPlans }) => {
|
||||
const [testimonials, setTestimonials] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const q = query(collection(db, 'testimonials'), orderBy('timestamp', 'desc'));
|
||||
const unsubscribe = onSnapshot(q, (snapshot) => {
|
||||
setTestimonials(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })));
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-brand-dark text-white overflow-x-hidden">
|
||||
{/* Navbar */}
|
||||
@@ -260,12 +273,19 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted, onViewPl
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-8 border-brand-red/10 space-y-4">
|
||||
<h3 className="font-black uppercase text-xl">Testemunhos</h3>
|
||||
<h3 className="font-black uppercase text-xl">Resultados</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm text-zinc-400">
|
||||
<p><em>"Desde que comecei a usar o IA QuantScanner, minhas entradas ficaram muito mais precisas. Reduzi minhas perdas e aumentei meus lucros em poucas semanas."</em> <br/>– **Carlos M.**</p>
|
||||
<p><em>"A velocidade com que o sistema identifica oportunidades é impressionante. É como ter um analista profissional trabalhando para mim 24h."</em> <br/>– **João D.**</p>
|
||||
{testimonials.map((t, i) => (
|
||||
<div key={i} className="space-y-4">
|
||||
{t.imageUrls && t.imageUrls.map((url: string, index: number) => (
|
||||
<img key={index} src={url} alt={`Resultado ${index}`} className="w-full rounded-lg mb-2" referrerPolicy="no-referrer" />
|
||||
))}
|
||||
<p><em>{t.text}</em> <br/>– **{t.userName.split('@')[0]}**</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<AddTestimonialForm />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
|
||||
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');
|
||||
window.open('https://wa.me/258869976193?text=Ol%C3%A1%2C%20gostaria%20de%20assinar%20o%20plano%20do%20QuantScan', '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AnalysisResponse, SignalType } from "../types";
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY || '' });
|
||||
|
||||
export const analyzeForexChart = async (imageBase64: string, userNotes?: string, preferredMode?: 'Técnico' | 'Fundamental' | 'Híbrido'): Promise<AnalysisResponse> => {
|
||||
const model = "gemini-3-flash-preview";
|
||||
const model = "gemini-3.1-pro-preview";
|
||||
|
||||
const systemInstruction = `
|
||||
Você é o QuantScan IA, um sistema avançado de análise de mercado financeiro com inteligência institucional.
|
||||
|
||||
Reference in New Issue
Block a user