feat(flights): show a Planespotters photo even when it names no photographer

A photo whose answer gave no photographer was dropped as no photo. It is
now shown, still linked to its Planespotters page, with no credit line.
This commit is contained in:
Sameh Khamis committed 2026-11-10 14:12:42 -08:00
1 parent 85b1694bad
commit 259bc1a024
6 files changed
+36 -16

No files matched your search

+8 -7
View File
@@ -1,11 +1,12 @@
/**
* @module planespottersPhoto
* @description Reads a Planespotters photo answer into the one credited
* thumbnail every surface shows. Planespotters lets the thumbnail be hotlinked
* on the condition that the photographer is credited next to it and the image
* links back to the photo's page, so a photo without a photographer, or whose
* image or page is not on Planespotters' own hosts, is "no photo": nothing
* arbitrary is ever shown or linked.
* @description Reads a Planespotters photo answer into the one thumbnail
* every surface shows. Planespotters lets the thumbnail be hotlinked on the
* condition that the photographer is credited next to it and the image links
* back to the photo's page. A photo whose answer names no photographer is
* still shown, linked to its page, with no credit line; a photo whose image or
* page is not on Planespotters' own hosts is "no photo": nothing arbitrary is
* ever shown or linked.
*
* Accepts the upstream answer (`GET https://api.planespotters.net/pub/photos/hex/{hex}`,
* `{photos: [...]}`), the cached server shape
@@ -63,7 +64,7 @@ function checked({ src, width, height, photographer, link }) {
const credit = typeof photographer === 'string' ? photographer.trim() : '';
const image = imageSrc(src);
const page = pageLink(link);
if (!credit || !image || !page) return null;
if (!image || !page) return null;
return {
found: true,
src: image,
+10 -3
View File
@@ -105,6 +105,16 @@ test('a server’s cached shape falls back to its small thumbnail when the large
}
});
test('a photo that names no photographer is still a photo, uncredited', () => {
for (const photographer of ['', ' ', 42, undefined]) {
assert.deepEqual(
parsePlanespottersPhoto(photo({ photographer })),
{ ...PHOTO, photographer: '' },
String(photographer),
);
}
});
test('no photo, junk, and missing fields are "no photo"', () => {
for (const payload of [
{ photos: [] },
@@ -114,9 +124,6 @@ test('no photo, junk, and missing fields are "no photo"', () => {
'nope',
{ photos: 'nope' },
{ photos: [null] },
photo({ photographer: '' }),
photo({ photographer: ' ' }),
photo({ photographer: 42 }),
photo({ link: '' }),
photo({ thumbnail: null, thumbnail_large: null }),
]) {
+4 -3
View File
@@ -198,9 +198,10 @@ export function createPanel({ state: layerState, parts }) {
width: photo.width ?? null,
height: photo.height ?? null,
link: photo.link,
credit: t('Photo © {photographer}', {
photographer: photo.photographer,
}),
// A photo whose answer names no photographer shows no credit line.
credit: photo.photographer
? t('Photo © {photographer}', { photographer: photo.photographer })
: '',
};
}
+11
View File
@@ -87,6 +87,17 @@ test('the Contacts view carries the tracked flight’s photo with its credit', (
});
});
test('the Contacts view shows an uncredited photo with no credit line', () => {
const view = photoView(FLIGHT, {
layerId: 'flights',
id: 'abc123',
photo: { ...PHOTO, photographer: '' },
});
assert.equal(view.photo.src, PHOTO.src);
assert.equal(view.photo.link, PHOTO.link);
assert.equal(view.photo.credit, '');
});
test('the Contacts view has no photo without one, or with another subject’s', () => {
assert.equal(photoView(FLIGHT, null).photo, null);
assert.equal(
+1 -1
View File
@@ -61,7 +61,7 @@ onUpdated(() => {
<div class="military-awareness-subject">{{ view.subject }}</div>
<a v-if="view.photo && view.photo.src !== failedPhoto" class="military-awareness-photo" :href="view.photo.link" target="_blank" rel="noopener">
<img :src="view.photo.src" :width="view.photo.width ?? undefined" :height="view.photo.height ?? undefined" alt="" @error="fail" />
<small>{{ view.photo.credit }}</small>
<small v-if="view.photo.credit">{{ view.photo.credit }}</small>
</a>
<AwarenessNames v-if="view.subjectNames" :names="view.subjectNames" />
<div class="military-awareness-controls" role="group" :aria-label="view.navigation.label">