Skip to content

Commit e657f2b

Browse files
authored
Merge pull request #129 from CityOfPhiladelphia/test
Test
2 parents df6273c + 120bd2f commit e657f2b

66 files changed

Lines changed: 3893 additions & 1713 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.beads/issues.jsonl

Lines changed: 2 additions & 0 deletions
Large diffs are not rendered by default.

apps/oem-flood-finder/frontend/src/composables/useLocations.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ref, computed, onBeforeMount, type Ref, type ComputedRef } from 'vue'
1+
import { ref, computed, markRaw, onBeforeMount, type Ref, type ComputedRef } from 'vue'
22
import { IconWater, IconCamera } from '@phila/phila-ui-core/icons'
33
import { PinboardUtilities } from '@pinboard/ui'
44
import type { MapCardProps } from '@phila/phila-ui-cards'
@@ -66,13 +66,13 @@ export function useLocations(): {
6666

6767
function getLocationTags(loc: LocationPanelDTO): NonNullable<MapCardProps['tags']> {
6868
if (loc.deviceType === 'Camera') {
69-
return [{ text: 'Camera', color: 'purple' as const, icon: IconCamera }]
69+
return [{ text: 'Camera', color: 'purple' as const, icon: markRaw(IconCamera) }]
7070
}
7171
const gaugeValue =
7272
Number.isNaN(loc.gaugeHeight) || loc.gaugeHeight === -9999.9
7373
? 'No data'
7474
: `${loc.gaugeHeight} ${loc.gaugeHeightUnit}`
75-
return [{ text: 'Gauge', color: 'blue' as const, icon: IconWater }, { text: gaugeValue }]
75+
return [{ text: 'Gauge', color: 'blue' as const, icon: markRaw(IconWater) }, { text: gaugeValue }]
7676
}
7777

7878
async function getLocationsProxy(errorMessageRef: Ref) {

apps/oem-flood-finder/frontend/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ app.use(router)
1010
app.use(i18n)
1111
app.use(
1212
createPinboard({
13+
appId: 'oem-flood',
1314
title: '',
1415
map: {
1516
center: [-75.12, 39.98],

apps/oem-flood-finder/frontend/src/views/FinderView.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
MapIconTextPin,
1414
MapNavigationControl,
1515
BasemapToggle,
16+
GeolocationButton,
1617
FillLayer,
1718
MapCheckboxLegend,
1819
PinboardComposables,
@@ -160,6 +161,10 @@ function handleDeselect(id: string) {
160161
visitedIds.value.add(id)
161162
}
162163
164+
function handleGeolocate(data: { latitude: number; longitude: number; accuracy: number }) {
165+
userLocation.value = { latitude: data.latitude, longitude: data.longitude }
166+
}
167+
163168
function asOemLocation(location: PinboardTypes.BasicLocation) {
164169
return location as OemLocation
165170
}
@@ -201,6 +206,15 @@ function asOemLocation(location: PinboardTypes.BasicLocation) {
201206
>
202207
<MapNavigationControl v-if="!isMobile" position="bottom-right" />
203208
<BasemapToggle position="top-right" :teleport-to="isMobile ? mobileControlsTarget : null" />
209+
<!-- No @error handler by design: GeolocationButton shows its own callout
210+
when location is blocked; other geolocation errors are non-blocking
211+
since the finder works without location. -->
212+
<GeolocationButton
213+
:position="isMobile ? 'top-right' : 'bottom-right'"
214+
:teleport-to="isMobile ? mobileControlsTarget : undefined"
215+
:show-location-marker="false"
216+
@located="handleGeolocate"
217+
/>
204218

205219
<FillLayer
206220
v-for="id in FLOOD_LAYER_IDS"

apps/primary-care-finder/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"format:check": "prettier --check ."
1717
},
1818
"dependencies": {
19+
"@phila/phila-ui-bottom-sheet": "0.1.0-beta.14",
1920
"@phila/phila-ui-button": "2.3.0-beta.9",
2021
"@phila/phila-ui-core": "3.0.0-beta.8",
2122
"@phila/phila-ui-tags": "0.1.0-beta.9",

apps/primary-care-finder/frontend/src/App.vue

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
<script setup lang="ts">
2-
import { PinboardShell, languages } from '@pinboard/ui'
2+
import { PinboardShell, PinboardComposables, languages } from '@pinboard/ui'
33
import '@pinboard/ui/style.css'
4+
import { BottomSheet } from '@phila/phila-ui-bottom-sheet'
5+
import { CloseButton } from '@phila/phila-ui-button'
6+
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
7+
import { useRoute, useRouter } from 'vue-router'
48
import { useI18n } from 'vue-i18n'
59
import { useLocale } from './composables/useLocale'
610
@@ -10,6 +14,73 @@ init()
1014
1115
const feedbackHref =
1216
'https://www.phila.gov/departments/department-of-public-health/about-us/contact-us/#send-us-a-message'
17+
18+
const infoSheetOpen = ref(false)
19+
20+
function closeInfoSheet() {
21+
infoSheetOpen.value = false
22+
dragY.value = 0
23+
}
24+
25+
/* Drag-down-to-dismiss. BottomSheet's built-in drag is snap-point based
26+
* and a single snap point ([60]) clamps it to no movement, so we layer
27+
* our own pointer tracking on top: translate the sheet to follow the
28+
* pointer, dismiss past DRAG_DISMISS_THRESHOLD on release, otherwise
29+
* spring back. Clicks (zero delta) pass through. */
30+
const DRAG_DISMISS_THRESHOLD = 160
31+
const dragY = ref(0)
32+
const isDraggingSheet = ref(false)
33+
let dragStartY = 0
34+
35+
function onSheetPointerDown(e: PointerEvent) {
36+
dragStartY = e.clientY
37+
dragY.value = 0
38+
isDraggingSheet.value = true
39+
document.addEventListener('pointermove', onSheetPointerMove)
40+
document.addEventListener('pointerup', onSheetPointerUp)
41+
document.addEventListener('pointercancel', onSheetPointerUp)
42+
}
43+
44+
function onSheetPointerMove(e: PointerEvent) {
45+
if (!isDraggingSheet.value) return
46+
dragY.value = Math.max(0, e.clientY - dragStartY)
47+
}
48+
49+
function onSheetPointerUp() {
50+
if (!isDraggingSheet.value) return
51+
isDraggingSheet.value = false
52+
document.removeEventListener('pointermove', onSheetPointerMove)
53+
document.removeEventListener('pointerup', onSheetPointerUp)
54+
document.removeEventListener('pointercancel', onSheetPointerUp)
55+
if (dragY.value > DRAG_DISMISS_THRESHOLD) {
56+
closeInfoSheet()
57+
} else {
58+
dragY.value = 0
59+
}
60+
}
61+
62+
const isMobile = PinboardComposables.useIsMobile()
63+
64+
watch(isMobile, (mobile) => {
65+
if (!mobile) infoSheetOpen.value = false
66+
})
67+
68+
const route = useRoute()
69+
const router = useRouter()
70+
71+
onMounted(async () => {
72+
await router.isReady()
73+
74+
if (route.path === '/' && isMobile.value) {
75+
infoSheetOpen.value = true
76+
}
77+
})
78+
79+
onBeforeUnmount(() => {
80+
document.removeEventListener('pointermove', onSheetPointerMove)
81+
document.removeEventListener('pointerup', onSheetPointerUp)
82+
document.removeEventListener('pointercancel', onSheetPointerUp)
83+
})
1384
</script>
1485

1586
<template>
@@ -31,6 +102,28 @@ const feedbackHref =
31102
>
32103
<RouterView />
33104
</PinboardShell>
105+
106+
<Teleport to="body">
107+
<Transition name="scrim-fade">
108+
<div v-if="infoSheetOpen" class="info-sheet-scrim" @click="closeInfoSheet" />
109+
</Transition>
110+
<BottomSheet
111+
v-if="infoSheetOpen"
112+
v-model="infoSheetOpen"
113+
class="info-sheet"
114+
:class="{ 'info-sheet--dragging': isDraggingSheet }"
115+
:style="{ zIndex: 101, '--drag-y': `${dragY}px` }"
116+
:snap-points="[60]"
117+
@pointerdown="onSheetPointerDown"
118+
>
119+
<CloseButton class="info-sheet-close" @click="closeInfoSheet" />
120+
<h2 class="has-text-heading-5">{{ t('callout.title') }}</h2>
121+
<span class="has-text-body-small">
122+
{{ t('callout.message') }}
123+
<RouterLink to="/info" @click="closeInfoSheet">{{ t('callout.linkText') }}</RouterLink>
124+
</span>
125+
</BottomSheet>
126+
</Teleport>
34127
</template>
35128

36129
<style>
@@ -49,4 +142,46 @@ const feedbackHref =
49142
overflow: visible !important;
50143
overflow-x: clip !important;
51144
}
145+
146+
.info-sheet-scrim {
147+
position: fixed;
148+
inset: 0;
149+
z-index: 100;
150+
background: rgba(0, 0, 0, 0.25);
151+
}
152+
153+
/* Sheet sizes to its content; snap-points value is ignored visually.
154+
* --drag-y is set inline by the drag handler; transform-only transition
155+
* springs the sheet back when the user releases under threshold, while
156+
* keeping height static (animating to auto doesn't work cleanly). */
157+
.info-sheet .bottom-sheet {
158+
height: auto !important;
159+
max-height: 90dvh;
160+
padding: 0 var(--spacing-m) 50px;
161+
transform: translateY(var(--drag-y, 0px));
162+
transition: transform 0.25s ease-out !important;
163+
}
164+
165+
.info-sheet.info-sheet--dragging .bottom-sheet {
166+
transition: none !important;
167+
}
168+
169+
.scrim-fade-leave-active {
170+
transition: opacity 0.25s ease-out;
171+
pointer-events: none;
172+
}
173+
174+
.scrim-fade-leave-to {
175+
opacity: 0;
176+
}
177+
178+
.info-sheet-close {
179+
position: absolute;
180+
top: 8px;
181+
right: 12px;
182+
}
183+
184+
.info-sheet h2 {
185+
margin-bottom: var(--spacing-s);
186+
}
52187
</style>

apps/primary-care-finder/frontend/src/components/LocationCard.vue

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { PhilaLink } from '@pinboard/ui'
33
import { IconPhone, IconLocationDot, IconGlobe } from '@phila/phila-ui-core/icons'
44
import type { PrimaryCareLocation } from '@/types'
5+
import { formatFullAddress } from '@/utilities/formatAddress'
56
import LocationTags from './LocationTags.vue'
67
78
defineProps<{
@@ -17,22 +18,36 @@ function mapsUrl(location: PrimaryCareLocation): string {
1718

1819
<template>
1920
<div class="card-content">
20-
<strong class="card-heading">{{ location.name }}</strong>
21+
<span class="card-heading has-text-label-default">{{ location.name }}</span>
2122
<span v-if="location.locationCardInfo.subheader" class="card-distance">
2223
{{ location.locationCardInfo.subheader }}
2324
</span>
2425
<LocationTags :location="location" class="card-tags" />
2526
<div class="card-links">
26-
<PhilaLink
27-
v-if="location.properties.med_phone_num"
28-
:href="`tel:${location.properties.med_phone_num}`"
29-
:icon="IconPhone"
30-
size="small"
31-
class="card-link"
32-
@click.stop
33-
>
34-
{{ location.properties.med_phone_num }}
35-
</PhilaLink>
27+
<div class="card-links-primary">
28+
<PhilaLink
29+
v-if="location.properties.med_phone_num"
30+
:href="`tel:${location.properties.med_phone_num}`"
31+
:icon="IconPhone"
32+
size="small"
33+
class="card-link"
34+
@click.stop
35+
>
36+
{{ location.properties.med_phone_num }}
37+
</PhilaLink>
38+
<PhilaLink
39+
v-if="location.properties.website"
40+
:href="location.properties.website"
41+
:icon="IconGlobe"
42+
size="small"
43+
target="_blank"
44+
rel="noopener noreferrer"
45+
class="card-link"
46+
@click.stop
47+
>
48+
{{ $t('providerWebsite') }}
49+
</PhilaLink>
50+
</div>
3651
<PhilaLink
3752
v-if="location.properties.address"
3853
:href="mapsUrl(location)"
@@ -43,19 +58,7 @@ function mapsUrl(location: PrimaryCareLocation): string {
4358
class="card-link"
4459
@click.stop
4560
>
46-
{{ location.properties.address }}
47-
</PhilaLink>
48-
<PhilaLink
49-
v-if="location.properties.website"
50-
:href="location.properties.website"
51-
:icon="IconGlobe"
52-
size="small"
53-
target="_blank"
54-
rel="noopener noreferrer"
55-
class="card-link card-link--full"
56-
@click.stop
57-
>
58-
{{ $t('providerWebsite') }}
61+
{{ formatFullAddress(location.properties) }}
5962
</PhilaLink>
6063
</div>
6164
</div>
@@ -69,36 +72,52 @@ function mapsUrl(location: PrimaryCareLocation): string {
6972
gap: 0.25rem;
7073
}
7174
75+
/* The site name is a span, so it misses the design system's h1-h6 heading
76+
balancing. Balance it here to match the detail panel's heading. */
7277
.card-heading {
73-
font-size: var(--Heading-H5-font-heading-5-size);
74-
line-height: var(--Heading-H5-font-heading-5-lineheight);
78+
text-wrap: balance;
7579
}
7680
7781
.card-distance {
78-
font-size: 0.875rem;
82+
font-size: var(--Body-Small-font-body-small-size);
7983
color: var(--Schemes-On-Surface-Variant, #666);
8084
}
8185
86+
/* Two columns: the phone + website stack on the left, the address on the right —
87+
the address wraps within its column and stays there at every width. */
8288
.card-links {
8389
display: grid;
8490
grid-template-columns: 1fr 1fr;
91+
align-items: start;
8592
justify-items: start;
8693
gap: 0.25rem 0.5rem;
8794
margin-top: 0.25rem;
8895
}
8996
97+
.card-links-primary {
98+
display: flex;
99+
flex-direction: column;
100+
gap: 0.75rem;
101+
min-width: 0;
102+
}
103+
104+
/* The link is a flex row (.phila-link). min-width:0 lets the grid column shrink and
105+
flex-start keeps the icon at the top once the text wraps to a second line. */
90106
.card-link {
91-
overflow: hidden;
92-
text-overflow: ellipsis;
93-
white-space: nowrap;
107+
min-width: 0;
108+
align-items: flex-start;
94109
}
95110
96-
.card-link :deep(.phila-icon-core) {
97-
color: var(--Schemes-On-Surface-Low);
111+
/* Let the link's text (the unclassed span ActionContent renders) wrap within its
112+
column instead of forcing the column wider. */
113+
.card-link :deep(span:not(.phila-icon-core)) {
114+
min-width: 0;
115+
overflow-wrap: anywhere;
98116
}
99117
100-
.card-link--full {
101-
grid-column: 1 / -1;
118+
.card-link :deep(.phila-icon-core) {
119+
color: var(--Schemes-On-Surface-Low);
120+
flex-shrink: 0;
102121
}
103122
104123
.card-tags {

0 commit comments

Comments
 (0)