Feature: bootstrapping an app to show a modal for address details and showing it inside twig address's render box

Feature: showing map and link to external map, and address details inside address details modal

Feature: AddressDetailsMap show a warning if the address is incomplete
This commit is contained in:
Julien Fastré 2023-03-14 22:12:30 +01:00
parent c56ae08fae
commit b740a88ae3
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
16 changed files with 283 additions and 11 deletions

View File

@ -1,7 +1,7 @@
{
"name": "chill",
"version": "2.0.0",
"devDependencies": {
"name": "chill",
"version": "2.0.0",
"devDependencies": {
"@alexlafroscia/yaml-merge": "^4.0.0",
"@apidevtools/swagger-cli": "^4.0.4",
"@babel/core": "^7.20.5",
@ -41,6 +41,7 @@
"@fullcalendar/timegrid": "^5.11.0",
"@fullcalendar/vue3": "^5.11.1",
"@popperjs/core": "^2.9.2",
"@types/leaflet": "^1.9.3",
"dropzone": "^5.7.6",
"es6-promise": "^4.2.8",
"leaflet": "^1.7.1",

View File

@ -0,0 +1,14 @@
import {Address} from "../../types";
export const getAddressById = async (address_id: number): Promise<Address> =>
{
const url = `/api/1.0/main/address/${address_id}.json`;
const response = await fetch(url);
if (response.ok) {
return response.json();
}
throw Error('Error with request resource response');
};

View File

@ -0,0 +1,25 @@
import AddressDetailsButton from "../../vuejs/_components/AddressDetails/AddressDetailsButton.vue";
import {createApp} from "vue";
import {createI18n} from "vue-i18n";
import {_createI18n} from "../../vuejs/_js/i18n";
const i18n = _createI18n({});
document.querySelectorAll<HTMLSpanElement>('span[data-address-details]').forEach((el) => {
const dataset = el.dataset as {
addressId: string
};
const app = createApp({
components: {AddressDetailsButton},
data() {
return {
addressId: Number.parseInt(dataset.addressId),
}
},
template: '<address-details-button :address_id="addressId"></address-details-button>'
});
app.use(i18n);
app.mount(el);
});

View File

@ -70,6 +70,8 @@ export interface Country {
code: string;
}
export type AddressRefStatus = 'match'|'to_review'|'reviewed';
export interface Address {
type: "address";
address_id: number;
@ -90,6 +92,13 @@ export interface Address {
addressReference: AddressReference | null;
validFrom: DateTime;
validTo: DateTime | null;
point: Point | null;
refStatus: AddressRefStatus;
isNoAddress: boolean;
}
export interface AddressWithPoint extends Address {
point: Point
}
export interface AddressReference {

View File

@ -1,3 +1,5 @@
import {getAddressById} from 'ChillMainAssets/lib/api/address';
/**
* Endpoint chill_api_single_country__index
* method GET, get Country Object
@ -188,13 +190,7 @@ const postPostalCode = (postalCode) => { //<--
* @returns {Promise} a promise containing a Address object
*/
const getAddress = (id) => {
//console.log('<< get address');
const url = `/api/1.0/main/address/${id}.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
return getAddressById(id);
};
export {

View File

@ -0,0 +1,45 @@
<template>
<a v-if="data.loading === false" @click.prevent="clickOrOpen"><span class="fa fa-map"></span></a>
<span v-if="data.loading" class="fa fa-spin fa-spinner "></span>
<AddressModal :address="data.working_address" ref="address_modal"></AddressModal>
</template>
<script lang="ts" setup>
import {Address} from "../../../types";
import {reactive, ref} from "vue";
import {getAddressById} from "../../../lib/api/address";
import AddressModal from "./AddressModal.vue";
export interface AddressModalContentProps {
//address?: Address|null,
address_id: number,
}
const data = reactive<{
loading: boolean,
working_address: Address | null,
}>({
loading: false,
working_address: null,
});
const props = defineProps<AddressModalContentProps>();
const address_modal = ref<InstanceType<typeof AddressModal> | null>(null)
async function clickOrOpen(): Promise<void> {
if (data.working_address === null) {
data.loading = true;
data.working_address = await getAddressById(props.address_id);
data.loading = false;
}
// open the modal
address_modal.value?.open();
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,22 @@
<template>
<address-details-map :address="props.address"></address-details-map>
<address-render-box :address="props.address"></address-render-box>
</template>
<script lang="ts" setup>
import {Address} from "../../../types";
import AddressDetailsMap from "./Parts/AddressDetailsMap.vue";
import AddressRenderBox from "../Entity/AddressRenderBox.vue";
interface AddressModalContentProps {
address: Address,
}
const props = defineProps<AddressModalContentProps>();
</script>
<style scoped>
</style>

View File

@ -0,0 +1,49 @@
<template>
<teleport to="body">
<modal v-if="state.show_modal" @close="close">
<template v-slot:header>
<h2>Détails d'une adresse</h2>
</template>
<template v-slot:body>
<address-details-content :address="props.address"></address-details-content>
</template>
</modal>
</teleport>
</template>
<script lang="ts" setup>
import {reactive, ref} from "vue";
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import {Address} from "../../../types";
import AddressDetailsContent from "./AddressDetailsContent.vue";
interface AddressModalProps {
address: Address
}
interface AddressModalState {
show_modal: boolean,
}
const props = defineProps<AddressModalProps>();
const state: AddressModalState = reactive({show_modal: false});
const dummy = ref(false);
const open = (): void => {
state.show_modal = true;
}
const close = (): void => {
state.show_modal = false;
}
defineExpose({
close,
open,
});
</script>
<style scoped>
</style>

View File

@ -0,0 +1,91 @@
<template>
<div v-if="props.address.isNoAddress" class="alert alert-info">
Cette adresse est incomplète. La position géographique est approximative.
</div>
<div v-if="props.address.point !== null" class="address_details_map" ref="map_div"></div>
<a :href="makeUrlGoogleMap(props.address)" target="_blank">Google Maps</a>
<a :href="makeUrlOsm(props.address)" target="_blank">OSM</a>
</template>
<script lang="ts" setup>
import {onMounted, ref} from "vue";
import 'leaflet/dist/leaflet.css';
import markerIconPng from "leaflet/dist/images/marker-icon.png";
import L, {LatLngExpression, LatLngTuple} from "leaflet";
import {Address, Point} from "../../../../types";
const lonLatForLeaflet = (point: Point): LatLngTuple => {
return [point.coordinates[1], point.coordinates[0]];
}
export interface MapProps {
address: Address,
}
const props = defineProps<MapProps>();
const map_div = ref<HTMLDivElement | null>(null)
let map: L.Map|null = null;
let marker: L.Marker|null = null;
onMounted(() => {
console.log(map_div.value);
if (map_div.value === null) {
throw new Error('map div not found');
}
if (props.address.point !== null) {
map = L.map(map_div.value);
map.setView(lonLatForLeaflet(props.address.point), 15);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
const markerIcon = L.icon({
iconUrl: markerIconPng,
iconAnchor: [12, 41],
});
marker = L.marker(lonLatForLeaflet(props.address.point), {icon: markerIcon});
marker.addTo(map);
}
});
const makeUrlGoogleMap = (address: Address): string => {
const params = new URLSearchParams();
params.append('api', '1');
if (address.point !== null) {
params.append('query', `${address.point.coordinates[1]} ${address.point.coordinates[0]}`);
} else {
params.append('query', address.lines.join(', '));
}
return `https://www.google.com/maps/search/?${params.toString()}`;
}
const makeUrlOsm = (address: Address): string => {
if (address.point !== null) {
const params = new URLSearchParams();
params.append('mlat', `${address.point.coordinates[1]}`);
params.append('mlon', `${address.point.coordinates[0]}`);
const hashParams = new URLSearchParams();
hashParams.append('map', `18/${address.point.coordinates[1]}/${address.point.coordinates[0]}`);
return `https://www.openstreetmap.org/?${params.toString()}#${hashParams.toString()}`;
}
const params = new URLSearchParams();
params.append('query', address.lines.join(', '));
return `https://www.openstreetmap.org/search?${params.toString()}`;
}
</script>
<style scoped>
div.address_details_map {
height: 250px;
}
</style>

View File

@ -69,6 +69,7 @@
<i class="fa fa-li fa-map-marker"></i>
{% endif %}
{{ _self.inline(address, options, streetLine, lines) }}
<span data-address-details="1" data-address-id="{{ address.id|escape('html_attr') }}" ></span>
</li>
{%- endif -%}
@ -78,6 +79,7 @@
<i class="fa fa-fw fa-map-marker"></i>
{% endif %}
{{ _self.inline(address, options, streetLine, lines) }}
<span data-address-details="1" data-address-id="{{ address.id|escape('html_attr') }}" ></span>
</span>
{%- endif -%}
@ -102,6 +104,7 @@
<div class="noaddress">
{{ 'address.consider homeless'|trans }}
</div>
<p><span data-address-details="1" data-address-id="{{ address.id|escape('html_attr') }}" ></span></p>
{% else %}
<div class="address{% if options['multiline'] %} multiline{% endif %}{% if options['with_delimiter'] %} delimiter{% endif %}">
@ -109,6 +112,7 @@
<i class="fa fa-fw fa-map-marker"></i>
{% endif %}
{{ _self.raw(lines) }}
<p><span data-address-details="1" data-address-id="{{ address.id|escape('html_attr') }}" ></span></p>
</div>
{% endif %}
{{ _self.validity(address, options) }}

View File

@ -20,6 +20,7 @@
{{ encore_entry_link_tags('chill') }}
{{ encore_entry_link_tags('mod_blur') }}
{{ encore_entry_link_tags('vue_onthefly') }}
{{ encore_entry_link_tags('mod_address_details') }}
{% block css %}<!-- nothing added to css -->{% endblock %}
</head>
@ -112,6 +113,7 @@
{{ encore_entry_script_tags('mod_blur') }}
{{ encore_entry_script_tags('chill') }}
{{ encore_entry_script_tags('vue_onthefly') }}
{{ encore_entry_script_tags('mod_address_details') }}
<script type="text/javascript">
window.addEventListener('DOMContentLoaded', function(e) {

View File

@ -97,6 +97,10 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
);
$data['validFrom'] = $address->getValidFrom();
$data['validTo'] = $address->getValidTo();
$data['refStatus'] = $address->getRefStatus();
$data['point'] = $this->normalizer->normalize($address->getPoint(), $format,
[AbstractNormalizer::GROUPS => ['read']]);
$data['isNoAddress'] = $address->isNoAddress();
} elseif ('docgen' === $format) {
$dateContext = array_merge($context, ['docgen:expects' => DateTimeInterface::class]);
$data['validFrom'] = $this->normalizer->normalize($address->getValidFrom(), $format, $dateContext);

View File

@ -13,6 +13,8 @@ namespace Chill\MainBundle\Templating\Entity;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Templating\EngineInterface;
use function array_merge;
@ -35,7 +37,9 @@ class AddressRender implements ChillEntityRenderInterface
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(EngineInterface $templating, TranslatableStringHelperInterface $translatableStringHelper)
public function __construct(
EngineInterface $templating,
TranslatableStringHelperInterface $translatableStringHelper)
{
$this->templating = $templating;
$this->translatableStringHelper = $translatableStringHelper;

View File

@ -18,6 +18,7 @@ use Chill\MainBundle\Templating\Entity\AddressRender;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Iterator;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Templating\EngineInterface;
/**

View File

@ -75,6 +75,7 @@ module.exports = function(encore, entries)
encore.addEntry('mod_wopi_link', __dirname + '/Resources/public/module/wopi-link/index.js');
encore.addEntry('mod_pick_postal_code', __dirname + '/Resources/public/module/pick-postal-code/index.js');
encore.addEntry('mod_pick_rolling_date', __dirname + '/Resources/public/module/pick-rolling-date/index.js');
encore.addEntry('mod_address_details', __dirname + '/Resources/public/module/address-details/index');
// Vue entrypoints
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');

4
src/Bundle/import-png.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "*.png" {
const value: any;
export default value;
}