mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-09-28 00:44:58 -04:00
Make the crop chip the planting's menu, for annuals and perennials
Clicking a chip opens the planting's menu under it (view, edit, add photo, record harvest, save seeds, mark as finished), in place of the three dots on annual rows, so perennials get the same menu. A caret shows the chip opens something. Someone who can't change the planting still gets a plain link chip. The menu items are lowercase for every planting menu. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6987d66f95
commit
eb18622b78
7 files changed
+102
-48
No files matched your search
@@ -80,7 +80,7 @@ $garden-card-line: #e6e6e6;
|
||||
// One annual planting: crop | how it's getting on | actions, in aligned columns.
|
||||
.planting-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(11rem, 15rem) minmax(0, 1fr) 2.5rem;
|
||||
grid-template-columns: minmax(11rem, 15rem) minmax(0, 1fr);
|
||||
align-items: center;
|
||||
padding: 0.6rem 0.5rem;
|
||||
border-top: 1px solid $garden-card-line;
|
||||
@@ -192,21 +192,17 @@ $garden-card-line: #e6e6e6;
|
||||
color: $garden-card-muted;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.planting-row-actions {
|
||||
text-align: right;
|
||||
}
|
||||
// The planting menu's items are lowercase. Done in CSS because the labels are
|
||||
// the shared button translations, which other pages show in sentence case.
|
||||
.planting-menu .dropdown-item {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
// The "more" button: three dots in a small round button. The theme gives every
|
||||
// .btn a 5.5rem minimum width, which made the hover background a wide box, and
|
||||
// adds a dropdown caret we don't want.
|
||||
// The planting menu's items are lowercase. Done in CSS because the labels are
|
||||
// the shared button translations, which other pages show in sentence case.
|
||||
.planting-row .dropdown-item {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.actions-toggle-dots {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -263,7 +259,7 @@ $garden-card-line: #e6e6e6;
|
||||
}
|
||||
|
||||
.planting-row {
|
||||
grid-template-columns: minmax(0, 1fr) 2.5rem;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
|
||||
.planting-row-status {
|
||||
grid-column: 1 / -1;
|
||||
@@ -315,3 +311,22 @@ $garden-card-line: #e6e6e6;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
// A crop chip that is also the planting's menu: a caret says it opens something,
|
||||
// and it takes a slightly deeper tint while pointed at or open.
|
||||
.chip-menu-toggle {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&::after {
|
||||
margin-left: 0.5rem;
|
||||
vertical-align: 0.15em;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible,
|
||||
&.show {
|
||||
background-color: var(--bs-success-border-subtle);
|
||||
color: var(--bs-success-text-emphasis);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ function handled(event, buttonId) {
|
||||
if (dropdown) dropdown.hide();
|
||||
}
|
||||
|
||||
export default function ActionsMenu({id, actions, label = 'Actions', ariaLabel, className = 'btn dropdown-toggle', onSelect}) {
|
||||
export default function ActionsMenu({id, actions, label = 'Actions', ariaLabel, className = 'btn dropdown-toggle', menuClassName = 'dropdown-menu-end', onSelect}) {
|
||||
if (!actions || actions.length === 0) return null;
|
||||
|
||||
const buttonId = `actions-${id}`;
|
||||
@@ -35,7 +35,7 @@ export default function ActionsMenu({id, actions, label = 'Actions', ariaLabel,
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
<div className="dropdown-menu dropdown-menu-end" aria-labelledby={buttonId}>
|
||||
<div className={`dropdown-menu ${menuClassName}`} aria-labelledby={buttonId}>
|
||||
{actions.map((action) => (
|
||||
<React.Fragment key={action.key}>
|
||||
{action.divider && <div className="dropdown-divider" />}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import {isPlainClick} from '../events';
|
||||
import ActionsMenu from './ActionsMenu';
|
||||
import CropChip from './CropChip';
|
||||
import PlantingChip from './PlantingChip';
|
||||
import PlantingRow from './PlantingRow';
|
||||
|
||||
// One garden: its name and actions menu (top right), a picture, then what is
|
||||
@@ -11,6 +11,14 @@ import PlantingRow from './PlantingRow';
|
||||
export default function GardenCard({garden, defaultIconUrl, onPlant, highlightedId, highlightKind, onPlantingUpdated, onEditPlanting, onHarvestPlanting, onSaveSeedsPlanting, onAddPhoto, onFinishPlanting}) {
|
||||
const {id, name, url, image_url: imageUrl, owner, actions, plant_url: plantUrl, perennials, annuals} = garden;
|
||||
const empty = perennials.length === 0 && annuals.length === 0;
|
||||
// What each planting's menu items open, for annuals and perennials alike.
|
||||
const handlers = {
|
||||
edit: onEditPlanting,
|
||||
harvest: onHarvestPlanting,
|
||||
seeds: onSaveSeedsPlanting,
|
||||
finish: onFinishPlanting,
|
||||
photo: onAddPhoto && ((planting, action) => onAddPhoto({label: planting.crop.name, href: action.href})),
|
||||
};
|
||||
function plant(event) {
|
||||
if (onPlant && isPlainClick(event)) {
|
||||
event.preventDefault();
|
||||
@@ -59,12 +67,12 @@ export default function GardenCard({garden, defaultIconUrl, onPlant, highlighted
|
||||
{perennials.length > 0 ? (
|
||||
<div className="garden-card-chips">
|
||||
{perennials.map((planting) => (
|
||||
<CropChip
|
||||
<PlantingChip
|
||||
key={planting.id}
|
||||
url={planting.url}
|
||||
crop={planting.crop}
|
||||
planting={planting}
|
||||
defaultIconUrl={defaultIconUrl}
|
||||
highlighted={planting.id === highlightedId}
|
||||
handlers={handlers}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -84,11 +92,7 @@ export default function GardenCard({garden, defaultIconUrl, onPlant, highlighted
|
||||
highlighted={planting.id === highlightedId}
|
||||
highlightKind={highlightKind}
|
||||
onUpdated={onPlantingUpdated}
|
||||
onEdit={onEditPlanting}
|
||||
onHarvest={onHarvestPlanting}
|
||||
onSaveSeeds={onSaveSeedsPlanting}
|
||||
onFinish={onFinishPlanting}
|
||||
onAddPhoto={onAddPhoto && ((planting, action) => onAddPhoto({label: planting.crop.name, href: action.href}))}
|
||||
handlers={handlers}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import {selectPlantingAction} from '../plantingActions';
|
||||
import ActionsMenu from './ActionsMenu';
|
||||
import CropChip from './CropChip';
|
||||
|
||||
// A planting's crop chip, which is also its menu: click it for view, edit,
|
||||
// harvest and the rest, the same for annuals and perennials. Someone who can't
|
||||
// change the planting has no menu, so for them it stays a plain link to it.
|
||||
export default function PlantingChip({planting, defaultIconUrl, highlighted, handlers}) {
|
||||
const {id, url, crop, actions} = planting;
|
||||
|
||||
if (!actions || actions.length === 0) {
|
||||
return <CropChip url={url} crop={crop} defaultIconUrl={defaultIconUrl} highlighted={highlighted} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionsMenu
|
||||
id={`planting-${id}`}
|
||||
actions={actions}
|
||||
label={(
|
||||
<>
|
||||
<img className="crop-icon" src={crop.icon_url || defaultIconUrl} alt="" />
|
||||
{crop.name}
|
||||
</>
|
||||
)}
|
||||
ariaLabel={`Actions for ${crop.name}`}
|
||||
className={`chip crop-chip chip-menu-toggle dropdown-toggle${highlighted ? ' planting-just-added' : ''}`}
|
||||
menuClassName="dropdown-menu-start planting-menu"
|
||||
onSelect={selectPlantingAction(planting, handlers)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import {formatDate} from '../dates';
|
||||
import {isPlainClick} from '../events';
|
||||
import ActionsMenu from './ActionsMenu';
|
||||
import CropChip from './CropChip';
|
||||
import PlantedDate from './PlantedDate';
|
||||
import PlantingChip from './PlantingChip';
|
||||
|
||||
// One annual planting as a row of three aligned columns: the crop (and when it
|
||||
// was planted), how it is getting on (badges, then a progress bar or a note
|
||||
// when there is nothing to predict from), and its own actions menu.
|
||||
export default function PlantingRow({planting, defaultIconUrl, highlighted, highlightKind = 'added', onUpdated, onEdit, onHarvest, onSaveSeeds, onAddPhoto, onFinish}) {
|
||||
const {id, url, crop, badges} = planting;
|
||||
// One annual planting as a row of two aligned columns: the crop, which is also
|
||||
// the planting's menu (and when it was planted), and how it is getting on
|
||||
// (badges, then a progress bar or a note when there is nothing to predict from).
|
||||
// `handlers` is what the menu's items open (see selectPlantingAction).
|
||||
export default function PlantingRow({planting, defaultIconUrl, highlighted, highlightKind = 'added', onUpdated, handlers}) {
|
||||
const {badges} = planting;
|
||||
|
||||
return (
|
||||
<div className={`planting-row${highlighted ? ` planting-just-${highlightKind}` : ''}`}>
|
||||
<div className="planting-row-crop">
|
||||
<div className="planting-row-chip">
|
||||
<CropChip url={url} crop={crop} defaultIconUrl={defaultIconUrl} highlighted={highlighted && highlightKind === 'added'} />
|
||||
<PlantingChip planting={planting} defaultIconUrl={defaultIconUrl} highlighted={highlighted && highlightKind === 'added'} handlers={handlers} />
|
||||
{highlighted && highlightKind === 'harvested' && (
|
||||
<span className="harvest-recorded-tag"><i className="fa fa-check" aria-hidden="true" /> Harvest recorded</span>
|
||||
)}
|
||||
@@ -39,22 +38,6 @@ export default function PlantingRow({planting, defaultIconUrl, highlighted, high
|
||||
<Progress percentage={planting.percentage_grown} state={planting.progress_state} finishLabel={planting.finish_predicted_label} harvests={planting.harvests} />
|
||||
)}
|
||||
</div>
|
||||
<div className="planting-row-actions">
|
||||
<ActionsMenu
|
||||
id={`planting-${id}`}
|
||||
actions={planting.actions}
|
||||
label={<i className="fa fa-ellipsis-v" aria-hidden="true" />}
|
||||
ariaLabel={`Actions for ${crop.name}`}
|
||||
className="btn btn-sm btn-link actions-toggle-dots"
|
||||
onSelect={(action, event) => {
|
||||
const open = {edit: onEdit, harvest: onHarvest, seeds: onSaveSeeds, photo: onAddPhoto, finish: onFinish}[action.key];
|
||||
if (open && isPlainClick(event)) {
|
||||
event.preventDefault();
|
||||
open(planting, action);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import {isPlainClick} from './events';
|
||||
|
||||
// What a click on one of a planting's menu items does: if there is a dialog for
|
||||
// that item (handlers is keyed by the action's key: edit, harvest, seeds, photo,
|
||||
// finish) it opens it, over the list; anything else, and ctrl/cmd-click, follows
|
||||
// the link as usual.
|
||||
export function selectPlantingAction(planting, handlers) {
|
||||
return (action, event) => {
|
||||
const open = handlers[action.key];
|
||||
if (open && isPlainClick(event)) {
|
||||
event.preventDefault();
|
||||
open(planting, action);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -119,7 +119,11 @@ class GardenCardSerializer
|
||||
end
|
||||
|
||||
def perennial(planting)
|
||||
{ id: planting.id, url: routes.planting_path(planting), crop: crop_chip(planting) }
|
||||
{
|
||||
id: planting.id, url: routes.planting_path(planting), crop: crop_chip(planting),
|
||||
planted_at: planting.planted_at, today: Time.zone.today, can_edit: can?(:edit, planting),
|
||||
actions: planting_actions(planting)
|
||||
}
|
||||
end
|
||||
|
||||
def annual(planting)
|
||||
|
||||
Reference in new issue
Block a user