Finish the additions of reviews
This commit is contained in:
parent
c02406241e
commit
cdfffd6120
29 changed files with 675 additions and 142 deletions
|
@ -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":"<div'><span>I am affiliated with this object</span><br/><span class='subtle'>Check if you are the owner, creator, employee, ... or similar</span></div>","nl":"<div style='display:inline-block;max-width: 40%;'><span>I am affiliated with this object</span><br/><span class='subtle'>Vink aan indien je de oprichter, maker, werknemer, ... of dergelijke bent</span></div>"} ),
|
||||
affiliated_reviewer_warning: new Translation( {"en":"(Affiliated review)","nl":"(Review door betrokkene)"} ),
|
||||
saving_review: new Translation( {"en":"Saving...","nl":"Opslaan..."} ),
|
||||
saved: new Translation( {"en":"<span class='thanks'>Review saved. Thanks for sharing!</span>","nl":"<span class='thanks'>Bedankt om je beoordeling te delen!</span>"} ),
|
||||
attribution: new Translation( {"en":"Reviews are powered by <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> and are available under <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>","nl":"De beoordelingen worden voorzien door <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> en zijn beschikbaar onder de<a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0-licentie</a> "} ),
|
||||
plz_login: new Translation( {"en":"Login to leave a review","nl":"Meld je aan om een beoordeling te geven"} ),
|
||||
},
|
||||
}}
|
|
@ -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;
|
||||
|
|
|
@ -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) []
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ export class OsmConnection {
|
|||
|
||||
public auth;
|
||||
public userDetails: UIEventSource<UserDetails>;
|
||||
private _dryRun: boolean;
|
||||
_dryRun: boolean;
|
||||
|
||||
public preferencesHandler: OsmPreferences;
|
||||
public changesetHandler: ChangesetHandler;
|
||||
|
|
|
@ -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<string>;
|
||||
public keypair: any = undefined;
|
||||
|
||||
constructor(mangroveIdentity: UIEventSource<string>) {
|
||||
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<Review[]> = new UIEventSource<Review[]>([]);
|
||||
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
|
||||
}[]> {
|
||||
public GetReviews(): UIEventSource<Review[]> {
|
||||
|
||||
let uri = `geo:${lat},${lon}?u=50`;
|
||||
if (name !== undefined && name !== null) {
|
||||
uri += "&q=" + name;
|
||||
if(this._lastUpdate !== undefined && this._reviews.data !== undefined &&
|
||||
(new Date().getTime() - this._lastUpdate.getTime()) < 15000
|
||||
){
|
||||
// Last update was pretty recent
|
||||
return this._reviews;
|
||||
}
|
||||
const reviewsSource : UIEventSource< {
|
||||
comment?: string,
|
||||
author: string,
|
||||
date: Date,
|
||||
rating: number
|
||||
}[]> = new UIEventSource([]);
|
||||
this._lastUpdate = new Date();
|
||||
|
||||
mangrove.getReviews({sub: uri}).then(
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
|
|
7
Logic/Web/Review.ts
Normal file
7
Logic/Web/Review.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export interface Review {
|
||||
comment?: string,
|
||||
author: string,
|
||||
date: Date,
|
||||
rating: number,
|
||||
affiliated: boolean
|
||||
}
|
7
State.ts
7
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,6 +65,8 @@ export default class State {
|
|||
*/
|
||||
public osmConnection: OsmConnection;
|
||||
|
||||
public mangroveIdentity: MangroveIdentity;
|
||||
|
||||
public favouriteLayers: UIEventSource<string[]>;
|
||||
|
||||
public layerUpdater: UpdateFromOverpass;
|
||||
|
@ -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 => {
|
||||
|
|
12
Svg.ts
12
Svg.ts
File diff suppressed because one or more lines are too long
|
@ -13,7 +13,7 @@ export default class SettingsTable extends UIElement {
|
|||
public selectedSetting: UIEventSource<SingleSetting<any>>;
|
||||
|
||||
constructor(elements: (SingleSetting<any> | string)[],
|
||||
currentSelectedSetting: UIEventSource<SingleSetting<any>>) {
|
||||
currentSelectedSetting?: UIEventSource<SingleSetting<any>>) {
|
||||
super(undefined);
|
||||
const self = this;
|
||||
this.selectedSetting = currentSelectedSetting ?? new UIEventSource<SingleSetting<any>>(undefined);
|
||||
|
|
|
@ -63,11 +63,11 @@ export class TextField extends InputElement<string> {
|
|||
|
||||
InnerRender(): string {
|
||||
|
||||
const placeholder = this._placeholder.InnerRender().replace("'", "'");
|
||||
if (this._htmlType === "area") {
|
||||
return `<span id="${this.id}"><textarea id="txt-${this.id}" class="form-text-field" rows="${this._textAreaRows}" cols="50" style="max-width: 100%; width: 100%; box-sizing: border-box"></textarea></span>`
|
||||
return `<span id="${this.id}"><textarea id="txt-${this.id}" placeholder='${placeholder}' class="form-text-field" rows="${this._textAreaRows}" cols="50" style="max-width: 100%; width: 100%; box-sizing: border-box"></textarea></span>`
|
||||
}
|
||||
|
||||
const placeholder = this._placeholder.InnerRender().replace("'", "'");
|
||||
let label = "";
|
||||
if (this._label != undefined) {
|
||||
label = this._label.Render();
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<br/><spann class='subtle'>${e}</spann>`
|
||||
const msg = new Combine([Translations.t.general.opening_hours.error_loading,
|
||||
State.state?.osmConnection?.userDetails?.data?.csCount >= State.userJourney.tagsVisibleAndWikiLinked ?
|
||||
`<span class='subtle'>${e}</span>`
|
||||
: ""
|
||||
]);
|
||||
return msg.Render();
|
||||
}
|
||||
|
||||
if (!oh.getState() && !oh.getUnknown()) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
() => {
|
||||
|
|
|
@ -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<any>;
|
||||
private _friendlyLogin: UIElement;
|
||||
private readonly _value: UIEventSource<any>;
|
||||
private readonly _friendlyLogin: UIElement;
|
||||
private readonly _userDetails: UIEventSource<UserDetails>;
|
||||
|
||||
constructor(value: UIEventSource<any>) {
|
||||
constructor(value: UIEventSource<any>, 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();
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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<Review[]>;
|
||||
private readonly _subject: string;
|
||||
private _middleElement: UIElement;
|
||||
|
||||
constructor(subject: string, reviews: UIEventSource<Review[]>, 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([
|
||||
"<img src='./assets/svg/star.svg' />".repeat(Math.floor(scoreTen / 2)),
|
||||
|
@ -38,12 +47,16 @@ export default class ReviewElement extends UIElement {
|
|||
elements.push(
|
||||
new Combine([
|
||||
genStars(avg).SetClass("stars"),
|
||||
`<a href='https://mangrove.reviews/search?sub=${this._subject}'>`,
|
||||
Translations.t.reviews.title
|
||||
.Subs({count: "" + revs.length})
|
||||
.Subs({count: "" + revs.length}),
|
||||
"</a>"
|
||||
])
|
||||
|
||||
.SetClass("review-title"));
|
||||
|
||||
elements.push(this._middleElement);
|
||||
|
||||
elements.push(...revs.map(review => {
|
||||
const d = review.date;
|
||||
return new Combine(
|
||||
|
@ -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")
|
116
UI/Reviews/ReviewForm.ts
Normal file
116
UI/Reviews/ReviewForm.ts
Normal file
|
@ -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<Review> {
|
||||
|
||||
private readonly _value: UIEventSource<Review>;
|
||||
private readonly _comment: UIElement;
|
||||
private readonly _stars: UIElement;
|
||||
private _saveButton: UIElement;
|
||||
private readonly _isAffiliated: UIElement;
|
||||
private userDetails: UIEventSource<UserDetails>;
|
||||
private readonly _postingAs: UIElement;
|
||||
|
||||
|
||||
constructor(onSave: ((r: Review, doneSaving: (() => void)) => void), userDetails: UIEventSource<UserDetails>) {
|
||||
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<Review> {
|
||||
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<boolean> = new UIEventSource<boolean>(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;
|
||||
}
|
||||
|
||||
|
||||
}
|
11
UI/Reviews/ReviewPanel.ts
Normal file
11
UI/Reviews/ReviewPanel.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
|
||||
export default class ReviewPanel extends UIElement {
|
||||
|
||||
|
||||
|
||||
InnerRender(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
|
@ -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<any>;
|
||||
|
@ -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'.",
|
||||
|
|
57
assets/svg/star_outline.svg
Normal file
57
assets/svg/star_outline.svg
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.0"
|
||||
width="1278.000000pt"
|
||||
height="1280.000000pt"
|
||||
viewBox="0 0 1278.000000 1280.000000"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
id="svg8"
|
||||
sodipodi:docname="star_outline.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<defs
|
||||
id="defs12" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="995"
|
||||
id="namedview10"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.2765625"
|
||||
inkscape:cx="372.04589"
|
||||
inkscape:cy="721.1437"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg8" />
|
||||
<metadata
|
||||
id="metadata2">
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:107.38591003;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4"
|
||||
d="m 674.32114,62.94898 c -13.07722,2.481807 -28.82715,15.559025 -43.24073,35.795304 -19.759,27.968066 -36.65439,61.567926 -76.55422,152.535726 -33.12259,75.40877 -46.39071,102.13593 -63.66791,127.62219 -20.14083,29.68623 -34.07713,35.60439 -83.80874,35.69985 -28.92261,0 -54.69523,-2.1 -125.5222,-10.11814 -29.59079,-3.43635 -63.57246,-6.96816 -88.77235,-9.25906 -18.23175,-1.62272 -75.59969,-1.62272 -86.386,0 -34.268043,5.34544 -50.11343,16.60903 -51.354334,36.46349 -0.668179,12.21813 4.104528,25.29535 15.368117,42.19073 18.804466,28.06352 49.063427,58.41794 125.713107,126.28583 91.15871,80.65875 119.03132,112.06317 123.13585,138.59942 3.5318,22.33627 -9.06815,62.61792 -43.24073,139.17214 -34.74531,77.89058 -41.04528,91.92234 -46.77253,105.9541 -24.05445,58.32244 -33.59986,95.26324 -30.35442,117.98134 2.95908,21.1908 13.84085,31.7862 34.07713,33.0271 29.68624,2.0046 73.30878,-16.1317 162.17659,-67.1997 71.49515,-41.1407 84.47692,-48.4907 100.32231,-56.8907 43.04981,-22.90897 68.53607,-32.26347 88.19962,-32.54983 11.54996,-0.0955 15.36812,0.95454 29.59079,8.01814 25.29535,12.69541 54.79068,36.27259 124.09039,99.27229 96.02687,87.436 134.11307,115.1177 167.23566,121.7995 9.73632,2.0045 16.51356,1.2409 24.3408,-2.5773 9.83178,-4.7727 15.27267,-12.8863 19.47265,-29.018 2.00454,-7.7318 2.19544,-10.5954 2.19544,-30.0681 0,-11.9317 -0.47727,-25.4862 -1.14545,-30.5453 -4.86816,-36.1771 -10.21359,-64.3361 -24.14989,-127.43127 -21.859,-98.69959 -26.63171,-126.66765 -26.63171,-157.21298 0,-15.46357 1.52727,-24.81808 5.24998,-33.02713 9.64087,-21.09537 44.09981,-46.77253 121.70404,-90.87235 63.0952,-35.7953 79.3224,-45.14981 95.9314,-55.17249 70.5406,-42.57255 101.6587,-72.64061 101.6587,-98.03141 0,-14.79539 -9.1636,-26.05898 -29.209,-36.08167 -28.6362,-14.31812 -71.3997,-22.52717 -168.3811,-32.64531 C 925.17463,474.35634 898.25656,470.53817 869.33396,463.18821 832.3932,453.64279 820.27053,444.00192 807.47967,414.02932 796.31154,387.68398 787.33885,353.98866 770.1571,272.56628 757.27079,211.09381 749.92082,179.6894 742.57085,154.4895 731.59363,116.49875 719.66186,90.726135 706.29828,75.835289 697.32559,65.812604 685.20291,60.944443 674.32114,62.94898 Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
63
assets/svg/star_outline_half.svg
Normal file
63
assets/svg/star_outline_half.svg
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.0"
|
||||
width="1278.000000pt"
|
||||
height="1280.000000pt"
|
||||
viewBox="0 0 1278.000000 1280.000000"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
id="svg8"
|
||||
sodipodi:docname="star_outline_half.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<defs
|
||||
id="defs12" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="995"
|
||||
id="namedview10"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.2765625"
|
||||
inkscape:cx="-194.78152"
|
||||
inkscape:cy="790.56206"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg8" />
|
||||
<metadata
|
||||
id="metadata2">
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:107.38591003;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4"
|
||||
d="m 674.32114,62.94898 c -13.07722,2.481807 -28.82715,15.559025 -43.24073,35.795304 -19.759,27.968066 -36.65439,61.567926 -76.55422,152.535726 -33.12259,75.40877 -46.39071,102.13593 -63.66791,127.62219 -20.14083,29.68623 -34.07713,35.60439 -83.80874,35.69985 -28.92261,0 -54.69523,-2.1 -125.5222,-10.11814 -29.59079,-3.43635 -63.57246,-6.96816 -88.77235,-9.25906 -18.23175,-1.62272 -75.59969,-1.62272 -86.386,0 -34.268043,5.34544 -50.11343,16.60903 -51.354334,36.46349 -0.668179,12.21813 4.104528,25.29535 15.368117,42.19073 18.804466,28.06352 49.063427,58.41794 125.713107,126.28583 91.15871,80.65875 119.03132,112.06317 123.13585,138.59942 3.5318,22.33627 -9.06815,62.61792 -43.24073,139.17214 -34.74531,77.89058 -41.04528,91.92234 -46.77253,105.9541 -24.05445,58.32244 -33.59986,95.26324 -30.35442,117.98134 2.95908,21.1908 13.84085,31.7862 34.07713,33.0271 29.68624,2.0046 73.30878,-16.1317 162.17659,-67.1997 71.49515,-41.1407 84.47692,-48.4907 100.32231,-56.8907 43.04981,-22.90897 68.53607,-32.26347 88.19962,-32.54983 11.54996,-0.0955 15.36812,0.95454 29.59079,8.01814 25.29535,12.69541 54.79068,36.27259 124.09039,99.27229 96.02687,87.436 134.11307,115.1177 167.23566,121.7995 9.73632,2.0045 16.51356,1.2409 24.3408,-2.5773 9.83178,-4.7727 15.27267,-12.8863 19.47265,-29.018 2.00454,-7.7318 2.19544,-10.5954 2.19544,-30.0681 0,-11.9317 -0.47727,-25.4862 -1.14545,-30.5453 -4.86816,-36.1771 -10.21359,-64.3361 -24.14989,-127.43127 -21.859,-98.69959 -26.63171,-126.66765 -26.63171,-157.21298 0,-15.46357 1.52727,-24.81808 5.24998,-33.02713 9.64087,-21.09537 44.09981,-46.77253 121.70404,-90.87235 63.0952,-35.7953 79.3224,-45.14981 95.9314,-55.17249 70.5406,-42.57255 101.6587,-72.64061 101.6587,-98.03141 0,-14.79539 -9.1636,-26.05898 -29.209,-36.08167 -28.6362,-14.31812 -71.3997,-22.52717 -168.3811,-32.64531 C 925.17463,474.35634 898.25656,470.53817 869.33396,463.18821 832.3932,453.64279 820.27053,444.00192 807.47967,414.02932 796.31154,387.68398 787.33885,353.98866 770.1571,272.56628 757.27079,211.09381 749.92082,179.6894 742.57085,154.4895 731.59363,116.49875 719.66186,90.726135 706.29828,75.835289 697.32559,65.812604 685.20291,60.944443 674.32114,62.94898 Z" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:107.38574982;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4-3"
|
||||
d="m 674.32114,62.94898 c -13.0772,2.48181 -28.8271,15.55903 -43.2407,35.795312 -19.759,27.968058 -36.6544,61.567928 -76.5542,152.535718 -33.1226,75.40878 -46.3907,102.13593 -63.6679,127.62219 -20.1408,29.68623 -34.0771,35.60439 -83.8088,35.69985 -28.9226,0 -54.6952,-2.1 -125.5222,-10.11814 -29.5907,-3.43635 -63.5724,-6.96816 -88.7723,-9.25906 -18.2318,-1.62272 -75.5997,-1.62272 -86.386,0 -34.268097,5.34544 -50.113397,16.60903 -51.354297,36.4635 -0.6682,12.21813 4.1045,25.29534 15.3681,42.19072 18.8044,28.06352 49.063397,58.41794 125.713097,126.28583 91.1587,80.65878 119.0313,112.06318 123.1358,138.59948 3.5318,22.33615 -9.0681,62.61785 -43.2407,139.17205 -34.7453,77.8906 -41.0453,91.9224 -46.7725,105.9541 -24.0545,58.32247 -33.5999,95.26327 -30.3545,117.98137 2.9591,21.1908 13.8409,31.7862 34.0772,33.0271 29.6862,2.0046 73.3088,-16.1317 162.1766,-67.1997 71.4951,-41.1407 84.4769,-48.4907 100.3223,-56.8907 44.4196,-22.95047 93.6333,-33.02397 88.1996,-32.54977 z"
|
||||
sodipodi:nodetypes="cccccccccccccccccccc" />
|
||||
</svg>
|
After Width: | Height: | Size: 5.1 KiB |
|
@ -2,6 +2,9 @@
|
|||
"images": {
|
||||
"render": "{image_carousel()}{image_upload()}"
|
||||
},
|
||||
"reviews": {
|
||||
"render": "{reviews()}"
|
||||
},
|
||||
"phone": {
|
||||
"question": {
|
||||
"en": "What is the phone number of {name}?",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": "<div'><span>I am affiliated with this object</span><br/><span class='subtle'>Check if you are an owner, creator, employee, ...</span></div>",
|
||||
"nl": "<div style='display:inline-block;max-width: 40%;'><span>I am affiliated with this object</span><br/><span class='subtle'>Vink aan indien je de oprichter, maker, werknemer, ... of dergelijke bent</span></div>"
|
||||
},
|
||||
"affiliated_reviewer_warning": {
|
||||
"en": "(Affiliated review)",
|
||||
"nl": "(Review door betrokkene)"
|
||||
},
|
||||
"saving_review": {
|
||||
"en": "Saving...",
|
||||
"nl": "Opslaan..."
|
||||
},
|
||||
"saved": {
|
||||
"en": "<span class='thanks'>Review saved. Thanks for sharing!</span>",
|
||||
"nl": "<span class='thanks'>Bedankt om je beoordeling te delen!</span>"
|
||||
},
|
||||
"attribution": {
|
||||
"en": "Reviews are powered by <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> and are available under <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>",
|
||||
"nl": "De beoordelingen worden voorzien door <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> en zijn beschikbaar onder de<a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0-licentie</a> "
|
||||
},
|
||||
"plz_login": {
|
||||
"en": "Login to leave a review",
|
||||
"nl": "Meld je aan om een beoordeling te geven"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
84
test.ts
84
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<string>(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
|
||||
})*/
|
||||
|
||||
|
||||
/*/
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue