-
Notifications
You must be signed in to change notification settings - Fork 24
feat(rating): add RatingStars, RatingModal; integrate maintainer rati… #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
09e49c6
0eb6416
fa54563
93af1fd
d216b3f
a0bfcdd
e6ab1d5
38d0d8f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import React, { useState } from 'react'; | ||
| import { RatingStars } from './rating-stars'; | ||
|
|
||
| interface RatingModalProps { | ||
| contributor: { | ||
| id: string; | ||
| name: string; | ||
| reputation: number; | ||
| }; | ||
| bounty: { | ||
| id: string; | ||
| title: string; | ||
| }; | ||
| onSubmit: (rating: number, feedback: string) => Promise<void>; | ||
| onClose: () => void; | ||
| } | ||
|
|
||
| export const RatingModal: React.FC<RatingModalProps> = ({ contributor, bounty, onSubmit, onClose }) => { | ||
| const [rating, setRating] = useState(0); | ||
| 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.'); | ||
| return; | ||
| } | ||
| setLoading(true); | ||
| setError(null); | ||
| try { | ||
| await onSubmit(rating, feedback); | ||
| setSuccess(true); | ||
| } catch (err) { | ||
| console.error(err) | ||
| setError('Failed to submit rating. Please try again.'); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| if (success) { | ||
| return ( | ||
| <div className="modal"> | ||
| <h2>Success!</h2> | ||
| <p>Rating submitted. Contributor reputation updated.</p> | ||
| <button onClick={onClose}>Close</button> | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </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> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| interface RatingStarsProps { | ||
| value: number; | ||
| onChange?: (value: number) => void; | ||
| disabled?: boolean; | ||
| displayOnly?: boolean; | ||
| } | ||
|
|
||
| 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); | ||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| tabIndex={displayOnly ? -1 : 0} | ||
| role={displayOnly ? 'img' : 'slider'} | ||
| aria-valuenow={value} | ||
| aria-valuemin={1} | ||
| aria-valuemax={5} | ||
| onKeyDown={handleKeyDown} | ||
| style={{ display: 'flex', gap: 4, outline: 'none', cursor: displayOnly ? 'default' : 'pointer' }} | ||
| > | ||
| {[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', | ||
| }} | ||
|
Comment on lines
+10
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "rating-stars.tsx" | head -20Repository: boundlessfi/bounties Length of output: 100 🏁 Script executed: cat -n ./components/rating/rating-stars.tsxRepository: boundlessfi/bounties Length of output: 1975 Ensure When Suggested fix export const RatingStars: React.FC<RatingStarsProps> = ({ value, onChange, disabled, displayOnly }) => {
const [hovered, setHovered] = useState<number | null>(null);
+ const isDisplayOnly = displayOnly || !onChange;
+ const isInteractive = !isDisplayOnly && !disabled;
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
- if (!onChange || disabled || displayOnly) return;
+ if (!isInteractive) return;
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'}
- aria-valuenow={value}
- aria-valuemin={1}
- aria-valuemax={5}
+ tabIndex={isInteractive ? 0 : -1}
+ role={isDisplayOnly ? 'img' : 'slider'}
+ aria-label={isDisplayOnly ? `${value} out of 5 stars` : 'Rating'}
+ aria-valuenow={isDisplayOnly ? undefined : value}
+ aria-valuemin={isDisplayOnly ? undefined : 0}
+ aria-valuemax={isDisplayOnly ? undefined : 5}
+ aria-disabled={isDisplayOnly ? undefined : disabled}
onKeyDown={handleKeyDown}
- style={{ display: 'flex', gap: 4, outline: 'none', cursor: displayOnly ? 'default' : 'pointer' }}
+ style={{ display: 'flex', gap: 4, outline: 'none', cursor: isInteractive ? 'pointer' : 'default' }}
>
{[1, 2, 3, 4, 5].map((star) => (
<span
key={star}
- onMouseEnter={() => !displayOnly && setHovered(star)}
- onMouseLeave={() => !displayOnly && setHovered(null)}
- onClick={() => onChange && !disabled && !displayOnly && onChange(star)}
+ onMouseEnter={() => isInteractive && setHovered(star)}
+ onMouseLeave={() => isInteractive && setHovered(null)}
+ onClick={() => isInteractive && onChange?.(star)}
style={{
color: (hovered ?? value) >= star ? '#FFD700' : '#CCC',
fontSize: 28,
transition: 'color 0.2s',
- pointerEvents: displayOnly ? 'none' : 'auto',
+ pointerEvents: isInteractive ? 'auto' : 'none',
userSelect: 'none',
}}
aria-label={star + ' star'}🤖 Prompt for AI Agents |
||
| aria-label={star + ' star'} | ||
| > | ||
| ★ | ||
| </span> | ||
| ))} | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // Ensure localStorage methods are configurable and writable so tests can spyOn/set mocks | ||
| const createLocalStorageMock = () => { | ||
| let store = {} | ||
| return { | ||
| getItem: (key) => (Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null), | ||
| setItem: (key, value) => { store[String(key)] = String(value) }, | ||
| removeItem: (key) => { delete store[String(key)] }, | ||
| clear: () => { store = {} }, | ||
| } | ||
| } | ||
|
|
||
| Object.defineProperty(window, 'localStorage', { | ||
| configurable: true, | ||
| writable: true, | ||
| value: createLocalStorageMock(), | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow reopening the rating flow after completion.
Once the modal is dismissed, there’s no way to rate later because the “Mark as Completed” button disappears and no “Rate Contributor” action is shown. This blocks a core flow if the maintainer closes the modal accidentally.
🛠️ Suggested fix (add a “Rate Contributor” action)
const renderActionButton = () => { if (bounty.status === 'claimed' && IS_MAINTAINER && !completed) { return ( <Button onClick={handleMarkCompleted} disabled={loading} className="w-full gap-2 bg-green-600 text-white hover:bg-green-700"> {loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Check className="mr-2 h-4 w-4" />} Mark as Completed </Button> ) } + if (bounty.status === 'claimed' && IS_MAINTAINER && completed && !hasRated) { + return ( + <Button onClick={() => setShowRating(true)} className="w-full gap-2 bg-primary text-primary-foreground hover:bg-primary/90"> + Rate Contributor + </Button> + ) + }🤖 Prompt for AI Agents