Add email question, some tweaks
This commit is contained in:
commit
f67508336a
12 changed files with 123 additions and 69 deletions
|
@ -9,29 +9,20 @@ import Website from "../Questions/Website";
|
||||||
import CafeRepair from "../Questions/bike/CafeRepair";
|
import CafeRepair from "../Questions/bike/CafeRepair";
|
||||||
import CafeDiy from "../Questions/bike/CafeDiy";
|
import CafeDiy from "../Questions/bike/CafeDiy";
|
||||||
import CafePump from "../Questions/bike/CafePump";
|
import CafePump from "../Questions/bike/CafePump";
|
||||||
|
import {EmailQuestion} from "../Questions/EmailQuestion";
|
||||||
|
|
||||||
|
|
||||||
export default class BikeCafes extends LayerDefinition {
|
export default class BikeCafes extends LayerDefinition {
|
||||||
private readonly repairsBikes = anyValueExcept("service:bicycle:repair", "no")
|
|
||||||
private readonly hasPump = new Tag("service:bicycle:pump", "yes")
|
|
||||||
private readonly diy = new Tag("service:bicycle:diy", "yes")
|
|
||||||
private readonly bikeServices = [
|
|
||||||
this.diy,
|
|
||||||
this.repairsBikes,
|
|
||||||
this.hasPump
|
|
||||||
]
|
|
||||||
private readonly to = Translations.t.cyclofix.cafe
|
private readonly to = Translations.t.cyclofix.cafe
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super()
|
||||||
this.name = this.to.name;
|
this.name = this.to.name
|
||||||
this.icon = "./assets/bike/cafe.svg";
|
this.icon = "./assets/bike/cafe.svg"
|
||||||
this.overpassFilter = new And([
|
this.overpassFilter = new And([
|
||||||
|
new Tag("amenity", /^pub|bar|cafe$/),
|
||||||
new Or([
|
new Or([
|
||||||
new Regex("amenity", "^pub|bar|cafe")
|
new Tag(/^service:bicycle:/, "*"),
|
||||||
]),
|
|
||||||
new Or([
|
|
||||||
...this.bikeServices,
|
|
||||||
new Tag("pub", "cycling")
|
new Tag("pub", "cycling")
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
|
@ -49,24 +40,24 @@ export default class BikeCafes extends LayerDefinition {
|
||||||
|
|
||||||
this.maxAllowedOverlapPercentage = 10;
|
this.maxAllowedOverlapPercentage = 10;
|
||||||
|
|
||||||
this.minzoom = 13;
|
this.minzoom = 13
|
||||||
this.style = this.generateStyleFunction();
|
this.style = this.generateStyleFunction()
|
||||||
this.title = new FixedText(this.to.title)
|
this.title = new FixedText(this.to.title)
|
||||||
this.elementsToShow = [
|
this.elementsToShow = [
|
||||||
new ImageCarouselWithUploadConstructor(),
|
new ImageCarouselWithUploadConstructor(),
|
||||||
new CafeName(),
|
new CafeName(),
|
||||||
new PhoneNumberQuestion("{name}"),
|
|
||||||
new Website("{name}"),
|
new Website("{name}"),
|
||||||
|
new PhoneNumberQuestion("{name}"),
|
||||||
|
new EmailQuestion("{name}"),
|
||||||
new CafeRepair(),
|
new CafeRepair(),
|
||||||
new CafeDiy(),
|
new CafeDiy(),
|
||||||
new CafePump()
|
new CafePump()
|
||||||
];
|
]
|
||||||
this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY;
|
this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateStyleFunction() {
|
private generateStyleFunction() {
|
||||||
const self = this;
|
const self = this
|
||||||
return function (properties: any) {
|
return function (properties: any) {
|
||||||
return {
|
return {
|
||||||
color: "#00bb00",
|
color: "#00bb00",
|
||||||
|
@ -75,7 +66,7 @@ export default class BikeCafes extends LayerDefinition {
|
||||||
iconSize: [50, 50],
|
iconSize: [50, 50],
|
||||||
iconAnchor: [25,50]
|
iconAnchor: [25,50]
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,19 +16,6 @@ import Website from "../Questions/Website";
|
||||||
|
|
||||||
export default class BikeOtherShops extends LayerDefinition {
|
export default class BikeOtherShops extends LayerDefinition {
|
||||||
private readonly sellsBikes = new Tag("service:bicycle:retail", "yes")
|
private readonly sellsBikes = new Tag("service:bicycle:retail", "yes")
|
||||||
private readonly repairsBikes = anyValueExcept("service:bicycle:repair", "no")
|
|
||||||
private readonly rentsBikes = new Tag("service:bicycle:rental", "yes")
|
|
||||||
private readonly hasPump = new Tag("service:bicycle:pump", "yes")
|
|
||||||
private readonly hasDiy = new Tag("service:bicycle:diy", "yes")
|
|
||||||
private readonly sellsSecondHand = anyValueExcept("service:bicycle:repair", "no")
|
|
||||||
private readonly hasBikeServices = new Or([
|
|
||||||
this.sellsBikes,
|
|
||||||
this.repairsBikes,
|
|
||||||
// this.rentsBikes,
|
|
||||||
// this.hasPump,
|
|
||||||
// this.hasDiy,
|
|
||||||
// this.sellsSecondHand
|
|
||||||
])
|
|
||||||
|
|
||||||
private readonly to = Translations.t.cyclofix.nonBikeShop
|
private readonly to = Translations.t.cyclofix.nonBikeShop
|
||||||
|
|
||||||
|
@ -38,7 +25,7 @@ export default class BikeOtherShops extends LayerDefinition {
|
||||||
this.icon = "./assets/bike/non_bike_repair_shop.svg"
|
this.icon = "./assets/bike/non_bike_repair_shop.svg"
|
||||||
this.overpassFilter = new And([
|
this.overpassFilter = new And([
|
||||||
anyValueExcept("shop", "bicycle"),
|
anyValueExcept("shop", "bicycle"),
|
||||||
this.hasBikeServices
|
new Tag(/^service:bicycle:/, "*"),
|
||||||
])
|
])
|
||||||
this.presets = []
|
this.presets = []
|
||||||
this.maxAllowedOverlapPercentage = 10
|
this.maxAllowedOverlapPercentage = 10
|
||||||
|
|
|
@ -13,6 +13,7 @@ import ShopSecondHand from "../Questions/bike/ShopSecondHand";
|
||||||
import { TagRenderingOptions } from "../TagRendering";
|
import { TagRenderingOptions } from "../TagRendering";
|
||||||
import {PhoneNumberQuestion} from "../Questions/PhoneNumberQuestion";
|
import {PhoneNumberQuestion} from "../Questions/PhoneNumberQuestion";
|
||||||
import Website from "../Questions/Website";
|
import Website from "../Questions/Website";
|
||||||
|
import {EmailQuestion} from "../Questions/EmailQuestion";
|
||||||
|
|
||||||
|
|
||||||
export default class BikeShops extends LayerDefinition {
|
export default class BikeShops extends LayerDefinition {
|
||||||
|
@ -56,8 +57,9 @@ export default class BikeShops extends LayerDefinition {
|
||||||
this.elementsToShow = [
|
this.elementsToShow = [
|
||||||
new ImageCarouselWithUploadConstructor(),
|
new ImageCarouselWithUploadConstructor(),
|
||||||
new ShopName(),
|
new ShopName(),
|
||||||
new PhoneNumberQuestion("{name}"),
|
|
||||||
new Website("{name}"),
|
new Website("{name}"),
|
||||||
|
new PhoneNumberQuestion("{name}"),
|
||||||
|
new EmailQuestion("{name}"),
|
||||||
new ShopRetail(),
|
new ShopRetail(),
|
||||||
new ShopRental(),
|
new ShopRental(),
|
||||||
new ShopRepair(),
|
new ShopRepair(),
|
||||||
|
|
18
Customizations/Questions/EmailQuestion.ts
Normal file
18
Customizations/Questions/EmailQuestion.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {TagRenderingOptions} from "../TagRendering";
|
||||||
|
import {UIElement} from "../../UI/UIElement";
|
||||||
|
import Translations from "../../UI/i18n/Translations";
|
||||||
|
|
||||||
|
export class EmailQuestion extends TagRenderingOptions {
|
||||||
|
|
||||||
|
constructor(category: string | UIElement) {
|
||||||
|
super({
|
||||||
|
question: Translations.t.general.questions.emailOf.Subs({category: category}),
|
||||||
|
freeform: {
|
||||||
|
renderTemplate: Translations.t.general.questions.emailIs.Subs({category: category}),
|
||||||
|
template: "$email$",
|
||||||
|
key: "email"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -55,24 +55,37 @@ export class Regex extends TagsFilter {
|
||||||
|
|
||||||
|
|
||||||
export class Tag extends TagsFilter {
|
export class Tag extends TagsFilter {
|
||||||
public key: string
|
public key: string | RegExp
|
||||||
public value: string
|
public value: string | RegExp
|
||||||
public invertValue: boolean
|
public invertValue: boolean
|
||||||
|
|
||||||
constructor(key: string, value: string, invertValue = false) {
|
constructor(key: string | RegExp, value: string | RegExp, invertValue = false) {
|
||||||
|
if (value === "*" && invertValue) {
|
||||||
|
throw new Error("Invalid combination: invertValue && value == *")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof RegExp && invertValue) {
|
||||||
|
throw new Error("Unsupported combination: RegExp value and inverted value (use regex to invert the match)")
|
||||||
|
}
|
||||||
|
|
||||||
super()
|
super()
|
||||||
this.key = key
|
this.key = key
|
||||||
this.value = value
|
this.value = value
|
||||||
this.invertValue = invertValue
|
this.invertValue = invertValue
|
||||||
|
|
||||||
if (value === "*" && invertValue) {
|
|
||||||
throw new Error("Invalid combination: invertValue && value == *")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static regexOrStrMatches(regexOrStr: string | RegExp, testStr: string) {
|
||||||
|
if (typeof regexOrStr === 'string') {
|
||||||
|
return regexOrStr === testStr
|
||||||
|
} else if (regexOrStr instanceof RegExp) {
|
||||||
|
return (regexOrStr as RegExp).test(testStr)
|
||||||
|
}
|
||||||
|
throw new Error("<regexOrStr> must be of type RegExp or string")
|
||||||
}
|
}
|
||||||
|
|
||||||
matches(tags: { k: string; v: string }[]): boolean {
|
matches(tags: { k: string; v: string }[]): boolean {
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
if (tag.k === this.key) {
|
if (Tag.regexOrStrMatches(this.key, tag.k)) {
|
||||||
if (tag.v === "") {
|
if (tag.v === "") {
|
||||||
// This tag has been removed
|
// This tag has been removed
|
||||||
return this.value === ""
|
return this.value === ""
|
||||||
|
@ -82,7 +95,7 @@ export class Tag extends TagsFilter {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.value === tag.v !== this.invertValue
|
return Tag.regexOrStrMatches(this.value, tag.v) !== this.invertValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,19 +107,33 @@ export class Tag extends TagsFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
asOverpass(): string[] {
|
asOverpass(): string[] {
|
||||||
if (this.value === "*") {
|
const keyIsRegex = this.key instanceof RegExp
|
||||||
return ['["' + this.key + '"]'];
|
const key = keyIsRegex ? (this.key as RegExp).source : this.key
|
||||||
|
|
||||||
|
const valIsRegex = this.value instanceof RegExp
|
||||||
|
const val = valIsRegex ? (this.value as RegExp).source : this.value
|
||||||
|
|
||||||
|
const regexKeyPrefix = keyIsRegex ? '~' : ''
|
||||||
|
const anyVal = this.value === "*"
|
||||||
|
|
||||||
|
if (anyVal && !keyIsRegex) {
|
||||||
|
return [`[${regexKeyPrefix}"${key}"]`];
|
||||||
}
|
}
|
||||||
if (this.value === "") {
|
if (this.value === "") {
|
||||||
// NOT having this key
|
// NOT having this key
|
||||||
return ['[!"' + this.key + '"]'];
|
return ['[!"' + key + '"]'];
|
||||||
}
|
}
|
||||||
const compareOperator = this.invertValue ? '!=' : '='
|
|
||||||
return ['["' + this.key + '"' + compareOperator + '"' + this.value + '"]'];
|
const compareOperator = (valIsRegex || keyIsRegex) ? '~' : (this.invertValue ? '!=' : '=')
|
||||||
|
return [`[${regexKeyPrefix}"${key}"${compareOperator}"${keyIsRegex && anyVal ? '.' : val}"]`];
|
||||||
}
|
}
|
||||||
|
|
||||||
substituteValues(tags: any) {
|
substituteValues(tags: any) {
|
||||||
return new Tag(this.key, TagUtils.ApplyTemplate(this.value, tags));
|
if (typeof this.value !== 'string') {
|
||||||
|
throw new Error("substituteValues() only possible with tag value of type string")
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,33 @@
|
||||||
import {UIElement} from "../UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Combine from "./Combine";
|
import Combine from "./Combine";
|
||||||
|
import {link} from "fs";
|
||||||
|
|
||||||
|
|
||||||
export class SubtleButton extends UIElement{
|
export class SubtleButton extends UIElement{
|
||||||
private imageUrl: string;
|
private imageUrl: string;
|
||||||
private message: UIElement;
|
private message: UIElement;
|
||||||
|
private linkTo: string = undefined;
|
||||||
|
|
||||||
constructor(imageUrl: string, message: string | UIElement) {
|
constructor(imageUrl: string, message: string | UIElement, linkTo : string = undefined) {
|
||||||
super(undefined);
|
super(undefined);
|
||||||
|
this.linkTo = linkTo;
|
||||||
this.message = Translations.W(message);
|
this.message = Translations.W(message);
|
||||||
this.imageUrl = imageUrl;
|
this.imageUrl = imageUrl;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): string {
|
||||||
|
|
||||||
|
if(this.linkTo != undefined){
|
||||||
|
return new Combine([
|
||||||
|
`<a class="subtle-button" href="${this.linkTo}" target="_blank">`,
|
||||||
|
this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "",
|
||||||
|
this.message,
|
||||||
|
'</a>'
|
||||||
|
]).Render();
|
||||||
|
}
|
||||||
|
|
||||||
return new Combine([
|
return new Combine([
|
||||||
'<span class="subtle-button">',
|
'<span class="subtle-button">',
|
||||||
this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "",
|
this.imageUrl !== undefined ? `<img src='${this.imageUrl}'>` : "",
|
||||||
|
|
|
@ -26,7 +26,7 @@ export class LayerSelection extends UIElement{
|
||||||
this._checkboxes.push(new CheckBox(
|
this._checkboxes.push(new CheckBox(
|
||||||
new Combine([checkbox, icon, name]),
|
new Combine([checkbox, icon, name]),
|
||||||
new Combine([
|
new Combine([
|
||||||
Img.checkmark,
|
Img.no_checkmark,
|
||||||
icon,
|
icon,
|
||||||
layer.layerDef.name]),
|
layer.layerDef.name]),
|
||||||
layer.isDisplayed));
|
layer.isDisplayed));
|
||||||
|
|
|
@ -34,16 +34,12 @@ export class MoreScreen extends UIElement {
|
||||||
const link =
|
const link =
|
||||||
new SubtleButton(layout.icon,
|
new SubtleButton(layout.icon,
|
||||||
new Combine([
|
new Combine([
|
||||||
`<a href="${linkText}" target="_blank">`,
|
|
||||||
"<div>",
|
|
||||||
"<b>",
|
"<b>",
|
||||||
Translations.W(layout.title),
|
Translations.W(layout.title),
|
||||||
"</b>",
|
"</b>",
|
||||||
"<br/>",
|
"<br/>",
|
||||||
Translations.W(layout.description),
|
Translations.W(layout.description),
|
||||||
"</div>",
|
]), linkText);
|
||||||
"</a>"
|
|
||||||
]));
|
|
||||||
|
|
||||||
els.push(link)
|
els.push(link)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,8 @@ export class SimpleAddUI extends UIElement {
|
||||||
= new UIEventSource<Preset>(undefined);
|
= new UIEventSource<Preset>(undefined);
|
||||||
private confirmButton: UIElement = undefined;
|
private confirmButton: UIElement = undefined;
|
||||||
private cancelButton: UIElement;
|
private cancelButton: UIElement;
|
||||||
|
private goToInboxButton: UIElement = new SubtleButton("/assets/envelope.svg",
|
||||||
|
Translations.t.general.goToInbox, "https://www.openstreetmap.org/messages/inbox");
|
||||||
|
|
||||||
constructor(zoomlevel: UIEventSource<{ zoom: number }>,
|
constructor(zoomlevel: UIEventSource<{ zoom: number }>,
|
||||||
lastClickLocation: UIEventSource<{ lat: number, lon: number }>,
|
lastClickLocation: UIEventSource<{ lat: number, lon: number }>,
|
||||||
|
@ -132,7 +134,9 @@ export class SimpleAddUI extends UIElement {
|
||||||
if (this._userDetails.data.unreadMessages > 0) {
|
if (this._userDetails.data.unreadMessages > 0) {
|
||||||
return new Combine([header, "<span class='alert'>",
|
return new Combine([header, "<span class='alert'>",
|
||||||
Translations.t.general.readYourMessages,
|
Translations.t.general.readYourMessages,
|
||||||
"</span>"]).Render();
|
"</span>",
|
||||||
|
this.goToInboxButton
|
||||||
|
]).Render();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._userDetails.data.dryRun) {
|
if (this._userDetails.data.dryRun) {
|
||||||
|
|
|
@ -748,7 +748,16 @@ export default class Translations {
|
||||||
websiteIs: new T({
|
websiteIs: new T({
|
||||||
en: "Website: <a href='{website}' target='_blank'>{website}</a>",
|
en: "Website: <a href='{website}' target='_blank'>{website}</a>",
|
||||||
nl: "Website: <a href='{website}' target='_blank'>{website}</a>"
|
nl: "Website: <a href='{website}' target='_blank'>{website}</a>"
|
||||||
})
|
}),
|
||||||
|
emailOf: new T({
|
||||||
|
en: "What is the email address of {category}?",
|
||||||
|
nl: "Wat is het email-adres van {category}?"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
emailIs: new T({
|
||||||
|
en: "The email address of this {category} is <a href='mailto:{email}' target='_blank'>{email}</a>",
|
||||||
|
nl: "Het email-adres van {category} is <a href='mailto:{email}' target='_blank'>{email}</a>"
|
||||||
|
}),
|
||||||
|
|
||||||
},
|
},
|
||||||
openStreetMapIntro: new T({
|
openStreetMapIntro: new T({
|
||||||
|
@ -794,6 +803,10 @@ export default class Translations {
|
||||||
fewChangesBefore: new T({
|
fewChangesBefore: new T({
|
||||||
en: "Please, answer a few questions of existing points before adding a new point.",
|
en: "Please, answer a few questions of existing points before adding a new point.",
|
||||||
nl: "Gelieve eerst enkele vragen van bestaande punten te beantwoorden vooraleer zelf punten toe te voegen."
|
nl: "Gelieve eerst enkele vragen van bestaande punten te beantwoorden vooraleer zelf punten toe te voegen."
|
||||||
|
}),
|
||||||
|
goToInbox: new T({
|
||||||
|
en: "Open inbox",
|
||||||
|
nl: "Ga naar de berichten"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
11
index.css
11
index.css
|
@ -4,10 +4,6 @@ html, body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: unset;
|
|
||||||
color:unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
@ -1206,6 +1202,13 @@ form {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subtle-button a {
|
||||||
|
text-decoration: unset !important;
|
||||||
|
color:unset !important;
|
||||||
|
display: block ruby;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.subtle-button img{
|
.subtle-button img{
|
||||||
width: 3em;
|
width: 3em;
|
||||||
max-height: 3em;
|
max-height: 3em;
|
||||||
|
|
6
index.ts
6
index.ts
|
@ -105,11 +105,11 @@ const fullScreenMessage = new UIEventSource<UIElement>(undefined);
|
||||||
// The latest element that was selected - used to generate the right UI at the right place
|
// The latest element that was selected - used to generate the right UI at the right place
|
||||||
const selectedElement = new UIEventSource<{ feature: any }>(undefined);
|
const selectedElement = new UIEventSource<{ feature: any }>(undefined);
|
||||||
|
|
||||||
const zoom = QueryParameters.GetQueryParameter("z", "" + layoutToUse.startzoom)
|
const zoom = QueryParameters.GetQueryParameter("z", undefined)
|
||||||
.syncWith(LocalStorageSource.Get("zoom"));
|
.syncWith(LocalStorageSource.Get("zoom"));
|
||||||
const lat = QueryParameters.GetQueryParameter("lat", "" + layoutToUse.startLat)
|
const lat = QueryParameters.GetQueryParameter("lat", undefined)
|
||||||
.syncWith(LocalStorageSource.Get("lat"));
|
.syncWith(LocalStorageSource.Get("lat"));
|
||||||
const lon = QueryParameters.GetQueryParameter("lon", "" + layoutToUse.startLon)
|
const lon = QueryParameters.GetQueryParameter("lon", undefined)
|
||||||
.syncWith(LocalStorageSource.Get("lon"));
|
.syncWith(LocalStorageSource.Get("lon"));
|
||||||
|
|
||||||
const featureSwitchUserbadge = QueryParameters.GetQueryParameter("fs-userbadge", ""+layoutToUse.enableUserBadge);
|
const featureSwitchUserbadge = QueryParameters.GetQueryParameter("fs-userbadge", ""+layoutToUse.enableUserBadge);
|
||||||
|
|
Loading…
Reference in a new issue