Skip to content

Commit

Permalink
feat(pickup): support dragging map to load more locations
Browse files Browse the repository at this point in the history
Note: only supported for PostNL
  • Loading branch information
EdieLemoine committed Jul 10, 2024
1 parent e2ac4c7 commit 4d5c938
Show file tree
Hide file tree
Showing 34 changed files with 580 additions and 200 deletions.
3 changes: 2 additions & 1 deletion apps/delivery-options/src/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ exports[`exports > exports from index.ts 1`] = `
"createCarrierMarkerIcon",
"createDeliveryTypeTranslatable",
"createGetDeliveryOptionsParameters",
"createLatLngParameters",
"createNextDate",
"createPackageTypeTranslatable",
"createRelativeDateFormatter",
Expand All @@ -77,7 +78,6 @@ exports[`exports > exports from index.ts 1`] = `
"getConfigPriceKey",
"getDefaultAddress",
"getDeliveryTypePrice",
"getFullPickupLocation",
"getResolvedCarrier",
"getResolvedDeliveryType",
"getResolvedValue",
Expand All @@ -103,6 +103,7 @@ exports[`exports > exports from index.ts 1`] = `
"useMostEcoFriendly",
"useOptionsGroupedByCarrier",
"usePickupLocation",
"usePickupLocationsMap",
"useProvideElementWidth",
"useResolvedCarrier",
"useResolvedDeliveryDates",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {waitFor} from '@testing-library/vue';
import {useResolvedPickupLocations} from '../../composables';

export const waitForPickupLocations = async (): Promise<void> => {
const locations = useResolvedPickupLocations();
const {locations} = useResolvedPickupLocations();

await waitFor(() => locations.value.length > 0, {timeout: 1000});
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</template>

<template #default>
<LeafletMapInner v-bind="$props">
<LeafletMapInner>
<slot />
</LeafletMapInner>
</template>
Expand All @@ -19,8 +19,4 @@
<script lang="ts" setup>
import {Loader} from '@myparcel-do/shared';
import LeafletMapInner from '../LeafletMapInner/LeafletMapInner.vue';
import {type LeafletMapProps} from '../../../types';
// eslint-disable-next-line vue/no-unused-properties
defineProps<LeafletMapProps>();
</script>
Original file line number Diff line number Diff line change
@@ -1,112 +1,48 @@
<template>
<div
ref="container"
:style="{height}">
style="height: 100%">
<LeafletMapLoadMoreButton />

<slot />
</div>
</template>

<script lang="ts" setup>
/* eslint-disable new-cap */
import {computed, onActivated, onMounted, onUnmounted, provide, ref, toRefs, watch} from 'vue';
import {isString} from 'radash';
import {type Control, type Map, type Marker, type TileLayer, type LatLng, type FeatureGroup} from 'leaflet';
import {isDef, useScriptTag, useStyleTag, useDebounceFn} from '@vueuse/core';
import {type MapTileLayerData} from '@myparcel-do/shared';
import {type LeafletMapProps} from '../../../types';
import {useConfigStore} from '../../../stores';
import {MAP_MARKER_CLASS_ACTIVE} from '../../../data';
// eslint-disable-next-line vue/no-unused-properties
const props = withDefaults(defineProps<LeafletMapProps>(), {
zoom: 14,
height: '100%',
center: () => [0, 0],
});
const propRefs = toRefs(props);
import {ref, onMounted, toValue, onUnmounted, onActivated} from 'vue';
import {asyncComputed, useStyleTag, useScriptTag} from '@vueuse/core';
import {usePickupLocationsMap} from '../../../composables';
import LeafletMapLoadMoreButton from './LeafletMapLoadMoreButton.vue';
const unmountHooks = [];
const container = ref<HTMLElement>();
const map = ref<Map>();
const tileLayer = ref<TileLayer>();
const scale = ref<Control.Scale>();
const markers = ref<Marker[]>([]);
provide('map', map);
provide('tileLayer', tileLayer);
provide('scale', scale);
provide('markers', markers);
const css = await (await fetch('https://cdn.jsdelivr.net/npm/leaflet@1/dist/leaflet.min.css')).text();
useStyleTag(css);
const tag = useScriptTag('https://cdn.jsdelivr.net/npm/leaflet@1/dist/leaflet.js');
await tag.load();
const config = useConfigStore();
const tileLayerData = computed<MapTileLayerData>(() => {
if (isString(config.pickupLocationsMapTileLayerData)) {
return JSON.parse(config.pickupLocationsMapTileLayerData);
}
return config.pickupLocationsMapTileLayerData;
const css = asyncComputed(async () => {
return (await fetch('https://cdn.jsdelivr.net/npm/leaflet@1/dist/leaflet.min.css')).text();
});
const getActiveMarkerLatLng = (): undefined | LatLng => {
const activeMarker = markers.value.find((marker) => marker.getElement()?.classList.contains(MAP_MARKER_CLASS_ACTIVE));
return activeMarker?.getLatLng();
};
const styleTag = useStyleTag(css);
const scriptTag = useScriptTag('https://cdn.jsdelivr.net/npm/leaflet@1/dist/leaflet.js');
const fitBounds = useDebounceFn(() => {
if (!container.value) {
return;
}
const {initializeMap, activeMarker, center, map} = usePickupLocationsMap();
const group: FeatureGroup = new L.featureGroup(markers.value as Marker[]);
onMounted(async () => {
styleTag.load();
await scriptTag.load();
map.value?.setView(getActiveMarkerLatLng() ?? props.center, props.zoom);
const teardownMap = initializeMap(container);
const bounds = group.getBounds();
if (bounds.isValid()) {
map.value?.fitBounds(bounds);
}
}, 50);
onMounted(() => {
if (!isDef(container.value)) {
return;
}
const {center, scroll, zoom} = propRefs;
const {attribution, maxZoom, url, token} = tileLayerData.value;
map.value = new L.Map(container.value, {preferCanvas: true, scrollWheelZoom: scroll.value});
if (!isDef(map.value)) {
return;
}
map.value.setView(center.value, zoom.value);
tileLayer.value = new L.TileLayer(url, {attribution, maxZoom, accessToken: token});
scale.value = new L.Control.Scale();
tileLayer.value?.addTo(map.value);
scale.value?.addTo(map.value);
map.value.on('layeradd', fitBounds);
map.value.on('layerremove', fitBounds);
unmountHooks.push(teardownMap);
});
onUnmounted(() => {
map.value?.remove();
scriptTag.unload();
styleTag.unload();
unmountHooks.forEach((hook) => hook());
});
onUnmounted(watch(propRefs.zoom, () => map.value?.setZoom(propRefs.zoom.value)));
onActivated(() => {
map.value?.panTo(getActiveMarkerLatLng() ?? propRefs.center.value);
map.value?.panTo(toValue(activeMarker)?.getLatLng() ?? toValue(center));
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<template>
<div class="mp-absolute mp-bottom-10 mp-text-center mp-w-full mp-z-[99999]">
<DoButton
v-show="showLoadMoreButton"
:class="{
'mp-cursor-not-allowed mp-bg-gray-100 mp-opacity-50': loading,
}"
class="mp-inline-flex mp-px-2 mp-py-0.5 mp-transition-colors"
no-spacing
@click="loadMore">
{{ translate(SHOW_MORE_LOCATIONS) }}
</DoButton>
</div>
</template>

<script lang="ts" setup>
import {computed} from 'vue';
import {SHOW_MORE_LOCATIONS} from '@myparcel-do/shared';
import {DoButton} from '../../common';
import {useLanguage, usePickupLocationsMap, useResolvedPickupLocations} from '../../../composables';
const {translate} = useLanguage();
const {locations, loadMoreLocations} = useResolvedPickupLocations();
const {showLoadMoreButton, map} = usePickupLocationsMap();
const loading = computed(() => locations.loading.value);
const loadMore = async () => {
const center = map.value?.getCenter();
if (!center) {
return;
}
await loadMoreLocations(center.lat, center.lng);
showLoadMoreButton.value = false;
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,22 @@
</template>

<script lang="ts" setup>
import {inject, onUnmounted, type Ref, ref, toRefs, watch} from 'vue';
import {type Map, type MarkerOptions} from 'leaflet';
import {isDef} from '@vueuse/core';
import {onUnmounted, ref, toRefs, watch, onMounted, toValue} from 'vue';
import {type MarkerOptions} from 'leaflet';
import {isDef, watchOnce} from '@vueuse/core';
import {ElementEvent} from '@myparcel-do/shared';
import {type Marker} from '../../../types';
import {type MapMarker} from '../../../types';
import {MAP_MARKER_CLASS_ACTIVE} from '../../../data';
import {usePickupLocationsMap} from '../../../composables';
const props = defineProps<{center: [number, number]; options: MarkerOptions; active?: boolean}>();
const propRefs = toRefs(props);
const emit = defineEmits<(event: ElementEvent.Click, marker: Marker) => void>();
const emit = defineEmits<(event: ElementEvent.Click, marker: MapMarker) => void>();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const map = inject<Ref<Map | undefined>>('map')!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const markers = inject<Ref<Marker[]>>('markers')!;
const {map, markers, loaded: mapLoaded} = usePickupLocationsMap();
const marker = ref<Marker>();
const marker = ref<MapMarker>();
const onMarkerClick = (): void => {
if (!isDef(marker.value)) {
Expand Down Expand Up @@ -52,37 +50,45 @@ const addMarker = (): void => {
if (isDef(marker.value)) {
marker.value.options = options.value;
} else {
marker.value = L.marker(center.value, options.value) as Marker;
marker.value = L.marker(center.value, options.value) as MapMarker;
if (!isDef(marker.value)) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const markerValue = toValue(marker)!;
marker.value.on(ElementEvent.Click, onMarkerClick);
marker.value.on(ElementEvent.Keydown, (event) => {
markerValue.on(ElementEvent.Click, onMarkerClick);
markerValue.on(ElementEvent.Keydown, (event) => {
if (!['Enter', 'Space'].includes(event.originalEvent.key)) {
return;
}
onMarkerClick();
});
markers.value.push(marker.value);
markers.value.push(markerValue);
map?.value?.addLayer(marker.value);
// @ts-expect-error todo: fix leaflet type errors
map.value?.addLayer(markerValue);
}
watch(propRefs.active, setActive, {immediate: true});
};
onUnmounted(watch([propRefs.options, map], addMarker, {immediate: true}));
onMounted(() => {
if (mapLoaded.value) {
addMarker();
} else {
watchOnce(mapLoaded, addMarker);
}
});
onUnmounted(() => {
if (!isDef(marker.value)) {
return;
}
map?.value?.removeLayer(marker.value);
// @ts-expect-error todo: fix leaflet type errors
map.value?.removeLayer(marker.value);
// eslint-disable-next-line no-underscore-dangle
markers.value = markers.value.filter((item) => item._leaflet_id !== marker.value?._leaflet_id);
});
Expand Down
1 change: 1 addition & 0 deletions apps/delivery-options/src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './useLanguage';
export * from './useMostEcoFriendly';
export * from './useOptionsGroupedByCarrier';
export * from './usePickupLocation';
export * from './usePickupLocationsMap';
export * from './useProvideElementWidth';
export * from './useResolvedCarrier';
export * from './useResolvedDeliveryDates';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {flushPromises} from '@vue/test-utils';
import {CLOSED} from '@myparcel-do/shared';
import {createUtcDate} from '../utils';
import {mockDeliveryOptionsConfig, waitForPickupLocations} from '../__tests__';
import {getFullPickupLocation, usePickupLocation} from './usePickupLocation';
import {usePickupLocation} from './usePickupLocation';
import {useLanguage} from './useLanguage';

describe.concurrent.skip('usePickupLocation', (it) => {
Expand All @@ -19,7 +19,6 @@ describe.concurrent.skip('usePickupLocation', (it) => {
});

afterEach(() => {
getFullPickupLocation.clear();
vi.setSystemTime(vi.getRealSystemTime());
});

Expand Down Expand Up @@ -47,7 +46,8 @@ describe.concurrent.skip('usePickupLocation', (it) => {
const {pickupLocation} = usePickupLocation('217862');
await flushPromises();

const {openingHours} = pickupLocation.value;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const {openingHours} = pickupLocation.value!;

// Expect to be ordered by closest date
expect(openingHours).toEqual([
Expand Down
20 changes: 8 additions & 12 deletions apps/delivery-options/src/composables/usePickupLocation.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import {computed, type MaybeRef, type ComputedRef} from 'vue';
import {computed, type MaybeRef, type ComputedRef, toValue} from 'vue';
import {get} from '@vueuse/core';
import {type ResolvedPickupLocation} from '../types';
import {useResolvedPickupLocations} from './useResolvedPickupLocations';
import {useResolvedCarrier, type UseResolvedCarrier} from './useResolvedCarrier';

export const getFullPickupLocation = (locationCode: string): ResolvedPickupLocation | undefined => {
const locations = useResolvedPickupLocations();

return (locations.value ?? []).find((location) => location.locationCode === get(locationCode));
};

type UsePickupLocation = {
interface UsePickupLocation {
pickupLocation: ComputedRef<ResolvedPickupLocation | undefined>;
resolvedCarrier: ComputedRef<UseResolvedCarrier | undefined>;
};
}

export const usePickupLocation = (locationCode: MaybeRef<string | undefined>): UsePickupLocation => {
const {locations} = useResolvedPickupLocations();

export const usePickupLocation = (locationCode: MaybeRef<string>): UsePickupLocation => {
const pickupLocation = computed(() => {
return getFullPickupLocation(get(locationCode));
return toValue(locations).find((location) => location.locationCode === get(locationCode));
});

const resolvedCarrier = computed(() => {
const carrierIdentifier = pickupLocation.value?.carrier;
const carrierIdentifier = toValue(pickupLocation)?.carrier;

if (carrierIdentifier) {
return useResolvedCarrier(carrierIdentifier);
Expand Down
Loading

0 comments on commit 4d5c938

Please sign in to comment.