Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -65,6 +66,7 @@ function App() {
<Route path="/PrivacyPolicy" element={<PrivacyPolicy />} />
<Route path="/termsandconditions" element={<Termsandconditions />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="/report/:id" element={<ReportInfo />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
Expand Down
123 changes: 121 additions & 2 deletions src/pages/AdminDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -62,6 +73,7 @@ export default function AdminDashboard() {
const [pendingNGOs, setPendingNGOs] = useState<NGORequest[]>([]);
const [approvedNGOs, setApprovedNGOs] = useState<ApprovedNGO[]>([]);
const [stories, setStories] = useState<Story[]>([]);
const [reports, setReports] = useState<Report[]>([]);
const [loading, setLoading] = useState(true);
const [isAuthorized, setIsAuthorized] = useState(false);

Expand Down Expand Up @@ -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(
Expand All @@ -121,11 +138,34 @@ export default function AdminDashboard() {
);
setApprovedNGOs(approvedList);

// Process reports and calculate reportCount map
const reportsCountMap: Record<string, number> = {};
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<string, number> = { HIGH: 0, MEDIUM: 1, LOW: 2 };
Expand Down Expand Up @@ -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 <div className="text-center p-10 text-gray-900 dark:text-white">Loading Admin Panel...</div>;
}
Expand Down Expand Up @@ -318,6 +387,56 @@ export default function AdminDashboard() {
)}
</section>

{/* Reported Stories Section */}
<section className="mb-16">
<h2 className="text-2xl font-semibold text-gray-800 dark:text-white mb-6">Reported Stories</h2>
{reports.length === 0 ? (
<p className="text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">No pending reports.</p>
) : (
<div className="space-y-6">
{reports.map(report => (
<div key={report.id} className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 border border-gray-100 dark:border-gray-700">
<div className="flex justify-between items-start flex-wrap gap-4 mb-4">
<div>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-300 border border-red-200 dark:border-red-800/60 mb-2">
{report.reason}
</span>
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
Story: <span className="italic">"{report.story_title}"</span>
</h3>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
Reported on {report.reported_at ? new Date(report.reported_at.seconds * 1000).toLocaleString() : 'N/A'}
</p>
</div>
</div>

<div className="bg-gray-55 dark:bg-gray-900/35 p-4 rounded-lg border border-gray-100 dark:border-gray-700 mb-4">
<span className="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider block mb-1">Reason Description / Additional Details</span>
<p className="text-gray-600 dark:text-gray-300 text-sm italic">
{report.details ? `"${report.details}"` : 'No additional details provided.'}
</p>
</div>

<div className="flex space-x-4">
<button
onClick={() => 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"
>
<Trash2 className="h-4 w-4 mr-2" /> Delete Story
</button>
<button
onClick={() => 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"
>
<CheckCircle className="h-4 w-4 mr-2" /> Dismiss Report
</button>
</div>
</div>
))}
</div>
)}
</section>

{/* Story Moderation Section */}
<section>
<h2 className="text-2xl font-semibold text-gray-800 dark:text-white mb-6">Story Moderation</h2>
Expand Down
127 changes: 60 additions & 67 deletions src/pages/Auth.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -278,7 +278,7 @@ export default function Auth() {
</div>

<h1 className="mt-6 max-w-xl text-4xl font-black tracking-tight text-slate-900 dark:text-white sm:text-5xl lg:text-[3.4rem] lg:leading-[1.02]">
Create your{' '} <br/>
Create your{' '} <br />
<span className="bg-gradient-to-r from-pink-500 to-fuchsia-500 bg-clip-text text-transparent">
Safe Space
</span>
Expand All @@ -287,7 +287,7 @@ export default function Auth() {
<div className="mt-5 h-1 w-24 rounded-full bg-gradient-to-r from-pink-400 to-fuchsia-500" />

<p className="mt-6 max-w-lg text-lg leading-8 text-slate-900 dark:text-slate-300">
A supportive community where your <br/>story matters and your voice is heard.
A supportive community where your <br />story matters and your voice is heard.
</p>

<div className="mt-10 space-y-5">
Expand Down Expand Up @@ -352,11 +352,10 @@ export default function Auth() {
<button
type="button"
onClick={() => 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'
}`}
}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
Expand All @@ -371,11 +370,10 @@ export default function Auth() {
<button
type="button"
onClick={() => 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 */}
<GoogleColoredIcon size={16} />
Expand Down Expand Up @@ -458,76 +456,71 @@ export default function Auth() {
</button>
</div>
{isSignUp && (
<div className="mt-4 grid grid-cols-1 gap-y-3 text-sm sm:grid-cols-2 sm:gap-x-6">

<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
<div
className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] font-bold ${
passwordChecks.length
? 'bg-pink-500 text-white'
: 'bg-slate-200 text-slate-500 dark:bg-white/10 dark:text-slate-400'
<div className="mt-4 grid grid-cols-1 gap-y-3 text-sm sm:grid-cols-2 sm:gap-x-6">

<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
<div
className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] font-bold ${passwordChecks.length
? 'bg-pink-500 text-white'
: 'bg-slate-200 text-slate-500 dark:bg-white/10 dark:text-slate-400'
}`}
>
βœ“
</div>
<span>At least 8 characters</span>
>
βœ“
</div>
<span>At least 8 characters</span>
</div>

<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
<div
className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] font-bold ${
passwordChecks.number
? 'bg-pink-500 text-white'
: 'bg-slate-200 text-slate-500 dark:bg-white/10 dark:text-slate-400'
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
<div
className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] font-bold ${passwordChecks.number
? 'bg-pink-500 text-white'
: 'bg-slate-200 text-slate-500 dark:bg-white/10 dark:text-slate-400'
}`}
>
βœ“
</div>
<span>One number</span>
>
βœ“
</div>
<span>One number</span>
</div>

<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
<div
className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] font-bold ${
passwordChecks.upper
? 'bg-pink-500 text-white'
: 'bg-slate-200 text-slate-500 dark:bg-white/10 dark:text-slate-400'
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
<div
className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] font-bold ${passwordChecks.upper
? 'bg-pink-500 text-white'
: 'bg-slate-200 text-slate-500 dark:bg-white/10 dark:text-slate-400'
}`}
>
βœ“
</div>
<span>One uppercase letter</span>
>
βœ“
</div>
<span>One uppercase letter</span>
</div>

<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
<div
className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] font-bold ${
passwordChecks.special
? 'bg-pink-500 text-white'
: 'bg-slate-200 text-slate-500 dark:bg-white/10 dark:text-slate-400'
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
<div
className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] font-bold ${passwordChecks.special
? 'bg-pink-500 text-white'
: 'bg-slate-200 text-slate-500 dark:bg-white/10 dark:text-slate-400'
}`}
>
βœ“
</div>
<span>One special character</span>
>
βœ“
</div>
<span>One special character</span>
</div>

<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
<div
className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] font-bold ${
passwordChecks.lower
? 'bg-pink-500 text-white'
: 'bg-slate-200 text-slate-500 dark:bg-white/10 dark:text-slate-400'
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
<div
className={`flex h-4 w-4 items-center justify-center rounded-full text-[10px] font-bold ${passwordChecks.lower
? 'bg-pink-500 text-white'
: 'bg-slate-200 text-slate-500 dark:bg-white/10 dark:text-slate-400'
}`}
>
βœ“
</div>
<span>One lowercase letter</span>
>
βœ“
</div>

<span>One lowercase letter</span>
</div>
)};
</div>

</div>
)}
</div>

<button
type="submit"
Expand Down Expand Up @@ -606,10 +599,10 @@ export default function Auth() {
</div>

<p className="mt-6 text-center text-xs leading-5 text-slate-500 dark:text-slate-400">
By signing up, you agree to our{' '}
<span className="font-medium text-pink-600 dark:text-pink-400">Terms of Service</span>{' '}
By signing {isSignUp ? 'up' : 'in'}, you agree to our{' '}
<Link to="/termsandconditions" className="font-medium text-pink-600 hover:underline dark:text-pink-400">Terms of Service</Link>{' '}
and{' '}
<span className="font-medium text-pink-600 dark:text-pink-400">Privacy Policy</span>.
<Link to="/PrivacyPolicy" className="font-medium text-pink-600 hover:underline dark:text-pink-400">Privacy Policy</Link>.
</p>
</div>
</section>
Expand Down
Loading
Loading