Updated timeline
This commit is contained in:
+1
-1
@@ -3779,7 +3779,7 @@ app.get('/api/locations/search', requireAuth, locationSearchLimiter, async (req:
|
|||||||
searchUrl.searchParams.set('access_token', mapboxAccessToken);
|
searchUrl.searchParams.set('access_token', mapboxAccessToken);
|
||||||
searchUrl.searchParams.set('autocomplete', 'false');
|
searchUrl.searchParams.set('autocomplete', 'false');
|
||||||
searchUrl.searchParams.set('types', 'place,locality,region,country');
|
searchUrl.searchParams.set('types', 'place,locality,region,country');
|
||||||
searchUrl.searchParams.set('limit', '5');
|
searchUrl.searchParams.set('limit', '3');
|
||||||
searchUrl.searchParams.set('permanent', 'true');
|
searchUrl.searchParams.set('permanent', 'true');
|
||||||
|
|
||||||
const mapboxResponse = await fetch(searchUrl);
|
const mapboxResponse = await fetch(searchUrl);
|
||||||
|
|||||||
+4
-1
@@ -8,7 +8,10 @@
|
|||||||
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@20..48,100..700,0..1,-50..200&icon_names=move,move_location"
|
||||||
|
/>
|
||||||
<title>FlockPal</title>
|
<title>FlockPal</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+278
-45
@@ -265,8 +265,17 @@ type BirdTimelineEvent = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BirdTimelineGraphEvent = {
|
||||||
|
id: string;
|
||||||
|
eventType: BirdTimelineEvent['eventType'] | 'hatch_date';
|
||||||
|
eventDate: string;
|
||||||
|
createdAt: string;
|
||||||
|
sourceEvent?: BirdTimelineEvent;
|
||||||
|
};
|
||||||
|
|
||||||
type BirdTimelineEventFormState = {
|
type BirdTimelineEventFormState = {
|
||||||
eventType: 'location_updated' | 'owner_changed' | 'manual_note';
|
eventType: 'location_updated' | 'owner_changed' | 'manual_note';
|
||||||
|
ownerChanged: boolean;
|
||||||
eventDate: string;
|
eventDate: string;
|
||||||
locationLabel: string;
|
locationLabel: string;
|
||||||
locationDetails: VerifiedLocationDetails;
|
locationDetails: VerifiedLocationDetails;
|
||||||
@@ -780,7 +789,7 @@ const VerifiedLocationSearchField = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onClear,
|
onClear,
|
||||||
}: VerifiedLocationSearchFieldProps) => {
|
}: VerifiedLocationSearchFieldProps) => {
|
||||||
const selectedLabel = formatVerifiedLocationLabel(location) || fallbackLabel || '';
|
const hasSelectedLocation = Boolean(formatVerifiedLocationLabel(location) || fallbackLabel);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="verified-location-field wide-field">
|
<div className="verified-location-field wide-field">
|
||||||
@@ -802,19 +811,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" 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}
|
||||||
@@ -867,6 +870,7 @@ const emptyBirdForm: BirdFormState = {
|
|||||||
|
|
||||||
const emptyBirdTimelineEventForm: BirdTimelineEventFormState = {
|
const emptyBirdTimelineEventForm: BirdTimelineEventFormState = {
|
||||||
eventType: 'location_updated',
|
eventType: 'location_updated',
|
||||||
|
ownerChanged: false,
|
||||||
eventDate: new Date().toISOString().slice(0, 10),
|
eventDate: new Date().toISOString().slice(0, 10),
|
||||||
locationLabel: '',
|
locationLabel: '',
|
||||||
locationDetails: emptyVerifiedLocationDetails,
|
locationDetails: emptyVerifiedLocationDetails,
|
||||||
@@ -1179,11 +1183,11 @@ const formatBirdTimelineTitle = (event: BirdTimelineEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.eventType === 'location_updated') {
|
if (event.eventType === 'location_updated') {
|
||||||
return 'Location updated';
|
return 'Location change';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.eventType === 'owner_changed') {
|
if (event.eventType === 'owner_changed') {
|
||||||
return 'Owner record changed';
|
return 'Move location';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.eventType === 'manual_note') {
|
if (event.eventType === 'manual_note') {
|
||||||
@@ -1234,7 +1238,80 @@ const formatBirdTimelineSecondary = (event: BirdTimelineEvent) => {
|
|||||||
const getBirdTimelineLocation = (event: BirdTimelineEvent) =>
|
const getBirdTimelineLocation = (event: BirdTimelineEvent) =>
|
||||||
event.locationLabel || event.toWorkspaceName || event.fromWorkspaceName || 'Unknown location';
|
event.locationLabel || event.toWorkspaceName || event.fromWorkspaceName || 'Unknown location';
|
||||||
|
|
||||||
|
const commonTimelineLocationAliases: Record<string, string> = {
|
||||||
|
Alabama: 'AL',
|
||||||
|
Alaska: 'AK',
|
||||||
|
Arizona: 'AZ',
|
||||||
|
Arkansas: 'AR',
|
||||||
|
California: 'CA',
|
||||||
|
Colorado: 'CO',
|
||||||
|
Connecticut: 'CT',
|
||||||
|
Delaware: 'DE',
|
||||||
|
'District of Columbia': 'DC',
|
||||||
|
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',
|
||||||
|
'United States': 'US',
|
||||||
|
'United States of America': 'US',
|
||||||
|
'United Kingdom': 'UK',
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatBirdTimelineGraphLocation = (event: BirdTimelineEvent) =>
|
||||||
|
getBirdTimelineLocation(event)
|
||||||
|
.split(',')
|
||||||
|
.map((part) => {
|
||||||
|
const trimmedPart = part.trim();
|
||||||
|
return commonTimelineLocationAliases[trimmedPart] ?? trimmedPart;
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
const formatBirdTimelineGraphTitle = (event: BirdTimelineGraphEvent) =>
|
||||||
|
event.eventType === 'hatch_date' ? 'Hatch date' : formatBirdTimelineTitle(event.sourceEvent!);
|
||||||
|
|
||||||
|
const formatBirdTimelineGraphMeta = (event: BirdTimelineGraphEvent) => formatShortDate(getBirdTimelineGraphEventDate(event));
|
||||||
|
|
||||||
const getBirdTimelineEventDate = (event: BirdTimelineEvent) => event.eventDate || event.createdAt.slice(0, 10);
|
const getBirdTimelineEventDate = (event: BirdTimelineEvent) => event.eventDate || event.createdAt.slice(0, 10);
|
||||||
|
const getBirdTimelineGraphEventDate = (event: BirdTimelineGraphEvent) => event.eventDate || event.createdAt.slice(0, 10);
|
||||||
|
|
||||||
const sortBirdTimelineEvents = (events: BirdTimelineEvent[]) =>
|
const sortBirdTimelineEvents = (events: BirdTimelineEvent[]) =>
|
||||||
[...events].sort((left, right) => {
|
[...events].sort((left, right) => {
|
||||||
@@ -1242,7 +1319,86 @@ 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 sortBirdTimelineGraphEvents = (events: BirdTimelineGraphEvent[]) =>
|
||||||
|
[...events].sort((left, right) => {
|
||||||
|
const dateComparison = getBirdTimelineGraphEventDate(left).localeCompare(getBirdTimelineGraphEventDate(right));
|
||||||
|
return dateComparison || left.createdAt.localeCompare(right.createdAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getBirdTimelineGraphEvents = (events: BirdTimelineEvent[], hatchDate?: string | null) => {
|
||||||
|
const timelineEvents = sortBirdTimelineEvents(events)
|
||||||
|
.reverse()
|
||||||
|
.map<BirdTimelineGraphEvent>((event) => ({
|
||||||
|
id: event.id,
|
||||||
|
eventType: event.eventType,
|
||||||
|
eventDate: getBirdTimelineEventDate(event),
|
||||||
|
createdAt: event.createdAt,
|
||||||
|
sourceEvent: event,
|
||||||
|
}));
|
||||||
|
const hatchEvent = hatchDate
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: 'hatch-date',
|
||||||
|
eventType: 'hatch_date' as const,
|
||||||
|
eventDate: hatchDate,
|
||||||
|
createdAt: `${hatchDate}T00:00:00.000Z`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
const graphEvents = sortBirdTimelineGraphEvents([...hatchEvent, ...timelineEvents]);
|
||||||
|
|
||||||
|
if (graphEvents.length <= 8) {
|
||||||
|
return graphEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hatchEvent.length) {
|
||||||
|
const latestEvents = graphEvents.filter((event) => event.id !== 'hatch-date').slice(-7);
|
||||||
|
return sortBirdTimelineGraphEvents([...hatchEvent, ...latestEvents]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return graphEvents.slice(-8);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildBirdTimelineGraph = (events: BirdTimelineEvent[], hatchDate?: string | null) => {
|
||||||
|
const graphEvents = getBirdTimelineGraphEvents(events, hatchDate);
|
||||||
|
const left = 76;
|
||||||
|
const right = 564;
|
||||||
|
const centerY = 88;
|
||||||
|
const width = right - left;
|
||||||
|
const points = graphEvents.map((timelineEvent, index) => {
|
||||||
|
const x = graphEvents.length === 1 ? 320 : left + (index / (graphEvents.length - 1)) * width;
|
||||||
|
const labelY = index % 2 === 0 ? 30 : 134;
|
||||||
|
const textAnchor = index === 0 ? 'start' : index === graphEvents.length - 1 ? 'end' : 'middle';
|
||||||
|
|
||||||
|
return {
|
||||||
|
event: timelineEvent,
|
||||||
|
x,
|
||||||
|
y: centerY,
|
||||||
|
labelY,
|
||||||
|
connectorEndY: index % 2 === 0 ? 54 : 112,
|
||||||
|
textAnchor,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const path = points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x.toFixed(1)} ${point.y}`).join(' ');
|
||||||
|
const yearMarkers = points.reduce<Array<{ year: string; x: number }>>((markers, point) => {
|
||||||
|
const year = getBirdTimelineGraphEventDate(point.event).slice(0, 4);
|
||||||
|
|
||||||
|
if (!markers.some((marker) => marker.year === year)) {
|
||||||
|
markers.push({ year, x: point.x });
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
points,
|
||||||
|
path,
|
||||||
|
yearMarkers,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
centerY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
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`;
|
||||||
@@ -4000,7 +4156,7 @@ function App() {
|
|||||||
setSavingBirdTimelineEvent(true);
|
setSavingBirdTimelineEvent(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const eventType = birdTimelineEventForm.eventType;
|
const eventType = birdTimelineEventForm.ownerChanged ? 'owner_changed' : birdTimelineEventForm.eventType;
|
||||||
const verifiedLocationLabel = formatVerifiedLocationLabel(birdTimelineEventForm.locationDetails);
|
const verifiedLocationLabel = formatVerifiedLocationLabel(birdTimelineEventForm.locationDetails);
|
||||||
const locationLabel =
|
const locationLabel =
|
||||||
birdTimelineEventForm.eventType === 'location_updated'
|
birdTimelineEventForm.eventType === 'location_updated'
|
||||||
@@ -7918,41 +8074,103 @@ function App() {
|
|||||||
<div className="bird-timeline-graph-card">
|
<div className="bird-timeline-graph-card">
|
||||||
<svg
|
<svg
|
||||||
className="bird-timeline-graph"
|
className="bird-timeline-graph"
|
||||||
viewBox="0 0 640 150"
|
viewBox="0 0 640 200"
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={`${selectedBird.name} location and owner timeline`}
|
aria-label={`${selectedBird.name} location and owner timeline`}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="timelineLine" x1="0%" y1="0%" x2="100%" y2="0%">
|
<linearGradient id={`timelineLine-${selectedBird.id}`} x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
<stop offset="0%" stopColor="rgba(39, 105, 179, 0.42)" />
|
<stop offset="0%" stopColor={selectedBird.chartColor} stopOpacity="0.45" />
|
||||||
<stop offset="100%" stopColor="rgba(35, 138, 90, 0.88)" />
|
<stop offset="100%" stopColor={selectedBird.chartColor} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<line x1="52" y1="74" x2="588" y2="74" className="bird-timeline-graph-line" />
|
{(() => {
|
||||||
{getBirdTimelineGraphEvents(birdTimelineEvents).map((timelineEvent, index, graphEvents) => {
|
const timelineGraph = buildBirdTimelineGraph(birdTimelineEvents, selectedBird.dateOfBirth);
|
||||||
const x = graphEvents.length === 1 ? 320 : 52 + (index / (graphEvents.length - 1)) * 536;
|
|
||||||
const y = 74;
|
|
||||||
const labelY = index % 2 === 0 ? 34 : 124;
|
|
||||||
const connectorEndY = index % 2 === 0 ? 50 : 104;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g key={timelineEvent.id}>
|
<>
|
||||||
<line x1={x} y1={y} x2={x} y2={connectorEndY} className="bird-timeline-graph-connector" />
|
<line
|
||||||
<circle
|
x1={timelineGraph.left}
|
||||||
cx={x}
|
y1={timelineGraph.centerY}
|
||||||
cy={y}
|
x2={timelineGraph.right}
|
||||||
r={timelineEvent.eventType === 'owner_changed' ? 7 : 6}
|
y2={timelineGraph.centerY}
|
||||||
className={`bird-timeline-graph-dot ${timelineEvent.eventType}`}
|
className="bird-timeline-graph-axis"
|
||||||
/>
|
/>
|
||||||
<text x={x} y={labelY} textAnchor="middle" className="bird-timeline-graph-label">
|
{timelineGraph.yearMarkers.map((marker) => (
|
||||||
{getBirdTimelineLocation(timelineEvent)}
|
<g key={marker.year}>
|
||||||
</text>
|
<line
|
||||||
<text x={x} y={labelY + 14} textAnchor="middle" className="bird-timeline-graph-meta">
|
x1={marker.x}
|
||||||
{formatShortDate(getBirdTimelineEventDate(timelineEvent))}
|
y1="66"
|
||||||
</text>
|
x2={marker.x}
|
||||||
</g>
|
y2="110"
|
||||||
|
className="bird-timeline-graph-year-line"
|
||||||
|
/>
|
||||||
|
<text x={marker.x} y="188" textAnchor="middle" className="bird-timeline-graph-year">
|
||||||
|
{marker.year}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
{timelineGraph.points.length > 1 ? (
|
||||||
|
<path
|
||||||
|
d={timelineGraph.path}
|
||||||
|
fill="none"
|
||||||
|
stroke={`url(#timelineLine-${selectedBird.id})`}
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="bird-timeline-graph-line"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{timelineGraph.points.map(({ event: timelineEvent, x, y, labelY, connectorEndY, textAnchor }) => (
|
||||||
|
<g key={timelineEvent.id}>
|
||||||
|
<line x1={x} y1={y} x2={x} y2={connectorEndY} className="bird-timeline-graph-connector" />
|
||||||
|
{timelineEvent.eventType === 'hatch_date' ? (
|
||||||
|
<g
|
||||||
|
className="bird-timeline-graph-hatch-dot"
|
||||||
|
transform={`translate(${x - 11}, ${y - 11})`}
|
||||||
|
>
|
||||||
|
<title>Egg</title>
|
||||||
|
<path d="M12 21c-2.18 0-3.93-.7-5.25-2.1C5.58 17.66 5 16.05 5 14.06c0-1.28.22-2.56.67-3.84.45-1.28 1.04-2.43 1.78-3.46.74-1.02 1.53-1.88 2.37-2.58C10.66 3.49 11.39 3.15 12 3.15s1.34.34 2.18 1.03c.84.7 1.63 1.56 2.37 2.58.74 1.03 1.33 2.18 1.78 3.46.45 1.28.67 2.56.67 3.84 0 1.99-.58 3.6-1.75 4.84C15.93 20.3 14.18 21 12 21Z" />
|
||||||
|
</g>
|
||||||
|
) : (
|
||||||
|
<circle
|
||||||
|
cx={x}
|
||||||
|
cy={y}
|
||||||
|
r={timelineEvent.eventType === 'owner_changed' ? 7 : 6}
|
||||||
|
className={`bird-timeline-graph-dot ${timelineEvent.eventType}`}
|
||||||
|
style={{ stroke: selectedBird.chartColor }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{timelineEvent.eventType === 'location_updated' ? (
|
||||||
|
<text x={x} y={labelY} textAnchor={textAnchor} className="bird-timeline-graph-event-icon">
|
||||||
|
move
|
||||||
|
</text>
|
||||||
|
) : timelineEvent.eventType === 'owner_changed' ? (
|
||||||
|
<text x={x} y={labelY} textAnchor={textAnchor} className="bird-timeline-graph-event-icon">
|
||||||
|
move_location
|
||||||
|
</text>
|
||||||
|
) : timelineEvent.eventType === 'hatch_date' ? (
|
||||||
|
<text x={x} y={labelY} textAnchor={textAnchor} className="bird-timeline-graph-label">
|
||||||
|
Hatch date
|
||||||
|
</text>
|
||||||
|
) : (
|
||||||
|
<text x={x} y={labelY} textAnchor={textAnchor} className="bird-timeline-graph-label">
|
||||||
|
{formatBirdTimelineGraphTitle(timelineEvent)}
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
<text x={x} y={labelY + 14} textAnchor={textAnchor} className="bird-timeline-graph-meta">
|
||||||
|
{formatBirdTimelineGraphMeta(timelineEvent)}
|
||||||
|
</text>
|
||||||
|
{timelineEvent.sourceEvent && timelineEvent.eventType !== 'owner_changed' ? (
|
||||||
|
<text x={x} y={labelY + 28} textAnchor={textAnchor} className="bird-timeline-graph-meta">
|
||||||
|
{formatBirdTimelineGraphLocation(timelineEvent.sourceEvent)}
|
||||||
|
</text>
|
||||||
|
) : null}
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})()}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -7969,15 +8187,15 @@ function App() {
|
|||||||
setBirdTimelineEventForm({
|
setBirdTimelineEventForm({
|
||||||
...birdTimelineEventForm,
|
...birdTimelineEventForm,
|
||||||
eventType,
|
eventType,
|
||||||
|
ownerChanged: eventType === 'location_updated' ? birdTimelineEventForm.ownerChanged : false,
|
||||||
locationLabel: eventType === 'owner_changed' ? '' : birdTimelineEventForm.locationLabel,
|
locationLabel: eventType === 'owner_changed' ? '' : birdTimelineEventForm.locationLabel,
|
||||||
locationDetails:
|
locationDetails:
|
||||||
eventType === 'owner_changed' ? emptyVerifiedLocationDetails : birdTimelineEventForm.locationDetails,
|
eventType === 'owner_changed' ? emptyVerifiedLocationDetails : birdTimelineEventForm.locationDetails,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="location_updated">Location note</option>
|
<option value="location_updated">Location change</option>
|
||||||
<option value="owner_changed">Owner</option>
|
<option value="owner_changed">Owner Change</option>
|
||||||
<option value="manual_note">General note</option>
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
@@ -8016,6 +8234,21 @@ function App() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{birdTimelineEventForm.eventType === 'location_updated' ? (
|
||||||
|
<label className="checkbox-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={birdTimelineEventForm.ownerChanged}
|
||||||
|
onChange={(event) =>
|
||||||
|
setBirdTimelineEventForm({ ...birdTimelineEventForm, ownerChanged: event.target.checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<strong>Owner changed</strong>
|
||||||
|
<small>Records a change without owner names.</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
<label className="wide-field">
|
<label className="wide-field">
|
||||||
Note
|
Note
|
||||||
<textarea
|
<textarea
|
||||||
@@ -8023,7 +8256,7 @@ function App() {
|
|||||||
value={birdTimelineEventForm.note}
|
value={birdTimelineEventForm.note}
|
||||||
onChange={(event) => setBirdTimelineEventForm({ ...birdTimelineEventForm, note: event.target.value })}
|
onChange={(event) => setBirdTimelineEventForm({ ...birdTimelineEventForm, note: event.target.value })}
|
||||||
placeholder={
|
placeholder={
|
||||||
birdTimelineEventForm.eventType === 'owner_changed'
|
birdTimelineEventForm.ownerChanged || birdTimelineEventForm.eventType === 'owner_changed'
|
||||||
? 'Optional context without owner names'
|
? 'Optional context without owner names'
|
||||||
: 'Optional timeline context'
|
: 'Optional timeline context'
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-25
@@ -1337,20 +1337,29 @@ 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;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 140px;
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-graph-line {
|
.bird-timeline-graph-axis {
|
||||||
stroke: url(#timelineLine);
|
stroke: rgba(31, 42, 42, 0.18);
|
||||||
stroke-width: 4;
|
stroke-width: 1.2;
|
||||||
stroke-linecap: round;
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-year-line {
|
||||||
|
stroke: rgba(39, 105, 179, 0.16);
|
||||||
|
stroke-width: 1;
|
||||||
|
stroke-dasharray: 4 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-year {
|
||||||
|
fill: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bird-timeline-graph-connector {
|
.bird-timeline-graph-connector {
|
||||||
@@ -1375,7 +1384,7 @@ textarea {
|
|||||||
|
|
||||||
.bird-timeline-graph-label,
|
.bird-timeline-graph-label,
|
||||||
.bird-timeline-graph-meta {
|
.bird-timeline-graph-meta {
|
||||||
font-size: 0.72rem;
|
font-size: 11px;
|
||||||
fill: var(--ink);
|
fill: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1387,6 +1396,27 @@ textarea {
|
|||||||
fill: var(--muted);
|
fill: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-event-icon {
|
||||||
|
fill: #1f1f1f;
|
||||||
|
font-family: "Material Symbols Outlined";
|
||||||
|
font-size: 18px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24;
|
||||||
|
font-feature-settings: "liga";
|
||||||
|
-webkit-font-feature-settings: "liga";
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bird-timeline-graph-hatch-dot {
|
||||||
|
fill: #fffdf9;
|
||||||
|
stroke: #1f1f1f;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
.bird-timeline-form {
|
.bird-timeline-form {
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
border: 1px solid rgba(35, 138, 90, 0.14);
|
border: 1px solid rgba(35, 138, 90, 0.14);
|
||||||
@@ -2034,28 +2064,11 @@ label {
|
|||||||
|
|
||||||
.verified-location-search-row {
|
.verified-location-search-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.verified-location-selected {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0.85rem 0.95rem;
|
|
||||||
border: 1px solid rgba(35, 138, 90, 0.2);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: 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