Fixed Timeline
This commit is contained in:
+5
-2
@@ -320,6 +320,7 @@ const birdSchema = z.object({
|
|||||||
vetAccountNumber: z.string().trim().max(120).optional().or(z.literal('')),
|
vetAccountNumber: z.string().trim().max(120).optional().or(z.literal('')),
|
||||||
vetDoctorName: z.string().trim().max(160).optional().or(z.literal('')),
|
vetDoctorName: z.string().trim().max(160).optional().or(z.literal('')),
|
||||||
gender: birdGenderSchema.optional(),
|
gender: birdGenderSchema.optional(),
|
||||||
|
hatchDay: dateStringSchema.optional().or(z.literal('')),
|
||||||
dateOfBirth: dateStringSchema.optional().or(z.literal('')),
|
dateOfBirth: dateStringSchema.optional().or(z.literal('')),
|
||||||
gotchaDay: dateStringSchema.optional().or(z.literal('')),
|
gotchaDay: dateStringSchema.optional().or(z.literal('')),
|
||||||
chartColor: chartColorSchema.optional(),
|
chartColor: chartColorSchema.optional(),
|
||||||
@@ -862,6 +863,7 @@ const normalizeBird = (row: BirdRow) => ({
|
|||||||
vetAccountNumber: row.vet_account_number,
|
vetAccountNumber: row.vet_account_number,
|
||||||
vetDoctorName: row.vet_doctor_name,
|
vetDoctorName: row.vet_doctor_name,
|
||||||
gender: row.gender,
|
gender: row.gender,
|
||||||
|
hatchDay: row.date_of_birth,
|
||||||
dateOfBirth: row.date_of_birth,
|
dateOfBirth: row.date_of_birth,
|
||||||
gotchaDay: row.gotcha_day,
|
gotchaDay: row.gotcha_day,
|
||||||
chartColor: row.chart_color,
|
chartColor: row.chart_color,
|
||||||
@@ -890,6 +892,7 @@ const normalizePublicBirdProfile = (row: BirdRow) => ({
|
|||||||
name: row.name,
|
name: row.name,
|
||||||
favoriteSnack: row.favorite_snack,
|
favoriteSnack: row.favorite_snack,
|
||||||
gender: row.gender,
|
gender: row.gender,
|
||||||
|
hatchDay: row.date_of_birth,
|
||||||
dateOfBirth: row.date_of_birth,
|
dateOfBirth: row.date_of_birth,
|
||||||
photoDataUrl: getBirdPhotoUrl(row),
|
photoDataUrl: getBirdPhotoUrl(row),
|
||||||
});
|
});
|
||||||
@@ -3978,7 +3981,7 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
|
|||||||
vetAccountNumber: emptyToNull(parsed.data.vetAccountNumber),
|
vetAccountNumber: emptyToNull(parsed.data.vetAccountNumber),
|
||||||
vetDoctorName: emptyToNull(parsed.data.vetDoctorName),
|
vetDoctorName: emptyToNull(parsed.data.vetDoctorName),
|
||||||
gender: (parsed.data.gender ?? 'unknown') as BirdGender,
|
gender: (parsed.data.gender ?? 'unknown') as BirdGender,
|
||||||
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
|
dateOfBirth: emptyToNull(parsed.data.hatchDay || parsed.data.dateOfBirth),
|
||||||
gotchaDay: emptyToNull(parsed.data.gotchaDay),
|
gotchaDay: emptyToNull(parsed.data.gotchaDay),
|
||||||
chartColor: parsed.data.chartColor ?? '#cb3a35',
|
chartColor: parsed.data.chartColor ?? '#cb3a35',
|
||||||
photoDataUrl: photoStorage.photoDataUrl,
|
photoDataUrl: photoStorage.photoDataUrl,
|
||||||
@@ -4347,7 +4350,7 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
|
|||||||
vetAccountNumber: emptyToNull(parsed.data.vetAccountNumber),
|
vetAccountNumber: emptyToNull(parsed.data.vetAccountNumber),
|
||||||
vetDoctorName: emptyToNull(parsed.data.vetDoctorName),
|
vetDoctorName: emptyToNull(parsed.data.vetDoctorName),
|
||||||
gender: (parsed.data.gender ?? 'unknown') as BirdGender,
|
gender: (parsed.data.gender ?? 'unknown') as BirdGender,
|
||||||
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
|
dateOfBirth: emptyToNull(parsed.data.hatchDay || parsed.data.dateOfBirth),
|
||||||
gotchaDay: emptyToNull(parsed.data.gotchaDay),
|
gotchaDay: emptyToNull(parsed.data.gotchaDay),
|
||||||
chartColor: parsed.data.chartColor ?? '#cb3a35',
|
chartColor: parsed.data.chartColor ?? '#cb3a35',
|
||||||
photoDataUrl: photoStorage.photoDataUrl,
|
photoDataUrl: photoStorage.photoDataUrl,
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
type="image/svg+xml"
|
type="image/svg+xml"
|
||||||
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='featherFill' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23cb3a35'/%3E%3Cstop offset='30%25' stop-color='%23f0b63f'/%3E%3Cstop offset='58%25' stop-color='%23238a5a'/%3E%3Cstop offset='100%25' stop-color='%232769b3'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M50.8 10.4C37.9 10.3 27 18.5 22.7 31.1c-3.1 9.1-2.1 18.5-8.6 24.8c-1.5 1.5-0.2 4 1.9 3.6c8.4-1.5 14.6-6.7 18.6-13.7c1 0.5 2.2 0.8 3.4 0.8c3.5 0 6.5-2.3 7.5-5.4c1.9-0.4 3.7-1.3 5.1-2.7c2-2 3-4.6 3.1-7.2c3.3-5.8 4.9-12.9 1.4-20.2c-0.7-1.3-2-0.7-4.3-0.7Z' fill='url(%23featherFill)'/%3E%3Cpath d='M18 56c8.5-3.4 14.2-9.8 18.1-17.8M26.9 48.9c6.9-7.2 13.5-14.8 20.3-22.1M31.8 41.2c6.4-1.3 12.1-4.6 16.5-9.4M36.8 33.8c4.9-0.9 9.2-3.4 12.6-7.1' fill='none' stroke='%23fff8ef' stroke-linecap='round' stroke-width='2.6'/%3E%3Cpath d='M18 56c8.5-3.4 14.2-9.8 18.1-17.8' fill='none' stroke='%2363562d' stroke-linecap='round' stroke-width='2.2'/%3E%3C/svg%3E"
|
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='featherFill' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23cb3a35'/%3E%3Cstop offset='30%25' stop-color='%23f0b63f'/%3E%3Cstop offset='58%25' stop-color='%23238a5a'/%3E%3Cstop offset='100%25' stop-color='%232769b3'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M50.8 10.4C37.9 10.3 27 18.5 22.7 31.1c-3.1 9.1-2.1 18.5-8.6 24.8c-1.5 1.5-0.2 4 1.9 3.6c8.4-1.5 14.6-6.7 18.6-13.7c1 0.5 2.2 0.8 3.4 0.8c3.5 0 6.5-2.3 7.5-5.4c1.9-0.4 3.7-1.3 5.1-2.7c2-2 3-4.6 3.1-7.2c3.3-5.8 4.9-12.9 1.4-20.2c-0.7-1.3-2-0.7-4.3-0.7Z' fill='url(%23featherFill)'/%3E%3Cpath d='M18 56c8.5-3.4 14.2-9.8 18.1-17.8M26.9 48.9c6.9-7.2 13.5-14.8 20.3-22.1M31.8 41.2c6.4-1.3 12.1-4.6 16.5-9.4M36.8 33.8c4.9-0.9 9.2-3.4 12.6-7.1' fill='none' stroke='%23fff8ef' stroke-linecap='round' stroke-width='2.6'/%3E%3Cpath d='M18 56c8.5-3.4 14.2-9.8 18.1-17.8' fill='none' stroke='%2363562d' stroke-linecap='round' stroke-width='2.2'/%3E%3C/svg%3E"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=egg,manage_accounts,map,move,move_location,sticky_note_2,timeline" />
|
||||||
<title>FlockPal</title>
|
<title>FlockPal</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+355
-59
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState, type Dispatch, type SetStateAction } from 'react';
|
import { useEffect, useMemo, useState, type CSSProperties, type Dispatch, type SetStateAction } from 'react';
|
||||||
import birdSilhouette from './assets/bird-silhouette.jpg';
|
import birdSilhouette from './assets/bird-silhouette.jpg';
|
||||||
import flockPalLandingArt from './assets/flockpal-landing-art.png';
|
import flockPalLandingArt from './assets/flockpal-landing-art.png';
|
||||||
import flockPalTextArt from './assets/flockpal-text.png';
|
import flockPalTextArt from './assets/flockpal-text.png';
|
||||||
@@ -53,6 +53,7 @@ type Bird = {
|
|||||||
vetAccountNumber: string | null;
|
vetAccountNumber: string | null;
|
||||||
vetDoctorName: string | null;
|
vetDoctorName: string | null;
|
||||||
gender: BirdGender;
|
gender: BirdGender;
|
||||||
|
hatchDay: string | null;
|
||||||
dateOfBirth: string | null;
|
dateOfBirth: string | null;
|
||||||
gotchaDay: string | null;
|
gotchaDay: string | null;
|
||||||
chartColor: string;
|
chartColor: string;
|
||||||
@@ -342,6 +343,7 @@ type PublicBirdProfile = {
|
|||||||
name: string;
|
name: string;
|
||||||
favoriteSnack: string | null;
|
favoriteSnack: string | null;
|
||||||
gender: BirdGender;
|
gender: BirdGender;
|
||||||
|
hatchDay: string | null;
|
||||||
dateOfBirth: string | null;
|
dateOfBirth: string | null;
|
||||||
photoDataUrl: string | null;
|
photoDataUrl: string | null;
|
||||||
};
|
};
|
||||||
@@ -782,15 +784,16 @@ const VerifiedLocationSearchField = ({
|
|||||||
onClear,
|
onClear,
|
||||||
}: VerifiedLocationSearchFieldProps) => {
|
}: VerifiedLocationSearchFieldProps) => {
|
||||||
const selectedLabel = formatVerifiedLocationLabel(location) || fallbackLabel || '';
|
const selectedLabel = formatVerifiedLocationLabel(location) || fallbackLabel || '';
|
||||||
|
const hasSelectedLocation = Boolean(selectedLabel);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="verified-location-field wide-field">
|
<div className="verified-location-field wide-field">
|
||||||
<div className="verified-location-label">
|
<div className="verified-location-label">
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<div className="verified-location-search-row">
|
<div className={`verified-location-search-row${hasSelectedLocation ? ' has-selected-location' : ''}`}>
|
||||||
<input
|
<input
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
value={searchState.query}
|
value={searchState.query || selectedLabel}
|
||||||
onChange={(event) => onSearchStateChange({ ...searchState, query: event.target.value, error: '' })}
|
onChange={(event) => onSearchStateChange({ ...searchState, query: event.target.value, error: '' })}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
@@ -803,19 +806,13 @@ const VerifiedLocationSearchField = ({
|
|||||||
<button className="secondary-button" type="button" onClick={onSearch} disabled={searchState.searching}>
|
<button className="secondary-button" type="button" onClick={onSearch} disabled={searchState.searching}>
|
||||||
{searchState.searching ? 'Searching...' : 'Search'}
|
{searchState.searching ? 'Searching...' : 'Search'}
|
||||||
</button>
|
</button>
|
||||||
|
{hasSelectedLocation ? (
|
||||||
|
<button className="secondary-button verified-location-clear" type="button" onClick={onClear}>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{selectedLabel ? (
|
|
||||||
<div className="verified-location-selected">
|
|
||||||
<div>
|
|
||||||
<strong>{selectedLabel}</strong>
|
|
||||||
<small>{location.provider === 'mapbox' ? 'Verified by Mapbox' : 'Saved location'}</small>
|
|
||||||
</div>
|
|
||||||
<button className="secondary-button" type="button" onClick={onClear}>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{searchState.error ? (
|
{searchState.error ? (
|
||||||
<p className="error-banner" role="alert">
|
<p className="error-banner" role="alert">
|
||||||
{searchState.error}
|
{searchState.error}
|
||||||
@@ -1024,7 +1021,7 @@ const toBirdForm = (bird: Bird): BirdFormState => ({
|
|||||||
vetAccountNumber: bird.vetAccountNumber ?? '',
|
vetAccountNumber: bird.vetAccountNumber ?? '',
|
||||||
vetDoctorName: bird.vetDoctorName ?? '',
|
vetDoctorName: bird.vetDoctorName ?? '',
|
||||||
gender: bird.gender,
|
gender: bird.gender,
|
||||||
dateOfBirth: bird.dateOfBirth ?? '',
|
dateOfBirth: getBirdHatchDay(bird) ?? '',
|
||||||
gotchaDay: bird.gotchaDay ?? '',
|
gotchaDay: bird.gotchaDay ?? '',
|
||||||
chartColor: bird.chartColor,
|
chartColor: bird.chartColor,
|
||||||
photoDataUrl: bird.photoDataUrl ?? '',
|
photoDataUrl: bird.photoDataUrl ?? '',
|
||||||
@@ -1244,7 +1241,289 @@ const sortBirdTimelineEvents = (events: BirdTimelineEvent[]) =>
|
|||||||
return dateComparison || right.createdAt.localeCompare(left.createdAt);
|
return dateComparison || right.createdAt.localeCompare(left.createdAt);
|
||||||
});
|
});
|
||||||
|
|
||||||
const getBirdTimelineGraphEvents = (events: BirdTimelineEvent[]) => sortBirdTimelineEvents(events).reverse().slice(-8);
|
const TIMELINE_GRAPH_START_X = 52;
|
||||||
|
const TIMELINE_GRAPH_END_X = 588;
|
||||||
|
|
||||||
|
type BirdTimelineGraphItem = {
|
||||||
|
id: string;
|
||||||
|
eventType: BirdTimelineEvent['eventType'] | 'hatch_date';
|
||||||
|
date: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BirdTimelineGraphTick = {
|
||||||
|
id: string;
|
||||||
|
year: number;
|
||||||
|
date: string;
|
||||||
|
label: string;
|
||||||
|
isToday?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BirdTimelineGraphDomain = {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBirdHatchDay = (bird: Pick<Bird, 'hatchDay' | 'dateOfBirth'>) => {
|
||||||
|
const hatchDay = bird.hatchDay?.trim();
|
||||||
|
const dateOfBirth = bird.dateOfBirth?.trim();
|
||||||
|
return hatchDay || dateOfBirth || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBirdTimelineGraphLabel = (event: BirdTimelineEvent) => {
|
||||||
|
if (event.eventType === 'location_updated') {
|
||||||
|
return event.locationLabel || 'Location updated';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.eventType === 'owner_changed') {
|
||||||
|
return 'Owner changed';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.eventType === 'transferred') {
|
||||||
|
return event.toWorkspaceName || 'Flock transfer';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.eventType === 'manual_note') {
|
||||||
|
return event.locationLabel || 'Timeline note';
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.locationLabel || 'Added to flock';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBirdTimelineGraphIcon = (item: BirdTimelineGraphItem) => {
|
||||||
|
if (item.eventType === 'hatch_date') {
|
||||||
|
return 'egg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.eventType === 'owner_changed') {
|
||||||
|
return 'manage_accounts';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.eventType === 'transferred') {
|
||||||
|
return 'move';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.eventType === 'manual_note') {
|
||||||
|
return 'sticky_note_2';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'map';
|
||||||
|
};
|
||||||
|
|
||||||
|
const compactTimelineLocationPartMap: Record<string, string> = {
|
||||||
|
alabama: 'AL',
|
||||||
|
alaska: 'AK',
|
||||||
|
arizona: 'AZ',
|
||||||
|
arkansas: 'AR',
|
||||||
|
california: 'CA',
|
||||||
|
colorado: 'CO',
|
||||||
|
connecticut: 'CT',
|
||||||
|
delaware: 'DE',
|
||||||
|
florida: 'FL',
|
||||||
|
georgia: 'GA',
|
||||||
|
hawaii: 'HI',
|
||||||
|
idaho: 'ID',
|
||||||
|
illinois: 'IL',
|
||||||
|
indiana: 'IN',
|
||||||
|
iowa: 'IA',
|
||||||
|
kansas: 'KS',
|
||||||
|
kentucky: 'KY',
|
||||||
|
louisiana: 'LA',
|
||||||
|
maine: 'ME',
|
||||||
|
maryland: 'MD',
|
||||||
|
massachusetts: 'MA',
|
||||||
|
michigan: 'MI',
|
||||||
|
minnesota: 'MN',
|
||||||
|
mississippi: 'MS',
|
||||||
|
missouri: 'MO',
|
||||||
|
montana: 'MT',
|
||||||
|
nebraska: 'NE',
|
||||||
|
nevada: 'NV',
|
||||||
|
'new hampshire': 'NH',
|
||||||
|
'new jersey': 'NJ',
|
||||||
|
'new mexico': 'NM',
|
||||||
|
'new york': 'NY',
|
||||||
|
'north carolina': 'NC',
|
||||||
|
'north dakota': 'ND',
|
||||||
|
ohio: 'OH',
|
||||||
|
oklahoma: 'OK',
|
||||||
|
oregon: 'OR',
|
||||||
|
pennsylvania: 'PA',
|
||||||
|
'rhode island': 'RI',
|
||||||
|
'south carolina': 'SC',
|
||||||
|
'south dakota': 'SD',
|
||||||
|
tennessee: 'TN',
|
||||||
|
texas: 'TX',
|
||||||
|
utah: 'UT',
|
||||||
|
vermont: 'VT',
|
||||||
|
virginia: 'VA',
|
||||||
|
washington: 'WA',
|
||||||
|
'west virginia': 'WV',
|
||||||
|
wisconsin: 'WI',
|
||||||
|
wyoming: 'WY',
|
||||||
|
'district of columbia': 'DC',
|
||||||
|
'united states': 'US',
|
||||||
|
'united states of america': 'US',
|
||||||
|
};
|
||||||
|
|
||||||
|
const compactTimelineLocationLabel = (label: string) =>
|
||||||
|
label
|
||||||
|
.split(',')
|
||||||
|
.map((part) => {
|
||||||
|
const trimmedPart = part.trim();
|
||||||
|
return compactTimelineLocationPartMap[trimmedPart.toLowerCase()] || trimmedPart;
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
const getBirdTimelineGraphItems = (bird: Bird, events: BirdTimelineEvent[]): BirdTimelineGraphItem[] => {
|
||||||
|
const graphEvents: BirdTimelineGraphItem[] = sortBirdTimelineEvents(events)
|
||||||
|
.reverse()
|
||||||
|
.filter((event) => event.eventType !== 'profile_created')
|
||||||
|
.map((event) => ({
|
||||||
|
id: event.id,
|
||||||
|
eventType: event.eventType,
|
||||||
|
date: getBirdTimelineEventDate(event),
|
||||||
|
label: compactTimelineLocationLabel(getBirdTimelineGraphLabel(event)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const hatchDay = getBirdHatchDay(bird);
|
||||||
|
|
||||||
|
if (!hatchDay) {
|
||||||
|
return graphEvents.slice(-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hatchItem: BirdTimelineGraphItem = {
|
||||||
|
id: `${bird.id}-hatch-date`,
|
||||||
|
eventType: 'hatch_date',
|
||||||
|
date: hatchDay,
|
||||||
|
label: 'Hatch Day',
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
hatchItem,
|
||||||
|
...graphEvents.filter((event) => event.date >= hatchDay).slice(-7),
|
||||||
|
].sort((left, right) => left.date.localeCompare(right.date));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBirdTimelineGraphBranch = (item: BirdTimelineGraphItem, graphItems: BirdTimelineGraphItem[], index: number) => {
|
||||||
|
if (item.eventType === 'hatch_date') {
|
||||||
|
return {
|
||||||
|
placement: 'on-line',
|
||||||
|
distance: 0,
|
||||||
|
offset: 0,
|
||||||
|
angle: 0,
|
||||||
|
connectorLength: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemX = getBirdTimelineGraphX(item, graphItems, index);
|
||||||
|
const closeItems = graphItems
|
||||||
|
.map((graphItem, graphItemIndex) => ({
|
||||||
|
item: graphItem,
|
||||||
|
index: graphItemIndex,
|
||||||
|
x: getBirdTimelineGraphX(graphItem, graphItems, graphItemIndex),
|
||||||
|
}))
|
||||||
|
.filter((entry) => Math.abs(entry.x - itemX) < 112)
|
||||||
|
.sort((left, right) => left.x - right.x || left.item.date.localeCompare(right.item.date) || left.index - right.index);
|
||||||
|
const hasNearbyHatch = closeItems.some((entry) => entry.item.eventType === 'hatch_date');
|
||||||
|
const closeEventItems = closeItems.filter((entry) => entry.item.eventType !== 'hatch_date');
|
||||||
|
const closeEventIndex = Math.max(0, closeEventItems.findIndex((entry) => entry.item.id === item.id));
|
||||||
|
const useCollisionLane = closeItems.length > 1;
|
||||||
|
const placement = useCollisionLane
|
||||||
|
? hasNearbyHatch
|
||||||
|
? closeEventIndex % 2 === 0 ? 'below' : 'above'
|
||||||
|
: closeEventIndex % 2 === 0 ? 'above' : 'below'
|
||||||
|
: index % 2 === 0 ? 'above' : 'below';
|
||||||
|
const lane = useCollisionLane ? Math.floor(closeEventIndex / 2) : 0;
|
||||||
|
const distance = useCollisionLane ? 70 + lane * 56 : 42;
|
||||||
|
const offset = useCollisionLane ? 44 + lane * 14 : 0;
|
||||||
|
const connectorLength = Math.round(Math.sqrt(distance ** 2 + offset ** 2));
|
||||||
|
|
||||||
|
return {
|
||||||
|
placement,
|
||||||
|
distance,
|
||||||
|
offset,
|
||||||
|
angle: offset ? (placement === 'above' ? 1 : -1) * Math.round((Math.atan(offset / distance) * 180) / Math.PI) : 0,
|
||||||
|
connectorLength,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLocalDateString = (date = new Date()) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
||||||
|
const day = `${date.getDate()}`.padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBirdTimelineGraphDomain = (graphItems: BirdTimelineGraphItem[]): BirdTimelineGraphDomain | null => {
|
||||||
|
if (!graphItems.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const todayTime = parseDateValue(getLocalDateString()).getTime();
|
||||||
|
const eventTimes = graphItems.map((entry) => parseDateValue(entry.date).getTime());
|
||||||
|
return {
|
||||||
|
startTime: Math.min(...eventTimes),
|
||||||
|
endTime: Math.max(todayTime, ...eventTimes),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimelineGraphPosition = (date: string, domain: BirdTimelineGraphDomain | null, fallbackIndex = 0, fallbackCount = 1) => {
|
||||||
|
if (!domain || domain.startTime === domain.endTime) {
|
||||||
|
if (fallbackCount <= 1) {
|
||||||
|
return (TIMELINE_GRAPH_START_X + TIMELINE_GRAPH_END_X) / 2;
|
||||||
|
}
|
||||||
|
return TIMELINE_GRAPH_START_X + (fallbackIndex / (fallbackCount - 1)) * (TIMELINE_GRAPH_END_X - TIMELINE_GRAPH_START_X);
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTime = parseDateValue(date).getTime();
|
||||||
|
return TIMELINE_GRAPH_START_X + ((eventTime - domain.startTime) / (domain.endTime - domain.startTime)) * (TIMELINE_GRAPH_END_X - TIMELINE_GRAPH_START_X);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBirdTimelineGraphX = (item: BirdTimelineGraphItem, graphItems: BirdTimelineGraphItem[], index: number) =>
|
||||||
|
getTimelineGraphPosition(item.date, getBirdTimelineGraphDomain(graphItems), index, graphItems.length);
|
||||||
|
|
||||||
|
const getBirdTimelineGraphTicks = (graphItems: BirdTimelineGraphItem[]): BirdTimelineGraphTick[] => {
|
||||||
|
const domain = getBirdTimelineGraphDomain(graphItems);
|
||||||
|
if (!domain) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { startTime, endTime } = domain;
|
||||||
|
const today = getLocalDateString();
|
||||||
|
const startDate = new Date(startTime);
|
||||||
|
const endDate = new Date(endTime);
|
||||||
|
const startYear = startDate.getFullYear();
|
||||||
|
const endYear = endDate.getFullYear();
|
||||||
|
|
||||||
|
const ticks: BirdTimelineGraphTick[] = Array.from({ length: endYear - startYear + 1 }, (_, index) => {
|
||||||
|
const year = startYear + index;
|
||||||
|
const yearStart = parseDateValue(`${year}-01-01`).getTime();
|
||||||
|
const tickTime = Math.min(Math.max(yearStart, startTime), endTime);
|
||||||
|
return {
|
||||||
|
id: `timeline-year-${year}`,
|
||||||
|
year,
|
||||||
|
date: new Date(tickTime).toISOString().slice(0, 10),
|
||||||
|
label: `${year}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ticks.some((tick) => tick.date === today)) {
|
||||||
|
ticks.push({
|
||||||
|
id: 'timeline-today',
|
||||||
|
year: parseDateValue(today).getFullYear(),
|
||||||
|
date: today,
|
||||||
|
label: 'Today',
|
||||||
|
isToday: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ticks.sort((left, right) => left.date.localeCompare(right.date));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBirdTimelineGraphTickX = (tick: BirdTimelineGraphTick, graphItems: BirdTimelineGraphItem[]) =>
|
||||||
|
getTimelineGraphPosition(tick.date, getBirdTimelineGraphDomain(graphItems));
|
||||||
|
|
||||||
const formatWeight = (value: number | null) => (value ? `${value.toFixed(1)} g` : 'Pending');
|
const formatWeight = (value: number | null) => (value ? `${value.toFixed(1)} g` : 'Pending');
|
||||||
const formatRange = (minGrams: number, maxGrams: number) => `${minGrams.toFixed(0)}-${maxGrams.toFixed(0)} g`;
|
const formatRange = (minGrams: number, maxGrams: number) => `${minGrams.toFixed(0)}-${maxGrams.toFixed(0)} g`;
|
||||||
@@ -3861,6 +4140,7 @@ function App() {
|
|||||||
demotivators: profile.demotivators,
|
demotivators: profile.demotivators,
|
||||||
favoriteSnack: profile.favoriteSnack,
|
favoriteSnack: profile.favoriteSnack,
|
||||||
gender: profile.gender,
|
gender: profile.gender,
|
||||||
|
hatchDay: profile.dateOfBirth,
|
||||||
dateOfBirth: profile.dateOfBirth,
|
dateOfBirth: profile.dateOfBirth,
|
||||||
gotchaDay: profile.gotchaDay,
|
gotchaDay: profile.gotchaDay,
|
||||||
chartColor: profile.chartColor || '#cb3a35',
|
chartColor: profile.chartColor || '#cb3a35',
|
||||||
@@ -3957,7 +4237,7 @@ function App() {
|
|||||||
const response = await apiFetch(isEditing ? `/birds/${editingBirdId}` : '/birds', authToken, {
|
const response = await apiFetch(isEditing ? `/birds/${editingBirdId}` : '/birds', authToken, {
|
||||||
method,
|
method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ...birdForm, locationLabel }),
|
body: JSON.stringify({ ...birdForm, hatchDay: birdForm.dateOfBirth, locationLabel }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -4911,7 +5191,7 @@ function App() {
|
|||||||
['Species', selectedBird.species],
|
['Species', selectedBird.species],
|
||||||
['Band/tag ID', selectedBird.tagId || 'Not recorded'],
|
['Band/tag ID', selectedBird.tagId || 'Not recorded'],
|
||||||
['Sex', getBirdGenderLabel(selectedBird)],
|
['Sex', getBirdGenderLabel(selectedBird)],
|
||||||
['Hatch day', formatDate(selectedBird.dateOfBirth)],
|
['Hatch day', formatDate(getBirdHatchDay(selectedBird))],
|
||||||
['Favorite snack', selectedBird.favoriteSnack || 'Not recorded'],
|
['Favorite snack', selectedBird.favoriteSnack || 'Not recorded'],
|
||||||
['Latest weight', selectedBird.latestWeightGrams ? `${formatWeight(selectedBird.latestWeightGrams)}${selectedBird.latestRecordedOn ? ` on ${formatShortDate(selectedBird.latestRecordedOn)}` : ''}` : 'Pending'],
|
['Latest weight', selectedBird.latestWeightGrams ? `${formatWeight(selectedBird.latestWeightGrams)}${selectedBird.latestRecordedOn ? ` on ${formatShortDate(selectedBird.latestRecordedOn)}` : ''}` : 'Pending'],
|
||||||
];
|
];
|
||||||
@@ -5624,7 +5904,7 @@ function App() {
|
|||||||
</h1>
|
</h1>
|
||||||
<article className="summary-card">
|
<article className="summary-card">
|
||||||
<span>Hatch Day</span>
|
<span>Hatch Day</span>
|
||||||
<strong>{formatDate(publicProfile.dateOfBirth)}</strong>
|
<strong>{formatDate(getBirdHatchDay(publicProfile))}</strong>
|
||||||
</article>
|
</article>
|
||||||
<article className="summary-card">
|
<article className="summary-card">
|
||||||
<span>Favorite treat</span>
|
<span>Favorite treat</span>
|
||||||
@@ -7352,7 +7632,7 @@ function App() {
|
|||||||
<div className="detail-grid">
|
<div className="detail-grid">
|
||||||
<article className="detail-card">
|
<article className="detail-card">
|
||||||
<span>Hatch Day</span>
|
<span>Hatch Day</span>
|
||||||
<strong>{formatDate(selectedBird.dateOfBirth)}</strong>
|
<strong>{formatDate(getBirdHatchDay(selectedBird))}</strong>
|
||||||
</article>
|
</article>
|
||||||
<article className="detail-card">
|
<article className="detail-card">
|
||||||
<span>Gotcha day</span>
|
<span>Gotcha day</span>
|
||||||
@@ -7916,48 +8196,64 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="muted">{birdTimelineLoading ? 'Loading...' : `${birdTimelineEvents.length} events`}</p>
|
<p className="muted">{birdTimelineLoading ? 'Loading...' : `${birdTimelineEvents.length} events`}</p>
|
||||||
</div>
|
</div>
|
||||||
{birdTimelineEvents.length ? (
|
{(() => {
|
||||||
<div className="bird-timeline-graph-card">
|
const timelineGraphItems = getBirdTimelineGraphItems(selectedBird, birdTimelineEvents);
|
||||||
<svg
|
const timelineGraphTicks = getBirdTimelineGraphTicks(timelineGraphItems);
|
||||||
className="bird-timeline-graph"
|
|
||||||
viewBox="0 0 640 150"
|
return timelineGraphItems.length ? (
|
||||||
role="img"
|
<div className="bird-timeline-graph-card">
|
||||||
aria-label={`${selectedBird.name} location and owner timeline`}
|
<div className="bird-timeline-graph" role="img" aria-label={`${selectedBird.name} location and owner timeline`}>
|
||||||
>
|
<div className="bird-timeline-graph-line" />
|
||||||
<defs>
|
<div className="bird-timeline-graph-scale" aria-hidden="true">
|
||||||
<linearGradient id="timelineLine" x1="0%" y1="0%" x2="100%" y2="0%">
|
{timelineGraphTicks.map((tick) => {
|
||||||
<stop offset="0%" stopColor="rgba(39, 105, 179, 0.42)" />
|
const x = getBirdTimelineGraphTickX(tick, timelineGraphItems);
|
||||||
<stop offset="100%" stopColor="rgba(35, 138, 90, 0.88)" />
|
return (
|
||||||
</linearGradient>
|
<span
|
||||||
</defs>
|
key={tick.id}
|
||||||
<line x1="52" y1="74" x2="588" y2="74" className="bird-timeline-graph-line" />
|
className={`bird-timeline-graph-tick${tick.isToday ? ' today' : ''}`}
|
||||||
{getBirdTimelineGraphEvents(birdTimelineEvents).map((timelineEvent, index, graphEvents) => {
|
style={{ left: `${(x / 640) * 100}%` }}
|
||||||
const x = graphEvents.length === 1 ? 320 : 52 + (index / (graphEvents.length - 1)) * 536;
|
>
|
||||||
const y = 74;
|
{tick.label}
|
||||||
const labelY = index % 2 === 0 ? 34 : 124;
|
</span>
|
||||||
const connectorEndY = index % 2 === 0 ? 50 : 104;
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{timelineGraphItems.map((timelineItem, index, graphItems) => {
|
||||||
|
const x = getBirdTimelineGraphX(timelineItem, graphItems, index);
|
||||||
|
const position = `${(x / 640) * 100}%`;
|
||||||
|
const branch = getBirdTimelineGraphBranch(timelineItem, graphItems, index);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g key={timelineEvent.id}>
|
<div
|
||||||
<line x1={x} y1={y} x2={x} y2={connectorEndY} className="bird-timeline-graph-connector" />
|
key={timelineItem.id}
|
||||||
<circle
|
className={`bird-timeline-graph-point ${branch.placement} ${timelineItem.eventType}`}
|
||||||
cx={x}
|
style={
|
||||||
cy={y}
|
{
|
||||||
r={timelineEvent.eventType === 'owner_changed' ? 7 : 6}
|
left: position,
|
||||||
className={`bird-timeline-graph-dot ${timelineEvent.eventType}`}
|
'--branch-distance': `${branch.distance}px`,
|
||||||
/>
|
'--branch-offset': `${branch.offset}px`,
|
||||||
<text x={x} y={labelY} textAnchor="middle" className="bird-timeline-graph-label">
|
'--branch-angle': `${branch.angle}deg`,
|
||||||
{getBirdTimelineLocation(timelineEvent)}
|
'--branch-connector-length': `${branch.connectorLength}px`,
|
||||||
</text>
|
} as CSSProperties
|
||||||
<text x={x} y={labelY + 14} textAnchor="middle" className="bird-timeline-graph-meta">
|
}
|
||||||
{formatShortDate(getBirdTimelineEventDate(timelineEvent))}
|
>
|
||||||
</text>
|
<div className="bird-timeline-graph-connector" />
|
||||||
</g>
|
<div className="bird-timeline-graph-dot">
|
||||||
|
<span className="material-symbols-outlined bird-timeline-graph-icon">
|
||||||
|
{getBirdTimelineGraphIcon(timelineItem)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="bird-timeline-graph-copy">
|
||||||
|
<strong>{timelineItem.label}</strong>
|
||||||
|
<span>{formatShortDate(timelineItem.date)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null;
|
||||||
|
})()}
|
||||||
<form className="form-panel inline-form care-entry-form bird-timeline-form" onSubmit={handleBirdTimelineEventSubmit}>
|
<form className="form-panel inline-form care-entry-form bird-timeline-form" onSubmit={handleBirdTimelineEventSubmit}>
|
||||||
<label>
|
<label>
|
||||||
Type
|
Type
|
||||||
|
|||||||
+199
-40
@@ -1337,54 +1337,220 @@ textarea {
|
|||||||
border: 1px solid rgba(39, 105, 179, 0.12);
|
border: 1px solid rgba(39, 105, 179, 0.12);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: rgba(255, 255, 255, 0.72);
|
background: rgba(255, 255, 255, 0.72);
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-graph {
|
.bird-timeline-graph {
|
||||||
display: block;
|
position: relative;
|
||||||
width: 100%;
|
min-height: 340px;
|
||||||
height: auto;
|
isolation: isolate;
|
||||||
min-height: 140px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-graph-line {
|
.bird-timeline-graph-line {
|
||||||
stroke: url(#timelineLine);
|
position: absolute;
|
||||||
stroke-width: 4;
|
left: calc(8.125% + 18px);
|
||||||
stroke-linecap: round;
|
right: 8.125%;
|
||||||
|
top: 50%;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, rgba(39, 105, 179, 0.42), rgba(35, 138, 90, 0.88));
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-graph-connector {
|
.bird-timeline-graph-scale {
|
||||||
stroke: rgba(39, 105, 179, 0.18);
|
position: absolute;
|
||||||
stroke-width: 2;
|
inset: 0;
|
||||||
stroke-dasharray: 4 5;
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-tick {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% + 78px);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-tick::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: calc(100% + 0.35rem);
|
||||||
|
width: 1px;
|
||||||
|
height: 68px;
|
||||||
|
background: linear-gradient(to bottom, rgba(39, 105, 179, 0.22), rgba(39, 105, 179, 0));
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-tick.today {
|
||||||
|
color: var(--accent-green);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-tick.today::before {
|
||||||
|
background: linear-gradient(to bottom, rgba(35, 138, 90, 0.42), rgba(35, 138, 90, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 136px;
|
||||||
|
height: 0;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-graph-dot {
|
.bird-timeline-graph-dot {
|
||||||
fill: #fffdf9;
|
position: absolute;
|
||||||
stroke: var(--accent-green);
|
left: 50%;
|
||||||
stroke-width: 4;
|
z-index: 2;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border: 4px solid var(--accent-green);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fffdf9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-graph-dot.owner_changed {
|
.bird-timeline-graph-point.above .bird-timeline-graph-dot {
|
||||||
stroke: var(--accent-blue);
|
left: calc(50% + var(--branch-offset, 0px));
|
||||||
|
bottom: var(--branch-distance, 34px);
|
||||||
|
transform: translate(-50%, 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-graph-dot.transferred {
|
.bird-timeline-graph-point.below .bird-timeline-graph-dot {
|
||||||
stroke: var(--accent-red);
|
left: calc(50% + var(--branch-offset, 0px));
|
||||||
|
top: var(--branch-distance, 34px);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-graph-label,
|
.bird-timeline-graph-point.on-line .bird-timeline-graph-dot {
|
||||||
.bird-timeline-graph-meta {
|
top: 0;
|
||||||
font-size: 0.72rem;
|
transform: translate(-50%, -50%);
|
||||||
fill: var(--ink);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-graph-label {
|
.bird-timeline-graph-point.owner_changed .bird-timeline-graph-dot {
|
||||||
|
border-color: var(--accent-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point.transferred .bird-timeline-graph-dot {
|
||||||
|
border-color: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point.hatch_date .bird-timeline-graph-dot {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fffdf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-connector {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
width: 2px;
|
||||||
|
height: var(--branch-connector-length, var(--branch-distance, 34px));
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(39, 105, 179, 0.18) 0 4px,
|
||||||
|
transparent 4px 9px
|
||||||
|
);
|
||||||
|
transform: translateX(-50%) rotate(var(--branch-angle, 0deg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point.above .bird-timeline-graph-connector {
|
||||||
|
bottom: 0;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point.below .bird-timeline-graph-connector {
|
||||||
|
top: 0;
|
||||||
|
transform-origin: top center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point.on-line .bird-timeline-graph-connector {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--accent-green);
|
||||||
|
font-family: "Material Symbols Outlined";
|
||||||
|
font-size: 18px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
direction: ltr;
|
||||||
|
font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24;
|
||||||
|
font-feature-settings: "liga";
|
||||||
|
-webkit-font-feature-settings: "liga";
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point.hatch_date .bird-timeline-graph-icon {
|
||||||
|
color: var(--accent-gold);
|
||||||
|
font-size: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point.owner_changed .bird-timeline-graph-icon {
|
||||||
|
color: var(--accent-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point.transferred .bird-timeline-graph-icon {
|
||||||
|
color: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-copy {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
width: 124px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: grid;
|
||||||
|
gap: 0.08rem;
|
||||||
|
justify-items: center;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-copy strong {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
line-height: 1.15;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-graph-meta {
|
.bird-timeline-graph-copy span {
|
||||||
fill: var(--muted);
|
color: var(--muted);
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point.above .bird-timeline-graph-copy {
|
||||||
|
left: calc(50% + var(--branch-offset, 0px));
|
||||||
|
bottom: calc(var(--branch-distance, 34px) + 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point.below .bird-timeline-graph-copy {
|
||||||
|
left: calc(50% + var(--branch-offset, 0px));
|
||||||
|
top: calc(var(--branch-distance, 34px) + 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-point.on-line .bird-timeline-graph-copy {
|
||||||
|
bottom: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-form {
|
.bird-timeline-form {
|
||||||
@@ -2058,23 +2224,16 @@ label {
|
|||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.verified-location-selected {
|
.verified-location-search-row.has-selected-location {
|
||||||
display: flex;
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
align-items: center;
|
}
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
.verified-location-search-row.has-selected-location input {
|
||||||
padding: 0.85rem 0.95rem;
|
border-color: rgba(35, 138, 90, 0.45);
|
||||||
border: 1px solid rgba(35, 138, 90, 0.2);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: rgba(35, 138, 90, 0.08);
|
background: rgba(35, 138, 90, 0.08);
|
||||||
|
box-shadow: 0 0 0 3px rgba(35, 138, 90, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.verified-location-selected div {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verified-location-selected small,
|
|
||||||
.verified-location-result small {
|
.verified-location-result small {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user