Moved edit bird profile to flock page rather than settings

This commit is contained in:
blaisadmin
2026-05-21 20:46:34 -04:00
parent df3fcbf885
commit 49d75f34be
2 changed files with 453 additions and 6 deletions
+449 -6
View File
@@ -1414,6 +1414,7 @@ function App() {
const [memorializedBirds, setMemorializedBirds] = useState<Bird[]>([]);
const [selectedBirdId, setSelectedBirdId] = useState<string>('');
const [editingBirdId, setEditingBirdId] = useState<string>('');
const [birdEditorOpen, setBirdEditorOpen] = useState(false);
const [weights, setWeights] = useState<WeightRecord[]>([]);
const [vetVisits, setVetVisits] = useState<VetVisit[]>([]);
const [medications, setMedications] = useState<Medication[]>([]);
@@ -1526,7 +1527,7 @@ function App() {
[allBirdWeights, birds, overviewWindowStartDate],
);
const showFlockDetailColumn = bulkWeightOpen || Boolean(selectedBird);
const showFlockDetailColumn = bulkWeightOpen || birdEditorOpen || Boolean(selectedBird);
useEffect(() => {
if (!publicProfile || !authSession || workspace?.id !== publicProfile.workspaceId || !birds.some((bird) => bird.id === publicProfile.id)) {
@@ -2344,12 +2345,13 @@ function App() {
const startCreateBird = () => {
setEditingBirdId('');
setBirdEditorOpen(true);
setBirdForm(emptyBirdForm);
setBirdPhotoName('');
setPhotoCrop(null);
setPhotoDrag(null);
setError('');
setActivePage('settings');
setActivePage('flock');
};
const handleAuthSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
@@ -2671,12 +2673,13 @@ function App() {
const startEditBird = (bird: Bird) => {
setSelectedBirdId(bird.id);
setEditingBirdId(bird.id);
setBirdEditorOpen(true);
setBirdForm(toBirdForm(bird));
setBirdPhotoName('');
setPhotoCrop(null);
setPhotoDrag(null);
setError('');
setActivePage('settings');
setActivePage('flock');
};
const handleBirdPhotoChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -2969,9 +2972,10 @@ function App() {
});
setSelectedBirdId(isEditing ? savedBird.id : '');
setEditingBirdId(savedBird.id);
setBirdEditorOpen(false);
setBirdForm(toBirdForm(savedBird));
setBirdPhotoName('');
setActivePage(isEditing ? 'settings' : 'flock');
setActivePage('flock');
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : 'Unable to save flock member.');
} finally {
@@ -4771,7 +4775,446 @@ function App() {
</section>
) : null}
{selectedBird ? (
{birdEditorOpen ? (
<section className="panel flock-member-panel flock-profile-editor">
<div className="panel-header">
<div>
<p className="eyebrow">Bird profile</p>
<h2>{editingBird ? `Edit ${editingBird.name}` : 'Add flock member'}</h2>
</div>
<button
className="secondary-button"
onClick={() => {
setBirdEditorOpen(false);
setEditingBirdId('');
setBirdPhotoName('');
setPhotoCrop(null);
setPhotoDrag(null);
}}
type="button"
>
Close
</button>
</div>
<form className="form-panel settings-nested-stack" onSubmit={handleBirdSubmit}>
<section className="settings-nested-card">
<div className="settings-nested-header">
<p className="eyebrow">Identity</p>
<h3>Profile details</h3>
</div>
<div className="settings-nested-grid">
<label>
Bird name
<input value={birdForm.name} onChange={(event) => setBirdForm({ ...birdForm, name: event.target.value })} required />
</label>
<label>
Band ID
<input
value={birdForm.tagId}
onChange={(event) => setBirdForm({ ...birdForm, tagId: event.target.value })}
placeholder="Optional if unknown"
/>
</label>
<label className="species-picker-field wide-field">
Species
<div className="species-picker">
<input
value={birdForm.species}
onChange={(event) => {
setBirdForm({ ...birdForm, species: event.target.value });
setSpeciesPickerOpen(true);
}}
onFocus={() => setSpeciesPickerOpen(true)}
onBlur={() => {
window.setTimeout(() => setSpeciesPickerOpen(false), 120);
}}
placeholder="Start typing a species"
autoComplete="off"
required
/>
{speciesPickerOpen ? (
<div className="species-picker-menu">
{filteredSpeciesOptions.length ? (
filteredSpeciesOptions.map((speciesOption) => (
<button
key={speciesOption}
className={`species-picker-option ${birdForm.species === speciesOption ? 'active' : ''}`}
onMouseDown={(event) => {
event.preventDefault();
setBirdForm({ ...birdForm, species: speciesOption });
setSpeciesPickerOpen(false);
}}
type="button"
>
{speciesOption}
</button>
))
) : (
<div className="species-picker-empty">No matching species yet. Keep typing to add a custom entry.</div>
)}
</div>
) : null}
</div>
</label>
<div className="segmented-field wide-field">
<span>Gender</span>
<div className="segmented-control" role="radiogroup" aria-label="Bird gender">
{(['unknown', 'male', 'female'] as BirdGender[]).map((gender) => (
<button
key={gender}
className={`segmented-option ${birdForm.gender === gender ? 'active' : ''}`}
onClick={() => setBirdForm({ ...birdForm, gender })}
type="button"
role="radio"
aria-checked={birdForm.gender === gender}
>
<span className={`gender-symbol ${gender}`} aria-hidden="true">
{getBirdGenderSymbol({ gender })}
</span>
{getBirdGenderLabel({ gender })}
</button>
))}
</div>
</div>
<div className="settings-inline-header wide-field">
<p className="eyebrow">Dates</p>
<h3>Milestones and reminders</h3>
</div>
<label>
Hatch Day
<input
type="date"
value={birdForm.dateOfBirth}
onChange={(event) => setBirdForm({ ...birdForm, dateOfBirth: event.target.value })}
/>
</label>
<label>
Gotcha day
<input
type="date"
value={birdForm.gotchaDay}
onChange={(event) => setBirdForm({ ...birdForm, gotchaDay: event.target.value })}
/>
</label>
<label className="toggle-card">
<span>Notify on Hatch Day</span>
<input
type="checkbox"
checked={birdForm.notifyOnDob}
onChange={(event) => setBirdForm({ ...birdForm, notifyOnDob: event.target.checked })}
/>
</label>
<label className="toggle-card">
<span>Notify on gotcha day</span>
<input
type="checkbox"
checked={birdForm.notifyOnGotchaDay}
onChange={(event) => setBirdForm({ ...birdForm, notifyOnGotchaDay: event.target.checked })}
/>
</label>
<label>
Favorite snack
<input
value={birdForm.favoriteSnack}
onChange={(event) => setBirdForm({ ...birdForm, favoriteSnack: event.target.value })}
placeholder="Optional"
/>
</label>
<label className="wide-field">
Motivators
<div className="profile-list-fields">
{getBirdProfileListFields(birdForm.motivators).map((motivator, index) => (
<input
key={`flock-motivator-${index}`}
value={motivator}
onChange={(event) =>
setBirdForm({
...birdForm,
motivators: updateBirdProfileListField(birdForm.motivators, index, event.target.value),
})
}
placeholder={index === 0 ? 'Training reward, sound, person, toy, or routine' : `Motivator ${index + 1}`}
/>
))}
</div>
</label>
<label className="wide-field">
Demotivators
<div className="profile-list-fields">
{getBirdProfileListFields(birdForm.demotivators).map((demotivator, index) => (
<input
key={`flock-demotivator-${index}`}
value={demotivator}
onChange={(event) =>
setBirdForm({
...birdForm,
demotivators: updateBirdProfileListField(birdForm.demotivators, index, event.target.value),
})
}
placeholder={index === 0 ? 'Stressor, disliked handling, noise, or situation' : `Demotivator ${index + 1}`}
/>
))}
</div>
</label>
</div>
</section>
<section className="settings-nested-card">
<div className="settings-nested-header">
<p className="eyebrow">Display</p>
<h3>Chart color and photo</h3>
</div>
<div className="settings-nested-grid">
<label>
Graph color
<input
type="color"
value={birdForm.chartColor}
onChange={(event) => setBirdForm({ ...birdForm, chartColor: event.target.value })}
/>
</label>
<label className="toggle-field wide-field">
<input
type="checkbox"
checked={birdForm.publicProfileEnabled}
onChange={(event) => setBirdForm({ ...birdForm, publicProfileEnabled: event.target.checked })}
/>
<span>Enable QR code</span>
</label>
<div className="photo-editor wide-field">
<div className="photo-preview-shell">
{photoCrop ? (
<div
className={`crop-preview-frame ${photoDrag ? 'dragging' : ''}`}
onPointerDown={handlePhotoCropPointerDown}
onPointerMove={handlePhotoCropPointerMove}
onPointerUp={handlePhotoCropPointerUp}
onPointerCancel={handlePhotoCropPointerUp}
>
<img
className="crop-preview-image"
src={photoCrop.sourceDataUrl}
alt="Crop preview"
style={{
width: `${getPhotoCropMetrics(photoCrop).displayWidth}px`,
height: `${getPhotoCropMetrics(photoCrop).displayHeight}px`,
left: `${getPhotoCropMetrics(photoCrop).left}px`,
top: `${getPhotoCropMetrics(photoCrop).top}px`,
}}
/>
<div className="crop-preview-overlay" aria-hidden="true" />
</div>
) : (
<img className="profile-photo" src={birdForm.photoDataUrl || defaultBirdPhoto} alt="Bird preview" />
)}
</div>
<div className="photo-copy">
<label className="file-picker">
Photo
<input accept="image/png,image/jpeg,image/jpg,image/webp,image/gif" onChange={handleBirdPhotoChange} type="file" />
</label>
{photoCrop ? (
<div className="crop-control-stack">
<p className="muted">{photoCrop.fileName} ready to crop.</p>
<label>
Zoom
<input
type="range"
min="1"
max="3"
step="0.01"
value={photoCrop.zoom}
onChange={(event) =>
setPhotoCrop((current) => (current ? { ...current, zoom: Number(event.target.value) } : current))
}
/>
</label>
<div className="button-row">
<button className="primary-button" onClick={handleApplyPhotoCrop} type="button" disabled={applyingPhotoCrop}>
{applyingPhotoCrop ? 'Applying crop...' : 'Apply crop'}
</button>
<button className="secondary-button" onClick={handleCancelPhotoCrop} type="button" disabled={applyingPhotoCrop}>
Cancel
</button>
</div>
</div>
) : null}
{birdForm.photoDataUrl ? (
<button className="secondary-button" onClick={handleRemovePhoto} type="button">
Remove photo
</button>
) : null}
</div>
</div>
</div>
</section>
<div className="settings-save-row">
<button className="primary-button" type="submit" disabled={savingBird}>
{savingBird ? 'Saving...' : editingBird ? 'Save profile changes' : 'Save bird profile'}
</button>
</div>
</form>
{editingBird && selectedBird ? (
<section className="settings-nested-card">
<div className="settings-nested-header">
<p className="eyebrow">Medication</p>
<h3>Medication configuration</h3>
</div>
<form className="form-panel inline-form care-entry-form" onSubmit={handleMedicationSubmit}>
<label>
Medication
<input
value={medicationForm.name}
onChange={(event) => setMedicationForm({ ...medicationForm, name: event.target.value })}
required
/>
</label>
<label>
Dosage
<input
value={medicationForm.dosage}
onChange={(event) => setMedicationForm({ ...medicationForm, dosage: event.target.value })}
required
/>
</label>
<label>
Frequency
<select
value={medicationForm.frequency}
onChange={(event) => {
const frequency = event.target.value as MedicationFrequency;
setMedicationForm({
...medicationForm,
frequency,
doseSchedule: getDefaultMedicationDoseSchedule(frequency),
});
}}
required
>
{medicationFrequencyOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label>
Route
<input value={medicationForm.route} onChange={(event) => setMedicationForm({ ...medicationForm, route: event.target.value })} />
</label>
<label>
Start date
<input
type="date"
value={medicationForm.startDate}
onChange={(event) => setMedicationForm({ ...medicationForm, startDate: event.target.value })}
required
/>
</label>
<label>
End date
<input
type="date"
value={medicationForm.endDate}
onChange={(event) => setMedicationForm({ ...medicationForm, endDate: event.target.value })}
/>
</label>
<label className="wide-field">
Dose labels and times
<div className="dose-schedule-editor">
{medicationForm.doseSchedule.map((slot, index) => (
<div className="dose-schedule-row" key={slot.key}>
<input
value={slot.label}
onChange={(event) =>
setMedicationForm({
...medicationForm,
doseSchedule: medicationForm.doseSchedule.map((currentSlot, currentIndex) =>
currentIndex === index ? { ...currentSlot, label: event.target.value } : currentSlot,
),
})
}
required
/>
<input
type="time"
value={slot.time}
onChange={(event) =>
setMedicationForm({
...medicationForm,
doseSchedule: medicationForm.doseSchedule.map((currentSlot, currentIndex) =>
currentIndex === index ? { ...currentSlot, time: event.target.value } : currentSlot,
),
})
}
/>
</div>
))}
</div>
</label>
<label className="wide-field">
Notes
<textarea
rows={3}
value={medicationForm.notes}
onChange={(event) => setMedicationForm({ ...medicationForm, notes: event.target.value })}
/>
</label>
<div className="button-row care-form-actions">
<button className="primary-button" type="submit">
{editingMedicationId ? 'Save medication changes' : 'Save medication'}
</button>
{editingMedicationId ? (
<button className="secondary-button" onClick={handleCancelMedicationEdit} type="button">
Cancel edit
</button>
) : null}
</div>
</form>
<div className="recent-list">{renderMedicationList({ showActions: true })}</div>
</section>
) : null}
{editingBird && selectedBird ? (
<section className="settings-nested-card settings-danger-card">
<div className="settings-nested-header">
<p className="eyebrow">Danger zone</p>
<h3>Destructive profile actions</h3>
</div>
<form className="form-panel inline-form care-entry-form" onSubmit={handleMemorializeBird}>
<label>
Memorial date
<input
type="date"
value={memorializeBirdForm.memorializedOn}
onChange={(event) => setMemorializeBirdForm({ ...memorializeBirdForm, memorializedOn: event.target.value })}
required
/>
</label>
<label className="wide-field">
Memorial note
<textarea
rows={3}
value={memorializeBirdForm.memorialNote}
onChange={(event) => setMemorializeBirdForm({ ...memorializeBirdForm, memorialNote: event.target.value })}
/>
</label>
<div className="button-row care-form-actions">
<button className="danger-button" type="submit" disabled={memorializingBird || activeMembership?.role !== 'owner'}>
{memorializingBird ? 'Memorializing...' : `Memorialize ${selectedBird.name}`}
</button>
</div>
</form>
<p className="muted">Removing permanently deletes weight records, vet visits, and medication history for this bird.</p>
<div className="button-row">
<button className="danger-button" onClick={handleRemoveBird} type="button" disabled={deletingBird}>
{deletingBird ? 'Removing...' : 'Remove from flock'}
</button>
</div>
</section>
) : null}
</section>
) : null}
{selectedBird && !birdEditorOpen ? (
<section className="panel flock-member-panel">
<div className="panel-header">
<div>
@@ -5813,7 +6256,7 @@ function App() {
) : null}
</article>
<article className="panel form-panel settings-card-bird-profiles">
<article className="panel form-panel settings-card-bird-profiles" hidden>
<div className="panel-header">
<div>
<p className="eyebrow">Bird profiles</p>