Moved edit bird profile to flock page rather than settings
This commit is contained in:
+449
-6
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user