Skip to content

Commit 56a224f

Browse files
committed
✨ feat: Add user ratings for spam predictions
1 parent 12c70da commit 56a224f

File tree

15 files changed

+370
-18
lines changed

15 files changed

+370
-18
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { NextResponse } from 'next/server';
2+
3+
import { addSpamPredictionUserRatingDownvote } from '@/services/api/SpamPredictionVotesService';
4+
5+
/**
6+
* @swagger
7+
* /ui/api/post/{postId}/spam-prediction-user-rating/downvote:
8+
* post:
9+
* description: Downvote the Spam Prediction.
10+
*/
11+
12+
export type PostParams = {
13+
postId: string;
14+
};
15+
16+
export async function POST(req: Request, { params }: { params: Promise<PostParams> }): Promise<NextResponse> {
17+
const { postId } = await params;
18+
const res = await addSpamPredictionUserRatingDownvote(postId);
19+
20+
return NextResponse.json(res.data, { status: res.status });
21+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { NextResponse } from 'next/server';
2+
3+
import { fetchSpamPredictionUserRating } from '@/services/api/SpamPredictionVotesService';
4+
import { PostParams } from '@/app/api/like/[postId]/route';
5+
6+
/**
7+
* @swagger
8+
* /ui/api/post/{postId}/spam-prediction-user-rating:
9+
* get:
10+
* description: Get the spam prediction user ratings for a post by its ID.
11+
*/
12+
13+
export async function GET(req: Request, { params }: { params: Promise<PostParams> }): Promise<NextResponse> {
14+
const { postId } = await params;
15+
const res = await fetchSpamPredictionUserRating(postId);
16+
17+
return NextResponse.json(res.data);
18+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { NextResponse } from 'next/server';
2+
3+
import { addSpamPredictionUserRatingUpvote } from '@/services/api/SpamPredictionVotesService';
4+
5+
/**
6+
* @swagger
7+
* /ui/api/post/{postId}/spam-prediction-user-rating/upvote:
8+
* post:
9+
* description: Handle spam prediction upvote action
10+
*/
11+
12+
export type PostParams = {
13+
postId: string;
14+
};
15+
16+
export async function POST(req: Request, { params }: { params: Promise<PostParams> }): Promise<NextResponse> {
17+
const { postId } = await params;
18+
const res = await addSpamPredictionUserRatingUpvote(postId);
19+
20+
return NextResponse.json(res.data, { status: res.status });
21+
}

src/frontend-nextjs/components/Timeline/Post.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function Post(props: PostProps) {
7373
</CardBody>
7474
<CardFooter className='gap-3 justify-between px-3'>
7575
<div className='flex items-center'>
76-
<PostSpamPrediction isSpamPredictedLabel={props.isSpamPredictedLabel} />
76+
<PostSpamPrediction isSpamPredictedLabel={props.isSpamPredictedLabel} postId={props.postId}/>
7777
</div>
7878

7979
{isLoggedIn && (

src/frontend-nextjs/components/Timeline/PostSpamPrediction.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,37 @@
1+
'use client';
2+
13
import React from 'react';
24
import {Alert} from "@heroui/react";
35

6+
import {SpamPredictionUserRating} from "@/components/Timeline/SpamPredictionUserRating";
7+
import {useCheckLogin} from "@/hooks/queries/useCheckLogin";
8+
import {ErrorBoundary} from "react-error-boundary";
9+
import {ErrorCard} from "@/components/ErrorCard";
10+
411
export interface PostSpamPredictionProps {
512
isSpamPredictedLabel?: boolean | null;
13+
postId: string;
614
}
715

8-
9-
1016
export function PostSpamPrediction(props: Readonly<PostSpamPredictionProps>) {
11-
console.log("PostSpamPrediction props:", props);
17+
const { isLoggedIn } = useCheckLogin();
1218
if (props.isSpamPredictedLabel == null) return null;
1319

20+
const color = props.isSpamPredictedLabel ? 'danger' : 'primary';
1421

15-
if (props.isSpamPredictedLabel) {
16-
const color='danger'
17-
return (
18-
<div key={color} className='w-full flex items-center my-3'>
19-
<div>
20-
<Alert color={color} title={'Potential Spam Detected'} />
21-
</div>
22-
</div>
23-
);
24-
}
25-
26-
const color = 'primary'
2722
return (
2823
<div key={color} className='w-full flex items-center my-3'>
29-
<div>
30-
<Alert color={color} title={'No Spam Detected'} />
24+
<div className="w-full">
25+
<Alert color={color}>
26+
<div className="flex w-full items-center justify-between gap-3">
27+
<div className="font-semibold">{props.isSpamPredictedLabel? "Potential Spam Detected" : "No Spam Detected"}</div>
28+
{isLoggedIn && (
29+
<ErrorBoundary fallbackRender={(props) => <ErrorCard message={props.error.message} />}>
30+
<SpamPredictionUserRating isSpamPredictedLabel={props.isSpamPredictedLabel} postId={props.postId} />
31+
</ErrorBoundary>
32+
)}
33+
</div>
34+
</Alert>
3135
</div>
3236
</div>
3337
);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import {BsHandThumbsDown, BsHandThumbsDownFill, BsHandThumbsUp, BsHandThumbsUpFill} from 'react-icons/bs';
5+
import { Button, Spinner } from '@heroui/react';
6+
import {useSpamPredictionUserRating} from "@/hooks/queries/useSpamPredictionUserRating";
7+
import {useRateSpamPrediction} from "@/hooks/mutations/useRateSpamPrediction";
8+
9+
export interface SpamPredictionUserRatingProps {
10+
isSpamPredictedLabel?: boolean | null;
11+
postId: string;
12+
}
13+
14+
export function SpamPredictionUserRating(props: Readonly<SpamPredictionUserRatingProps>) {
15+
16+
const { data: spamPredictionUserRatingData, isLoading} = useSpamPredictionUserRating(props.postId);
17+
const { handleSpamPredictionUpvote, handleSpamPredictionDownvote } = useRateSpamPrediction(props.postId);
18+
19+
if (isLoading) {
20+
return <Spinner />;
21+
}
22+
23+
return (
24+
<div>
25+
<Button className=' text-default-600 bg-transparent' name='upvoteSpamRating' onPress={() => handleSpamPredictionUpvote()}>
26+
<p>{spamPredictionUserRatingData?.spamPredictionUserUpvotes}</p>
27+
{spamPredictionUserRatingData?.isUpvotedByUser ? <BsHandThumbsUpFill /> : <BsHandThumbsUp />}
28+
</Button>
29+
<Button className=' text-default-600 bg-transparent' name='downvoteSpamRating' onPress={() => handleSpamPredictionDownvote()}>
30+
<p>{spamPredictionUserRatingData?.spamPredictionUserDownvotes}</p>
31+
{spamPredictionUserRatingData?.isDownvotedByUser ? <BsHandThumbsDownFill /> : <BsHandThumbsDown />}
32+
</Button>
33+
</div>
34+
)
35+
36+
}

src/frontend-nextjs/enums/queryKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ export enum QUERY_KEYS {
1717
ad_manager = 'ad-manager',
1818
ad_list = 'ad-list',
1919
deployment_health = 'deployment-health',
20+
spam_prediction_user_rating = 'spam-prediction-user-rating',
2021
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useMutation, useQueryClient } from '@tanstack/react-query';
2+
3+
import {handleSpamPredictionDownvote, handleSpamPredictionUpvote} from '@/services/SpamPredictionVotingService';
4+
import { QUERY_KEYS } from '@/enums/queryKeys';
5+
6+
export function useRateSpamPrediction(postId: string) {
7+
const queryClient = useQueryClient();
8+
9+
const handleUpvoteMutation = useMutation({
10+
mutationFn: () => handleSpamPredictionUpvote(postId),
11+
onSuccess: () => {
12+
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.spam_prediction_user_rating, postId] });
13+
},
14+
});
15+
16+
const handleDownvoteMutation = useMutation({
17+
mutationFn: () => handleSpamPredictionDownvote(postId),
18+
onSuccess: () => {
19+
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.spam_prediction_user_rating, postId] });
20+
},
21+
});
22+
23+
return {
24+
handleSpamPredictionUpvote: handleUpvoteMutation.mutate,
25+
handleSpamPredictionDownvote: handleDownvoteMutation.mutate
26+
};
27+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import path from 'path';
2+
3+
import {useQuery} from '@tanstack/react-query';
4+
5+
import {QUERY_KEYS} from '@/enums/queryKeys';
6+
import {BASE_PATH} from '@/constants';
7+
8+
type SpamPredictionUserRating = {
9+
spamPredictionUserUpvotes: number;
10+
spamPredictionUserDownvotes: boolean;
11+
isUpvotedByUser?: boolean;
12+
isDownvotedByUser?: boolean;
13+
};
14+
15+
async function fetchSpamPredictionUserRatings(postId: string): Promise<SpamPredictionUserRating> {
16+
const res = await fetch(path.join(BASE_PATH, `/api/post/${postId}/spam-prediction-user-rating/`));
17+
18+
if (!res.ok) {
19+
throw new Error('Failed to fetch spam prediction user ratings');
20+
}
21+
22+
return await res.json();
23+
}
24+
25+
export function useSpamPredictionUserRating(postId: string) {
26+
return useQuery({
27+
queryKey: [QUERY_KEYS.spam_prediction_user_rating, postId],
28+
queryFn: () => fetchSpamPredictionUserRatings(postId),
29+
throwOnError: true,
30+
});
31+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import path from 'path';
2+
3+
import { BASE_PATH } from '@/constants';
4+
5+
export async function handleSpamPredictionDownvote(postId: string): Promise<Response> {
6+
return await fetch(path.join(BASE_PATH, `/api/post/${postId}/spam-prediction-user-rating/downvote/`), {
7+
method: 'POST',
8+
headers: { 'Content-Type': 'application/json' },
9+
});
10+
}
11+
12+
export async function handleSpamPredictionUpvote(postId: string): Promise<Response> {
13+
return await fetch(path.join(BASE_PATH, `/api/post/${postId}/spam-prediction-user-rating/upvote/`), {
14+
method: 'POST',
15+
headers: { 'Content-Type': 'application/json' },
16+
});
17+
}

0 commit comments

Comments
 (0)