Files
growstuff/app/javascript/components/SeedStack.jsx
T
Brenda Wallace fdebe79b7d Refactor seed management features
- Introduced `@other_count` in SeedsController to track in-stock and finished seeds based on the filter applied.
- Enhanced SeedsHelper with new methods: `seed_owner_label`, `seed_stacks`, and `seed_choice_link` for better organization and readability.
- Updated views to utilize new helper methods, improving the display of seed information and filtering options.
- Adjusted the Seed model to include methods for determining if a seed's sow-by date has passed and to return the heirloom type.
- Improved tests for filters and helper methods to ensure accurate functionality and coverage.
2026-10-16 17:32:09 +13:00

107 lines
4.4 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, {useEffect, useRef, useState} from 'react';
// How long the packets take to fold back into the pile, in ms. Matches the
// seed-group-close animation in _seeds.scss.
const CLOSE_MS = 260;
const pill = (heirloom) => heirloom && (
<span className={`seed-pill seed-pill-${heirloom}`}>
{heirloom.charAt(0).toUpperCase() + heirloom.slice(1)}
</span>
);
// The owner's avatar, small, beside a pile on a page of other people's seeds.
const Owner = ({owner}) => owner && (
<a className="seed-owner" href={owner.path} title={owner.label} aria-label={owner.label}>
<img src={owner.avatar_url} alt="" width="32" height="32" />
</a>
);
// Several packets of one variety. Closed, they are a pile with a count on top;
// opened, the pile spreads out where it stood, one packet each, to pick from.
// Rails renders the top packet first (see seeds/_stack), so there is something
// to see before this loads and without JavaScript.
export default function SeedStack({crop, count, image_url: imageUrl, heirloom, rule, packets, owner}) {
const [state, setState] = useState('closed');
const toggle = useRef(null);
const closeTimer = useRef(null);
useEffect(() => () => clearTimeout(closeTimer.current), []);
const open = () => setState('open');
const close = () => {
setState('closing');
closeTimer.current = setTimeout(() => {
setState('closed');
toggle.current?.focus();
}, CLOSE_MS);
};
if (state === 'closed') {
return (
<div className="seed-stack seed-stack-island">
<div className="seed-stack-pile">
<span className="seed-stack-count" aria-hidden="true">×{count}</span>
<button
type="button"
ref={toggle}
className="seed-packet seed-stack-toggle"
aria-expanded="false"
aria-label={`${crop.name}, ${count} packets. Open to see each packet`}
onClick={open}
>
<span className="seed-packet-tear" aria-hidden="true" />
<img className="seed-packet-window" src={imageUrl} alt="" />
<span className="seed-packet-name">{crop.name}</span>
<span className="seed-packet-line seed-stack-hint">{count} packets <i aria-hidden="true">▸</i> open</span>
{heirloom && <span className="seed-packet-line">{pill(heirloom)}</span>}
<span className="seed-packet-rule">{rule}</span>
</button>
</div>
<Owner owner={owner} />
</div>
);
}
return (
<div
className={`seed-group${state === 'closing' ? ' is-closing' : ''}`}
role="group"
aria-label={`${crop.name}, ${count} packets`}
onKeyDown={(event) => { if (event.key === 'Escape' && state === 'open') close(); }}
>
<div className="seed-group-head">
<b>
{crop.name} · {count} packets
{owner && <> <span className="seed-group-owner">by <a href={owner.path}>{owner.name}</a></span></>}
</b>
<button type="button" className="seed-group-close" aria-expanded="true" onClick={close} disabled={state === 'closing'}>
Close ▴
</button>
</div>
<div className="seed-group-row">
{packets.map((packet, index) => (
<article key={packet.id} className="seed-packet seed-group-packet" style={{'--i': index, '--n': packets.length}}>
<div className="seed-packet-tear" aria-hidden="true" />
<a href={packet.path}><img className="seed-packet-window" src={packet.image_url} alt="" /></a>
<h4 className="seed-packet-name"><a href={packet.path}>{packet.name}</a></h4>
<p>{packet.when}</p>
{packet.heirloom && <p>{pill(packet.heirloom)}</p>}
<p className="seed-packet-detail">{packet.detail}</p>
{packet.past && <span className="seed-packet-stamp">Past sow-by</span>}
<div className="seed-packet-rule">{packet.rule}</div>
<div className="seed-packet-actions">
<a className="btn btn-primary btn-sm" href={packet.path}>View</a>
{packet.edit_path && <a className="btn btn-outline-primary btn-sm" href={packet.edit_path}>Edit</a>}
</div>
</article>
))}
</div>
<div className="seed-group-foot">
<a href={crop.path}>Open the {crop.name} page →</a>
<span className="text-muted"> history, finished packets, plantings these seeds grew</span>
</div>
</div>
);
}