feat: admin can view moderated content

This commit is contained in:
Danish Arora 2025-09-05 17:03:24 +05:30
parent cad1dcb5b4
commit 860b6a138f
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
10 changed files with 259 additions and 37 deletions

View File

@ -18,6 +18,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AuthProvider } from '@/contexts/AuthContext';
import { ForumProvider } from '@/contexts/ForumContext';
import { ModerationProvider } from '@/contexts/ModerationContext';
import CellPage from './pages/CellPage';
import PostPage from './pages/PostPage';
import NotFound from './pages/NotFound';
@ -39,18 +40,20 @@ const App = () => (
<Router>
<AuthProvider>
<ForumProvider>
<TooltipProvider>
<Toaster />
<Sonner />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/cells" element={<Index />} />
<Route path="/cell/:cellId" element={<CellPage />} />
<Route path="/post/:postId" element={<PostPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</TooltipProvider>
<ModerationProvider>
<TooltipProvider>
<Toaster />
<Sonner />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/cells" element={<Index />} />
<Route path="/cell/:cellId" element={<CellPage />} />
<Route path="/post/:postId" element={<PostPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</TooltipProvider>
</ModerationProvider>
</ForumProvider>
</AuthProvider>
</Router>

View File

@ -20,6 +20,7 @@ import {
} from '@/components/ui/select';
import { CypherImage } from './ui/CypherImage';
import { RelevanceIndicator } from './ui/relevance-indicator';
import { ModerationToggle } from './ui/moderation-toggle';
import { sortCells, SortOption } from '@/lib/utils/sorting';
import { Cell } from '@/types/forum';
import { usePending } from '@/hooks/usePending';
@ -121,6 +122,8 @@ const CellList = () => {
</div>
<div className="flex items-center gap-4">
<ModerationToggle />
<Select
value={sortOption}
onValueChange={(value: SortOption) => setSortOption(value)}

View File

@ -4,7 +4,7 @@ import { TrendingUp, Users, Eye } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { useForumData, useForumSelectors, useAuth } from '@/hooks';
import { useForumData, useAuth } from '@/hooks';
import { EVerificationStatus } from '@/types/identity';
import { CypherImage } from '@/components/ui/CypherImage';
import { useUserDisplay } from '@/hooks';
@ -12,7 +12,6 @@ import { useUserDisplay } from '@/hooks';
const FeedSidebar: React.FC = () => {
// ✅ Use reactive hooks for data
const forumData = useForumData();
const selectors = useForumSelectors(forumData);
const { currentUser, verificationStatus } = useAuth();
// Get user display information using the hook
@ -20,11 +19,29 @@ const FeedSidebar: React.FC = () => {
currentUser?.address || ''
);
// ✅ Get pre-computed stats and trending data from selectors
const stats = selectors.selectStats();
// Use cellsWithStats from forumData to get post counts
const { cellsWithStats } = forumData;
const trendingCells = cellsWithStats
// ✅ Get stats from filtered data
const {
filteredPosts,
filteredComments,
filteredCellsWithStats,
cells,
userVerificationStatus,
} = forumData;
const stats = {
totalCells: cells.length,
totalPosts: filteredPosts.length,
totalComments: filteredComments.length,
totalUsers: new Set([
...filteredPosts.map(post => post.author),
...filteredComments.map(comment => comment.author),
]).size,
verifiedUsers: Object.values(userVerificationStatus).filter(
status => status.isVerified
).length,
};
// Use filtered cells with stats for trending cells
const trendingCells = filteredCellsWithStats
.sort((a, b) => b.recentActivity - a.recentActivity)
.slice(0, 5);

View File

@ -51,7 +51,7 @@ const PostDetail = () => {
// ✅ Use reactive hooks for data and actions
const post = usePost(postId);
const comments = usePostComments(postId, { includeModerated: false });
const comments = usePostComments(postId);
const {
createComment,
votePost,

View File

@ -0,0 +1,46 @@
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Eye, EyeOff } from 'lucide-react';
import { useModeration } from '@/contexts/ModerationContext';
import { usePermissions } from '@/hooks/core/usePermissions';
import { useForumData } from '@/hooks/core/useForumData';
export function ModerationToggle() {
const { showModerated, toggleShowModerated } = useModeration();
const { canModerate } = usePermissions();
const { cellsWithStats } = useForumData();
// Check if user is admin of any cell
const isAdminOfAnyCell = cellsWithStats.some(cell => canModerate(cell.id));
// Only show the toggle if user is admin of at least one cell
if (!isAdminOfAnyCell) {
return null;
}
return (
<div className="flex items-center space-x-2">
<Switch
id="show-moderated"
checked={showModerated}
onCheckedChange={toggleShowModerated}
className="data-[state=checked]:bg-cyber-accent"
/>
<Label
htmlFor="show-moderated"
className="flex items-center gap-2 text-sm cursor-pointer"
>
{showModerated ? (
<Eye className="w-4 h-4 text-cyber-accent" />
) : (
<EyeOff className="w-4 h-4 text-cyber-neutral" />
)}
<span
className={showModerated ? 'text-cyber-accent' : 'text-cyber-neutral'}
>
Show Moderated
</span>
</Label>
</div>
);
}

View File

@ -0,0 +1,83 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { localDatabase } from '@/lib/database/LocalDatabase';
interface ModerationContextType {
showModerated: boolean;
setShowModerated: (show: boolean) => void;
toggleShowModerated: () => void;
}
const ModerationContext = createContext<ModerationContextType | undefined>(
undefined
);
export function ModerationProvider({
children,
}: {
children: React.ReactNode;
}) {
const [showModerated, setShowModerated] = useState<boolean>(false);
const [isInitialized, setIsInitialized] = useState<boolean>(false);
// Load initial state from IndexedDB
useEffect(() => {
const loadModerationPreference = async () => {
try {
const saved = await localDatabase.loadUIState('show-moderated');
setShowModerated(saved === true);
} catch (error) {
console.warn(
'Failed to load moderation preference from IndexedDB:',
error
);
setShowModerated(false);
} finally {
setIsInitialized(true);
}
};
loadModerationPreference();
}, []);
// Save to IndexedDB whenever the value changes (but only after initialization)
useEffect(() => {
if (!isInitialized) return;
const saveModerationPreference = async () => {
try {
await localDatabase.storeUIState('show-moderated', showModerated);
} catch (error) {
console.warn(
'Failed to save moderation preference to IndexedDB:',
error
);
}
};
saveModerationPreference();
}, [showModerated, isInitialized]);
const toggleShowModerated = () => {
setShowModerated(prev => !prev);
};
return (
<ModerationContext.Provider
value={{
showModerated,
setShowModerated,
toggleShowModerated,
}}
>
{children}
</ModerationContext.Provider>
);
}
export function useModeration() {
const context = useContext(ModerationContext);
if (context === undefined) {
throw new Error('useModeration must be used within a ModerationProvider');
}
return context;
}

View File

@ -1,6 +1,7 @@
import { useMemo } from 'react';
import { useForum } from '@/contexts/useForum';
import { useAuth } from '@/contexts/useAuth';
import { useModeration } from '@/contexts/ModerationContext';
import { Cell, Post, Comment, UserVerificationStatus } from '@/types/forum';
import { EVerificationStatus } from '@/types/identity';
@ -44,6 +45,12 @@ export interface ForumData {
postsWithVoteStatus: PostWithVoteStatus[];
commentsWithVoteStatus: CommentWithVoteStatus[];
// Filtered data based on moderation settings
filteredPosts: PostWithVoteStatus[];
filteredComments: CommentWithVoteStatus[];
filteredCellsWithStats: CellWithStats[];
filteredCommentsByPost: Record<string, CommentWithVoteStatus[]>;
// Organized data
postsByCell: Record<string, PostWithVoteStatus[]>;
commentsByPost: Record<string, CommentWithVoteStatus[]>;
@ -72,6 +79,7 @@ export function useForumData(): ForumData {
} = useForum();
const { currentUser } = useAuth();
const { showModerated } = useModeration();
// Compute cells with statistics
const cellsWithStats = useMemo((): CellWithStats[] => {
@ -287,6 +295,55 @@ export function useForumData(): ForumData {
return createdComments;
}, [comments, currentUser]);
// Filtered data based on moderation settings
const filteredPosts = useMemo(() => {
return showModerated
? postsWithVoteStatus
: postsWithVoteStatus.filter(post => !post.moderated);
}, [postsWithVoteStatus, showModerated]);
const filteredComments = useMemo(() => {
return showModerated
? commentsWithVoteStatus
: commentsWithVoteStatus.filter(comment => !comment.moderated);
}, [commentsWithVoteStatus, showModerated]);
// Filtered cells with stats based on filtered posts
const filteredCellsWithStats = useMemo((): CellWithStats[] => {
return cells.map(cell => {
const cellPosts = filteredPosts.filter(post => post.cellId === cell.id);
const recentPosts = cellPosts.filter(
post => Date.now() - post.timestamp < 7 * 24 * 60 * 60 * 1000 // 7 days
);
const uniqueAuthors = new Set(cellPosts.map(post => post.author));
return {
...cell,
postCount: cellPosts.length,
activeUsers: uniqueAuthors.size,
recentActivity: recentPosts.length,
};
});
}, [cells, filteredPosts]);
// Filtered comments organized by post
const filteredCommentsByPost = useMemo((): Record<
string,
CommentWithVoteStatus[]
> => {
const organized: Record<string, CommentWithVoteStatus[]> = {};
filteredComments.forEach(comment => {
if (!organized[comment.postId]) {
organized[comment.postId] = [];
}
organized[comment.postId].push(comment);
});
return organized;
}, [filteredComments]);
return {
// Raw data
cells,
@ -305,6 +362,12 @@ export function useForumData(): ForumData {
postsWithVoteStatus,
commentsWithVoteStatus,
// Filtered data based on moderation settings
filteredPosts,
filteredComments,
filteredCellsWithStats,
filteredCommentsByPost,
// Organized data
postsByCell,
commentsByPost,

View File

@ -1,6 +1,7 @@
import { useMemo } from 'react';
import { useForumData, PostWithVoteStatus } from '@/hooks/core/useForumData';
import { useAuth } from '@/hooks/core/useAuth';
import { useModeration } from '@/contexts/ModerationContext';
export interface CellPostsOptions {
includeModerated?: boolean;
@ -24,8 +25,13 @@ export function useCellPosts(
): CellPostsData {
const { postsByCell, isInitialLoading, cellsWithStats } = useForumData();
const { currentUser } = useAuth();
const { showModerated } = useModeration();
const { includeModerated = false, sortBy = 'relevance', limit } = options;
const {
includeModerated = showModerated,
sortBy = 'relevance',
limit,
} = options;
return useMemo(() => {
if (!cellId) {

View File

@ -1,6 +1,7 @@
import { useMemo } from 'react';
import { useForumData, CommentWithVoteStatus } from '@/hooks/core/useForumData';
import { useAuth } from '@/hooks/core/useAuth';
import { useModeration } from '@/contexts/ModerationContext';
export interface PostCommentsOptions {
includeModerated?: boolean;
@ -29,8 +30,13 @@ export function usePostComments(
cellsWithStats,
} = useForumData();
const { currentUser } = useAuth();
const { showModerated } = useModeration();
const { includeModerated = false, sortBy = 'timestamp', limit } = options;
const {
includeModerated = showModerated,
sortBy = 'timestamp',
limit,
} = options;
return useMemo(() => {
if (!postId) {

View File

@ -11,36 +11,29 @@ import {
} from '@/components/ui/select';
import PostCard from '@/components/PostCard';
import FeedSidebar from '@/components/FeedSidebar';
import { ModerationToggle } from '@/components/ui/moderation-toggle';
import { useForumData, useAuth, useForumActions } from '@/hooks';
import { EVerificationStatus } from '@/types/identity';
import { sortPosts, SortOption } from '@/lib/utils/sorting';
const FeedPage: React.FC = () => {
// ✅ Use reactive hooks
const forumData = useForumData();
// const selectors = useForumSelectors(forumData); // Available if needed
const { verificationStatus } = useAuth();
const { refreshData } = useForumActions();
const [sortOption, setSortOption] = useState<SortOption>('relevance');
const {
postsWithVoteStatus,
commentsByPost,
isInitialLoading,
isRefreshing,
} = forumData;
const { filteredPosts, filteredCommentsByPost, isInitialLoading, isRefreshing } =
forumData;
// ✅ Use pre-computed data and selectors
// ✅ Use pre-computed filtered data
const allPosts = useMemo(() => {
const filteredPosts = postsWithVoteStatus.filter(post => !post.moderated);
return sortPosts(filteredPosts, sortOption);
}, [postsWithVoteStatus, sortOption]);
}, [filteredPosts, sortOption]);
// ✅ Get comment count from organized data
// ✅ Get comment count from filtered organized data
const getCommentCount = (postId: string) => {
return (
commentsByPost[postId]?.filter(comment => !comment.moderated).length || 0
);
const comments = filteredCommentsByPost[postId] || [];
return comments.length;
};
// Loading skeleton
@ -112,6 +105,8 @@ const FeedPage: React.FC = () => {
</p>
</div>
<div className="flex items-center space-x-2">
<ModerationToggle />
<Select
value={sortOption}
onValueChange={(value: SortOption) => setSortOption(value)}