feat: Enhance testimonial display and admin management

Introduce a horizontal scrolling carousel for testimonials on the landing page. Update the admin dashboard to fetch and display existing testimonials, enabling their management.
This commit is contained in:
desartstudio95
2026-05-07 00:29:57 +02:00
parent 8b1baae8cc
commit ca2dbece3e
2 changed files with 61 additions and 9 deletions
+46 -2
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Users, ShieldCheck, Search, Loader2 } from 'lucide-react';
import { cn } from '../lib/utils';
import { collection, onSnapshot, doc, updateDoc, addDoc } from 'firebase/firestore';
import { collection, onSnapshot, doc, updateDoc, addDoc, deleteDoc, query, orderBy } from 'firebase/firestore';
import { db, handleFirestoreError, OperationType, auth } from '../lib/firebase';
import { ImageUploader } from './ImageUploader';
@@ -18,6 +18,7 @@ interface UserProfile {
export const AdminDashboard: React.FC = () => {
const [users, setUsers] = useState<UserProfile[]>([]);
const [testimonials, setTestimonials] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [testUser, setTestUser] = useState('');
@@ -62,7 +63,17 @@ export const AdminDashboard: React.FC = () => {
setLoading(false);
});
return () => unsub();
const q = query(collection(db, 'testimonials'), orderBy('timestamp', 'desc'));
const unsubTestimonials = onSnapshot(q, (snapshot) => {
setTestimonials(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })));
}, (error) => {
console.error("Testimonials fetch error:", error);
});
return () => {
unsub();
unsubTestimonials();
};
}, []);
const handleToggleApproval = async (userId: string, currentStatus: boolean) => {
@@ -106,6 +117,17 @@ export const AdminDashboard: React.FC = () => {
}
};
const handleDeleteTestimonial = async (id: string) => {
if (!confirm('Tem certeza que deseja remover este resultado?')) return;
try {
await deleteDoc(doc(db, 'testimonials', id));
alert('Resultado removido com sucesso!');
} catch (err) {
console.error("Failed to delete testimonial", err);
alert('Erro ao remover resultado.');
}
};
const filteredUsers = users.filter(user =>
user.email?.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.name?.toLowerCase().includes(searchTerm.toLowerCase())
@@ -164,6 +186,28 @@ export const AdminDashboard: React.FC = () => {
</button>
</form>
{testimonials.length > 0 && (
<div className="glass-card p-6 space-y-4">
<h3 className="font-black uppercase tracking-widest text-brand-red text-sm">Resultados Postados</h3>
<div className="flex overflow-x-auto gap-4 pb-4 scrollbar-none snap-x">
{testimonials.map(t => (
<div key={t.id} className="min-w-[250px] w-[250px] bg-white/5 border border-white/10 rounded-lg p-4 space-y-3 shrink-0 snap-center relative group flex flex-col">
{t.imageUrls && t.imageUrls.length > 0 && (
<img src={t.imageUrls[0]} className="w-full h-24 object-cover rounded bg-black/20" alt="Result" referrerPolicy="no-referrer" />
)}
<p className="text-xs text-zinc-300 italic flex-grow">"{t.text}"</p>
<p className="text-[10px] text-brand-red font-bold uppercase"> {t.userName}</p>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => handleDeleteTestimonial(t.id)} className="bg-red-500/80 hover:bg-red-500 text-white p-1.5 rounded" title="Remover">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
</button>
</div>
</div>
))}
</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" />
+15 -7
View File
@@ -276,16 +276,24 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted, onViewPl
<div className="glass-card p-8 border-brand-red/10 space-y-4">
<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">
{testimonials.length > 0 ? (
<div className="flex overflow-x-auto snap-x snap-mandatory gap-6 pb-4 scrollbar-none">
{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" />
<div key={i} className="space-y-4 shrink-0 snap-center w-[85vw] md:w-[40vw] max-w-sm glass-card p-4">
{t.imageUrls && t.imageUrls.length > 0 && (
<div className="flex overflow-x-auto snap-x snap-mandatory gap-2 pb-2 scrollbar-none">
{t.imageUrls.map((url: string, index: number) => (
<img key={index} src={url} alt={`Resultado ${index}`} className="w-full shrink-0 snap-center rounded-lg object-contain bg-black/20" referrerPolicy="no-referrer" />
))}
<p><em>{t.text}</em> <br/> **{t.userName.split('@')[0]}**</p>
</div>
</div>
)}
<p className="text-sm text-zinc-300"><em>"{t.text}"</em> <br/><span className="text-brand-red/80 font-bold uppercase mt-2 block"> {t.userName.split('@')[0]}</span></p>
</div>
))}
</div>
</div>
) : (
<p className="text-zinc-500 text-sm">Nenhum resultado postado ainda.</p>
)}
</div>
<AddTestimonialForm />
</div>