Skip to content
Merged
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
139 changes: 95 additions & 44 deletions components/rating/rating-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import React, { useState } from 'react';
import { RatingStars } from './rating-stars';
"use client";

import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { RatingStars } from "./rating-stars";

interface RatingModalProps {
contributor: {
Expand All @@ -15,16 +27,21 @@ interface RatingModalProps {
onClose: () => void;
}

export const RatingModal: React.FC<RatingModalProps> = ({ contributor, bounty, onSubmit, onClose }) => {
export const RatingModal = ({
contributor,
bounty,
onSubmit,
onClose,
}: RatingModalProps) => {
const [rating, setRating] = useState(0);
const [feedback, setFeedback] = useState('');
const [feedback, setFeedback] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);

const handleSubmit = async () => {
if (rating < 1 || rating > 5) {
setError('Please select a rating between 1 and 5.');
setError("Please select a rating between 1 and 5.");
return;
}
setLoading(true);
Expand All @@ -33,50 +50,84 @@ export const RatingModal: React.FC<RatingModalProps> = ({ contributor, bounty, o
await onSubmit(rating, feedback);
setSuccess(true);
} catch (err) {
console.error(err)
setError('Failed to submit rating. Please try again.');
console.error(err);
setError("Failed to submit rating. Please try again.");
} finally {
setLoading(false);
}
};

if (success) {
return (
<div className="modal">
<h2>Success!</h2>
<p>Rating submitted. Contributor reputation updated.</p>
<button onClick={onClose}>Close</button>
</div>
);
}

return (
<div className="modal">
<h2>Rate Contributor</h2>
<div>
<strong>Bounty:</strong> {bounty.title}
</div>
<div>
<strong>Contributor:</strong> {contributor.name}
</div>
<div>
<strong>Current Reputation:</strong> {contributor.reputation}
</div>
<div style={{ margin: '16px 0' }}>
<RatingStars value={rating} onChange={setRating} />
</div>
<textarea
placeholder="Optional feedback"
value={feedback}
onChange={e => setFeedback(e.target.value)}
rows={3}
style={{ width: '100%', marginBottom: 8 }}
/>
{error && <div style={{ color: 'red', marginBottom: 8 }}>{error}</div>}
<button onClick={handleSubmit} disabled={loading}>
{loading ? 'Submitting...' : 'Submit'}
</button>
<button onClick={onClose} style={{ marginLeft: 8 }}>Cancel</button>
</div>
<Dialog open onOpenChange={(open: boolean) => !open && onClose()}>
<DialogContent>
{success ? (
<>
<DialogHeader>
<DialogTitle>Success!</DialogTitle>
<DialogDescription>
Rating submitted. Contributor reputation updated.
</DialogDescription>
</DialogHeader>

<DialogFooter>
<Button variant="ghost" onClick={onClose}>
Close
</Button>
</DialogFooter>
</>
) : (
<>
<DialogHeader>
<DialogTitle>Rate Contributor</DialogTitle>
<DialogDescription>
Provide a rating and optional feedback for the contributor.
</DialogDescription>
</DialogHeader>

<div className="space-y-2">
<div className="flex items-center">
<span className="text-muted-foreground">Bounty:</span>
<span className="ml-2 font-medium">{bounty.title}</span>
</div>
<div className="flex items-center">
<span className="text-muted-foreground">Contributor:</span>
<span className="ml-2 font-medium">{contributor.name}</span>
</div>
<div className="flex items-center">
<span className="text-muted-foreground">
Current Reputation:
</span>
<span className="ml-2 font-medium">
{contributor.reputation}
</span>
</div>

<div className="mt-4">
<RatingStars value={rating} onChange={setRating} />
</div>

<Textarea
placeholder="Optional feedback"
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
rows={3}
className="min-h-[6rem]"
/>

{error && <div className="text-destructive">{error}</div>}
</div>

<DialogFooter>
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={loading}>
{loading ? "Submitting..." : "Submit"}
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
);
};
42 changes: 27 additions & 15 deletions components/rating/rating-stars.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState } from "react";

interface RatingStarsProps {
value: number;
Expand All @@ -7,39 +7,51 @@ interface RatingStarsProps {
displayOnly?: boolean;
}

export const RatingStars: React.FC<RatingStarsProps> = ({ value, onChange, disabled, displayOnly }) => {
export const RatingStars: React.FC<RatingStarsProps> = ({
value,
onChange,
disabled,
displayOnly,
}) => {
const [hovered, setHovered] = useState<number | null>(null);

const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (!onChange || disabled || displayOnly) return;
if (e.key === 'ArrowLeft' && value > 1) onChange(value - 1);
if (e.key === 'ArrowRight' && value < 5) onChange(value + 1);
if (e.key === "ArrowLeft" && value > 1) onChange(value - 1);
if (e.key === "ArrowRight" && value < 5) onChange(value + 1);
};

return (
<div
tabIndex={displayOnly ? -1 : 0}
role={displayOnly ? 'img' : 'slider'}
role={displayOnly ? "img" : "slider"}
Comment on lines 26 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Honor disabled in focus and hover handling.

disabled still leaves the slider focusable and still updates the hover preview, so the control looks interactive even though click/keyboard changes are blocked.

Suggested fix
-      tabIndex={displayOnly ? -1 : 0}
+      tabIndex={displayOnly || disabled ? -1 : 0}
       role={displayOnly ? "img" : "slider"}
+      aria-disabled={disabled || undefined}
       aria-valuenow={value}
@@
-          ? "flex items-center gap-1 outline-none"
-          : "flex items-center gap-1 cursor-pointer outline-none"
+          ? "flex items-center gap-1 outline-none"
+          : `flex items-center gap-1 outline-none ${disabled ? "cursor-default" : "cursor-pointer"}`
@@
-          onMouseEnter={() => !displayOnly && setHovered(star)}
-          onMouseLeave={() => !displayOnly && setHovered(null)}
+          onMouseEnter={() => !displayOnly && !disabled && setHovered(star)}
+          onMouseLeave={() => !displayOnly && !disabled && setHovered(null)}

Also applies to: 32-35, 41-45

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/rating/rating-stars.tsx` around lines 26 - 27, Honor the disabled
state in RatingStars so it is not focusable and does not react to hover previews
when disabled. Update the focus/ARIA logic around tabIndex and role, and gate
the hover handlers and preview state updates in the RatingStars component so
disabled behaves like displayOnly for interaction while preserving the existing
click/keyboard blocking.

aria-valuenow={value}
aria-valuemin={1}
aria-valuemax={5}
onKeyDown={handleKeyDown}
style={{ display: 'flex', gap: 4, outline: 'none', cursor: displayOnly ? 'default' : 'pointer' }}
className={
displayOnly
? "flex items-center gap-1 outline-none"
: "flex items-center gap-1 cursor-pointer outline-none"
}
>
{[1, 2, 3, 4, 5].map((star) => (
<span
key={star}
onMouseEnter={() => !displayOnly && setHovered(star)}
onMouseLeave={() => !displayOnly && setHovered(null)}
onClick={() => onChange && !disabled && !displayOnly && onChange(star)}
style={{
color: (hovered ?? value) >= star ? '#FFD700' : '#CCC',
fontSize: 28,
transition: 'color 0.2s',
pointerEvents: displayOnly ? 'none' : 'auto',
userSelect: 'none',
}}
aria-label={star + ' star'}
onClick={() =>
onChange && !disabled && !displayOnly && onChange(star)
}
className={
displayOnly
? "select-none text-2xl text-muted-foreground"
: "select-none text-2xl transition-colors duration-200 " +
((hovered ?? value) >= star
? "text-yellow-400"
: "text-muted-foreground")
}
Comment on lines +46 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve filled stars in displayOnly mode.

This branch now renders every read-only star with the muted class, so a stored rating like 4 no longer shows four filled stars. That breaks the component’s read-only behavior.

Suggested fix
-          className={
-            displayOnly
-              ? "select-none text-2xl text-muted-foreground"
-              : "select-none text-2xl transition-colors duration-200 " +
-                ((hovered ?? value) >= star
-                  ? "text-yellow-400"
-                  : "text-muted-foreground")
-          }
+          className={
+            "select-none text-2xl " +
+            (!displayOnly ? "transition-colors duration-200 " : "") +
+            ((displayOnly ? value : hovered ?? value) >= star
+              ? "text-yellow-400"
+              : "text-muted-foreground")
+          }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
className={
displayOnly
? "select-none text-2xl text-muted-foreground"
: "select-none text-2xl transition-colors duration-200 " +
((hovered ?? value) >= star
? "text-yellow-400"
: "text-muted-foreground")
}
className={
"select-none text-2xl " +
(!displayOnly ? "transition-colors duration-200 " : "") +
((displayOnly ? value : hovered ?? value) >= star
? "text-yellow-400"
: "text-muted-foreground")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/rating/rating-stars.tsx` around lines 46 - 53, The read-only
branch in rating-stars.tsx is forcing every star to the muted style, which hides
the stored rating in displayOnly mode. Update the className logic in the
RatingStars rendering so the displayOnly path still compares the current star
against value and applies the filled/yellow class for rated stars, while keeping
the non-interactive styling separate; use the existing displayOnly, hovered,
value, and star symbols to preserve the component’s read-only behavior.

aria-label={star + " star"}
>
</span>
Expand Down
Loading