automated dev db build
This commit is contained in:
+12
-2
@@ -234,13 +234,23 @@ const lostBirdReportSchema = z.object({
|
||||
});
|
||||
|
||||
const publicProfileCodeSchema = z.string().trim().regex(/^[A-Za-z0-9_-]{8,32}$/);
|
||||
const birdProfileListSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.max(1000)
|
||||
.refine(
|
||||
(value) => value.split(/\r?\n/).map((item) => item.trim()).filter(Boolean).length <= 3,
|
||||
'Use no more than three list items.',
|
||||
)
|
||||
.optional()
|
||||
.or(z.literal(''));
|
||||
|
||||
const birdSchema = z.object({
|
||||
name: z.string().trim().min(1).max(120),
|
||||
tagId: z.string().trim().max(80).optional().or(z.literal('')),
|
||||
species: z.string().trim().min(1).max(120),
|
||||
motivators: z.string().trim().max(1000).optional().or(z.literal('')),
|
||||
demotivators: z.string().trim().max(1000).optional().or(z.literal('')),
|
||||
motivators: birdProfileListSchema,
|
||||
demotivators: birdProfileListSchema,
|
||||
favoriteSnack: z.string().trim().max(160).optional().or(z.literal('')),
|
||||
gender: birdGenderSchema.optional(),
|
||||
dateOfBirth: dateStringSchema.optional().or(z.literal('')),
|
||||
|
||||
+83
-44
@@ -472,6 +472,23 @@ const parseImportGender = (value: unknown): BirdGender | null => {
|
||||
const getBirdImportKey = (name: string, tagId: string) => (tagId ? `band:${tagId.toLowerCase()}` : `name:${name.toLowerCase()}`);
|
||||
|
||||
const mergeImportText = (current: string, next: string) => current || next;
|
||||
const birdProfileListLimit = 3;
|
||||
|
||||
const parseBirdProfileList = (value: string | null | undefined) =>
|
||||
(value ?? '')
|
||||
.split(/\r?\n/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, birdProfileListLimit);
|
||||
|
||||
const getBirdProfileListFields = (value: string) =>
|
||||
Array.from({ length: birdProfileListLimit }, (_, index) => parseBirdProfileList(value)[index] ?? '');
|
||||
|
||||
const updateBirdProfileListField = (value: string, index: number, nextItem: string) => {
|
||||
const items = getBirdProfileListFields(value);
|
||||
items[index] = nextItem;
|
||||
return items.map((item) => item.trim()).filter(Boolean).join('\n');
|
||||
};
|
||||
|
||||
const parseBirdImportRows = (rows: Record<string, unknown>[]): BirdImportPreview => {
|
||||
const errors: string[] = [];
|
||||
@@ -701,8 +718,8 @@ const toBirdForm = (bird: Bird): BirdFormState => ({
|
||||
name: bird.name,
|
||||
tagId: bird.tagId ?? '',
|
||||
species: bird.species,
|
||||
motivators: bird.motivators ?? '',
|
||||
demotivators: bird.demotivators ?? '',
|
||||
motivators: parseBirdProfileList(bird.motivators).join('\n'),
|
||||
demotivators: parseBirdProfileList(bird.demotivators).join('\n'),
|
||||
favoriteSnack: bird.favoriteSnack ?? '',
|
||||
gender: bird.gender,
|
||||
dateOfBirth: bird.dateOfBirth ?? '',
|
||||
@@ -4804,26 +4821,33 @@ function App() {
|
||||
<span>Gotcha day</span>
|
||||
<strong>{formatDate(selectedBird.gotchaDay)}</strong>
|
||||
</article>
|
||||
<article className="detail-card">
|
||||
<span>Gender</span>
|
||||
<strong className="detail-gender">
|
||||
<span aria-hidden="true" className={`gender-symbol ${selectedBird.gender}`}>
|
||||
{getBirdGenderSymbol(selectedBird)}
|
||||
</span>
|
||||
{getBirdGenderLabel(selectedBird)}
|
||||
</strong>
|
||||
</article>
|
||||
<article className="detail-card">
|
||||
<span>Favorite snack</span>
|
||||
<strong>{selectedBird.favoriteSnack || 'Not recorded'}</strong>
|
||||
</article>
|
||||
<article className="detail-card">
|
||||
<span>Motivators</span>
|
||||
<strong>{selectedBird.motivators || 'Not recorded'}</strong>
|
||||
{parseBirdProfileList(selectedBird.motivators).length ? (
|
||||
<ul className="detail-item-list">
|
||||
{parseBirdProfileList(selectedBird.motivators).map((motivator, index) => (
|
||||
<li key={`${motivator}-${index}`}>{motivator}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<strong>Not recorded</strong>
|
||||
)}
|
||||
</article>
|
||||
<article className="detail-card">
|
||||
<span>Demotivators</span>
|
||||
<strong>{selectedBird.demotivators || 'Not recorded'}</strong>
|
||||
{parseBirdProfileList(selectedBird.demotivators).length ? (
|
||||
<ul className="detail-item-list">
|
||||
{parseBirdProfileList(selectedBird.demotivators).map((demotivator, index) => (
|
||||
<li key={`${demotivator}-${index}`}>{demotivator}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<strong>Not recorded</strong>
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -5577,7 +5601,7 @@ function App() {
|
||||
</article>
|
||||
) : null}
|
||||
<article className="summary-card">
|
||||
<strong>{workspace?.billingEmail || authSession.user.email}</strong>
|
||||
<strong className="billing-contact-email">{workspace?.billingEmail || authSession.user.email}</strong>
|
||||
<span>Billing contact for invoices, receipts, and account notices.</span>
|
||||
</article>
|
||||
<article className="summary-card">
|
||||
@@ -5895,30 +5919,6 @@ function App() {
|
||||
</div>
|
||||
<small className="muted">Search or select a species so alerts and chart references stay consistent.</small>
|
||||
</label>
|
||||
<label>
|
||||
Favorite snack
|
||||
<input
|
||||
value={birdForm.favoriteSnack}
|
||||
onChange={(event) => setBirdForm({ ...birdForm, favoriteSnack: event.target.value })}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</label>
|
||||
<label className="wide-field">
|
||||
Motivators
|
||||
<textarea
|
||||
value={birdForm.motivators}
|
||||
onChange={(event) => setBirdForm({ ...birdForm, motivators: event.target.value })}
|
||||
placeholder="Training rewards, sounds, people, toys, routines"
|
||||
/>
|
||||
</label>
|
||||
<label className="wide-field">
|
||||
Demotivates
|
||||
<textarea
|
||||
value={birdForm.demotivators}
|
||||
onChange={(event) => setBirdForm({ ...birdForm, demotivators: event.target.value })}
|
||||
placeholder="Stressors, disliked handling, noises, situations"
|
||||
/>
|
||||
</label>
|
||||
<div className="segmented-field wide-field">
|
||||
<span>Gender</span>
|
||||
<div className="segmented-control" role="radiogroup" aria-label="Bird gender">
|
||||
@@ -5961,15 +5961,10 @@ function App() {
|
||||
</div>
|
||||
<small className="muted">Shown on the bird profile card as a symbol.</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="settings-nested-card">
|
||||
<div className="settings-nested-header">
|
||||
<div className="settings-inline-header wide-field">
|
||||
<p className="eyebrow">Dates</p>
|
||||
<h3>Milestones and reminders</h3>
|
||||
</div>
|
||||
<div className="settings-nested-grid">
|
||||
<label>
|
||||
Hatch Day
|
||||
<input
|
||||
@@ -6004,6 +5999,50 @@ function App() {
|
||||
/>
|
||||
<small className="muted">Send a reminder on this bird's gotcha day anniversary.</small>
|
||||
</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={`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={`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>
|
||||
|
||||
|
||||
+45
-4
@@ -1110,8 +1110,7 @@ textarea {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.profile-title,
|
||||
.detail-gender {
|
||||
.profile-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -1209,6 +1208,25 @@ textarea {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.settings-inline-header {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
padding-top: 0.35rem;
|
||||
}
|
||||
|
||||
.settings-inline-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-list-fields {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.profile-list-fields input {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.care-form-actions {
|
||||
align-self: start;
|
||||
margin-top: 0;
|
||||
@@ -1277,6 +1295,21 @@ textarea {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.billing-contact-email {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.detail-item-list {
|
||||
margin: 0;
|
||||
padding-left: 1.15rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-item-list li + li {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.summary-list {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
@@ -1833,16 +1866,24 @@ label {
|
||||
|
||||
.side-rail {
|
||||
position: static;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.brand-lockup {
|
||||
display: none;
|
||||
justify-items: start;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.side-nav-logo {
|
||||
width: min(120px, 27vw);
|
||||
}
|
||||
|
||||
.side-nav.panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.65rem;
|
||||
@@ -1850,7 +1891,7 @@ label {
|
||||
}
|
||||
|
||||
.page-tabs {
|
||||
grid-template-columns: repeat(auto-fit, minmax(82px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(64px, 1fr));
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user