Skip to content

Commit aeb4e18

Browse files
committed
fix: hide edge bubbles when inside any country
1 parent b22e2fa commit aeb4e18

File tree

9 files changed

+86
-58
lines changed

9 files changed

+86
-58
lines changed

app/app.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,24 @@ body {
6666
[data-vaul-drawer][data-vaul-drawer-direction='bottom']::after {
6767
background: transparent !important;
6868
}
69+
70+
/* Handle styles with larger hit target */
71+
[data-vaul-handle] {
72+
display: block;
73+
position: relative;
74+
margin-left: auto;
75+
margin-right: auto;
76+
border-radius: 1rem;
77+
touch-action: pan-y;
78+
}
79+
80+
[data-vaul-handle-hitarea] {
81+
position: absolute;
82+
left: 50%;
83+
top: 50%;
84+
transform: translate(-50%, -50%);
85+
width: max(100%, 4rem);
86+
height: max(100%, 3rem);
87+
touch-action: inherit;
88+
}
6989
</style>

app/components/CountryBubbles.vue

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,6 @@ const isFlying = ref(false)
4545
// This prevents race condition where queryRenderedFeatures returns 0 while tiles are loading on slow devices
4646
const MIN_ZOOM_FOR_PINS = 9
4747
48-
// Show bubbles when: not flying, zoomed out enough, and nothing visible
49-
const showBubbles = computed(() => {
50-
if (isFlying.value)
51-
return false
52-
// Primary gate: if zoomed in enough for pins to render, never show bubbles
53-
if (zoom.value >= MIN_ZOOM_FOR_PINS)
54-
return false
55-
// Secondary check: at low zoom, only show if no clusters visible
56-
return locationCount.value === 0 && clusterCount.value === 0
57-
})
58-
5948
// Check if a point is within current map viewport
6049
function isPointInViewport(point: { lat: number, lng: number }): boolean {
6150
if (!mapInstance.value)
@@ -98,6 +87,21 @@ function isViewportWithinCountry(country: CountryHotspot): boolean {
9887
}
9988
}
10089
90+
// Check if viewport is outside all country bounds
91+
function isViewportOutsideAllCountries(): boolean {
92+
return COUNTRY_HOTSPOTS.every(country => !isViewportWithinCountry(country))
93+
}
94+
95+
// Show bubbles when: not flying, and outside all countries (never show edge bubbles when inside a country)
96+
const showBubbles = computed(() => {
97+
if (isFlying.value)
98+
return false
99+
100+
// Only show bubbles when outside ALL country bounds
101+
// When inside any country, don't show edge bubbles pointing to other countries
102+
return isViewportOutsideAllCountries()
103+
})
104+
101105
// Calculate rhumb line bearing (straight line on Mercator projection)
102106
function calculateMercatorBearing(from: { lat: number, lng: number }, to: { lat: number, lng: number }): number {
103107
const toRad = Math.PI / 180

app/components/LocationDrawer.vue

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function handleClose() {
6868
</script>
6969

7070
<template>
71-
<DrawerRoot v-model:open="isOpen" v-model:active-snap-point="snap" :snap-points :should-scale-background="false" :modal="false">
71+
<DrawerRoot v-model:open="isOpen" v-model:active-snap-point="snap" :snap-points :should-scale-background="false" :modal="false" handle-only>
7272
<DrawerPortal>
7373
<div
7474
v-if="showOverlay"
@@ -80,13 +80,18 @@ function handleClose() {
8080
@click="handleClose"
8181
/>
8282
<DrawerContent flex="~ col" shadow="[0_-4px_24px_rgba(0,0,0,0.1)]" max-h="[calc(100dvh-env(safe-area-inset-top))]" outline-none rounded-t-10 bg-neutral-0 h-full w-full inset-x-0 bottom-0 fixed z-80>
83-
<DrawerHandle left="1/2" translate-x="-1/2" m-0 bg-neutral-400 h-6 top-8 absolute z-30 />
83+
<!-- Header with handle and close button -->
84+
<div flex="~ items-center justify-center" pb-0 pt-8 relative>
85+
<DrawerHandle bg-neutral-400 h-6 w-40 />
86+
<button bg="neutral-500 hocus:neutral-600" stack rounded-full shrink-0 size-24 transition-colors right-16 absolute @click.stop="handleClose">
87+
<Icon name="i-nimiq:cross-bold" text-neutral-0 size-10 />
88+
</button>
89+
</div>
8490
<div v-if="selectedLocation" flex-1 min-h-0 of-hidden>
8591
<LocationDrawerContent
8692
:key="selectedLocation.uuid"
8793
:location="selectedLocation as any"
8894
:snap
89-
@close="handleClose"
9095
/>
9196
</div>
9297
<div v-else p-8 flex flex-1 justify-center>

app/components/LocationDrawerContent.vue

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ const { location, snap } = defineProps<{
66
snap: string | number | null
77
}>()
88
9-
const emit = defineEmits<{
10-
(e: 'close'): void
11-
}>()
12-
139
const { t, locale } = useI18n()
1410
1511
// Simple derived values
@@ -97,17 +93,10 @@ const { addressRef, showCopiedTooltip } = useAddressCopy()
9793
</script>
9894

9995
<template>
100-
<div bg-neutral-0 h-full w-full relative of-hidden flex="~ col">
96+
<div h-full w-full relative of-hidden flex="~ col">
10197
<!-- Scrollable content -->
102-
<div flex-1 of-x-hidden of-y-auto :class="isCompact ? 'max-h-[calc(450px-32px)]' : ''">
103-
<header pt-36 bg-neutral-0 relative f-px-md>
104-
<!-- Close Button -->
105-
<div flex="~ shrink-0 gap-8" right-16 top-4 absolute z-20>
106-
<button bg="neutral-500 hocus:neutral-600" stack rounded-full shrink-0 size-24 transition-colors @click.stop="emit('close')">
107-
<Icon name="i-nimiq:cross-bold" text-neutral-0 size-10 />
108-
</button>
109-
</div>
110-
98+
<div bg-neutral-0 flex-1 of-x-hidden of-y-auto :class="isCompact ? 'max-h-[calc(450px-32px)]' : ''">
99+
<header pt-16 bg-neutral-0 relative f-px-md>
111100
<!-- Title -->
112101
<h2 leading-tight font-bold my-0 pr-40 line-clamp-2 text="f-xl neutral">
113102
{{ location.name }}
@@ -117,6 +106,9 @@ const { addressRef, showCopiedTooltip } = useAddressCopy()
117106
<div flex="~ col gap-6" text-14 lh-none>
118107
<div v-if="primaryCategory || hasRating" mt-4 flex="~ wrap items-center gap-x-8 gap-y-4">
119108
<span v-if="primaryCategory" text-neutral-700 font-semibold>{{ primaryCategory.name }}</span>
109+
<template v-if="primaryCategory && hasRating">
110+
<div aria-hidden rounded-full bg-neutral-600 size-3 />
111+
</template>
120112
<div v-if="hasRating" flex="~ items-center gap-4">
121113
<Icon name="i-nimiq:star" :class="starColor" size-14 />
122114
<span font-medium :class="starColor">{{ location.rating!.toFixed(1) }}</span>

app/components/PhotoCarousel.vue

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ const availablePhotos = computed(() => Array.from({ length: MAX_PHOTOS }, (_, i)
1414
</script>
1515

1616
<template>
17-
<div v-if="availablePhotos.length > 0" flex="~ gap-8" scroll-pe-24 scroll-ps-24 of-x-auto nq-scrollbar-hide snap="x mandatory" empty="hidden !p-0 !m-0">
18-
<div v-for="(photoIndex, i) in availablePhotos" :key="photoIndex" shrink-0 snap-start first:ps-24 last:pe-24>
19-
<img
20-
:src="`/blob/location/${uuid}/${photoIndex}`"
21-
:alt="t('photo.alt', { number: photoIndex + 1 })"
22-
:loading="i === 0 ? 'eager' : 'lazy'"
23-
w-280 aspect-0.8 object-cover f-rounded-lg outline="1.5 offset--1.5 white/14"
24-
@error="onError(photoIndex)"
25-
>
26-
</div>
17+
<div v-if="availablePhotos.length > 0" flex="~ gap-8" pe-24 ps-24 of-x-auto nq-scrollbar-hide snap="x mandatory" empty="hidden !p-0 !m-0">
18+
<img
19+
v-for="(photoIndex, i) in availablePhotos"
20+
:key="photoIndex"
21+
:src="`/blob/location/${uuid}/${photoIndex}`"
22+
:alt="t('photo.alt', { number: photoIndex + 1 })"
23+
:loading="i === 0 ? 'eager' : 'lazy'"
24+
shrink-0 w-280 aspect-1.2 object-cover snap-center f-rounded-lg outline="1.5 offset--1.5 white/14"
25+
@error="onError(photoIndex)"
26+
>
2727
</div>
2828
</template>

app/composables/useMapControls.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Map } from 'maplibre-gl'
1+
import type { Map, PaddingOptions } from 'maplibre-gl'
22
import type { Point } from '~/types/geo'
33

44
const MIN_ZOOM = 3
@@ -118,7 +118,7 @@ function useMapControlsBase() {
118118
mapInstance.value?.zoomOut()
119119
}
120120

121-
function flyTo(point: Point, options?: { zoom?: number, accuracyMeters?: number }) {
121+
function flyTo(point: Point, options?: { zoom?: number, accuracyMeters?: number, padding?: PaddingOptions }) {
122122
const map = mapInstance.value
123123
if (!map)
124124
return
@@ -130,7 +130,7 @@ function useMapControlsBase() {
130130
zoomLevel = calculateZoomForAccuracy(point.lat, options.accuracyMeters, minViewportSize)
131131
}
132132

133-
map.flyTo({ center: [point.lng, point.lat], zoom: zoomLevel, duration: 1000 })
133+
map.flyTo({ center: [point.lng, point.lat], zoom: zoomLevel, duration: 1000, ...(options?.padding && { padding: options.padding }) })
134134
}
135135

136136
function resetNorth() {

app/composables/useMapIcons.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,15 @@ export function useMapIcons() {
101101
'maxzoom': 24,
102102
'layout': {
103103
'icon-image': iconExpression,
104-
'icon-size': 0.057,
104+
'icon-size': 0.07125,
105105
'icon-allow-overlap': false,
106106
'icon-anchor': 'bottom',
107107
'icon-offset': [0, 0],
108108
'symbol-sort-key': ['-', 0, ['coalesce', ['get', 'rating'], 0]],
109109
'text-field': ['get', 'name'],
110110
'text-font': ['Mulish Regular'],
111111
'text-anchor': 'left',
112-
'text-offset': [0.9, -0.9],
112+
'text-offset': [1.1, -1.1],
113113
'text-justify': 'left',
114114
'text-size': 14,
115115
'text-optional': true, // Hide text if it collides, but keep the icon
@@ -136,7 +136,7 @@ export function useMapIcons() {
136136
'maxzoom': 24,
137137
'layout': {
138138
'icon-image': 'active',
139-
'icon-size': 0.0855,
139+
'icon-size': 0.107,
140140
'icon-allow-overlap': true, // Always visible
141141
'icon-ignore-placement': false, // Block other icons from rendering here
142142
'icon-anchor': 'bottom',
@@ -145,7 +145,7 @@ export function useMapIcons() {
145145
'text-field': ['get', 'name'],
146146
'text-font': ['Mulish Regular'],
147147
'text-anchor': 'left',
148-
'text-offset': [0.9, -0.9],
148+
'text-offset': [1.1, -1.1],
149149
'text-justify': 'left',
150150
'text-size': 16,
151151
'text-allow-overlap': true, // Always show text
@@ -288,8 +288,8 @@ export function useMapIcons() {
288288
const iconSizeExpression = [
289289
'case',
290290
['in', ['get', 'uuid'], ['literal', uuids]],
291-
0.0855,
292-
0.057,
291+
0.107,
292+
0.07125,
293293
]
294294
map.setLayoutProperty('location-icon', 'icon-size', iconSizeExpression as any)
295295

@@ -324,7 +324,7 @@ export function useMapIcons() {
324324
else {
325325
// Reset to normal styling
326326
map.setLayoutProperty('location-icon', 'icon-image', buildIconExpression() as any)
327-
map.setLayoutProperty('location-icon', 'icon-size', 0.057)
327+
map.setLayoutProperty('location-icon', 'icon-size', 0.07125)
328328
map.setLayoutProperty('location-icon', 'symbol-sort-key', ['-', 0, ['coalesce', ['get', 'rating'], 0]] as any)
329329
map.setLayoutProperty('location-icon', 'text-size', 14)
330330
map.setPaintProperty('location-icon', 'text-color', buildColorExpression() as any)

app/composables/useMapUrl.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { useDebounceFn } from '@vueuse/core'
22

3-
// Parse #@lat,lng,zoomz format from hash
3+
// Parse #lat,lng,zoomz format from hash
44
function parseMapHash(hash: string): { lat?: number, lng?: number, zoom?: number, bearing?: number, pitch?: number } {
5-
const match = hash.match(/^#@(-?\d+(?:\.\d*)?),(-?\d+(?:\.\d*)?),(\d+(?:\.\d*)?)z(?:,(\d+(?:\.\d*)?)b)?(?:,(\d+(?:\.\d*)?)p)?/)
5+
const match = hash.match(/^#(-?\d+(?:\.\d*)?),(-?\d+(?:\.\d*)?),(\d+(?:\.\d*)?)z(?:,(\d+(?:\.\d*)?)b)?(?:,(\d+(?:\.\d*)?)p)?/)
66
if (!match || !match[1] || !match[2] || !match[3])
77
return {}
88
return {
@@ -14,12 +14,12 @@ function parseMapHash(hash: string): { lat?: number, lng?: number, zoom?: number
1414
}
1515
}
1616

17-
// Build hash in #@lat,lng,zoomz format
17+
// Build hash in #lat,lng,zoomz format
1818
function buildMapHash(lat: number, lng: number, zoom: number, bearing?: number, pitch?: number): string {
1919
const latStr = lat.toFixed(5)
2020
const lngStr = lng.toFixed(5)
2121
const zoomStr = zoom.toFixed(2).replace(/\.?0+$/, '')
22-
let hash = `#@${latStr},${lngStr},${zoomStr}z`
22+
let hash = `#${latStr},${lngStr},${zoomStr}z`
2323
if (bearing && bearing !== 0)
2424
hash += `,${bearing.toFixed(0)}b`
2525
if (pitch && pitch !== 0)

app/pages/index.vue

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,20 +155,15 @@ watch([searchResults, mapInstance], ([results, map]) => {
155155
156156
function handleNavigate(uuid: string | undefined, latitude: number, longitude: number) {
157157
if (uuid) {
158-
// Location result - open drawer
159158
selectedLocationUuid.value = uuid
160159
isDrawerOpen.value = true
161160
162161
if (mapInstance.value) {
163162
setSelectedLocation(mapInstance.value as any, uuid)
164-
const bounds = mapInstance.value.getBounds()
165-
if (!bounds.contains([longitude, latitude])) {
166-
flyTo({ lat: latitude, lng: longitude }, { zoom: 14 })
167-
}
163+
flyTo({ lat: latitude, lng: longitude }, { zoom: 14, padding: { bottom: 450 } })
168164
}
169165
}
170166
else {
171-
// Geo result - just pan to location
172167
flyTo({ lat: latitude, lng: longitude }, { zoom: 12 })
173168
}
174169
}
@@ -179,6 +174,18 @@ function handleMarkerClick(uuid: string) {
179174
isDrawerOpen.value = true
180175
if (mapInstance.value) {
181176
setSelectedLocation(mapInstance.value as any, uuid)
177+
178+
// If marker is in bottom half, pan up so it's visible above drawer
179+
const features = mapInstance.value.querySourceFeatures('locations', { filter: ['==', ['get', 'uuid'], uuid] })
180+
const feature = features[0]
181+
if (feature?.geometry?.type === 'Point') {
182+
const coords = feature.geometry.coordinates as [number, number]
183+
const point = mapInstance.value.project(coords)
184+
const containerHeight = mapInstance.value.getContainer().clientHeight
185+
if (point.y > containerHeight / 2) {
186+
flyTo({ lat: coords[1], lng: coords[0] }, { zoom: mapInstance.value.getZoom(), padding: { bottom: 450 } })
187+
}
188+
}
182189
}
183190
}
184191

0 commit comments

Comments
 (0)