automated dev db build

This commit is contained in:
blaisadmin
2026-05-21 17:27:57 -04:00
parent 4715306d14
commit df3fcbf885
3 changed files with 177 additions and 87 deletions
+12 -2
View File
@@ -234,13 +234,23 @@ const lostBirdReportSchema = z.object({
}); });
const publicProfileCodeSchema = z.string().trim().regex(/^[A-Za-z0-9_-]{8,32}$/); 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({ const birdSchema = z.object({
name: z.string().trim().min(1).max(120), name: z.string().trim().min(1).max(120),
tagId: z.string().trim().max(80).optional().or(z.literal('')), tagId: z.string().trim().max(80).optional().or(z.literal('')),
species: z.string().trim().min(1).max(120), species: z.string().trim().min(1).max(120),
motivators: z.string().trim().max(1000).optional().or(z.literal('')), motivators: birdProfileListSchema,
demotivators: z.string().trim().max(1000).optional().or(z.literal('')), demotivators: birdProfileListSchema,
favoriteSnack: z.string().trim().max(160).optional().or(z.literal('')), favoriteSnack: z.string().trim().max(160).optional().or(z.literal('')),
gender: birdGenderSchema.optional(), gender: birdGenderSchema.optional(),
dateOfBirth: dateStringSchema.optional().or(z.literal('')), dateOfBirth: dateStringSchema.optional().or(z.literal('')),
+83 -44
View File
@@ -472,6 +472,23 @@ const parseImportGender = (value: unknown): BirdGender | null => {
const getBirdImportKey = (name: string, tagId: string) => (tagId ? `band:${tagId.toLowerCase()}` : `name:${name.toLowerCase()}`); const getBirdImportKey = (name: string, tagId: string) => (tagId ? `band:${tagId.toLowerCase()}` : `name:${name.toLowerCase()}`);
const mergeImportText = (current: string, next: string) => current || next; 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 parseBirdImportRows = (rows: Record<string, unknown>[]): BirdImportPreview => {
const errors: string[] = []; const errors: string[] = [];
@@ -701,8 +718,8 @@ const toBirdForm = (bird: Bird): BirdFormState => ({
name: bird.name, name: bird.name,
tagId: bird.tagId ?? '', tagId: bird.tagId ?? '',
species: bird.species, species: bird.species,
motivators: bird.motivators ?? '', motivators: parseBirdProfileList(bird.motivators).join('\n'),
demotivators: bird.demotivators ?? '', demotivators: parseBirdProfileList(bird.demotivators).join('\n'),
favoriteSnack: bird.favoriteSnack ?? '', favoriteSnack: bird.favoriteSnack ?? '',
gender: bird.gender, gender: bird.gender,
dateOfBirth: bird.dateOfBirth ?? '', dateOfBirth: bird.dateOfBirth ?? '',
@@ -4804,26 +4821,33 @@ function App() {
<span>Gotcha day</span> <span>Gotcha day</span>
<strong>{formatDate(selectedBird.gotchaDay)}</strong> <strong>{formatDate(selectedBird.gotchaDay)}</strong>
</article> </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"> <article className="detail-card">
<span>Favorite snack</span> <span>Favorite snack</span>
<strong>{selectedBird.favoriteSnack || 'Not recorded'}</strong> <strong>{selectedBird.favoriteSnack || 'Not recorded'}</strong>
</article> </article>
<article className="detail-card"> <article className="detail-card">
<span>Motivators</span> <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>
<article className="detail-card"> <article className="detail-card">
<span>Demotivators</span> <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> </article>
</div> </div>
@@ -5577,7 +5601,7 @@ function App() {
</article> </article>
) : null} ) : null}
<article className="summary-card"> <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> <span>Billing contact for invoices, receipts, and account notices.</span>
</article> </article>
<article className="summary-card"> <article className="summary-card">
@@ -5895,30 +5919,6 @@ function App() {
</div> </div>
<small className="muted">Search or select a species so alerts and chart references stay consistent.</small> <small className="muted">Search or select a species so alerts and chart references stay consistent.</small>
</label> </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"> <div className="segmented-field wide-field">
<span>Gender</span> <span>Gender</span>
<div className="segmented-control" role="radiogroup" aria-label="Bird gender"> <div className="segmented-control" role="radiogroup" aria-label="Bird gender">
@@ -5961,15 +5961,10 @@ function App() {
</div> </div>
<small className="muted">Shown on the bird profile card as a symbol.</small> <small className="muted">Shown on the bird profile card as a symbol.</small>
</div> </div>
</div> <div className="settings-inline-header wide-field">
</section>
<section className="settings-nested-card">
<div className="settings-nested-header">
<p className="eyebrow">Dates</p> <p className="eyebrow">Dates</p>
<h3>Milestones and reminders</h3> <h3>Milestones and reminders</h3>
</div> </div>
<div className="settings-nested-grid">
<label> <label>
Hatch Day Hatch Day
<input <input
@@ -6004,6 +5999,50 @@ function App() {
/> />
<small className="muted">Send a reminder on this bird's gotcha day anniversary.</small> <small className="muted">Send a reminder on this bird's gotcha day anniversary.</small>
</label> </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> </div>
</section> </section>
+45 -4
View File
@@ -1110,8 +1110,7 @@ textarea {
font-size: 1.6rem; font-size: 1.6rem;
} }
.profile-title, .profile-title {
.detail-gender {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
@@ -1209,6 +1208,25 @@ textarea {
grid-column: 1 / -1; 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 { .care-form-actions {
align-self: start; align-self: start;
margin-top: 0; margin-top: 0;
@@ -1277,6 +1295,21 @@ textarea {
font-size: 1.05rem; 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 { .summary-list {
display: grid; display: grid;
gap: 0.2rem; gap: 0.2rem;
@@ -1833,16 +1866,24 @@ label {
.side-rail { .side-rail {
position: static; position: static;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 0.55rem; gap: 0.55rem;
} }
.brand-lockup { .brand-lockup {
display: none; justify-items: start;
padding-left: 0;
}
.side-nav-logo {
width: min(120px, 27vw);
} }
.side-nav.panel { .side-nav.panel {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
min-width: 0;
align-items: center; align-items: center;
gap: 0.65rem; gap: 0.65rem;
padding: 0.65rem; padding: 0.65rem;
@@ -1850,7 +1891,7 @@ label {
} }
.page-tabs { .page-tabs {
grid-template-columns: repeat(auto-fit, minmax(82px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(64px, 1fr));
gap: 0.4rem; gap: 0.4rem;
} }