Skip to content

Commit b3fa0c9

Browse files
committed
feat: Add helpful vote functionality to reviews
1 parent 75636de commit b3fa0c9

File tree

9 files changed

+579
-363
lines changed

9 files changed

+579
-363
lines changed

apps/jobboard-frontend/public/locales/ar/common.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,13 @@
10161016
"loadMore": "تحميل المزيد",
10171017
"loading": "جارٍ التحميل...",
10181018
"helpful": "مفيد",
1019+
"toggleHelpful": "وضع علامة مفيد",
1020+
"helpfulMarked": "تم وضع علامة مفيد",
1021+
"helpfulMarkedDescription": "تساعد ملاحظاتك الآخرين في العثور على تقييمات مفيدة",
1022+
"helpfulRemoved": "تمت إزالة علامة مفيد",
1023+
"helpfulRemovedDescription": "لقد أزلت تصويتك المفيد",
1024+
"helpfulError": "فشل تحديث التصويت المفيد",
1025+
"helpfulErrorDescription": "يرجى المحاولة مرة أخرى لاحقًا",
10191026
"report": "إبلاغ",
10201027
"today": "اليوم",
10211028
"yesterday": "أمس",

apps/jobboard-frontend/public/locales/en/common.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,6 +1522,13 @@
15221522
"loadMore": "Load More Reviews",
15231523
"loading": "Loading...",
15241524
"helpful": "Helpful",
1525+
"toggleHelpful": "Mark as helpful",
1526+
"helpfulMarked": "Marked as helpful",
1527+
"helpfulMarkedDescription": "Your feedback helps others find useful reviews",
1528+
"helpfulRemoved": "Removed helpful mark",
1529+
"helpfulRemovedDescription": "You removed your helpful vote",
1530+
"helpfulError": "Failed to update helpful vote",
1531+
"helpfulErrorDescription": "Please try again later",
15251532
"report": "Report",
15261533
"today": "today",
15271534
"yesterday": "yesterday",

apps/jobboard-frontend/public/locales/tr/common.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,13 @@
12021202
"loadMore": "Daha Fazla Yükle",
12031203
"loading": "Yükleniyor...",
12041204
"helpful": "Yararlı",
1205+
"toggleHelpful": "Yararlı olarak işaretle",
1206+
"helpfulMarked": "Yararlı olarak işaretlendi",
1207+
"helpfulMarkedDescription": "Geri bildiriminiz, başkalarının yararlı değerlendirmeler bulmasına yardımcı olur",
1208+
"helpfulRemoved": "Yararlı işareti kaldırıldı",
1209+
"helpfulRemovedDescription": "Yararlı oyunuzu kaldırdınız",
1210+
"helpfulError": "Yararlı oyu güncellenemedi",
1211+
"helpfulErrorDescription": "Lütfen daha sonra tekrar deneyin",
12051212
"report": "Bildir",
12061213
"today": "bugün",
12071214
"yesterday": "dün",
@@ -1270,4 +1277,4 @@
12701277
"selectConversation": "Bir sohbet seçin",
12711278
"selectConversationDesc": "Sohbet başlatmak için listeden bir konuşma seçin"
12721279
}
1273-
}
1280+
}

apps/jobboard-frontend/src/components/reviews/ReviewCard.tsx

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,50 @@ import { useState } from 'react';
1111
import { formatDistanceToNow } from 'date-fns';
1212
import { useReportModal } from '@/hooks/useReportModal';
1313
import { reportWorkplaceReview } from '@/services/workplace-report.service';
14+
import { useReviewHelpful } from '@/hooks/useReviewHelpful';
1415

1516
interface ReviewCardProps {
1617
workplaceId: number;
1718
review: ReviewResponse;
1819
canReply?: boolean;
1920
onUpdate?: () => void;
21+
onHelpfulUpdate?: (reviewId: number, newHelpfulCount: number, helpfulByUser?: boolean) => void;
2022
}
2123

