mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-09-25 07:24:55 -04:00
Rate a planting with stars in the edit dialog
Overall rating is five clickable stars (real radio buttons underneath, so the keyboard works) with a Clear button, in place of a select. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6685e253c5
commit
fa5ef3eda6
3 files changed
+96
-7
No files matched your search
@@ -220,3 +220,46 @@ $garden-card-line: #e6e6e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Star rating in the planting edit dialog. The radios are visually hidden, so
|
||||
// show a ring on the star that has keyboard focus.
|
||||
.star-rating {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
|
||||
legend {
|
||||
float: none;
|
||||
width: auto;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.star-rating-stars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 2.25rem;
|
||||
}
|
||||
|
||||
.star-rating-star {
|
||||
padding: 0 0.1rem;
|
||||
color: #9e9e9e;
|
||||
font-size: 1.6rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.star-rating-star--lit {
|
||||
color: #f5a623;
|
||||
}
|
||||
|
||||
input:focus-visible + .star-rating-star {
|
||||
outline: 2px solid var(--bs-link-color, #0a58ca);
|
||||
outline-offset: 2px;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.star-rating-value {
|
||||
margin-left: 0.75rem;
|
||||
color: $garden-card-muted;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@ import React, {useEffect, useState} from 'react';
|
||||
import {getJson, patchJson} from '../api';
|
||||
import CropPicker from './CropPicker';
|
||||
import Modal from './Modal';
|
||||
|
||||
const RATINGS = [[1, 'Poor'], [2, ''], [3, ''], [4, ''], [5, 'Great']];
|
||||
import StarRating from './StarRating';
|
||||
|
||||
function humanize(field) {
|
||||
return field.replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase());
|
||||
@@ -148,11 +147,12 @@ export default function EditPlantingModal({planting, onClose, onSaved}) {
|
||||
<input id="edit-planting-quantity" type="number" min="1" className="form-control" value={values.quantity ?? ''} onChange={set('quantity')} />
|
||||
</div>
|
||||
<div className="col-md-8">
|
||||
<label className="form-label" htmlFor="edit-planting-rating">Overall rating</label>
|
||||
<select id="edit-planting-rating" className="form-select" value={values.overall_rating ?? ''} onChange={set('overall_rating')}>
|
||||
<option value="">Not rated</option>
|
||||
{RATINGS.map(([number, word]) => <option key={number} value={number}>{word ? `${number} – ${word}` : number}</option>)}
|
||||
</select>
|
||||
<StarRating
|
||||
id="edit-planting-rating"
|
||||
label="Overall rating"
|
||||
value={values.overall_rating}
|
||||
onChange={(rating) => setValues({...values, overall_rating: rating})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
const STARS = [1, 2, 3, 4, 5];
|
||||
const WORDS = {1: 'Poor', 5: 'Great'};
|
||||
|
||||
// A rating out of five as stars. They are real radio buttons underneath, so
|
||||
// the arrow keys and screen readers work as they do for any radio group; the
|
||||
// stars just light up as far as the one hovered, or chosen. `value` is a number
|
||||
// from 1 to 5, or null for no rating, and Clear puts it back to that.
|
||||
export default function StarRating({id, label, value, onChange}) {
|
||||
const [hovered, setHovered] = useState(null);
|
||||
const lit = hovered ?? value ?? 0;
|
||||
|
||||
return (
|
||||
<fieldset className="star-rating">
|
||||
<legend className="form-label">{label}</legend>
|
||||
<div className="star-rating-stars" onMouseLeave={() => setHovered(null)}>
|
||||
{STARS.map((number) => (
|
||||
<React.Fragment key={number}>
|
||||
<input
|
||||
type="radio"
|
||||
className="visually-hidden"
|
||||
id={`${id}-${number}`}
|
||||
name={id}
|
||||
checked={value === number}
|
||||
onChange={() => onChange(number)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${id}-${number}`}
|
||||
className={`star-rating-star${number <= lit ? ' star-rating-star--lit' : ''}`}
|
||||
title={WORDS[number] ? `${number} – ${WORDS[number]}` : `${number} of 5`}
|
||||
onMouseEnter={() => setHovered(number)}
|
||||
>
|
||||
<i className={`${number <= lit ? 'fa' : 'far'} fa-star`} aria-hidden="true" />
|
||||
<span className="visually-hidden">{number} of 5{WORDS[number] ? `, ${WORDS[number]}` : ''}</span>
|
||||
</label>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<span className="star-rating-value">{value ? `${value} of 5` : 'Not rated'}</span>
|
||||
{value && (
|
||||
<button type="button" className="btn btn-sm btn-link" onClick={() => onChange(null)}>Clear</button>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user