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.'}
+
+
+
+
+ handleDeleteStory(report.story_id, report.story_title)}
+ className="inline-flex items-center bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 transition-colors text-sm font-medium"
+ >
+ Delete Story
+
+ handleDismissReport(report.id)}
+ className="inline-flex items-center bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors text-sm font-medium"
+ >
+ Dismiss Report
+
+
+
+ ))}
+
+ )}
+
+
{/* 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() {
setAuthMethod('email')}
- className={`relative flex items-center gap-2 px-4 pb-3 text-sm font-semibold transition ${
- authMethod === 'email'
+ className={`relative flex items-center gap-2 px-4 pb-3 text-sm font-semibold transition ${authMethod === 'email'
? 'text-pink-600 dark:text-pink-400'
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
- }`}
+ }`}
>
@@ -371,11 +370,10 @@ export default function Auth() {
setAuthMethod('google')}
- className={`relative flex items-center gap-2 px-4 pb-3 text-sm font-semibold transition ${
- authMethod === 'google'
+ className={`relative flex items-center gap-2 px-4 pb-3 text-sm font-semibold transition ${authMethod === 'google'
? 'text-pink-600 dark:text-pink-400'
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
- }`}
+ }`}
>
{/* Google tab icon – colored, 16px */}
@@ -458,76 +456,71 @@ 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
- )};
-
+
+
+ )}
+
- By signing up, you agree to our{' '}
- Terms of Service {' '}
+ By signing {isSignUp ? 'up' : 'in'}, you agree to our{' '}
+ Terms of Service{' '}
and{' '}
- Privacy Policy .
+ Privacy Policy.
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 (
+
+
navigate(-1)}
+ className="inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 mb-6 transition-colors"
+ >
+ Back
+
+
+
+
+
+
+
+
+
Report Story
+
Help us keep our community safe and supportive.
+
+
+
+ {/* Story Preview */}
+
+
Story Title
+
{storyTitle}
+
{storyContent}
+
+
+
+
+
+ );
+}
diff --git a/src/pages/Stories.tsx b/src/pages/Stories.tsx
index 168e8b4..0559c11 100644
--- a/src/pages/Stories.tsx
+++ b/src/pages/Stories.tsx
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import { Heart, MessageCircle, Flag, Loader2, X } from 'lucide-react';
import { toast } from 'react-hot-toast';
import { auth } from '../lib/firebase';
@@ -117,6 +118,7 @@ function RiskBadge({ level }: { level?: string }) {
export default function Stories() {
+ const navigate = useNavigate();
const [stories, setStories] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedTags, setSelectedTags] = useState([]);
@@ -284,27 +286,13 @@ export default function Stories() {
}
};
- const handleReport = async (storyId: string) => {
+ const handleReport = (storyId: string) => {
const user = auth.currentUser;
if (!user) {
toast.error('Please sign in to report stories');
return;
}
-
- try {
- // Add a report document
- await addDoc(collection(db, 'reports'), {
- story_id: storyId,
- user_id: user.uid,
- reported_at: serverTimestamp(),
- status: 'pending' // For admin to review
- });
-
- toast.success('Story reported. Thank you.');
- } catch (error) {
- console.error('Error reporting story:', error);
- toast.error('Failed to report story.');
- }
+ navigate(`/report/${storyId}`);
};
// --- Translation Handler ---
@@ -602,6 +590,13 @@ export default function Stories() {
)}
+ {story.risk_level === 'HIGH' && (
+ e.stopPropagation()}>
+ 💡
In need of support? Immediate help is available.
+ Call the National {"Women's"} Helpline at
1091 or view our verified
NGO Directory .
+
+ )}
+
{/* Media Display - only show when expanded */}
{isExpanded && story.media_urls && story.media_urls.length > 0 && (
@@ -732,6 +727,13 @@ export default function Stories() {
{popupDisplayContent}
+ {selectedStoryForPopup.risk_level === 'HIGH' && (
+
+ 💡
In need of support? Immediate help is available.
+ Call the National {"Women's"} Helpline at
1091 or view our verified
NGO Directory .
+
+ )}
+
{/* Media Display Inside Popup */}
{selectedStoryForPopup.media_urls && selectedStoryForPopup.media_urls.length > 0 && (