diff --git a/src/App.tsx b/src/App.tsx index 78e2dc1..b7545d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import Termsandconditions from './pages/termsandconditions'; import ContactPage from './pages/ContactPage'; import NotFound from './pages/NotFound'; import { LoadingScreen } from './components/LoadingScreen'; +import ReportInfo from './pages/ReportInfo'; function App() { const [isLoading, setIsLoading] = useState(true); @@ -65,6 +66,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx index b90dd39..4ea6fbe 100644 --- a/src/pages/AdminDashboard.tsx +++ b/src/pages/AdminDashboard.tsx @@ -47,6 +47,17 @@ interface ApprovedNGO { }; } +interface Report { + id: string; + story_id: string; + story_title: string; + reason: string; + details: string; + reported_at: any; + user_id: string; + status: 'pending'; +} + interface Story { id: string; title: string; @@ -62,6 +73,7 @@ export default function AdminDashboard() { const [pendingNGOs, setPendingNGOs] = useState([]); const [approvedNGOs, setApprovedNGOs] = useState([]); const [stories, setStories] = useState([]); + const [reports, setReports] = useState([]); const [loading, setLoading] = useState(true); const [isAuthorized, setIsAuthorized] = useState(false); @@ -93,14 +105,19 @@ export default function AdminDashboard() { // Fetch stories and their report counts const fetchStoriesPromise = getDocs(query(collection(db, 'stories'), orderBy('created_at', 'desc'))); + // Fetch pending reports + const fetchReportsPromise = getDocs(query(collection(db, 'reports'), where('status', '==', 'pending'))); + const [ requestsSnapshot, approvedSnapshot, - storiesSnapshot + storiesSnapshot, + reportsSnapshot ] = await Promise.all([ fetchPendingPromise, fetchApprovedPromise, - fetchStoriesPromise + fetchStoriesPromise, + fetchReportsPromise ]); const requestsList = requestsSnapshot.docs.map( @@ -121,11 +138,34 @@ export default function AdminDashboard() { ); setApprovedNGOs(approvedList); + // Process reports and calculate reportCount map + const reportsCountMap: Record = {}; + const reportsList = reportsSnapshot.docs.map(doc => { + const data = doc.data(); + const sId = data.story_id; + if (sId) { + reportsCountMap[sId] = (reportsCountMap[sId] || 0) + 1; + } + return { + id: doc.id, + ...data + } as Report; + }); + + // Sort reports in memory by reported_at desc + reportsList.sort((a, b) => { + const timeA = a.reported_at?.seconds || 0; + const timeB = b.reported_at?.seconds || 0; + return timeB - timeA; + }); + setReports(reportsList); + // Process stories and fetch report counts for each const storiesList = storiesSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), risk_level: doc.data().risk_level || 'LOW', + reportCount: reportsCountMap[doc.id] || 0, } as Story)); const riskOrder: Record = { HIGH: 0, MEDIUM: 1, LOW: 2 }; @@ -232,12 +272,41 @@ export default function AdminDashboard() { toast.success(`Story "${storyTitle}" has been deleted.`, { id: toastId }); setStories(prev => prev.filter(item => item.id !== storyId)); + setReports(prev => prev.filter(item => item.story_id !== storyId)); } catch (error) { console.error('Error deleting story: ', error); toast.error('Failed to delete story.', { id: toastId }); } }; + const handleDismissReport = async (reportId: string) => { + if (!window.confirm("Are you sure you want to dismiss this report?")) return; + + const toastId = toast.loading("Dismissing report..."); + try { + await deleteDoc(doc(db, 'reports', reportId)); + toast.success("Report dismissed.", { id: toastId }); + + const dismissedReport = reports.find(r => r.id === reportId); + if (dismissedReport) { + setStories(prevStories => prevStories.map(story => { + if (story.id === dismissedReport.story_id) { + return { + ...story, + reportCount: Math.max(0, (story.reportCount || 1) - 1) + }; + } + return story; + })); + } + + setReports(prev => prev.filter(item => item.id !== reportId)); + } catch (error) { + console.error("Error dismissing report: ", error); + toast.error("Failed to dismiss report.", { id: toastId }); + } + }; + if (loading) { return
Loading Admin Panel...
; } @@ -318,6 +387,56 @@ export default function AdminDashboard() { )} + {/* Reported Stories Section */} +
+

Reported Stories

+ {reports.length === 0 ? ( +

No pending reports.

+ ) : ( +
+ {reports.map(report => ( +
+
+
+ + {report.reason} + +

+ Story: "{report.story_title}" +

+

+ Reported on {report.reported_at ? new Date(report.reported_at.seconds * 1000).toLocaleString() : 'N/A'} +

+
+
+ +
+ Reason Description / Additional Details +

+ {report.details ? `"${report.details}"` : 'No additional details provided.'} +

+
+ +
+ + +
+
+ ))} +
+ )} +
+ {/* Story Moderation Section */}

Story Moderation

diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx index e106bc7..d21c455 100644 --- a/src/pages/Auth.tsx +++ b/src/pages/Auth.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, Link } from 'react-router-dom'; import { toast } from 'react-hot-toast'; import { createUserWithEmailAndPassword, @@ -278,7 +278,7 @@ export default function Auth() {

- Create your{' '}
+ Create your{' '}
Safe Space @@ -287,7 +287,7 @@ export default function Auth() {

- A supportive community where your
story matters and your voice is heard. + A supportive community where your
story matters and your voice is heard.

@@ -352,11 +352,10 @@ export default function Auth() {
{isSignUp && ( -
- -
-
+ +
+
- ✓ -
- At least 8 characters + > + ✓
+ At least 8 characters +
-
-
+
- ✓ -
- One number + > + ✓
+ One number +
-
-
+
- ✓ -
- One uppercase letter + > + ✓
+ One uppercase letter +
-
-
+
- ✓ -
- One special character + > + ✓
+ One special character +
-
-
+
- ✓ -
- One lowercase letter + > + ✓
- + One lowercase letter
- )}; -
+ +
+ )} +

diff --git a/src/pages/ReportInfo.tsx b/src/pages/ReportInfo.tsx new file mode 100644 index 0000000..1cc4d93 --- /dev/null +++ b/src/pages/ReportInfo.tsx @@ -0,0 +1,191 @@ +import React, { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { auth } from '../lib/firebase'; +import { getFirestore, doc, getDoc, collection, addDoc, serverTimestamp } from 'firebase/firestore'; +import { toast } from 'react-hot-toast'; +import { Loader2, ArrowLeft, ShieldAlert } from 'lucide-react'; + +const db = getFirestore(); + +const REPORT_CATEGORIES = [ + 'Harassment', + 'Hate Speech', + 'Inappropriate Media', + 'Spam', + 'Doxxing' +]; + +export default function ReportInfo() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [storyTitle, setStoryTitle] = useState(''); + const [storyContent, setStoryContent] = useState(''); + const [loadingStory, setLoadingStory] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [reason, setReason] = useState(''); + const [details, setDetails] = useState(''); + const [user, setUser] = useState(auth.currentUser); + + useEffect(() => { + const unsubscribe = auth.onAuthStateChanged((currentUser) => { + setUser(currentUser); + if (!currentUser) { + toast.error('Please sign in to report a story'); + navigate('/auth'); + } + }); + return () => unsubscribe(); + }, [navigate]); + + useEffect(() => { + const fetchStory = async () => { + if (!id) return; + try { + const storyDoc = await getDoc(doc(db, 'stories', id)); + if (storyDoc.exists()) { + setStoryTitle(storyDoc.data().title || ''); + setStoryContent(storyDoc.data().content || ''); + } else { + toast.error('Story not found'); + navigate('/stories'); + } + } catch (error) { + console.error('Error fetching story:', error); + toast.error('Failed to load story details'); + navigate('/stories'); + } finally { + setLoadingStory(false); + } + }; + + fetchStory(); + }, [id, navigate]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!reason) { + toast.error('Please select a reason for reporting'); + return; + } + + if (!user) { + toast.error('Please sign in to report a story'); + navigate('/auth'); + return; + } + + setSubmitting(true); + try { + await addDoc(collection(db, 'reports'), { + story_id: id, + story_title: storyTitle, + reason, + details, + reported_at: serverTimestamp(), + user_id: user.uid, + status: 'pending' + }); + + toast.success('Story reported. Thank you for keeping SafeVoice safe.'); + navigate('/stories'); + } catch (error) { + console.error('Error submitting report:', error); + toast.error('Failed to submit report. Please try again.'); + } finally { + setSubmitting(false); + } + }; + + if (loadingStory) { + return ( +
+ +

Loading story details...

+
+ ); + } + + return ( +
+ + +
+
+
+ +
+
+

Report Story

+

Help us keep our community safe and supportive.

+
+
+ + {/* Story Preview */} +
+ Story Title +

{storyTitle}

+

{storyContent}

+
+ +
+ {/* Reason Selection */} +
+ + +
+ + {/* Additional Details */} +
+ +