Additional Genders
This commit is contained in:
+1
-1
@@ -223,7 +223,7 @@ const workspaceRoleSchema = z.enum(['owner', 'assistant', 'caregiver', 'viewer']
|
||||
const billingPlanSchema = z.enum(['household_basic', 'household_plus', 'household_macaw', 'household_hyacinth_macaw']);
|
||||
const billingIntervalSchema = z.enum(['monthly', 'yearly']);
|
||||
const integrationTokenScopeSchema = z.enum(['read_only', 'read_write']);
|
||||
const birdGenderSchema = z.enum(['unknown', 'male', 'female']);
|
||||
const birdGenderSchema = z.enum(['unknown', 'male', 'female', 'male_dna', 'female_dna']);
|
||||
const rescueVerificationStatusSchema = z.enum(['pending', 'approved', 'rejected']);
|
||||
const rescueOnboardingSchema = z.object({
|
||||
name: z.string().trim().max(160).optional().or(z.literal('')),
|
||||
|
||||
@@ -61,11 +61,17 @@ const formatWeight = (value: string | number | null) => {
|
||||
};
|
||||
|
||||
const genderLabel = (value: string) => {
|
||||
if (value === 'female_dna') {
|
||||
return 'Female (DNA confirmed)';
|
||||
}
|
||||
if (value === 'male_dna') {
|
||||
return 'Male (DNA confirmed)';
|
||||
}
|
||||
if (value === 'female') {
|
||||
return 'Female';
|
||||
return 'Female (assumed)';
|
||||
}
|
||||
if (value === 'male') {
|
||||
return 'Male';
|
||||
return 'Male (assumed)';
|
||||
}
|
||||
return 'Unknown';
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ export type SubscriptionStatus = 'active' | 'trialing' | 'past_due' | 'canceled'
|
||||
export type RescueVerificationStatus = 'not_required' | 'pending' | 'approved' | 'rejected';
|
||||
export type ProviderKey = 'google' | 'microsoft' | 'apple';
|
||||
export type IntegrationTokenScope = 'read_only' | 'read_write';
|
||||
export type BirdGender = 'unknown' | 'male' | 'female';
|
||||
export type BirdGender = 'unknown' | 'male' | 'female' | 'male_dna' | 'female_dna';
|
||||
|
||||
export type UserRow = {
|
||||
id: string;
|
||||
|
||||
@@ -212,7 +212,7 @@ Role requirements are called out per endpoint below. If the signed-in member lac
|
||||
"vetClinicAddress": "123 Feather Lane, Raleigh, NC",
|
||||
"vetAccountNumber": "FP-1001",
|
||||
"vetDoctorName": "Dr. Rivera",
|
||||
"gender": "female",
|
||||
"gender": "female_dna",
|
||||
"dateOfBirth": "2023-05-10",
|
||||
"gotchaDay": "2023-08-21",
|
||||
"chartColor": "#cb3a35",
|
||||
@@ -299,7 +299,7 @@ Role requirements are called out per endpoint below. If the signed-in member lac
|
||||
- Dates use `YYYY-MM-DD`
|
||||
- `workspaceType` is `standard` or `rescue`
|
||||
- member `role` is `owner`, `assistant`, `caregiver`, or `viewer`
|
||||
- bird `gender` is `unknown`, `male`, or `female`
|
||||
- bird `gender` is `unknown`, `male`, `female`, `male_dna`, or `female_dna`; `male` and `female` indicate assumed sex
|
||||
- bird `chartColor` must be a `#RRGGBB` hex color
|
||||
- `photoDataUrl` must be a base64 `data:image/...` URL
|
||||
- `weightGrams` must be a positive number up to `10000`
|
||||
@@ -801,7 +801,7 @@ Request body:
|
||||
"vetClinicAddress": "123 Feather Lane, Raleigh, NC",
|
||||
"vetAccountNumber": "FP-1001",
|
||||
"vetDoctorName": "Dr. Rivera",
|
||||
"gender": "female",
|
||||
"gender": "female_dna",
|
||||
"dateOfBirth": "2023-05-10",
|
||||
"gotchaDay": "2023-08-21",
|
||||
"chartColor": "#cb3a35",
|
||||
|
||||
+111
-51
@@ -14,7 +14,7 @@ type WorkspaceRole = 'owner' | 'assistant' | 'caregiver' | 'viewer';
|
||||
type SubscriptionStatus = 'active' | 'trialing' | 'past_due' | 'canceled' | 'unpaid' | 'none';
|
||||
type RescueVerificationStatus = 'not_required' | 'pending' | 'approved' | 'rejected';
|
||||
type IntegrationTokenScope = 'read_only' | 'read_write';
|
||||
type BirdGender = 'unknown' | 'male' | 'female';
|
||||
type BirdGender = 'unknown' | 'male' | 'female' | 'male_dna' | 'female_dna';
|
||||
|
||||
type Bird = {
|
||||
id: string;
|
||||
@@ -535,6 +535,14 @@ const parseImportGender = (value: unknown): BirdGender | null => {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
if (['male dna', 'dna male', 'male_dna', 'dna confirmed male', 'male dna confirmed'].includes(gender)) {
|
||||
return 'male_dna';
|
||||
}
|
||||
|
||||
if (['female dna', 'dna female', 'female_dna', 'dna confirmed female', 'female dna confirmed'].includes(gender)) {
|
||||
return 'female_dna';
|
||||
}
|
||||
|
||||
if (gender === 'male' || gender === 'female') {
|
||||
return gender;
|
||||
}
|
||||
@@ -682,6 +690,8 @@ const emptyBirdForm: BirdFormState = {
|
||||
publicProfileEnabled: false,
|
||||
};
|
||||
|
||||
const birdGenderOptions: BirdGender[] = ['female', 'female_dna', 'male', 'male_dna', 'unknown'];
|
||||
|
||||
const emptyVeterinaryInfoForm: VeterinaryInfoFormState = {
|
||||
vetClinicName: '',
|
||||
vetClinicAddress: '',
|
||||
@@ -867,25 +877,89 @@ const formatShortDate = (value: string | null) => {
|
||||
};
|
||||
|
||||
const getBirdGenderLabel = (bird: Pick<Bird, 'gender'>) => {
|
||||
if (bird.gender === 'female_dna') {
|
||||
return 'Female (DNA confirmed)';
|
||||
}
|
||||
if (bird.gender === 'male_dna') {
|
||||
return 'Male (DNA confirmed)';
|
||||
}
|
||||
if (bird.gender === 'female') {
|
||||
return 'Female';
|
||||
return 'Female (assumed)';
|
||||
}
|
||||
if (bird.gender === 'male') {
|
||||
return 'Male';
|
||||
return 'Male (assumed)';
|
||||
}
|
||||
return 'Unknown';
|
||||
};
|
||||
|
||||
const getBirdGenderSymbol = (bird: Pick<Bird, 'gender'>) => {
|
||||
if (bird.gender === 'female') {
|
||||
if (bird.gender === 'female' || bird.gender === 'female_dna') {
|
||||
return '♀';
|
||||
}
|
||||
if (bird.gender === 'male') {
|
||||
if (bird.gender === 'male' || bird.gender === 'male_dna') {
|
||||
return '♂';
|
||||
}
|
||||
return '?';
|
||||
};
|
||||
|
||||
const getBirdGenderClass = (bird: Pick<Bird, 'gender'>) => {
|
||||
if (bird.gender === 'female' || bird.gender === 'female_dna') {
|
||||
return 'female';
|
||||
}
|
||||
if (bird.gender === 'male' || bird.gender === 'male_dna') {
|
||||
return 'male';
|
||||
}
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
const isDnaConfirmedGender = (bird: Pick<Bird, 'gender'>) => bird.gender === 'male_dna' || bird.gender === 'female_dna';
|
||||
|
||||
const getBirdGenderSourceLabel = (bird: Pick<Bird, 'gender'>) => {
|
||||
if (bird.gender === 'unknown') {
|
||||
return 'Sex unknown';
|
||||
}
|
||||
return isDnaConfirmedGender(bird) ? 'DNA confirmed' : 'Assumed sex';
|
||||
};
|
||||
|
||||
const BirdGenderSourceIcon = ({ bird }: { bird: Pick<Bird, 'gender'> }) => {
|
||||
if (bird.gender === 'unknown') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceLabel = getBirdGenderSourceLabel(bird);
|
||||
|
||||
if (isDnaConfirmedGender(bird)) {
|
||||
return (
|
||||
<span className="gender-source-icon dna" aria-label={sourceLabel} title={sourceLabel}>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<circle className="dna-ring" cx="12" cy="12" r="8.7" />
|
||||
<path className="dna-strand" d="M14.9 5.4c-4.6 2.6-6.2 4.6-5.8 7.1.3 2.2 2.1 3.8 5.8 6.1" />
|
||||
<path className="dna-strand" d="M9.1 5.4c4.6 2.6 6.2 4.6 5.8 7.1-.3 2.2-2.1 3.8-5.8 6.1" />
|
||||
<path className="dna-rung" d="M10.4 8.1h3.2" />
|
||||
<path className="dna-rung" d="M9.4 10.7h5.2" />
|
||||
<path className="dna-rung" d="M9.5 13.3h5" />
|
||||
<path className="dna-rung" d="M10.4 15.9h3.2" />
|
||||
<circle className="dna-dot" cx="14.9" cy="5.4" r="0.7" />
|
||||
<circle className="dna-dot" cx="10.4" cy="8.1" r="0.85" />
|
||||
<circle className="dna-dot" cx="9.4" cy="10.7" r="0.85" />
|
||||
<circle className="dna-dot" cx="9.5" cy="13.3" r="0.85" />
|
||||
<circle className="dna-dot" cx="10.4" cy="15.9" r="0.85" />
|
||||
<circle className="dna-dot" cx="9.1" cy="18.6" r="0.7" />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="gender-source-icon assumed" aria-label={sourceLabel} title={sourceLabel}>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path className="assumed-eye" d="M3.8 12s3-4.6 8.2-4.6 8.2 4.6 8.2 4.6-3 4.6-8.2 4.6S3.8 12 3.8 12Z" />
|
||||
<circle className="assumed-pupil" cx="12" cy="12" r="2.1" />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const escapeReportHtml = (value: string | number | null | undefined) =>
|
||||
String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
@@ -5147,9 +5221,10 @@ function App() {
|
||||
<div className="public-profile-copy">
|
||||
<h1>
|
||||
<span>{publicProfile.name}</span>
|
||||
<span aria-label={getBirdGenderLabel(publicProfile)} className={`gender-symbol ${publicProfile.gender}`}>
|
||||
{getBirdGenderSymbol(publicProfile)}
|
||||
<span aria-label={getBirdGenderLabel(publicProfile)} className={`gender-symbol ${getBirdGenderClass(publicProfile)}`}>
|
||||
<span className="gender-symbol-mark">{getBirdGenderSymbol(publicProfile)}</span>
|
||||
</span>
|
||||
<BirdGenderSourceIcon bird={publicProfile} />
|
||||
</h1>
|
||||
<article className="summary-card">
|
||||
<span>Hatch Day</span>
|
||||
@@ -6055,8 +6130,11 @@ function App() {
|
||||
<div className="bird-card-copy">
|
||||
<span className="bird-card-title">
|
||||
<span>{bird.name}</span>
|
||||
<span aria-label={getBirdGenderLabel(bird)} className={`gender-inline ${bird.gender}`}>
|
||||
{getBirdGenderSymbol(bird)}
|
||||
<span className="bird-card-gender-cluster">
|
||||
<span aria-label={getBirdGenderLabel(bird)} className={`gender-inline ${getBirdGenderClass(bird)}`}>
|
||||
{getBirdGenderSymbol(bird)}
|
||||
</span>
|
||||
<BirdGenderSourceIcon bird={bird} />
|
||||
</span>
|
||||
</span>
|
||||
<small>{bird.species}</small>
|
||||
@@ -6253,7 +6331,7 @@ function App() {
|
||||
<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) => (
|
||||
{birdGenderOptions.map((gender) => (
|
||||
<button
|
||||
key={gender}
|
||||
className={`segmented-option ${birdForm.gender === gender ? 'active' : ''}`}
|
||||
@@ -6262,9 +6340,10 @@ function App() {
|
||||
role="radio"
|
||||
aria-checked={birdForm.gender === gender}
|
||||
>
|
||||
<span className={`gender-symbol ${gender}`} aria-hidden="true">
|
||||
{getBirdGenderSymbol({ gender })}
|
||||
<span className={`gender-symbol ${getBirdGenderClass({ gender })}`} aria-hidden="true">
|
||||
<span className="gender-symbol-mark">{getBirdGenderSymbol({ gender })}</span>
|
||||
</span>
|
||||
<BirdGenderSourceIcon bird={{ gender }} />
|
||||
{getBirdGenderLabel({ gender })}
|
||||
</button>
|
||||
))}
|
||||
@@ -6823,10 +6902,11 @@ function App() {
|
||||
<span>{selectedBird.name}</span>
|
||||
<span
|
||||
aria-label={getBirdGenderLabel(selectedBird)}
|
||||
className={`gender-symbol ${selectedBird.gender}`}
|
||||
className={`gender-symbol ${getBirdGenderClass(selectedBird)}`}
|
||||
>
|
||||
{getBirdGenderSymbol(selectedBird)}
|
||||
<span className="gender-symbol-mark">{getBirdGenderSymbol(selectedBird)}</span>
|
||||
</span>
|
||||
<BirdGenderSourceIcon bird={selectedBird} />
|
||||
</h3>
|
||||
<p className="muted">
|
||||
{selectedBird.species} • {selectedBird.tagId ? `Band ${selectedBird.tagId}` : 'Band ID not recorded'}
|
||||
@@ -8275,44 +8355,24 @@ function App() {
|
||||
<div className="segmented-field wide-field">
|
||||
<span>Gender</span>
|
||||
<div className="segmented-control" role="radiogroup" aria-label="Bird gender">
|
||||
<button
|
||||
className={`segmented-option ${birdForm.gender === 'unknown' ? 'active' : ''}`}
|
||||
onClick={() => setBirdForm({ ...birdForm, gender: 'unknown' })}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={birdForm.gender === 'unknown'}
|
||||
>
|
||||
<span className="gender-symbol unknown" aria-hidden="true">
|
||||
?
|
||||
</span>
|
||||
Unknown
|
||||
</button>
|
||||
<button
|
||||
className={`segmented-option ${birdForm.gender === 'male' ? 'active' : ''}`}
|
||||
onClick={() => setBirdForm({ ...birdForm, gender: 'male' })}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={birdForm.gender === 'male'}
|
||||
>
|
||||
<span className="gender-symbol male" aria-hidden="true">
|
||||
♂
|
||||
</span>
|
||||
Male
|
||||
</button>
|
||||
<button
|
||||
className={`segmented-option ${birdForm.gender === 'female' ? 'active' : ''}`}
|
||||
onClick={() => setBirdForm({ ...birdForm, gender: 'female' })}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={birdForm.gender === 'female'}
|
||||
>
|
||||
<span className="gender-symbol female" aria-hidden="true">
|
||||
♀
|
||||
</span>
|
||||
Female
|
||||
</button>
|
||||
{birdGenderOptions.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 ${getBirdGenderClass({ gender })}`} aria-hidden="true">
|
||||
<span className="gender-symbol-mark">{getBirdGenderSymbol({ gender })}</span>
|
||||
</span>
|
||||
<BirdGenderSourceIcon bird={{ gender }} />
|
||||
{getBirdGenderLabel({ gender })}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<small className="muted">Shown on the bird profile card as a symbol.</small>
|
||||
<small className="muted">Shown on the bird profile card and reports.</small>
|
||||
</div>
|
||||
<div className="settings-inline-header wide-field">
|
||||
<p className="eyebrow">Dates</p>
|
||||
|
||||
+118
-4
@@ -1000,10 +1000,19 @@ textarea {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.bird-card-title .bird-card-gender-cluster {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.28rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.gender-inline {
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gender-inline.male {
|
||||
@@ -1018,6 +1027,99 @@ textarea {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.gender-source-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.45rem;
|
||||
height: 1.45rem;
|
||||
flex: 0 0 1.45rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.gender-source-icon svg {
|
||||
width: 1.28rem;
|
||||
height: 1.28rem;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.gender-source-icon.dna {
|
||||
width: 1.9rem;
|
||||
height: 1.9rem;
|
||||
flex-basis: 1.9rem;
|
||||
background: rgba(91, 74, 161, 0.1);
|
||||
color: #5b4aa1;
|
||||
}
|
||||
|
||||
.gender-source-icon.dna svg {
|
||||
width: 1.72rem;
|
||||
height: 1.72rem;
|
||||
}
|
||||
|
||||
.gender-source-icon .dna-ring {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.15;
|
||||
}
|
||||
|
||||
.gender-source-icon .dna-strand {
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.35;
|
||||
}
|
||||
|
||||
.gender-source-icon .dna-rung {
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.05;
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.gender-source-icon .dna-dot {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.gender-source-icon.assumed {
|
||||
width: 1.9rem;
|
||||
height: 1.9rem;
|
||||
flex-basis: 1.9rem;
|
||||
background: rgba(93, 95, 89, 0.12);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.gender-source-icon.assumed svg {
|
||||
width: 1.72rem;
|
||||
height: 1.72rem;
|
||||
}
|
||||
|
||||
.gender-source-icon .assumed-eye {
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.7;
|
||||
}
|
||||
|
||||
.gender-source-icon .assumed-pupil {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.bird-card-title .gender-source-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
flex-basis: 1.05rem;
|
||||
}
|
||||
|
||||
.bird-card-title .gender-source-icon svg {
|
||||
display: block;
|
||||
width: 0.92rem;
|
||||
height: 0.92rem;
|
||||
}
|
||||
|
||||
.bird-avatar,
|
||||
.profile-photo {
|
||||
width: 56px;
|
||||
@@ -1375,14 +1477,26 @@ textarea {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.9rem;
|
||||
width: 1.9rem;
|
||||
height: 1.9rem;
|
||||
flex: 0 0 1.9rem;
|
||||
border-radius: 999px;
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.gender-symbol-mark {
|
||||
display: block;
|
||||
line-height: 0.82;
|
||||
transform: scale(1.16);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.profile-title .gender-symbol-mark {
|
||||
transform: scale(1.28);
|
||||
}
|
||||
|
||||
.gender-symbol.male {
|
||||
background: rgba(39, 105, 179, 0.12);
|
||||
color: var(--accent-blue);
|
||||
@@ -1405,7 +1519,7 @@ textarea {
|
||||
|
||||
.segmented-control {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user