diff options
Diffstat (limited to 'frontend/ts/src/pages/map')
-rw-r--r-- | frontend/ts/src/pages/map/footer.ts | 169 | ||||
-rw-r--r-- | frontend/ts/src/pages/map/marker.ts | 129 | ||||
-rw-r--r-- | frontend/ts/src/pages/map/markerForm.ts | 172 |
3 files changed, 470 insertions, 0 deletions
diff --git a/frontend/ts/src/pages/map/footer.ts b/frontend/ts/src/pages/map/footer.ts new file mode 100644 index 0000000..945bfd4 --- /dev/null +++ b/frontend/ts/src/pages/map/footer.ts @@ -0,0 +1,169 @@ +import { h, Html } from 'lib/rx' +import * as rx from 'lib/rx' +import * as request from 'request' +import * as modal from 'ui/modal' +import * as route from 'route' +import { Map } from 'models/map' +import * as form from 'lib/form' +import * as L from 'lib/loadable' + +export function view(mapInit: Map): Html { + return h('footer', + { className: 'g-Map__Footer' }, + rx.withState<Map>(mapInit, mapVar => + mapVar.map(map => [ + map.name, + h('div', + { className: 'g-Map__FooterButtons' }, + viewRenameButton(map, (map: Map) => mapVar.update(_ => map)), + viewDeleteButton(map) + ) + ]) + ) + ) +} + +// Rename modal + +function viewRenameButton(map: Map, onUpdate: (m: Map) => void): Html { + return rx.withState<boolean>(false, showVar => [ + h('button', + { className: 'g-Button', + onclick: (event: Event) => showVar.update(_ => true) + }, + 'Renommer' + ), + showVar.map(show => { + if (show) { + return renameModal({ + map, + onUpdate, + onClose: () => showVar.update(_ => false) + }) + } + }) + ]) +} + +interface RenameParams { + map: Map, + onUpdate: (m: Map) => void, + onClose: () => void, +} + +function renameModal({ map, onUpdate, onClose }: RenameParams): Html { + const initName = map.name + + return modal.view({ + header: `Renommer la carte ${map.name}`, + body: rx.withState2<string, L.Loadable<void>>([initName, L.init], (nameVar, requestVar) => + h('form', + { + onsubmit: nameVar.map(name => (event: Event) => { + event.preventDefault() + requestVar.update(_ => L.loading) + + request + .put<Map>(`/api/maps/${map.id}`, JSON.stringify({ name })) + .then(map => { + requestVar.update(_ => L.loaded(undefined)) + onUpdate(map) + }) + .catch(({ message }) => requestVar.update(_ => L.failure(message))) + }) + }, + form.input({ + label: 'Nom', + initValue: initName, + select: true, + required: true, + onUpdate: value => { + nameVar.update(_ => value) + requestVar.update(_ => L.init) + } + }), + form.error(requestVar), + form.submit({ + label: 'Renommer', + className: 'g-Button--FullWidth', + requestVar + }) + ) + ), + onClose: onClose + }) +} + +// Delete modal + +function viewDeleteButton(map: Map): Html { + return rx.withState<boolean>(false, showVar => [ + h('button', + { className: 'g-Button g-Button--Danger', + onclick: (event: Event) => showVar.update(_ => true) + }, + 'Supprimer' + ), + showVar.map(show => { + if (show) { + return deleteModal({ + map, + onClose: () => showVar.update(_ => false) + }) + } + }) + ]) +} + +interface DeleteParams { + map: Map, + onClose: () => void, +} + +function deleteModal({ map, onClose }: DeleteParams): Html { + const initName = '' + + return modal.view({ + className: 'g-Modal--Danger', + header: `Supprimer la carte ${map.name} ?`, + body: rx.withState2<string, L.Loadable<void>>([initName, L.init], (nameVar, requestVar) => + h('form', + { + onsubmit: (event: Event) => { + event.preventDefault() + requestVar.update(_ => L.loading) + + request + .del_(`/api/maps/${map.id}`) + .then(_ => { + requestVar.update(_ => L.loaded(undefined)) + route.push(route.maps) + }) + .catch(({ message }) => requestVar.update(_ => L.failure(message))) + } + }, + h('p', + 'Veuillez recopier le nom de la carte pour la supprimer : ', + h('b', map.name) + ), + form.input({ + label: 'Nom', + initValue: initName, + select: true, + onUpdate: value => { + nameVar.update(_ => value) + requestVar.update(_ => L.init) + } + }), + form.error(requestVar), + form.submit({ + label: 'Supprimer', + className: 'g-Button--Danger g-Button--FullWidth', + disabled: nameVar.map(name => name != map.name), + requestVar + }) + ) + ), + onClose: onClose + }) +} diff --git a/frontend/ts/src/pages/map/marker.ts b/frontend/ts/src/pages/map/marker.ts new file mode 100644 index 0000000..b690741 --- /dev/null +++ b/frontend/ts/src/pages/map/marker.ts @@ -0,0 +1,129 @@ +import { mount, h, s } from 'lib/rx' +import * as Color from 'lib/color' +import * as M from 'lib/leaflet' +import * as icons from 'lib/icons' +import * as markerModel from 'models/marker' + +interface CreateParams { + marker: markerModel.Marker, + onMove: (marker: M.Layer) => void, + onClick: (markerElem: M.FeatureGroup) => void, +} + +export function create({ marker, onMove, onClick }: CreateParams): M.FeatureGroup { + const { lat, lng, color, icon, name, description, radius } = marker + const pos = { lat, lng } + + const markerElem = M.marker(pos, { + draggable: true, + autoPan: true, + icon: divIcon({ icon, color, name, description }) + }) + + const circle = + radius !== undefined && radius !== 0 + ? M.circle(pos, { radius, color, fillColor: color }) + : undefined + + const layer = + circle !== undefined + ? M.featureGroup([ markerElem, circle ]) + : M.featureGroup([ markerElem ]) + + markerElem.addEventListener('drag', e => { + circle && circle.setLatLng(markerElem.getLatLng()) + }) + + markerElem.addEventListener('dragend', () => onMove(markerElem)) + + markerElem.addEventListener('click', (e: M.MapEvent) => onClick(layer)) + + return layer +} + +interface CreateIconParams { + icon?: string, + color: string, + name?: string, + description?: string +} + +function divIcon({ icon, color, name, description }: CreateIconParams): M.Icon { + const c = Color.parse(color) + const crBlack = Color.contrastRatio({ red: 0, green: 0, blue: 0 }, c) + const crWhite = Color.contrastRatio({ red: 255, green: 255, blue: 255 }, c) + const textCol = crBlack > crWhite ? 'black' : 'white' + const width = 10 + const height = 15 + const stroke = 'black' + const strokeWidth = 0.6 + // Triangle + const t = [ + { x: width * 0.15, y: 7.46 }, + { x: width / 2, y: height }, + { x: width * 0.85, y: 7.46 } + ] + + const html = document.createElement('div') + html.className = 'g-Marker' + if (description) html.title = description + + mount(html, [ + s('svg', + { + viewBox: `0 0 ${width} ${height}`, + class: 'g-Marker__Base' + }, + s('circle', + { + cx: width / 2, + cy: width / 2, + r: (width - 2 * strokeWidth) / 2, + stroke, + 'stroke-width': strokeWidth, + fill: color + } + ), + s('polygon', { points: `${t[0].x},${t[0].y} ${t[1].x},${t[1].y} ${t[2].x},${t[2].y}`, fill: color }), + s('line', { + x1: t[0].x, + y1: t[0].y, + x2: t[1].x, + y2: t[1].y, + stroke, + 'stroke-width': strokeWidth + }), + s('line', { + x1: t[1].x, + y1: t[1].y, + x2: t[2].x, + y2: t[2].y, + stroke, + 'stroke-width': strokeWidth + }), + ), + icon && icons.svg(icon, { + class: 'g-Marker__Icon', + style: `fill: ${textCol}; stroke: ${textCol}` + }), + name && h('div', + { + className: 'g-Marker__Title', + style: `text-shadow: ${textShadow('white', 1, 1)}` + }, + name + ) + ]) + + return M.divIcon({ + className: '', + popupAnchor: [ 0, -34 ], + html + }) +} + +function textShadow(color: string, w: number, blurr: number): string { + return [[-w, -w], [-w, 0], [-w, w], [0, -w], [0, w], [w, -w], [w, 0], [w, w]] + .map(xs => `${color} ${xs[0]}px ${xs[1]}px ${blurr}px`) + .join(', ') +} diff --git a/frontend/ts/src/pages/map/markerForm.ts b/frontend/ts/src/pages/map/markerForm.ts new file mode 100644 index 0000000..f04a008 --- /dev/null +++ b/frontend/ts/src/pages/map/markerForm.ts @@ -0,0 +1,172 @@ +import { h, Html, Rx } from 'lib/rx' +import * as rx from 'lib/rx' +import * as request from 'request' +import * as modal from 'ui/modal' +import * as layout from 'ui/layout' +import * as markerModel from 'models/marker' +import * as form from 'lib/form' +import * as icons from 'lib/icons' +import * as L from 'lib/loadable' + +interface MarkerFormParams { + lat: number + lng: number + marker?: markerModel.Marker + colors: Array<string> + label: string + req: (body: string) => Promise<markerModel.Marker> + onSuccess: (m: markerModel.Marker) => void + onClose: () => void + lastUserAdded?: markerModel.Marker + onDeleteMarker?: () => void +} + +export function view({ lat, lng, marker, colors, label, req, onSuccess, onClose, lastUserAdded, onDeleteMarker }: MarkerFormParams): Html { + const initName = marker?.name || '' + const initDescription = marker?.description || '' + const initColor = marker?.color || lastUserAdded?.color || (colors.length > 0 ? colors[0] : '#3584e4') + const initIcon = marker?.icon || '' + const initRadius = marker?.radius || 0 + + const isCreation = marker === undefined + + const iconValues: { [key: string]: string } = {} + Object.keys(icons.values).forEach(key => iconValues[key] = icons.values[key].name) + + return modal.view({ + header: marker == undefined ? 'Nouveau marqueur' : 'Modification du marqueur', + body: rx.withState7<string, string, string, string, number, L.Loadable<void>, L.Loadable<void>>( + [initName, initDescription, initColor, initIcon, initRadius, L.init, L.init], + (nameVar, descriptionVar, colorVar, iconVar, radiusVar, updateReqVar, deleteReqVar) => + h('form', + { + onsubmit: rx.map5([nameVar, descriptionVar, colorVar, iconVar, radiusVar], (name, description, color, icon, radius) => + (event: Event) => { + event.preventDefault() + updateReqVar.update(_ => L.loading) + const payload = { lat, lng, color, name, description, icon, radius } + + req(JSON.stringify(payload)) + .then((m: markerModel.Marker) => { + updateReqVar.update(_ => L.loaded(undefined)) + onSuccess(m) + }) + .catch(({ message }) => updateReqVar.update(_ => L.failure(message))) + } + ) + }, + form.input({ + label: 'Nom', + select: isCreation, + initValue: initName, + onUpdate: value => { + nameVar.update(_ => value) + updateReqVar.update(_ => L.init) + deleteReqVar.update(_ => L.init) + } + }), + form.textarea({ + label: 'Description', + initValue: initDescription, + onUpdate: value => { + descriptionVar.update(_ => value) + updateReqVar.update(_ => L.init) + deleteReqVar.update(_ => L.init) + } + }), + colorSection({ + colorRx: colorVar, + colors, + onUpdateColor: color => { + colorVar.update(_ => color) + updateReqVar.update(_ => L.init) + deleteReqVar.update(_ => L.init) + } + }), + layout.columns([ + form.select({ + label: 'Icône', + initValue: initIcon, + values: iconValues, + onUpdate: value => { + iconVar.update(_ => value) + updateReqVar.update(_ => L.init) + deleteReqVar.update(_ => L.init) + } + }), + form.input({ + type: 'number', + label: 'Radius', + initValue: initRadius.toString(), + onUpdate: value => { + radiusVar.update(_ => parseInt(value) || 0) + updateReqVar.update(_ => L.init) + deleteReqVar.update(_ => L.init) + } + }) + ]), + form.error(updateReqVar), + form.error(deleteReqVar), + form.submit({ + label, + className: 'g-Button--FullWidth', + requestVar: updateReqVar, + disabled: deleteReqVar.map(l => l != L.init) + }), + marker !== undefined && onDeleteMarker !== undefined && form.button({ + style: 'margin-top: 1rem', + label: 'Supprimer', + className: 'g-Button--FullWidth g-Button--Danger', + requestVar: deleteReqVar, + disabled: updateReqVar.map(l => l != L.init), + onClick: () => { + deleteReqVar.update(_ => L.loading) + request + .del_(`/api/markers/${marker.id}`) + .then(_ => onDeleteMarker()) + .catch(({ message }) => updateReqVar.update(_ => L.failure(message))) + } + }) + ) + ), + onClose: onClose + }) +} + +interface ColorSectionParams { + colorRx: Rx<string>, + colors: Array<string>, + onUpdateColor: (color: string) => void, +} + +function colorSection({ colorRx, colors, onUpdateColor }: ColorSectionParams): Html { + return h('label', + { className: 'g-Label' }, + 'Couleur', + h('div', + { className: 'g-ColorLine' }, + h('input', + { + type: 'color', + className: 'g-Input', + oninput: (event: Event) => onUpdateColor((event.target as HTMLInputElement).value), + value: colorRx + } + ), + h('div', + { className: 'g-ColorButtons' }, + colors.map(c => + h('input', + { + type: 'button', + className: 'g-ColorButton', + style: `background-color: ${c}`, + onclick: (event: Event) => onUpdateColor(c) + } + ) + ) + ) + ) + ) + +} |