atmo.rsvp
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add open with OSM link and copyable coordinates

01001100 036f4987 be379673

+96 -35
+2 -2
src/lib/components/EventView.svelte
··· 25 25 import EventLinksList from './event-view/EventLinksList.svelte'; 26 26 import AddToCalendarButton from './event-view/AddToCalendarButton.svelte'; 27 27 import InviteShareFlow from './event-view/InviteShareFlow.svelte'; 28 - import { buildDescriptionHtml, getLocationData, resolveGeoLocation } from './event-view/format'; 28 + import { buildDescriptionHtml, getLocationData, resolveGeoLocation, type GeoLocation } from './event-view/format'; 29 29 30 30 let { data } = $props(); 31 31 ··· 50 50 let endDate = $derived(eventData.endsAt ? new Date(eventData.endsAt) : null); 51 51 52 52 let locationData = $derived(getLocationData(eventData.locations)); 53 - let geoLocation: { lat: number; lng: number } | null = $state(null); 53 + let geoLocation: GeoLocation | null = $state(null); 54 54 55 55 let showShareModal = $state(false); 56 56 let shareModalTitle = $state('Event created!');
+1 -1
src/lib/components/event-view/EventLocationBlock.svelte
··· 6 6 7 7 {#if locationData} 8 8 <a 9 - href={locationData.mapsUrl} 9 + href={locationData.googleMapsUrl} 10 10 target="_blank" 11 11 rel="noopener noreferrer" 12 12 class="mb-6 flex items-center gap-4 transition-opacity hover:opacity-80"
+56 -19
src/lib/components/event-view/EventLocationMap.svelte
··· 1 1 <script lang="ts"> 2 + import { Badge } from '@foxui/core'; 2 3 import Map from '$lib/components/Map.svelte'; 3 - import type { LocationData } from './format'; 4 + import type { LocationData, GeoLocation } from './format'; 4 5 5 6 let { 6 7 locationData, 7 8 geoLocation 8 9 }: { 9 10 locationData: LocationData | null; 10 - geoLocation: { lat: number; lng: number } | null; 11 + geoLocation: GeoLocation | null; 11 12 } = $props(); 13 + 14 + let copied = $state(false); 15 + 16 + async function copyCoords() { 17 + if (!geoLocation) return; 18 + const text = `${geoLocation.lat.toFixed(5)}, ${geoLocation.lng.toFixed(5)}`; 19 + try { 20 + await navigator.clipboard.writeText(text); 21 + copied = true; 22 + setTimeout(() => (copied = false), 2000); 23 + } catch {} 24 + } 12 25 </script> 13 26 14 27 {#if geoLocation && locationData} 15 28 <div class="mt-8 mb-8"> 16 - <p 17 - class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase" 18 - > 19 - Location 29 + <div class="mb-3 flex items-baseline gap-2"> 30 + <p class="text-base-500 dark:text-base-400 text-xs font-semibold tracking-wider uppercase"> 31 + Location: 32 + </p> 33 + <button 34 + type="button" 35 + onclick={copyCoords} 36 + class="ml-auto cursor-pointer transition-opacity active:opacity-60" 37 + title="Copy coordinates" 38 + aria-label="Copy coordinates" 39 + > 40 + <Badge size="sm" variant="secondary" class="font-mono"> 41 + {copied 42 + ? 'Copied!' 43 + : `${geoLocation.lat.toFixed(5)}, ${geoLocation.lng.toFixed(5)}`} 44 + </Badge> 45 + </button> 46 + </div> 47 + <div class="h-64 w-full overflow-hidden rounded-xl"> 48 + <Map lat={geoLocation.lat} lng={geoLocation.lng} /> 49 + </div> 50 + <p class="text-base-700 dark:text-base-200 mt-3 text-sm">{locationData.fullString}</p> 51 + <p class="text-base-500 dark:text-base-400 mt-1 text-xs"> 52 + Open in 53 + <a 54 + href={geoLocation.googleMapsUrl} 55 + target="_blank" 56 + rel="noopener noreferrer" 57 + class="text-base-700 dark:text-base-300 hover:underline" 58 + > 59 + Google Maps 60 + </a> 61 + | 62 + <a 63 + href={geoLocation.osmUrl} 64 + target="_blank" 65 + rel="noopener noreferrer" 66 + class="text-base-700 dark:text-base-300 hover:underline" 67 + > 68 + OpenStreetMap 69 + </a> 20 70 </p> 21 - <a 22 - href={locationData.mapsUrl} 23 - target="_blank" 24 - rel="noopener noreferrer" 25 - class="block transition-opacity hover:opacity-80" 26 - > 27 - <div class="h-64 w-full overflow-hidden rounded-xl"> 28 - <Map lat={geoLocation.lat} lng={geoLocation.lng} /> 29 - </div> 30 - <p class="text-base-500 dark:text-base-400 mt-2 text-sm"> 31 - {locationData.fullString} 32 - </p> 33 - </a> 34 71 </div> 35 72 {/if}
+37 -13
src/lib/components/event-view/format.ts
··· 45 45 shortAddress: string; 46 46 fullAddress: string; 47 47 fullString: string; 48 - mapsUrl: string; 48 + googleMapsUrl: string; 49 49 }; 50 50 51 51 export function getLocationData(locations: FlatEventRecord['locations']): LocationData | null { 52 - if (!locations || locations.length === 0) return null; 52 + if (!locations?.length) return null; 53 53 54 54 const loc = locations.find((v) => v.$type === 'community.lexicon.location.address') as 55 55 | { name?: string; street?: string; locality?: string; region?: string; country?: string } ··· 64 64 const fullAddress = fullParts.join(', '); 65 65 const displayName = loc.name || undefined; 66 66 const fullString = displayName ? `${displayName}, ${fullAddress}` : fullAddress; 67 - const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullString)}`; 67 + const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullString)}`; 68 + 69 + return { name: displayName, shortAddress, fullAddress, fullString, googleMapsUrl }; 70 + } 71 + 72 + export type GeoLocation = { 73 + lat: number; 74 + lng: number; 75 + googleMapsUrl: string; 76 + osmUrl: string; 77 + }; 68 78 69 - return { name: displayName, shortAddress, fullAddress, fullString, mapsUrl }; 79 + function geoUrls(lat: number, lng: number, osmType?: string, osmId?: number) { 80 + return { 81 + googleMapsUrl: `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`, 82 + osmUrl: 83 + osmType && osmId 84 + ? `https://www.openstreetmap.org/${osmType}/${osmId}` 85 + : `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=17/${lat}/${lng}` 86 + }; 70 87 } 71 88 72 89 export async function resolveGeoLocation( 73 90 locations: FlatEventRecord['locations'], 74 91 locationData: LocationData | null 75 - ): Promise<{ lat: number; lng: number } | null> { 76 - if (!locations || locations.length === 0) return null; 92 + ): Promise<GeoLocation | null> { 93 + if (!locations?.length) return null; 77 94 78 95 const geo = locations.find((v) => v.$type === 'community.lexicon.location.geo') as 79 96 | { latitude?: string; longitude?: string } ··· 81 98 if (geo?.latitude && geo?.longitude) { 82 99 const lat = parseFloat(geo.latitude); 83 100 const lng = parseFloat(geo.longitude); 84 - if (!isNaN(lat) && !isNaN(lng)) return { lat, lng }; 101 + if (!isNaN(lat) && !isNaN(lng)) return { lat, lng, ...geoUrls(lat, lng) }; 85 102 } 86 103 87 - const addressQuery = locationData?.fullAddress; 88 - if (!addressQuery) return null; 104 + if (!locationData?.fullAddress) return null; 89 105 90 106 try { 91 - const r = await fetch(`/api/geocoding?q=${encodeURIComponent(addressQuery)}`); 107 + const r = await fetch(`/api/geocoding?q=${encodeURIComponent(locationData.fullAddress)}`); 92 108 if (!r.ok) return null; 93 - const data = (await r.json()) as Record<string, unknown> | null; 94 - if (!data || !data.lat || !data.lon) return null; 95 - return { lat: parseFloat(data.lat as string), lng: parseFloat(data.lon as string) }; 109 + const data = (await r.json()) as { 110 + lat?: string; 111 + lon?: string; 112 + osm_type?: string; 113 + osm_id?: number; 114 + } | null; 115 + if (!data?.lat || !data?.lon) return null; 116 + const lat = parseFloat(data.lat); 117 + const lng = parseFloat(data.lon); 118 + if (isNaN(lat) || isNaN(lng)) return null; 119 + return { lat, lng, ...geoUrls(lat, lng, data.osm_type, data.osm_id) }; 96 120 } catch { 97 121 return null; 98 122 }