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 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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user