diff --git a/AllTranslationAssets.ts b/AllTranslationAssets.ts
index 78242e7..b695479 100644
--- a/AllTranslationAssets.ts
+++ b/AllTranslationAssets.ts
@@ -109,7 +109,8 @@ export default class AllTranslationAssets {
saturday: new Translation( {"en":"Saturday","ca":"Dissabte","es":"Sábado","nl":"Zaterdag","fr":"Samedi"} ),
sunday: new Translation( {"en":"Sunday","ca":"Diumenge","es":"Domingo","nl":"Zondag","fr":"Dimance"} ),
},
- opening_hours: { open_during_ph: new Translation( {"nl":"Op een feestdag is deze zaak","ca":"Durant festes aquest servei és","es":"Durante fiestas este servicio está","en":"During a public holiday, this amenity is"} ),
+ opening_hours: { error_loading: new Translation( {"en":"Error: could not visualize these opening hours.","nl":"Sorry, deze openingsuren kunnen niet getoond worden"} ),
+ open_during_ph: new Translation( {"nl":"Op een feestdag is deze zaak","ca":"Durant festes aquest servei és","es":"Durante fiestas este servicio está","en":"During a public holiday, this amenity is"} ),
opensAt: new Translation( {"en":"from","ca":"des de","es":"desde","nl":"vanaf"} ),
openTill: new Translation( {"en":"till","ca":"fins","es":" hasta","nl":"tot"} ),
not_all_rules_parsed: new Translation( {"en":"The opening hours of this shop are complicated. The following rules are ignored in the input element:","ca":"L'horari d'aquesta botiga és complicat. Les normes següents seran ignorades en l'entrada:","es":"El horario de esta tienda es complejo. Las normas siguientes serán ignoradas en la entrada:"} ),
@@ -125,7 +126,16 @@ export default class AllTranslationAssets {
reload: new Translation( {"en":"Reload the data","es":"Recargar datos","ca":"Recarregar dades","gl":"Recargar os datos","de":"Daten neu laden"} ),
},
reviews: { title: new Translation( {"en":"{count} reviews","nl":"{count} beoordelingen"} ),
+ name_required: new Translation( {"en":"A name is required in order to display and create reviews","nl":"De naam van dit object moet gekend zijn om een review te kunnen maken"} ),
no_reviews_yet: new Translation( {"en":"There are no reviews yet. Be the first to write one and help open data and the business!","nl":"Er zijn nog geen beoordelingen. Wees de eerste om een beoordeling te schrijven en help open data en het bedrijf"} ),
+ write_a_comment: new Translation( {"en":"Leave a review...","nl":"Schrijf een beoordeling..."} ),
+ no_rating: new Translation( {"en":"No rating given","nl":"Geen score bekend"} ),
+ posting_as: new Translation( {"en":"Posting as","nl":"Ingelogd als"} ),
+ i_am_affiliated: new Translation( {"en":"
I am affiliated with this object Check if you are the owner, creator, employee, ... or similar
","nl":"
I am affiliated with this object Vink aan indien je de oprichter, maker, werknemer, ... of dergelijke bent
"} ),
+ affiliated_reviewer_warning: new Translation( {"en":"(Affiliated review)","nl":"(Review door betrokkene)"} ),
+ saving_review: new Translation( {"en":"Saving...","nl":"Opslaan..."} ),
+ saved: new Translation( {"en":"Review saved. Thanks for sharing!","nl":"Bedankt om je beoordeling te delen!"} ),
attribution: new Translation( {"en":"Reviews are powered by Mangrove Reviews and are available under CC-BY 4.0","nl":"De beoordelingen worden voorzien door Mangrove Reviews en zijn beschikbaar onder deCC-BY 4.0-licentie "} ),
+ plz_login: new Translation( {"en":"Login to leave a review","nl":"Meld je aan om een beoordeling te geven"} ),
},
}}
\ No newline at end of file
diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts
index da621a1..6c6298f 100644
--- a/Customizations/JSON/LayerConfig.ts
+++ b/Customizations/JSON/LayerConfig.ts
@@ -93,6 +93,12 @@ export default class LayerConfig {
return tagRenderings.map(
(renderingJson, i) => {
if (typeof renderingJson === "string") {
+
+ if(renderingJson === "questions"){
+ return new TagRenderingConfig("questions")
+ }
+
+
const shared = SharedTagRenderings.SharedTagRendering[renderingJson];
if (shared !== undefined) {
return shared;
diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts
index 28f5fb8..da3747b 100644
--- a/Customizations/JSON/LayerConfigJson.ts
+++ b/Customizations/JSON/LayerConfigJson.ts
@@ -143,6 +143,10 @@ export interface LayerConfigJson {
* Note that we can also use a string here - where the string refers to a tagrenering defined in `assets/questions/questions.json`,
* where a few very general questions are defined e.g. website, phone number, ...
*
+ * A special value is 'questions', which indicates the location of the questions box. If not specified, it'll be appended to the bottom of the featureInfobox.
+ *
*/
tagRenderings?: (string | TagRenderingConfigJson) []
+
+
}
\ No newline at end of file
diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts
index d6e9a92..53ff1dc 100644
--- a/Customizations/JSON/TagRenderingConfig.ts
+++ b/Customizations/JSON/TagRenderingConfig.ts
@@ -31,8 +31,15 @@ export default class TagRenderingConfig {
constructor(json: string | TagRenderingConfigJson, context?: string) {
- if(json === undefined){
- throw "Initing a TagRenderingConfig with undefined in "+context;
+ if (json === "questions") {
+ // Very special value
+ this.render = null;
+ this.question = null;
+ this.condition = null;
+ }
+
+ if (json === undefined) {
+ throw "Initing a TagRenderingConfig with undefined in " + context;
}
if (typeof json === "string") {
this.render = Translations.T(json);
@@ -63,13 +70,13 @@ export default class TagRenderingConfig {
throw "Invalid mapping: if without body"
}
let hideInAnswer : boolean | TagsFilter = false;
- if(typeof mapping.hideInAnswer === "boolean"){
+ if (typeof mapping.hideInAnswer === "boolean") {
hideInAnswer = mapping.hideInAnswer;
- }else{
- hideInAnswer = FromJSON.Tag(mapping.hideInAnswer);
+ } else if (mapping.hideInAnswer !== undefined) {
+ hideInAnswer = FromJSON.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`);
}
return {
- if: FromJSON.Tag(mapping.if, `${context}.mapping[${i}]`),
+ if: FromJSON.Tag(mapping.if, `${context}.mapping[${i}].if`),
then: Translations.T(mapping.then),
hideInAnswer: hideInAnswer
};
diff --git a/Customizations/SharedTagRenderings.ts b/Customizations/SharedTagRenderings.ts
index c6dae7d..772bd97 100644
--- a/Customizations/SharedTagRenderings.ts
+++ b/Customizations/SharedTagRenderings.ts
@@ -10,12 +10,11 @@ export default class SharedTagRenderings {
private static generatedSharedFields(iconsOnly = false) {
const dict = {}
-
function add(key, store) {
try {
- dict[key] = new TagRenderingConfig(store[key])
+ dict[key] = new TagRenderingConfig(store[key], key)
} catch (e) {
- console.error("BUG: could not parse", key, " from questions.json or icons.json", e)
+ console.error("BUG: could not parse", key, " from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings", e)
}
}
diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts
index 047198e..572ed63 100644
--- a/Logic/Osm/OsmConnection.ts
+++ b/Logic/Osm/OsmConnection.ts
@@ -24,7 +24,7 @@ export class OsmConnection {
public auth;
public userDetails: UIEventSource;
- private _dryRun: boolean;
+ _dryRun: boolean;
public preferencesHandler: OsmPreferences;
public changesetHandler: ChangesetHandler;
diff --git a/Logic/Web/MangroveReviews.ts b/Logic/Web/MangroveReviews.ts
index 80b6a44..2e6ec4c 100644
--- a/Logic/Web/MangroveReviews.ts
+++ b/Logic/Web/MangroveReviews.ts
@@ -1,75 +1,158 @@
import * as mangrove from 'mangrove-reviews'
import {UIEventSource} from "../UIEventSource";
+import {Review} from "./Review";
+
+export class MangroveIdentity {
+ private readonly _mangroveIdentity: UIEventSource;
+ public keypair: any = undefined;
+
+ constructor(mangroveIdentity: UIEventSource) {
+ const self = this;
+ this._mangroveIdentity = mangroveIdentity;
+ mangroveIdentity.addCallbackAndRun(str => {
+ if (str === undefined || str === "") {
+ return;
+ }
+ mangrove.jwkToKeypair(JSON.parse(str)).then(keypair => {
+ self.keypair = keypair;
+ console.log("Identity loaded")
+ })
+ })
+ if ((mangroveIdentity.data ?? "") === "") {
+ this.CreateIdentity();
+ }
+ }
+
+ /**
+ * Creates an identity if none exists already.
+ * Is written into the UIEventsource, which was passed into the constructor
+ * @constructor
+ */
+ private CreateIdentity() {
+ if ("" !== (this._mangroveIdentity.data ?? "")) {
+ throw "Identity already defined - not creating a new one"
+ }
+ const self = this;
+ mangrove.generateKeypair().then(
+ keypair => {
+ self.keypair = keypair;
+ mangrove.keypairToJwk(keypair).then(jwk => {
+ self._mangroveIdentity.setData(JSON.stringify(jwk));
+ })
+ });
+ }
+
+}
export default class MangroveReviews {
+ private readonly _lon: number;
+ private readonly _lat: number;
+ private readonly _name: string;
+ private readonly _reviews: UIEventSource = new UIEventSource([]);
+ private _dryRun: boolean;
+ private _mangroveIdentity: MangroveIdentity;
+ private _lastUpdate : Date = undefined;
- constructor() {
+ private static _reviewsCache = {};
+
+ public static Get(lon: number, lat: number, name: string,
+ identity: MangroveIdentity,
+ dryRun?: boolean){
+ const newReviews = new MangroveReviews(lon, lat, name, identity, dryRun);
+
+ const uri = newReviews.GetSubjectUri();
+ const cached = MangroveReviews._reviewsCache[uri];
+ if(cached !== undefined){
+ return cached;
+ }
+ MangroveReviews._reviewsCache[uri] = newReviews;
+
+ return newReviews;
}
+
+ private constructor(lon: number, lat: number, name: string,
+ identity: MangroveIdentity,
+ dryRun?: boolean) {
+
+ this._lon = lon;
+ this._lat = lat;
+ this._name = name;
+ this._mangroveIdentity = identity;
+ this._dryRun = dryRun;
+
+ }
+
+ /**
+ * Gets an URI which represents the item in a mangrove-compatible way
+ * @constructor
+ */
+ public GetSubjectUri() {
+ let uri = `geo:${this._lat},${this._lon}?u=50`;
+ if (this._name !== undefined && this._name !== null) {
+ uri += "&q=" + this._name;
+ }
+ return uri;
+ }
+
/**
* Gives a UIEVentsource with all reviews.
* Note: rating is between 1 and 100
*/
- public static GetReviewsFor(lon: number, lat: number, name: string): UIEventSource<{
- comment?: string,
- author: string,
- date: Date,
- rating: number
- }[]> {
-
- let uri = `geo:${lat},${lon}?u=50`;
- if (name !== undefined && name !== null) {
- uri += "&q=" + name;
- }
- const reviewsSource : UIEventSource< {
- comment?: string,
- author: string,
- date: Date,
- rating: number
- }[]> = new UIEventSource([]);
+ public GetReviews(): UIEventSource {
- mangrove.getReviews({sub: uri}).then(
+ if(this._lastUpdate !== undefined && this._reviews.data !== undefined &&
+ (new Date().getTime() - this._lastUpdate.getTime()) < 15000
+ ){
+ // Last update was pretty recent
+ return this._reviews;
+ }
+ this._lastUpdate = new Date();
+
+ const self = this;
+ mangrove.getReviews({sub: this.GetSubjectUri()}).then(
(data) => {
- const reviews = [{
- date: new Date(),
- comment: "Short",
- rating: 1,
- author: "Troll"
- },{
- date: new Date(),
- comment: "Not good",
- rating: 10,
- author: "Troll"
- },{
- date: new Date(),
- comment: "Not soo good",
- rating: 20,
- author: "Troll"
- },{
- date: new Date(),
- comment: "Meh",
- rating: 30,
- author: "Troll"
- },
- {
- date: new Date(),
- comment: "Lorum ipsum lorem qsmldkfj qsdfmqmsd qmlsdmlkjazmeliq dmqlsdkf amldkfjqmlskdbmaize qsmdl fka mqlsnkd azie qmxbilqmslef amlzdf qsmdlfk afdml kqbnqsdlkf m",
- rating: 50,
- author: "Troll"
- }];
+ const reviews = [];
for (const review of data.reviews) {
const r = review.payload;
reviews.push({
date: new Date(r.iat * 1000),
comment: r.opinion,
author: r.metadata.nickname,
+ affiliated: r.metadata.is_affiliated,
rating: r.rating // percentage points
})
}
- reviewsSource.setData(reviews)
+ self._reviews.setData(reviews)
}
);
- return reviewsSource;
+ return this._reviews;
+ }
+
+ AddReview(r: Review, callback?: (() => void)) {
+
+
+ callback = callback ?? (() => {
+ return undefined;
+ });
+
+ const payload = {
+ sub: this.GetSubjectUri(),
+ rating: r.rating,
+ opinion: r.comment,
+ metadata: {
+ is_affiliated: r.affiliated,
+ nickname: r.author,
+ }
+ };
+ if (this._dryRun) {
+ console.log("DRYRUNNING mangrove reviews: ", payload);
+ } else {
+ mangrove.signAndSubmitReview(this._mangroveIdentity.keypair, payload).then(callback)
+ }
+ this._reviews.data.push(r);
+ this._reviews.ping();
+ callback();
}
diff --git a/Logic/Web/Review.ts b/Logic/Web/Review.ts
new file mode 100644
index 0000000..24f397a
--- /dev/null
+++ b/Logic/Web/Review.ts
@@ -0,0 +1,7 @@
+export interface Review {
+ comment?: string,
+ author: string,
+ date: Date,
+ rating: number,
+ affiliated: boolean
+}
\ No newline at end of file
diff --git a/State.ts b/State.ts
index 72b7a83..d2864cf 100644
--- a/State.ts
+++ b/State.ts
@@ -13,6 +13,7 @@ import {QueryParameters} from "./Logic/Web/QueryParameters";
import {BaseLayer} from "./Logic/BaseLayer";
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
import Hash from "./Logic/Web/Hash";
+import {MangroveIdentity} from "./Logic/Web/MangroveReviews";
/**
* Contains the global state: a bunch of UI-event sources
@@ -64,13 +65,15 @@ export default class State {
*/
public osmConnection: OsmConnection;
+ public mangroveIdentity: MangroveIdentity;
+
public favouriteLayers: UIEventSource;
public layerUpdater: UpdateFromOverpass;
public filteredLayers: UIEventSource = new UIEventSource([])
-
+
/**
* The message that should be shown at the center of the screen
*/
@@ -209,6 +212,10 @@ export default class State {
true
);
+ this.mangroveIdentity = new MangroveIdentity(
+ this.osmConnection.GetLongPreference("identity", "mangrove")
+ );
+
const h = Hash.Get();
this.selectedElement.addCallback(selected => {
diff --git a/Svg.ts b/Svg.ts
index 5b6bafd..b6a57e2 100644
--- a/Svg.ts
+++ b/Svg.ts
@@ -244,6 +244,16 @@ export default class Svg {
public static star_half_svg() { return new FixedUiElement(Svg.star_half);}
public static star_half_ui() { return new FixedUiElement(Svg.star_half_img);}
+ public static star_outline = " "
+ public static star_outline_img = Img.AsImageElement(Svg.star_outline)
+ public static star_outline_svg() { return new FixedUiElement(Svg.star_outline);}
+ public static star_outline_ui() { return new FixedUiElement(Svg.star_outline_img);}
+
+ public static star_outline_half = " "
+ public static star_outline_half_img = Img.AsImageElement(Svg.star_outline_half)
+ public static star_outline_half_svg() { return new FixedUiElement(Svg.star_outline_half);}
+ public static star_outline_half_ui() { return new FixedUiElement(Svg.star_outline_half_img);}
+
public static statistics = " "
public static statistics_img = Img.AsImageElement(Svg.statistics)
public static statistics_svg() { return new FixedUiElement(Svg.statistics);}
@@ -269,4 +279,4 @@ export default class Svg {
public static wikipedia_svg() { return new FixedUiElement(Svg.wikipedia);}
public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);}
-public static All = {"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapillary.svg": Svg.mapillary,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"statistics.svg": Svg.statistics,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};}
+public static All = {"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapillary.svg": Svg.mapillary,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};}
diff --git a/UI/CustomGenerator/SettingsTable.ts b/UI/CustomGenerator/SettingsTable.ts
index f16fbce..28bb4bc 100644
--- a/UI/CustomGenerator/SettingsTable.ts
+++ b/UI/CustomGenerator/SettingsTable.ts
@@ -13,7 +13,7 @@ export default class SettingsTable extends UIElement {
public selectedSetting: UIEventSource>;
constructor(elements: (SingleSetting | string)[],
- currentSelectedSetting: UIEventSource>) {
+ currentSelectedSetting?: UIEventSource>) {
super(undefined);
const self = this;
this.selectedSetting = currentSelectedSetting ?? new UIEventSource>(undefined);
diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts
index 0cdf038..9b82ce3 100644
--- a/UI/Input/TextField.ts
+++ b/UI/Input/TextField.ts
@@ -63,11 +63,11 @@ export class TextField extends InputElement {
InnerRender(): string {
+ const placeholder = this._placeholder.InnerRender().replace("'", "'");
if (this._htmlType === "area") {
- return ``
+ return ``
}
- const placeholder = this._placeholder.InnerRender().replace("'", "'");
let label = "";
if (this._label != undefined) {
label = this._label.Render();
diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts
index 5fbfaf6..5d0e3a4 100644
--- a/UI/Input/ValidatedTextField.ts
+++ b/UI/Input/ValidatedTextField.ts
@@ -158,7 +158,6 @@ export default class ValidatedTextField {
if (str === undefined) {
return false;
}
- console.log("Validating phone number",str,"in country",country())
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.isValid() ?? false
},
(str, country: () => string) => parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational()
diff --git a/UI/OhVisualization.ts b/UI/OhVisualization.ts
index 8bad043..e665539 100644
--- a/UI/OhVisualization.ts
+++ b/UI/OhVisualization.ts
@@ -5,6 +5,7 @@ import Combine from "./Base/Combine";
import Translations from "./i18n/Translations";
import {FixedUiElement} from "./Base/FixedUiElement";
import {OH} from "../Logic/OpeningHours";
+import State from "../State";
export default class OpeningHoursVisualization extends UIElement {
private readonly _key: string;
@@ -165,7 +166,12 @@ export default class OpeningHoursVisualization extends UIElement {
}, {tag_key: this._key});
} catch (e) {
console.log(e);
- return `Error: could not visualize these opening hours ${e}`
+ const msg = new Combine([Translations.t.general.opening_hours.error_loading,
+ State.state?.osmConnection?.userDetails?.data?.csCount >= State.userJourney.tagsVisibleAndWikiLinked ?
+ `${e}`
+ : ""
+ ]);
+ return msg.Render();
}
if (!oh.getState() && !oh.getUnknown()) {
diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts
index 03eb19f..c714e5a 100644
--- a/UI/Popup/FeatureInfoBox.ts
+++ b/UI/Popup/FeatureInfoBox.ts
@@ -35,11 +35,25 @@ export class FeatureInfoBox extends UIElement {
this._titleIcons = new Combine(
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon)))
.SetClass("featureinfobox-icons");
- this._renderings = layerConfig.tagRenderings.map(tr => new EditableTagRendering(tags, tr));
- this._renderings[0]?.SetClass("first-rendering");
+
+ let questionBox : UIElement = undefined;
if (State.state.featureSwitchUserbadge.data) {
- this._questionBox = new QuestionBox(tags, layerConfig.tagRenderings);
+ questionBox = new QuestionBox(tags, layerConfig.tagRenderings);
}
+
+ let questionBoxIsUsed = false;
+ this._renderings = layerConfig.tagRenderings.map(tr => {
+ if(tr.question === null){
+ questionBoxIsUsed = true;
+ // This is the question box!
+ return questionBox;
+ }
+ return new EditableTagRendering(tags, tr);
+ });
+ this._renderings[0]?.SetClass("first-rendering");
+ if(!questionBoxIsUsed){
+ this._renderings.push(questionBox);
+ }
}
InnerRender(): string {
diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts
index 67d1775..e6caecb 100644
--- a/UI/Popup/QuestionBox.ts
+++ b/UI/Popup/QuestionBox.ts
@@ -22,7 +22,9 @@ export default class QuestionBox extends UIElement {
this.ListenTo(this._skippedQuestions);
this._tags = tags;
const self = this;
- this._tagRenderings = tagRenderings.filter(tr => tr.question !== undefined);
+ this._tagRenderings = tagRenderings
+ .filter(tr => tr.question !== undefined)
+ .filter(tr => tr.question !== null);
this._tagRenderingQuestions = this._tagRenderings
.map((tagRendering, i) => new TagRenderingQuestion(this._tags, tagRendering,
() => {
diff --git a/UI/Popup/SaveButton.ts b/UI/Popup/SaveButton.ts
index 52a78de..2886d9d 100644
--- a/UI/Popup/SaveButton.ts
+++ b/UI/Popup/SaveButton.ts
@@ -1,32 +1,33 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import {UIElement} from "../UIElement";
import Translations from "../i18n/Translations";
-import State from "../../State";
+import {OsmConnection, UserDetails} from "../../Logic/Osm/OsmConnection";
export class SaveButton extends UIElement {
- private _value: UIEventSource;
- private _friendlyLogin: UIElement;
+ private readonly _value: UIEventSource;
+ private readonly _friendlyLogin: UIElement;
+ private readonly _userDetails: UIEventSource;
- constructor(value: UIEventSource) {
+ constructor(value: UIEventSource, osmConnection: OsmConnection) {
super(value);
+ this._userDetails = osmConnection?.userDetails;
if(value === undefined){
throw "No event source for savebutton, something is wrong"
}
this._value = value;
-
this._friendlyLogin = Translations.t.general.loginToStart.Clone()
.SetClass("login-button-friendly")
- .onClick(() => State.state.osmConnection.AttemptLogin())
+ .onClick(() => osmConnection?.AttemptLogin())
}
InnerRender(): string {
let clss = "save";
- if(State.state !== undefined && !State.state.osmConnection.userDetails.data.loggedIn){
+ if(this._userDetails != undefined && !this._userDetails.data.loggedIn){
return this._friendlyLogin.Render();
}
- if ((this._value.data ?? "") === "") {
+ if (this._value.data === false || (this._value.data ?? "") === "") {
clss = "save-non-active";
}
return Translations.t.general.save.Clone().SetClass(clss).Render();
diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts
index 1cdd805..36f152f 100644
--- a/UI/Popup/TagRenderingQuestion.ts
+++ b/UI/Popup/TagRenderingQuestion.ts
@@ -64,7 +64,7 @@ export default class TagRenderingQuestion extends UIElement {
}
- this._saveButton = new SaveButton(this._inputElement.GetValue())
+ this._saveButton = new SaveButton(this._inputElement.GetValue(), State.state.osmConnection)
.onClick(save)
diff --git a/UI/ReviewElement.ts b/UI/Reviews/ReviewElement.ts
similarity index 60%
rename from UI/ReviewElement.ts
rename to UI/Reviews/ReviewElement.ts
index c908cbe..2ed01bd 100644
--- a/UI/ReviewElement.ts
+++ b/UI/Reviews/ReviewElement.ts
@@ -1,29 +1,38 @@
-import {UIElement} from "./UIElement";
-import {UIEventSource} from "../Logic/UIEventSource";
-import Translations from "./i18n/Translations";
-import Combine from "./Base/Combine";
-import {FixedUiElement} from "./Base/FixedUiElement";
-import {Utils} from "../Utils";
-
/**
* Shows the reviews and scoring base on mangrove.reviesw
*/
-export default class ReviewElement extends UIElement {
- private _reviews: UIEventSource<{ comment?: string; author: string; date: Date; rating: number }[]>;
+import {UIEventSource} from "../../Logic/UIEventSource";
+import {Review} from "../../Logic/Web/Review";
+import {UIElement} from "../UIElement";
+import {Utils} from "../../Utils";
+import Combine from "../Base/Combine";
+import {FixedUiElement} from "../Base/FixedUiElement";
+import Translations from "../i18n/Translations";
- constructor(reviews: UIEventSource<{
- comment?: string,
- author: string,
- date: Date,
- rating: number
- }[]>) {
+export default class ReviewElement extends UIElement {
+ private readonly _reviews: UIEventSource;
+ private readonly _subject: string;
+ private _middleElement: UIElement;
+
+ constructor(subject: string, reviews: UIEventSource, middleElement: UIElement) {
super(reviews);
+ this._middleElement = middleElement;
+ if(reviews === undefined){
+ throw "No reviews UIEVentsource Given!"
+ }
this._reviews = reviews;
+ this._subject = subject;
}
InnerRender(): string {
function genStars(rating: number) {
+ if(rating === undefined){
+ return Translations.t.reviews.no_rating;
+ }
+ if(rating < 10){
+ rating = 10;
+ }
const scoreTen = Math.round(rating / 10);
return new Combine([
"".repeat(Math.floor(scoreTen / 2)),
@@ -38,11 +47,15 @@ export default class ReviewElement extends UIElement {
elements.push(
new Combine([
genStars(avg).SetClass("stars"),
+ ``,
Translations.t.reviews.title
- .Subs({count: "" + revs.length})
+ .Subs({count: "" + revs.length}),
+ ""
])
.SetClass("review-title"));
+
+ elements.push(this._middleElement);
elements.push(...revs.map(review => {
const d = review.date;
@@ -55,8 +68,11 @@ export default class ReviewElement extends UIElement {
]).SetClass("review-stars-comment"),
new Combine([
+ new Combine([
- new FixedUiElement(review.author).SetClass("review-author"),
+ new FixedUiElement(review.author).SetClass("review-author"),
+ review.affiliated ? Translations.t.reviews.affiliated_reviewer_warning : "",
+ ]).SetStyle("margin-right: 0.5em"),
new FixedUiElement(`${d.getFullYear()}-${Utils.TwoDigits(d.getMonth() + 1)}-${Utils.TwoDigits(d.getDate())} ${Utils.TwoDigits(d.getHours())}:${Utils.TwoDigits(d.getMinutes())}`)
.SetClass("review-date")
]).SetClass("review-author-date")
diff --git a/UI/Reviews/ReviewForm.ts b/UI/Reviews/ReviewForm.ts
new file mode 100644
index 0000000..417d44e
--- /dev/null
+++ b/UI/Reviews/ReviewForm.ts
@@ -0,0 +1,116 @@
+import {UIElement} from "../UIElement";
+import {InputElement} from "../Input/InputElement";
+import {Review} from "../../Logic/Web/Review";
+import {UIEventSource} from "../../Logic/UIEventSource";
+import {TextField} from "../Input/TextField";
+import Translations from "../i18n/Translations";
+import Combine from "../Base/Combine";
+import Svg from "../../Svg";
+import {VariableUiElement} from "../Base/VariableUIElement";
+import {SaveButton} from "../Popup/SaveButton";
+import CheckBoxes from "../Input/Checkboxes";
+import {UserDetails} from "../../Logic/Osm/OsmConnection";
+
+export default class ReviewForm extends InputElement {
+
+ private readonly _value: UIEventSource;
+ private readonly _comment: UIElement;
+ private readonly _stars: UIElement;
+ private _saveButton: UIElement;
+ private readonly _isAffiliated: UIElement;
+ private userDetails: UIEventSource;
+ private readonly _postingAs: UIElement;
+
+
+ constructor(onSave: ((r: Review, doneSaving: (() => void)) => void), userDetails: UIEventSource) {
+ super();
+ this.userDetails = userDetails;
+ const t = Translations.t.reviews;
+ this._value = new UIEventSource({
+ rating: undefined,
+ comment: undefined,
+ author: userDetails.data.name,
+ affiliated: false,
+ date: new Date()
+ });
+ const comment = new TextField({
+ placeholder: Translations.t.reviews.write_a_comment,
+ textArea: true,
+ value: this._value.map(r => r?.comment),
+ textAreaRows: 5
+ })
+ comment.GetValue().addCallback(comment => {
+ self._value.data.comment = comment;
+ self._value.ping();
+ })
+ const self = this;
+
+ this._postingAs =
+ new Combine([t.posting_as, new VariableUiElement(userDetails.map((ud: UserDetails) => ud.name)).SetClass("review-author")])
+ .SetStyle("display:flex;flex-direction: column;align-items: flex-end;margin-right: 0.5;")
+ this._saveButton =
+ new SaveButton(this._value.map(r => self.IsValid(r)), undefined)
+ .onClick(() => {
+ self._saveButton = Translations.t.reviews.saving_review;
+ onSave(this._value.data, () => {
+ self._saveButton = Translations.t.reviews.saved;
+ });
+ })
+
+ this._isAffiliated = new CheckBoxes([t.i_am_affiliated]).SetStyle(" display:inline-block;")
+
+ this._comment = comment;
+ const stars = []
+ for (let i = 1; i <= 5; i++) {
+ stars.push(
+ new VariableUiElement(this._value.map(review => {
+ if (review.rating === undefined) {
+ return Svg.star_outline.replace(/#000000/g, "#ccc");
+ }
+ return review.rating < i * 20 ?
+ Svg.star_outline :
+ Svg.star
+ }
+ ))
+ .onClick(() => {
+ self._value.data.rating = i * 20;
+ self._value.ping();
+ })
+ )
+ }
+ this._stars = new Combine(stars).SetClass("review-form-rating")
+ }
+
+ GetValue(): UIEventSource {
+ return this._value;
+ }
+
+ InnerRender(): string {
+
+ if(!this.userDetails.data.loggedIn){
+ return Translations.t.reviews.plz_login.Render();
+ }
+
+ return new Combine([
+ new Combine([this._stars, this._postingAs]).SetClass("review-form-top"),
+ this._comment,
+ new Combine([
+ this._isAffiliated,
+ this._saveButton
+ ]).SetClass("review-form-bottom")
+ ])
+ .SetClass("review-form")
+ .Render();
+ }
+
+ IsSelected: UIEventSource = new UIEventSource(false);
+
+ IsValid(r: Review): boolean {
+ if (r === undefined) {
+ return false;
+ }
+ return (r.comment?.length ?? 0) <= 1000 && (r.author?.length ?? 0) <= 20 && r.rating >= 0 && r.rating <= 100;
+ }
+
+
+}
\ No newline at end of file
diff --git a/UI/Reviews/ReviewPanel.ts b/UI/Reviews/ReviewPanel.ts
new file mode 100644
index 0000000..9ad64b7
--- /dev/null
+++ b/UI/Reviews/ReviewPanel.ts
@@ -0,0 +1,11 @@
+import {UIElement} from "../UIElement";
+
+export default class ReviewPanel extends UIElement {
+
+
+
+ InnerRender(): string {
+ return "";
+ }
+
+}
\ No newline at end of file
diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts
index 260786c..1b45a32 100644
--- a/UI/SpecialVisualizations.ts
+++ b/UI/SpecialVisualizations.ts
@@ -12,6 +12,10 @@ import {Translation} from "./i18n/Translation";
import State from "../State";
import ShareButton from "./ShareButton";
import Svg from "../Svg";
+import ReviewElement from "./Reviews/ReviewElement";
+import MangroveReviews from "../Logic/Web/MangroveReviews";
+import Translations from "./i18n/Translations";
+import ReviewForm from "./Reviews/ReviewForm";
export class SubstitutedTranslation extends UIElement {
private readonly tags: UIEventSource;
@@ -119,6 +123,7 @@ export default class SpecialVisualizations {
})).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;")
})
},
+
{
funcName: "image_carousel",
docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)",
@@ -149,6 +154,24 @@ export default class SpecialVisualizations {
return new ImageUploadFlow(tags, args[0])
}
},
+
+ {
+ funcName: "reviews",
+ docs: "Adds an overview of the mangrove-reviews of this object. IMPORTANT: the _name_ of the object should be defined for this to work!",
+ args: [],
+ constr: (tags, args) => {
+ const tgs = tags.data;
+ if (tgs.name === undefined || tgs.name === "") {
+ return Translations.t.reviews.name_required;
+ }
+ const mangrove = MangroveReviews.Get(Number(tgs._lon), Number(tgs._lat), tgs.name,
+ State.state.mangroveIdentity,
+ State.state.osmConnection._dryRun
+ );
+ const form = new ReviewForm(r => mangrove.AddReview(r), State.state.osmConnection.userDetails);
+ return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form);
+ }
+ },
{
funcName: "opening_hours_table",
docs: "Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.",
diff --git a/assets/svg/star_outline.svg b/assets/svg/star_outline.svg
new file mode 100644
index 0000000..df40f26
--- /dev/null
+++ b/assets/svg/star_outline.svg
@@ -0,0 +1,57 @@
+
+
diff --git a/assets/svg/star_outline_half.svg b/assets/svg/star_outline_half.svg
new file mode 100644
index 0000000..d233cc2
--- /dev/null
+++ b/assets/svg/star_outline_half.svg
@@ -0,0 +1,63 @@
+
+
diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json
index d0591b1..33086aa 100644
--- a/assets/tagRenderings/questions.json
+++ b/assets/tagRenderings/questions.json
@@ -2,6 +2,9 @@
"images": {
"render": "{image_carousel()}{image_upload()}"
},
+ "reviews": {
+ "render": "{reviews()}"
+ },
"phone": {
"question": {
"en": "What is the phone number of {name}?",
diff --git a/assets/themes/shops/shops.json b/assets/themes/shops/shops.json
index 0b27f3f..29068dc 100644
--- a/assets/themes/shops/shops.json
+++ b/assets/themes/shops/shops.json
@@ -67,9 +67,6 @@
}
]
},
- "titleIcons": [
- "isOpen"
- ],
"description": {
"en": "A shop",
"fr": "Un magasin"
@@ -233,7 +230,9 @@
"key": "opening_hours",
"type": "opening_hours"
}
- }
+ },
+ "questions",
+ "reviews"
],
"hideUnderlayingFeaturesMinPercentage": 0,
"icon": {
diff --git a/assets/translations.json b/assets/translations.json
index 779aa05..a660e54 100644
--- a/assets/translations.json
+++ b/assets/translations.json
@@ -830,6 +830,10 @@
}
},
"opening_hours": {
+ "error_loading": {
+ "en": "Error: could not visualize these opening hours.",
+ "nl": "Sorry, deze openingsuren kunnen niet getoond worden"
+ },
"open_during_ph": {
"nl": "Op een feestdag is deze zaak",
"ca": "Durant festes aquest servei és",
@@ -913,13 +917,49 @@
"en": "{count} reviews",
"nl": "{count} beoordelingen"
},
+ "name_required": {
+ "en": "A name is required in order to display and create reviews",
+ "nl": "De naam van dit object moet gekend zijn om een review te kunnen maken"
+ },
"no_reviews_yet": {
"en": "There are no reviews yet. Be the first to write one and help open data and the business!",
"nl": "Er zijn nog geen beoordelingen. Wees de eerste om een beoordeling te schrijven en help open data en het bedrijf"
},
+ "write_a_comment": {
+ "en": "Leave a review...",
+ "nl": "Schrijf een beoordeling..."
+ },
+ "no_rating": {
+ "en": "No rating given",
+ "nl": "Geen score bekend"
+ },
+ "posting_as": {
+ "en": "Posting as",
+ "nl": "Ingelogd als"
+ },
+ "i_am_affiliated": {
+ "en": "
I am affiliated with this object Check if you are an owner, creator, employee, ...
",
+ "nl": "
I am affiliated with this object Vink aan indien je de oprichter, maker, werknemer, ... of dergelijke bent
"
+ },
+ "affiliated_reviewer_warning": {
+ "en": "(Affiliated review)",
+ "nl": "(Review door betrokkene)"
+ },
+ "saving_review": {
+ "en": "Saving...",
+ "nl": "Opslaan..."
+ },
+ "saved": {
+ "en": "Review saved. Thanks for sharing!",
+ "nl": "Bedankt om je beoordeling te delen!"
+ },
"attribution": {
"en": "Reviews are powered by Mangrove Reviews and are available under CC-BY 4.0",
"nl": "De beoordelingen worden voorzien door Mangrove Reviews en zijn beschikbaar onder deCC-BY 4.0-licentie "
+ },
+ "plz_login": {
+ "en": "Login to leave a review",
+ "nl": "Meld je aan om een beoordeling te geven"
}
}
}
\ No newline at end of file
diff --git a/css/ReviewElement.css b/css/ReviewElement.css
index 4d2a1dd..7e04f34 100644
--- a/css/ReviewElement.css
+++ b/css/ReviewElement.css
@@ -1,3 +1,8 @@
+.review {
+ display: block;
+ margin-top: 1em;
+}
+
.review-title {
font-size: x-large;
display: flex;
@@ -39,7 +44,6 @@
.review-author {
font-weight: bold;
- margin-right: 0.5em;
}
.review-author-date {
@@ -66,7 +70,7 @@
}
.review-attribution span {
- width: calc(65% - 3em);
+ width: calc(75% - 3em);
text-align: right;
max-width: 20em;
}
@@ -76,3 +80,45 @@
margin-left: 0.5em;
}
+.review-form {
+ display: block;
+ border-radius: 1em;
+ padding: 1em;
+ background-color: var(--subtle-detail-color);
+ color: var(--subtle-detail-color-contrast);
+ border: 2px solid var(--subtle-detail-color-contrast)
+}
+
+.review-form-bottom {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 0.5em;
+}
+
+.review-form-top {
+ display: flex;
+ justify-content: space-between;
+}
+
+
+.review-form-rating {
+}
+
+.review-form .save {
+ display: block ruby;
+}
+
+.review-form .save-non-active {
+ display: block ruby;
+}
+.review-form textarea {
+ resize: unset;
+}
+
+.review-form-rating svg {
+ width: 2em;
+ height: 2em;
+ margin: 0;
+ padding: 0;
+ display: inline-block;
+}
\ No newline at end of file
diff --git a/test.ts b/test.ts
index 31382ee..2c47420 100644
--- a/test.ts
+++ b/test.ts
@@ -1,50 +1,54 @@
//*
import MangroveReviews from "./Logic/Web/MangroveReviews";
-import ReviewElement from "./UI/ReviewElement";
+import ReviewElement from "./UI/Reviews/ReviewElement";
+import {UIEventSource} from "./Logic/UIEventSource";
+import ReviewForm from "./UI/Reviews/ReviewForm";
+import Combine from "./UI/Base/Combine";
+import {FixedUiElement} from "./UI/Base/FixedUiElement";
-const review = MangroveReviews.GetReviewsFor(3.22000, 51.21576, "Pietervdvn Software Consultancy")
-new ReviewElement(review).AttachTo("maindiv");
-/*
-mangrove.getReviews({sub: 'geo:,?q=&u=15'}).then(
- (data) => {
- for (const review of data.reviews) {
- console.log(review.payload);
- // .signature
- // .kid
- // .jwt
- }
- }
-);*/
+const identity = '{"crv":"P-256","d":"6NHPmTFRedjNl-ZfLRAXhOaNKtRR9GYzPHsO1CzN5wQ","ext":true,"key_ops":["sign"],"kty":"EC","x":"Thm_pL5m0m9Jl41z9vgMTHNyja-9H58v0stJWT4KhTI","y":"PjBldCW85b8K6jEZbw0c2UZskpo-rrkwfPnD7s1MXSM","metadata":"Mangrove private key"}'
+
+const mangroveReviews = new MangroveReviews(0, 0, "Null Island",
+ new UIEventSource(identity), true)
+
+new ReviewElement(mangroveReviews.GetSubjectUri(), mangroveReviews.GetReviews()).AttachTo("maindiv");
+const form = new ReviewForm((r,done) => {
+ mangroveReviews.AddReview(r, done);
+});
+form.AttachTo("extradiv")
+
+form.GetValue().map(r => form.IsValid(r)).addCallback(d => console.log(d))
/*
-mangrove.generateKeypair().then(
- keypair => {
- mangrove.keypairToJwk(keypair).then(jwk => {
- console.log(jwk)
- // const restoredKeypair = await mangrove.jwkToKeypair(jwk).
-// Sign and submit a review (reviews of this example subject are removed from the database).
- mangrove.signAndSubmitReview(keypair, {
- // Lat,lon!
- sub: "geo:51.21576,3.22000?q=Pietervdvn Software Consultancy&u=15",
- rating: 100,
- opinion: "Excellent knowledge about OSM",
- metadata: {
- nickname: "Pietervdvn",
- }
- })
- })
- }
+window.setTimeout(
+ () => {
+mangroveReviews.AddReview({
+ comment: "These are liars - not even an island here!",
+ author: "Lost Tourist",
+ date: new Date(),
+ affiliated: false,
+ rating: 10
+}, (() => {alert("Review added");return undefined;}));
+
+ }, 1000
+)
+
+window.setTimeout(
+ () => {
+ mangroveReviews.AddReview({
+ comment: "Excellent conditions to measure weather!!",
+ author: "Weather-Boy",
+ date: new Date(),
+ affiliated: true,
+ rating: 90
+ }, (() => {
+ alert("Review added");
+ return undefined;
+ }));
+
+ }, 1000
)
*/
-
-/*
-// Given by a particular user since certain time.
-const userReviews = await getReviews({
- kid: '-----BEGIN PUBLIC KEY-----MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDo6mN4kY6YFhpvF0u3hfVWD1RnDElPweX3U3KiUAx0dVeFLPAmeKdQY3J5agY3VspnHo1p/wH9hbZ63qPbCr6g==-----END PUBLIC KEY-----',
- gt_iat: 1580860800
-})*/
-
-
/*/