forked from MapComplete/MapComplete
		
	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 | ||||
|     }[]> { | ||||
| 
 | ||||
|         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<Review[]> { | ||||
|          | ||||
|         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(); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										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 | ||||
| } | ||||
							
								
								
									
										9
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										9
									
								
								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<string[]>; | ||||
| 
 | ||||
|     public layerUpdater: UpdateFromOverpass; | ||||
| 
 | ||||
| 
 | ||||
|     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([]) | ||||
|      | ||||
| 
 | ||||
|     /** | ||||
|      *  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 => { | ||||
|  |  | |||
							
								
								
									
										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,11 +47,15 @@ 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; | ||||
|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue