Files
growstuff/app/javascript/components/PlantSomethingModal.jsx
T
Brenda WallaceandClaude Opus 5 7e181982ff Upgrade Font Awesome to 6.7.2 (#4846)
* Restore the Ruby 4.0.6 upgrade

PR #4831 committed a stale Gemfile.lock over the newer one, taking
.ruby-version back to 3.2.3 and rolling back active_median, dalli,
parallel, rbs and selenium-webdriver. Dockerfile and
.devcontainer/Dockerfile still build on Ruby 4, so the Gemfile's version
check fails against the image.

Restores both files to what they were before that merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Upgrade Font Awesome from 5.15.1 to 6.7.2

Font names have moved on twice since this was pinned: the codebase still
carried Font Awesome 4 names that render as an empty box, and FA6 icons
such as fa-ellipsis-vertical were unavailable.

The 6.x gem no longer ships _font-awesome-sprockets.scss, but its
@font-face rules still call fa-font-path(), so application.scss defines
that against Sprockets' font-path helper. Its fonts live under
assets/fonts/font-awesome, which is already in the precompile list.

Icon names:
- The bare "fa" prefix is not a style in 6. Every "fa fa-<name>" becomes
  "fas fa-<name>" (38 call sites, all solid; the one brands icon already
  used fab).
- fa-sun-o was a Font Awesome 4 name and had been rendering as a box;
  now fa-sun.
- fa-pencil was likewise broken under 5, where it was fa-pencil-alt. It
  exists again in 6, so the existing name now works.
- activity_icon asked for 'fa-truck-pickup' through a helper that already
  adds the prefix, so it emitted "fa-fa-truck-pickup".

Verified that the solid, regular and brands fonts all resolve through
the asset pipeline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* couple more places to upgrade font awesome

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:39:38 +09:30

108 lines
4.3 KiB
React

import React, {useEffect, useRef, useState} from 'react';
import {postJson} from '../api';
import CropPicker from './CropPicker';
import Modal from './Modal';
import Steps from './Steps';
const FIELD_NAMES = {crop: 'Crop', garden: 'Garden'};
const STEPS = ['Choose a crop', 'Confirm'];
function humanize(field) {
return FIELD_NAMES[field] || field.replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase());
}
function errorMessages(status, data) {
if (status === 422 && data && data.errors) {
const messages = Object.entries(data.errors).flatMap(([field, list]) => list.map((m) => `${humanize(field)} ${m}`));
if (messages.length > 0) return messages;
}
if (status === 403) return ['You can\'t plant in this garden.'];
if (status === 401) return ['Please sign in again to plant something.'];
return ['Something went wrong saving that. Please try again.'];
}
// "Add planting", as a dialog over the garden cards. Two steps: search
// for a crop and choose it, then confirm what you chose and it is planted in the
// card's garden (today, with no other details; those can be added later). On
// success it hands back the garden's updated card.
export default function PlantSomethingModal({garden, iconUrl, onClose, onCreated}) {
const [crop, setCrop] = useState(null); // chosen, waiting for confirmation
const [saving, setSaving] = useState(false);
const [errors, setErrors] = useState([]);
const confirmButton = useRef(null);
// Once a crop is chosen the search goes away; Enter then confirms.
useEffect(() => {
if (crop && confirmButton.current) confirmButton.current.focus();
}, [crop]);
async function confirm() {
setSaving(true);
setErrors([]);
try {
const {ok, status, data} = await postJson('/plantings.json', {planting: {garden_id: garden.id, crop_id: crop.id}});
if (ok) {
onCreated(data.garden, crop); // closes this dialog
return;
}
setErrors(errorMessages(status, data));
} catch (error) {
setErrors(['Couldn\'t reach the server. Please try again.']);
}
setSaving(false);
}
const title = (
<>
{iconUrl && <img src={iconUrl} alt="" className="modal-title-icon" />}
Plant something in {garden.name}
</>
);
return (
<Modal title={title} titleId={`plant-something-title-${garden.id}`} onClose={onClose}>
<div className="modal-body plant-dialog-body">
<Steps steps={STEPS} current={crop ? 2 : 1} />
{errors.length > 0 && (
<div className="alert alert-danger" role="alert">
<i className="fas fa-exclamation-triangle" aria-hidden="true" />{' '}
<strong>That didn&rsquo;t work.</strong>
<ul className="mb-0">{errors.map((message) => <li key={message}>{message}</li>)}</ul>
<span>Your choice is kept, so you can press Plant it to try again.</span>
</div>
)}
{crop ? (
<div className="crop-confirm">
<p className="crop-confirm-label">Ready to plant?</p>
<dl className="crop-confirm-details">
<dt>Crop</dt>
<dd className="crop-confirm-name">
{crop.name}
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={() => setCrop(null)} disabled={saving}>
<i className="fas fa-edit" aria-hidden="true" /> Change<span className="visually-hidden"> crop</span>
</button>
</dd>
<dt>Garden</dt>
<dd>{garden.name}</dd>
<dt>When</dt>
<dd>Today</dd>
</dl>
</div>
) : (
<CropPicker id="planting-crop" onChoose={setCrop} />
)}
<div className="visually-hidden" role="status">{saving ? `Planting ${crop.name} in ${garden.name}…` : ''}</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-link" onClick={onClose} disabled={saving}>Cancel</button>
{crop && (
<button ref={confirmButton} type="button" className="btn btn-success btn-plant" onClick={confirm} disabled={saving} aria-busy={saving}>
{saving ? <><i className="fas fa-spinner fa-spin" aria-hidden="true" /> Planting…</> : 'Plant it'}
</button>
)}
</div>
</Modal>
);
}