Finish the additions of reviews

This commit is contained in:
Pieter Vander Vennet 2020-12-08 23:44:34 +01:00
parent c02406241e
commit cdfffd6120
29 changed files with 675 additions and 142 deletions

View file

@ -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);

View file

@ -63,11 +63,11 @@ export class TextField extends InputElement<string> {
InnerRender(): string {
const placeholder = this._placeholder.InnerRender().replace("'", "&#39");
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("'", "&#39");
let label = "";
if (this._label != undefined) {
label = this._label.Render();

View file

@ -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()

View file

@ -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()) {

View file

@ -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 {

View file

@ -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,
() => {

View file

@ -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();

View file

@ -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)

View file

@ -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
View 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
View file

@ -0,0 +1,11 @@
import {UIElement} from "../UIElement";
export default class ReviewPanel extends UIElement {
InnerRender(): string {
return "";
}
}

View file

@ -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'.",