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:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user