add outbound links to place suggestions

This commit is contained in:
alex wiesner
2026-03-14 12:05:55 +00:00
parent 1b004b9e65
commit 4b3f432640
2 changed files with 136 additions and 24 deletions

View File

@@ -24,6 +24,18 @@
rating?: number; rating?: number;
latitude?: number | string; latitude?: number | string;
longitude?: number | string; longitude?: number | string;
link?: string | null;
preferred_link?: string | null;
url?: string | null;
website?: string | null;
map_link?: string | null;
links?: {
link?: string | null;
preferred?: string | null;
official?: string | null;
website?: string | null;
map?: string | null;
} | null;
}; };
type Conversation = { type Conversation = {
@@ -661,6 +673,43 @@
return null; return null;
} }
function normalizeHttpUrl(value: unknown): string | null {
if (typeof value !== 'string') {
return null;
}
const candidate = value.trim();
if (!candidate) {
return null;
}
try {
const parsed = new URL(candidate);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null;
}
return parsed.toString();
} catch {
return null;
}
}
function getPreferredPlaceLink(place: PlaceResult): string | null {
return (
normalizeHttpUrl(place.link) ||
normalizeHttpUrl(place.preferred_link) ||
normalizeHttpUrl(place.url) ||
normalizeHttpUrl(place.website) ||
normalizeHttpUrl(place.map_link) ||
normalizeHttpUrl(place.links?.link) ||
normalizeHttpUrl(place.links?.preferred) ||
normalizeHttpUrl(place.links?.official) ||
normalizeHttpUrl(place.links?.website) ||
normalizeHttpUrl(place.links?.map)
);
}
function hasPlaceCoordinates(place: PlaceResult): boolean { function hasPlaceCoordinates(place: PlaceResult): boolean {
return parseCoordinate(place.latitude) !== null && parseCoordinate(place.longitude) !== null; return parseCoordinate(place.latitude) !== null && parseCoordinate(place.longitude) !== null;
} }
@@ -737,6 +786,7 @@
location: place.address || place.name, location: place.address || place.name,
latitude, latitude,
longitude, longitude,
link: getPreferredPlaceLink(place),
collections: [collectionId], collections: [collectionId],
is_public: false is_public: false
}) })
@@ -1149,6 +1199,7 @@
{#if hasPlaceResults(result)} {#if hasPlaceResults(result)}
<div class="grid gap-2"> <div class="grid gap-2">
{#each getPlaceResults(result) as place} {#each getPlaceResults(result) as place}
{@const placeLink = getPreferredPlaceLink(place)}
<div class="card card-compact bg-base-200 p-3"> <div class="card card-compact bg-base-200 p-3">
<h4 class="font-semibold">{place.name}</h4> <h4 class="font-semibold">{place.name}</h4>
{#if place.address} {#if place.address}
@@ -1160,6 +1211,18 @@
<span>{place.rating}</span> <span>{place.rating}</span>
</div> </div>
{/if} {/if}
{#if placeLink}
<div class="mt-2">
<a
href={placeLink}
target="_blank"
rel="noopener noreferrer"
class="btn btn-ghost btn-xs"
>
{$t('adventures.external_link')}
</a>
</div>
{/if}
{#if collectionId} {#if collectionId}
{@const isDuplicate = mergedLocationNames.has( {@const isDuplicate = mergedLocationNames.has(
normalizeLocationName(place.name) normalizeLocationName(place.name)

View File

@@ -20,6 +20,7 @@
price_level?: string | null; price_level?: string | null;
latitude?: number | string | null; latitude?: number | string | null;
longitude?: number | string | null; longitude?: number | string | null;
link?: string | null;
}; };
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -166,6 +167,40 @@
return null; return null;
} }
function normalizeHttpUrl(value: unknown): string | null {
if (typeof value !== 'string') return null;
const candidate = value.trim();
if (!candidate) return null;
try {
const parsed = new URL(candidate);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null;
}
return parsed.toString();
} catch {
return null;
}
}
function getPreferredSuggestionLink(item: Record<string, unknown>): string | null {
const links = asRecord(item.links);
return (
normalizeHttpUrl(item.link) ||
normalizeHttpUrl(item.preferred_link) ||
normalizeHttpUrl(item.url) ||
normalizeHttpUrl(item.website) ||
normalizeHttpUrl(item.map_link) ||
(links
? normalizeHttpUrl(links.link) ||
normalizeHttpUrl(links.preferred) ||
normalizeHttpUrl(links.official) ||
normalizeHttpUrl(links.website) ||
normalizeHttpUrl(links.map)
: null)
);
}
function normalizeSuggestionItem(value: unknown): SuggestionItem | null { function normalizeSuggestionItem(value: unknown): SuggestionItem | null {
const item = asRecord(value); const item = asRecord(value);
if (!item) return null; if (!item) return null;
@@ -191,6 +226,7 @@
const rating = normalizeRating(item.rating ?? item.score); const rating = normalizeRating(item.rating ?? item.score);
const latitude = normalizeCoordinate(item.latitude ?? item.lat); const latitude = normalizeCoordinate(item.latitude ?? item.lat);
const longitude = normalizeCoordinate(item.longitude ?? item.lon ?? item.lng); const longitude = normalizeCoordinate(item.longitude ?? item.lon ?? item.lng);
const link = getPreferredSuggestionLink(item);
const finalName = name || location; const finalName = name || location;
if (!finalName) return null; if (!finalName) return null;
@@ -204,7 +240,8 @@
rating, rating,
price_level: priceLevel || null, price_level: priceLevel || null,
latitude, latitude,
longitude longitude,
link
}; };
} }
@@ -225,6 +262,7 @@
const rating = normalizeRating(suggestion.rating); const rating = normalizeRating(suggestion.rating);
const latitude = normalizeCoordinate(suggestion.latitude); const latitude = normalizeCoordinate(suggestion.latitude);
const longitude = normalizeCoordinate(suggestion.longitude); const longitude = normalizeCoordinate(suggestion.longitude);
const link = normalizeHttpUrl(suggestion.link);
return { return {
name, name,
@@ -233,6 +271,7 @@
rating, rating,
latitude, latitude,
longitude, longitude,
link,
collections: [collection.id], collections: [collection.id],
is_public: Boolean(collection?.is_public) is_public: Boolean(collection?.is_public)
}; };
@@ -508,7 +547,17 @@
{/if} {/if}
</div> </div>
<div class="card-actions justify-end mt-3"> <div class="card-actions justify-between items-center mt-3">
{#if suggestion.link}
<a
href={suggestion.link}
target="_blank"
rel="noopener noreferrer"
class="btn btn-ghost btn-xs"
>
{$t('adventures.external_link')}
</a>
{/if}
<button <button
type="button" type="button"
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"