22-
export function ReviewCard({ workplaceId, review, canReply, onUpdate }: ReviewCardProps) {
24+
export function ReviewCard({
25+
workplaceId,
26+
review,
27+
canReply,
28+
onUpdate,
29+
onHelpfulUpdate,
30+
}: ReviewCardProps) {
2331
const { t } = useTranslation('common');
2432
const [isReplyDialogOpen, setIsReplyDialogOpen] = useState(false);
2533
const { openReport, ReportModalElement } = useReportModal();
2634

35+
// Initialize helpful vote hook
36+
const {
37+
helpfulCount,
38+
userVoted,
39+
isLoading: isHelpfulLoading,
40+
toggleHelpful,
41+
canVote,
42+
} = useReviewHelpful({
43+
workplaceId,
44+
reviewId: review.id,
45+
initialHelpfulCount: review.helpfulCount,
46+
initialUserVoted: review.helpfulByUser ?? false, // sync initial state from API
47+
});
48+
49+
// Handle helpful count updates
50+
const handleHelpfulClick = async () => {
51+
const updatedReview = await toggleHelpful();
52+
// Notify parent component of helpful count change
53+
if (updatedReview) {
54+
onHelpfulUpdate?.(review.id, updatedReview.helpfulCount, updatedReview.helpfulByUser);
55+
}
56+
};
57+
2758
const handleReport = () => {
2859
openReport({
2960
title: t('reviews.report'),
@@ -96,9 +127,7 @@ export function ReviewCard({ workplaceId, review, canReply, onUpdate }: ReviewCa
96127
</div>
97128

98129
{/* Title */}
99-
{review.title && (
100-
<h5 className="font-medium text-foreground mb-2">{review.title}</h5>
101-
)}
130+
{review.title && <h5 className="font-medium text-foreground mb-2">{review.title}</h5>}
102131

103132
{/* Content */}
104133
{review.content && (
@@ -123,12 +152,23 @@ export function ReviewCard({ workplaceId, review, canReply, onUpdate }: ReviewCa
123152

124153
{/* Actions */}
125154
<div className="flex items-center gap-4 pt-3 border-t">
126-
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
127-
<ThumbsUp className="h-4 w-4" />
155+
<Button
156+
variant="ghost"
157+
size="sm"
158+
onClick={handleHelpfulClick}
159+
disabled={isHelpfulLoading || !canVote}
160+
className={`flex items-center gap-1.5 text-sm ${
161+
userVoted
162+
? 'text-primary hover:text-primary/80'
163+
: 'text-muted-foreground hover:text-foreground'
164+
}`}
165+
title={canVote ? t('reviews.toggleHelpful') : t('reviews.helpful')}
166+
>
167+
<ThumbsUp className={`h-4 w-4 ${userVoted ? 'fill-current' : ''}`} />
128168
<span>
129-
{t('reviews.helpful')} ({review.helpfulCount})
169+
{t('reviews.helpful')} ({helpfulCount})
130170
</span>
131-
</div>
171+
</Button>
132172
{canReply && !review.reply && (
133173
<Button
134174
variant="outline"

apps/jobboard-frontend/src/components/reviews/ReviewList.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface ReviewListProps {
2121
reviewsPerPage?: number;
2222
actions?: ReactNode;
2323
onTotalsChange?: (total: number) => void;
24+
onReviewUpdate?: (reviewId: number, updates: Partial<ReviewResponse>) => void;
2425
}
2526

2627
export function ReviewList({
@@ -30,6 +31,7 @@ export function ReviewList({
3031
reviewsPerPage = 10,
3132
actions,
3233
onTotalsChange,
34+
onReviewUpdate,
3335
}: ReviewListProps) {
3436
const { t } = useTranslation('common');
3537
const [reviews, setReviews] = useState<ReviewResponse[]>([]);
@@ -74,6 +76,23 @@ export function ReviewList({
7476
loadReviews(currentPage);
7577
};
7678

79+
const handleHelpfulUpdate = (
80+
reviewId: number,
81+
newHelpfulCount: number,
82+
helpfulByUser?: boolean,
83+
) => {
84+
// Update the specific review's helpful count and user state in local state
85+
setReviews((prevReviews) =>
86+
prevReviews.map((review) =>
87+
review.id === reviewId
88+
? { ...review, helpfulCount: newHelpfulCount, helpfulByUser }
89+
: review,
90+
),
91+
);
92+
// Notify parent component if needed
93+
onReviewUpdate?.(reviewId, { helpfulCount: newHelpfulCount, helpfulByUser });
94+
};
95+
7796
// Generate page numbers for pagination (0-indexed to 1-indexed for display)
7897
const getPageNumbers = () => {
7998
const pages: (number | 'ellipsis')[] = [];
@@ -144,6 +163,7 @@ export function ReviewList({
144163
review={review}
145164
canReply={canReply}
146165
onUpdate={handleReviewUpdate}
166+
onHelpfulUpdate={handleHelpfulUpdate}
147167
/>
148168
))}
149169
</div>
@@ -155,9 +175,7 @@ export function ReviewList({
155175
<PaginationPrevious
156176
onClick={() => currentPage > 0 && handlePageChange(currentPage - 1)}
157177
className={
158-
currentPage === 0
159-
? 'pointer-events-none opacity-50'
160-
: 'cursor-pointer'
178+
currentPage === 0 ? 'pointer-events-none opacity-50' : 'cursor-pointer'
161179
}
162180
/>
163181
</PaginationItem>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* useReviewHelpful Hook
3+
* Manages helpful vote state and operations for reviews
4+
*/
5+
6+
import { useState, useCallback } from 'react';
7+
import { markReviewHelpful } from '@/services/reviews.service';
8+
import type { ReviewResponse } from '@/types/workplace.types';
9+
import { toast } from 'react-toastify';
10+
import { useTranslation } from 'react-i18next';
11+
12+
interface UseReviewHelpfulProps {
13+
workplaceId: number;
14+
reviewId: number;
15+
initialHelpfulCount: number;
16+
initialUserVoted?: boolean;
17+
}
18+
19+
interface UseReviewHelpfulReturn {
20+
helpfulCount: number;
21+
userVoted: boolean;
22+
isLoading: boolean;
23+
toggleHelpful: () => Promise<ReviewResponse | undefined>;
24+
canVote: boolean;
25+
}
26+
27+
export function useReviewHelpful({
28+
workplaceId,
29+
reviewId,
30+
initialHelpfulCount,
31+
initialUserVoted = false,
32+
}: UseReviewHelpfulProps): UseReviewHelpfulReturn {
33+
const [helpfulCount, setHelpfulCount] = useState(initialHelpfulCount);
34+
const [userVoted, setUserVoted] = useState(initialUserVoted);
35+
const [isLoading, setIsLoading] = useState(false);
36+
const { t } = useTranslation('common');
37+
38+
const canVote = true; // will be enhanced with auth checks when available
39+
40+
const toggleHelpful = useCallback(async (): Promise<ReviewResponse | undefined> => {
41+
if (isLoading || !canVote) return;
42+
43+
setIsLoading(true);
44+
45+
// Optimistic update
46+
const previousCount = helpfulCount;
47+
const previousVoted = userVoted;
48+
const optimisticCount = previousVoted ? previousCount - 1 : previousCount + 1;
49+
50+
setUserVoted(!previousVoted);
51+
setHelpfulCount(Math.max(optimisticCount, 0));
52+
53+
try {
54+
// Backend toggles helpful status via single POST endpoint
55+
const updatedReview = await markReviewHelpful(workplaceId, reviewId);
56+
57+
const newUserVoted = !!updatedReview.helpfulByUser;
58+
setHelpfulCount(updatedReview.helpfulCount);
59+
setUserVoted(newUserVoted);
60+
61+
toast.success(
62+
newUserVoted
63+
? t('reviews.helpfulMarked') || 'Marked as helpful'
64+
: t('reviews.helpfulRemoved') || 'Removed helpful mark',
65+
);
66+
67+
return updatedReview;
68+
} catch (error) {
69+
// Revert optimistic update on error
70+
setHelpfulCount(previousCount);
71+
setUserVoted(previousVoted);
72+
73+
console.error('Failed to toggle helpful vote:', error);
74+
75+
toast.error(t('reviews.helpfulError') || 'Failed to update helpful vote');
76+
} finally {
77+
setIsLoading(false);
78+
}
79+
}, [workplaceId, reviewId, helpfulCount, isLoading, canVote, toast, t, userVoted]);
80+
81+
return {
82+
helpfulCount,
83+
userVoted,
84+
isLoading,
85+
toggleHelpful,
86+
canVote,
87+
};
88+
}

0 commit comments

Comments
 (0)