mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 04:27:56 +00:00
d282f609aa
Add full internationalization across 127+ components and pages. 790+ translation keys in en, tr, kmr, ckb, ar, fa locales. Remove duplicate keys and delete unused .json locale files.
281 lines
8.6 KiB
TypeScript
281 lines
8.6 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Star, Loader2, ThumbsUp, ThumbsDown } from 'lucide-react';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { toast } from 'sonner';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { supabase } from '@/lib/supabase';
|
|
|
|
interface RatingModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
tradeId: string;
|
|
counterpartyId: string;
|
|
counterpartyWallet: string;
|
|
isBuyer: boolean;
|
|
}
|
|
|
|
export function RatingModal({
|
|
isOpen,
|
|
onClose,
|
|
tradeId,
|
|
counterpartyId,
|
|
counterpartyWallet,
|
|
isBuyer,
|
|
}: RatingModalProps) {
|
|
const { t } = useTranslation();
|
|
const { user } = useAuth();
|
|
const [rating, setRating] = useState(0);
|
|
const [hoveredRating, setHoveredRating] = useState(0);
|
|
const [review, setReview] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const handleSubmit = async () => {
|
|
if (!user || rating === 0) {
|
|
toast.error(t('p2pRating.selectRatingError'));
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
// Check if already rated
|
|
const { data: existingRating } = await supabase
|
|
.from('p2p_ratings')
|
|
.select('id')
|
|
.eq('trade_id', tradeId)
|
|
.eq('rater_id', user.id)
|
|
.single();
|
|
|
|
if (existingRating) {
|
|
toast.error(t('p2pRating.alreadyRated'));
|
|
onClose();
|
|
return;
|
|
}
|
|
|
|
// Insert rating
|
|
const { error: ratingError } = await supabase.from('p2p_ratings').insert({
|
|
trade_id: tradeId,
|
|
rater_id: user.id,
|
|
rated_id: counterpartyId,
|
|
rating,
|
|
review: review.trim() || null,
|
|
});
|
|
|
|
if (ratingError) throw ratingError;
|
|
|
|
// Update reputation score
|
|
const { data: repData } = await supabase
|
|
.from('p2p_reputation')
|
|
.select('*')
|
|
.eq('user_id', counterpartyId)
|
|
.single();
|
|
|
|
if (repData) {
|
|
// Calculate new average rating
|
|
const { data: allRatings } = await supabase
|
|
.from('p2p_ratings')
|
|
.select('rating')
|
|
.eq('rated_id', counterpartyId);
|
|
|
|
const totalRatings = allRatings?.length || 0;
|
|
const avgRating = allRatings
|
|
? allRatings.reduce((sum, r) => sum + r.rating, 0) / totalRatings
|
|
: rating;
|
|
|
|
// Update reputation
|
|
await supabase
|
|
.from('p2p_reputation')
|
|
.update({
|
|
reputation_score: Math.round(avgRating * 20), // Convert 5-star to 100-point scale
|
|
})
|
|
.eq('user_id', counterpartyId);
|
|
}
|
|
|
|
// Create notification
|
|
await supabase.from('p2p_notifications').insert({
|
|
user_id: counterpartyId,
|
|
type: 'new_rating',
|
|
title: 'New Rating Received',
|
|
message: `You received a ${rating}-star rating`,
|
|
reference_type: 'trade',
|
|
reference_id: tradeId,
|
|
is_read: false,
|
|
});
|
|
|
|
toast.success(t('p2pRating.submitted'));
|
|
onClose();
|
|
} catch (error) {
|
|
console.error('Submit rating error:', error);
|
|
toast.error(t('p2pRating.failedToSubmit'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const renderStars = () => {
|
|
return (
|
|
<div className="flex items-center gap-1">
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
<button
|
|
key={star}
|
|
type="button"
|
|
onClick={() => setRating(star)}
|
|
onMouseEnter={() => setHoveredRating(star)}
|
|
onMouseLeave={() => setHoveredRating(0)}
|
|
className="p-1 transition-transform hover:scale-110"
|
|
>
|
|
<Star
|
|
className={`w-8 h-8 transition-colors ${
|
|
star <= (hoveredRating || rating)
|
|
? 'fill-yellow-400 text-yellow-400'
|
|
: 'text-gray-600'
|
|
}`}
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const getRatingLabel = (r: number): string => {
|
|
switch (r) {
|
|
case 1: return t('p2pRating.poor');
|
|
case 2: return t('p2pRating.fair');
|
|
case 3: return t('p2pRating.good');
|
|
case 4: return t('p2pRating.veryGood');
|
|
case 5: return t('p2pRating.excellent');
|
|
default: return t('p2pRating.selectRating');
|
|
}
|
|
};
|
|
|
|
const quickReviews = [
|
|
{ icon: ThumbsUp, text: t('p2pRating.fastPayment'), positive: true },
|
|
{ icon: ThumbsUp, text: t('p2pRating.goodCommunication'), positive: true },
|
|
{ icon: ThumbsUp, text: t('p2pRating.smoothTransaction'), positive: true },
|
|
{ icon: ThumbsDown, text: t('p2pRating.slowResponse'), positive: false },
|
|
{ icon: ThumbsDown, text: t('p2pRating.delayedPayment'), positive: false },
|
|
];
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="bg-gray-900 border-gray-800 text-white max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{t('p2pRating.title')}</DialogTitle>
|
|
<DialogDescription className="text-gray-400">
|
|
{t('p2pRating.description', { address: `${counterpartyWallet.slice(0, 6)}...${counterpartyWallet.slice(-4)}` })}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-6 py-4">
|
|
{/* Star Rating */}
|
|
<div className="flex flex-col items-center gap-2">
|
|
{renderStars()}
|
|
<p className={`text-sm font-medium ${
|
|
rating >= 4 ? 'text-green-400' :
|
|
rating >= 3 ? 'text-yellow-400' :
|
|
rating >= 1 ? 'text-red-400' : 'text-gray-500'
|
|
}`}>
|
|
{getRatingLabel(hoveredRating || rating)}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Quick Review Buttons */}
|
|
<div>
|
|
<Label className="text-gray-400 text-sm">{t('p2pRating.quickFeedback')}</Label>
|
|
<div className="flex flex-wrap gap-2 mt-2">
|
|
{quickReviews.map((qr, i) => (
|
|
<button
|
|
key={i}
|
|
type="button"
|
|
onClick={() => setReview(prev =>
|
|
prev ? `${prev}, ${qr.text}` : qr.text
|
|
)}
|
|
className={`
|
|
flex items-center gap-1 px-3 py-1.5 rounded-full text-sm
|
|
border transition-colors
|
|
${qr.positive
|
|
? 'border-green-500/30 text-green-400 hover:bg-green-500/10'
|
|
: 'border-red-500/30 text-red-400 hover:bg-red-500/10'
|
|
}
|
|
`}
|
|
>
|
|
<qr.icon className="w-3 h-3" />
|
|
{qr.text}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Review Text */}
|
|
<div>
|
|
<Label htmlFor="review" className="text-gray-400 text-sm">
|
|
{t('p2pRating.additionalComments')}
|
|
</Label>
|
|
<Textarea
|
|
id="review"
|
|
value={review}
|
|
onChange={(e) => setReview(e.target.value)}
|
|
placeholder={t('p2pRating.sharePlaceholder')}
|
|
className="mt-2 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 resize-none"
|
|
rows={3}
|
|
maxLength={500}
|
|
/>
|
|
<p className="text-xs text-gray-500 text-right mt-1">
|
|
{review.length}/500
|
|
</p>
|
|
</div>
|
|
|
|
{/* Role Badge */}
|
|
<div className="flex items-center justify-center">
|
|
<span className={`
|
|
px-3 py-1 rounded-full text-xs
|
|
${isBuyer
|
|
? 'bg-green-500/20 text-green-400'
|
|
: 'bg-blue-500/20 text-blue-400'
|
|
}
|
|
`}>
|
|
{isBuyer ? t('p2pRating.ratingAsBuyer') : t('p2pRating.ratingAsSeller')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={onClose}
|
|
disabled={loading}
|
|
className="border-gray-700"
|
|
>
|
|
{t('p2pRating.skip')}
|
|
</Button>
|
|
<Button
|
|
onClick={handleSubmit}
|
|
disabled={rating === 0 || loading}
|
|
className="bg-green-600 hover:bg-green-700"
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
{t('p2pRating.submitting')}
|
|
</>
|
|
) : (
|
|
t('p2pRating.submitRating')
|
|
)}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|