forked from MapComplete/MapComplete
Merge latest master
This commit is contained in:
commit
c693faea95
249 changed files with 8610 additions and 2657 deletions
30
UI/Base/LazyElement.ts
Normal file
30
UI/Base/LazyElement.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
|
||||
export default class LazyElement extends UIElement {
|
||||
|
||||
|
||||
private _content: UIElement = undefined;
|
||||
|
||||
public Activate: () => void;
|
||||
|
||||
constructor(content: (() => UIElement)) {
|
||||
super();
|
||||
this.dumbMode = false;
|
||||
const self = this;
|
||||
this.Activate = () => {
|
||||
if (this._content === undefined) {
|
||||
self._content = content();
|
||||
}
|
||||
self.Update();
|
||||
}
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
if (this._content === undefined) {
|
||||
return "Rendering...";
|
||||
}
|
||||
return this._content.InnerRender();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -42,7 +42,7 @@ export default class SharePanel extends UIElement {
|
|||
"Copy the json configuration from the 'save-tab', paste it between the 'nowiki'-tags in the Wiki",
|
||||
"Click 'save' to save the wiki page",
|
||||
"Share the link with the url parameter <span class='literal-code'>userlayout=wiki:YOURWIKIPAGE</span>, e.g. " +
|
||||
`<a href='./index.html?userlayout=${proposedNameEnc}' target='_blank'>https://kletterspots.de?userlayout=${proposedNameEnc}</a>`
|
||||
`<a href='./index.html?userlayout=${proposedNameEnc}' target='_blank'>https://${window.location.host}?userlayout=${proposedNameEnc}</a>`
|
||||
].map(li => `<li>${li}</li>`),
|
||||
|
||||
"</ol>",
|
||||
|
|
|
@ -12,7 +12,7 @@ export class TextField extends InputElement<string> {
|
|||
private readonly _htmlType: string;
|
||||
private readonly _textAreaRows: number;
|
||||
|
||||
private readonly _isValid: (string, country) => boolean;
|
||||
private readonly _isValid: (string,country) => boolean;
|
||||
private _label: UIElement;
|
||||
|
||||
constructor(options?: {
|
||||
|
@ -22,7 +22,7 @@ export class TextField extends InputElement<string> {
|
|||
htmlType?: string,
|
||||
label?: UIElement,
|
||||
textAreaRows?: number,
|
||||
isValid?: ((s: string, country?: string) => boolean)
|
||||
isValid?: ((s: string, country?: () => string) => boolean)
|
||||
}) {
|
||||
super(undefined);
|
||||
const self = this;
|
||||
|
@ -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();
|
||||
|
|
|
@ -14,8 +14,8 @@ import DirectionInput from "./DirectionInput";
|
|||
interface TextFieldDef {
|
||||
name: string,
|
||||
explanation: string,
|
||||
isValid: ((s: string, country?: string) => boolean),
|
||||
reformat?: ((s: string, country?: string) => string),
|
||||
isValid: ((s: string, country?:() => string) => boolean),
|
||||
reformat?: ((s: string, country?: () => string) => string),
|
||||
inputHelper?: (value: UIEventSource<string>, options?: {
|
||||
location: [number, number]
|
||||
}) => InputElement<string>,
|
||||
|
@ -26,8 +26,8 @@ export default class ValidatedTextField {
|
|||
|
||||
private static tp(name: string,
|
||||
explanation: string,
|
||||
isValid?: ((s: string, country?: string) => boolean),
|
||||
reformat?: ((s: string, country?: string) => string),
|
||||
isValid?: ((s: string, country?: () => string) => boolean),
|
||||
reformat?: ((s: string, country?: () => string) => string),
|
||||
inputHelper?: (value: UIEventSource<string>, options?:{
|
||||
location: [number, number]
|
||||
}) => InputElement<string>): TextFieldDef {
|
||||
|
@ -154,13 +154,13 @@ export default class ValidatedTextField {
|
|||
ValidatedTextField.tp(
|
||||
"phone",
|
||||
"A phone number",
|
||||
(str, country: any) => {
|
||||
(str, country: () => string) => {
|
||||
if (str === undefined) {
|
||||
return false;
|
||||
}
|
||||
return parsePhoneNumberFromString(str, country?.toUpperCase())?.isValid() ?? false
|
||||
return parsePhoneNumberFromString(str, (country())?.toUpperCase() as any)?.isValid() ?? false
|
||||
},
|
||||
(str, country: any) => parsePhoneNumberFromString(str, country?.toUpperCase()).formatInternational()
|
||||
(str, country: () => string) => parsePhoneNumberFromString(str, (country())?.toUpperCase() as any).formatInternational()
|
||||
),
|
||||
ValidatedTextField.tp(
|
||||
"opening_hours",
|
||||
|
@ -200,8 +200,8 @@ export default class ValidatedTextField {
|
|||
value?: UIEventSource<string>,
|
||||
textArea?: boolean,
|
||||
textAreaRows?: number,
|
||||
isValid?: ((s: string, country: string) => boolean),
|
||||
country?: string,
|
||||
isValid?: ((s: string, country: () => string) => boolean),
|
||||
country?: () => string,
|
||||
location?: [number /*lat*/, number /*lon*/]
|
||||
}): InputElement<string> {
|
||||
options = options ?? {};
|
||||
|
@ -304,7 +304,7 @@ export default class ValidatedTextField {
|
|||
textArea?: boolean,
|
||||
textAreaRows?: number,
|
||||
isValid?: ((string: string) => boolean),
|
||||
country?: string
|
||||
country?: () => string
|
||||
}): InputElement<T> {
|
||||
let textField: InputElement<string>;
|
||||
if (options?.type) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import State from "../State";
|
|||
import Translations from "./i18n/Translations";
|
||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
|
||||
export class LayerSelection extends UIElement {
|
||||
|
||||
|
@ -15,15 +16,15 @@ export class LayerSelection extends UIElement {
|
|||
this._checkboxes = [];
|
||||
|
||||
for (const layer of State.state.filteredLayers.data) {
|
||||
let iconUrl = "./asets/checkbox.svg";
|
||||
if (layer.layerDef.icon ) {
|
||||
iconUrl = layer.layerDef.icon.GetRenderValue({id:"node/-1"}).txt;
|
||||
}
|
||||
const icon = new FixedUiElement(`<img style="height:2em;max-width: 2em;" src="${iconUrl}">`);
|
||||
const leafletStyle = layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>({id: "node/-1"}), true)
|
||||
const leafletHtml = leafletStyle.icon.html;
|
||||
const icon =
|
||||
new FixedUiElement(leafletHtml.Render())
|
||||
.SetClass("single-layer-selection-toggle")
|
||||
let iconUnselected: UIElement = new FixedUiElement(leafletHtml.Render())
|
||||
.SetClass("single-layer-selection-toggle")
|
||||
.SetStyle("opacity:0.2;");
|
||||
|
||||
let iconUnselected: UIElement;
|
||||
iconUnselected = new FixedUiElement(`<img style="height:2em;max-width: 2em; opacity:0.2;" src="${iconUrl}">`);
|
||||
|
||||
const name = Translations.WT(layer.layerDef.name).Clone()
|
||||
.SetStyle("font-size:large;margin-left: 0.5em;");
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -151,7 +152,7 @@ export default class OpeningHoursVisualization extends UIElement {
|
|||
|
||||
const tags = this._source.data;
|
||||
if (tags._country === undefined) {
|
||||
return "Loading...";
|
||||
return "Loading country information...";
|
||||
}
|
||||
let oh = null;
|
||||
|
||||
|
@ -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"
|
||||
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()) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import QuestionBox from "./QuestionBox";
|
|||
import Combine from "../Base/Combine";
|
||||
import TagRenderingAnswer from "./TagRenderingAnswer";
|
||||
import State from "../../State";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
export class FeatureInfoBox extends UIElement {
|
||||
private _tags: UIEventSource<any>;
|
||||
|
@ -34,10 +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));
|
||||
|
||||
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 {
|
||||
|
@ -46,7 +62,8 @@ export class FeatureInfoBox extends UIElement {
|
|||
.SetClass("featureinfobox-titlebar"),
|
||||
new Combine([
|
||||
...this._renderings,
|
||||
this._questionBox
|
||||
this._questionBox,
|
||||
new FixedUiElement("").SetClass("featureinfobox-tail")
|
||||
]
|
||||
).SetClass("featureinfobox-content"),
|
||||
]).SetClass("featureinfobox")
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
@ -198,9 +198,12 @@ export default class TagRenderingQuestion extends UIElement {
|
|||
private GenerateMappingElement(mapping: {
|
||||
if: TagsFilter,
|
||||
then: Translation,
|
||||
hideInAnswer: boolean
|
||||
hideInAnswer: boolean | TagsFilter
|
||||
}): InputElement<TagsFilter> {
|
||||
if (mapping.hideInAnswer) {
|
||||
if (mapping.hideInAnswer === true) {
|
||||
return undefined;
|
||||
}
|
||||
if(typeof(mapping.hideInAnswer) !== "boolean" && mapping.hideInAnswer.matches(this._tags.data)){
|
||||
return undefined;
|
||||
}
|
||||
return new FixedInputElement(
|
||||
|
@ -251,7 +254,7 @@ export default class TagRenderingQuestion extends UIElement {
|
|||
|
||||
const textField = ValidatedTextField.InputForType(this._configuration.freeform.type, {
|
||||
isValid: (str) => (str.length <= 255),
|
||||
country: this._tags.data._country,
|
||||
country: () => this._tags.data._country,
|
||||
location: [this._tags.data._lat, this._tags.data._lon]
|
||||
});
|
||||
|
||||
|
|
59
UI/Reviews/ReviewElement.ts
Normal file
59
UI/Reviews/ReviewElement.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Shows the reviews and scoring base on mangrove.reviesw
|
||||
*/
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {Review} from "../../Logic/Web/Review";
|
||||
import {UIElement} from "../UIElement";
|
||||
import Combine from "../Base/Combine";
|
||||
import Translations from "../i18n/Translations";
|
||||
import SingleReview from "./SingleReview";
|
||||
|
||||
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 {
|
||||
|
||||
const elements = [];
|
||||
const revs = this._reviews.data;
|
||||
revs.sort((a, b) => (b.date.getTime() - a.date.getTime())); // Sort with most recent first
|
||||
const avg = (revs.map(review => review.rating).reduce((a, b) => a + b, 0) / revs.length);
|
||||
elements.push(
|
||||
new Combine([
|
||||
SingleReview.GenStars(avg).SetClass("stars"),
|
||||
`<a target="_blank" href='https://mangrove.reviews/search?sub=${encodeURIComponent(this._subject)}'>`,
|
||||
Translations.t.reviews.title
|
||||
.Subs({count: "" + revs.length}),
|
||||
"</a>"
|
||||
])
|
||||
|
||||
.SetClass("review-title"));
|
||||
|
||||
elements.push(this._middleElement);
|
||||
|
||||
elements.push(...revs.map(review => new SingleReview(review)));
|
||||
elements.push(
|
||||
new Combine([
|
||||
Translations.t.reviews.attribution,
|
||||
"<img src='./assets/mangrove_logo.png'>"
|
||||
])
|
||||
|
||||
.SetClass("review-attribution"))
|
||||
|
||||
return new Combine(elements).SetClass("review").Render();
|
||||
}
|
||||
|
||||
}
|
119
UI/Reviews/ReviewForm.ts
Normal file
119
UI/Reviews/ReviewForm.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
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({
|
||||
made_by_user: new UIEventSource<boolean>(true),
|
||||
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.SetClass("thanks");
|
||||
});
|
||||
})
|
||||
|
||||
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"),
|
||||
"<br/>",
|
||||
Translations.t.reviews.tos.SetClass("subtle")
|
||||
])
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
}
|
59
UI/Reviews/SingleReview.ts
Normal file
59
UI/Reviews/SingleReview.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {UIElement} from "../UIElement";
|
||||
import {Review} from "../../Logic/Web/Review";
|
||||
import Combine from "../Base/Combine";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {Utils} from "../../Utils";
|
||||
import ReviewElement from "./ReviewElement";
|
||||
|
||||
export default class SingleReview extends UIElement{
|
||||
private _review: Review;
|
||||
constructor(review: Review) {
|
||||
super(review.made_by_user);
|
||||
this._review = review;
|
||||
|
||||
}
|
||||
public static GenStars(rating: number): UIElement {
|
||||
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)),
|
||||
scoreTen % 2 == 1 ? "<img src='./assets/svg/star_half.svg' />" : ""
|
||||
])
|
||||
}
|
||||
InnerRender(): string {
|
||||
const d = this._review.date;
|
||||
let review = this._review;
|
||||
const el= new Combine(
|
||||
[
|
||||
new Combine([
|
||||
SingleReview.GenStars(review.rating)
|
||||
.SetClass("review-rating"),
|
||||
new FixedUiElement(review.comment).SetClass("review-comment")
|
||||
]).SetClass("review-stars-comment"),
|
||||
|
||||
new Combine([
|
||||
new Combine([
|
||||
|
||||
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")
|
||||
|
||||
]
|
||||
);
|
||||
el.SetClass("review-element");
|
||||
if(review.made_by_user){
|
||||
el.SetClass("review-by-current-user")
|
||||
}
|
||||
return el.Render();
|
||||
}
|
||||
|
||||
}
|
|
@ -12,9 +12,6 @@ export default class ShareButton extends UIElement{
|
|||
super();
|
||||
this._embedded = embedded;
|
||||
this._shareData = shareData;
|
||||
if(this._shareData.url.indexOf("#")> 0){
|
||||
this._shareData.url = this._shareData.url.replace("#","&hash_content=");
|
||||
}
|
||||
}
|
||||
|
||||
InnerRender(): string {
|
||||
|
|
|
@ -146,14 +146,14 @@ export class ShareScreen extends UIElement {
|
|||
this._options = new VerticalCombine(optionCheckboxes)
|
||||
const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
|
||||
|
||||
|
||||
let literalText = "https://kletterspots.de/" + layout.id.toLowerCase() + ".html"
|
||||
const host = window.location.host;
|
||||
let literalText = `https://${host}/${layout.id.toLowerCase()}.html`
|
||||
|
||||
const parts = Utils.NoEmpty(Utils.NoNull(optionParts.map((eventSource) => eventSource.data)));
|
||||
|
||||
let hash = "";
|
||||
if (layoutDefinition !== undefined) {
|
||||
literalText = "https://kletterspots.de/"
|
||||
literalText = `https://${host}/index.html`
|
||||
if (layout.id.startsWith("wiki:")) {
|
||||
parts.push("userlayout=" + encodeURIComponent(layout.id))
|
||||
} else {
|
||||
|
|
|
@ -9,6 +9,7 @@ import State from "../State";
|
|||
|
||||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import Svg from "../Svg";
|
||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||
|
||||
/**
|
||||
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
|
||||
|
@ -21,12 +22,13 @@ export class SimpleAddUI extends UIElement {
|
|||
private _confirmPreset: UIEventSource<{
|
||||
description: string | UIElement,
|
||||
name: string | UIElement,
|
||||
icon: string,
|
||||
icon: UIElement,
|
||||
tags: Tag[],
|
||||
layerToAddTo: FilteredLayer
|
||||
}>
|
||||
= new UIEventSource(undefined);
|
||||
private confirmButton: UIElement = undefined;
|
||||
private _confirmDescription: UIElement = undefined;
|
||||
private openLayerControl: UIElement;
|
||||
private cancelButton: UIElement;
|
||||
private goToInboxButton: UIElement = new SubtleButton(Svg.envelope_ui(),
|
||||
|
@ -52,8 +54,8 @@ export class SimpleAddUI extends UIElement {
|
|||
|
||||
const presets = layer.layerDef.presets;
|
||||
for (const preset of presets) {
|
||||
let icon: string = layer.layerDef.icon.GetRenderValue(
|
||||
TagUtils.KVtoProperties(preset.tags ?? [])).txt
|
||||
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
|
||||
let icon: UIElement = new FixedUiElement(layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html.Render()).SetClass("simple-add-ui-icon");
|
||||
|
||||
const csCount = State.state.osmConnection.userDetails.data.csCount;
|
||||
let tagInfo = "";
|
||||
|
@ -68,7 +70,7 @@ export class SimpleAddUI extends UIElement {
|
|||
"<b>",
|
||||
preset.title,
|
||||
"</b>",
|
||||
preset.description !== undefined ? new Combine(["<br/>", preset.description]) : "",
|
||||
preset.description !== undefined ? new Combine(["<br/>", preset.description.FirstSentence()]) : "",
|
||||
tagInfo
|
||||
])
|
||||
).onClick(
|
||||
|
@ -77,9 +79,9 @@ export class SimpleAddUI extends UIElement {
|
|||
new Combine([
|
||||
"<b>",
|
||||
Translations.t.general.add.confirmButton.Subs({category: preset.title}),
|
||||
"</b><br/>",
|
||||
preset.description !== undefined ? preset.description : ""]));
|
||||
"</b>"]));
|
||||
self.confirmButton.onClick(self.CreatePoint(preset.tags, layer));
|
||||
self._confirmDescription = preset.description;
|
||||
self._confirmPreset.setData({
|
||||
tags: preset.tags,
|
||||
layerToAddTo: layer,
|
||||
|
@ -147,6 +149,7 @@ export class SimpleAddUI extends UIElement {
|
|||
userDetails.data.dryRun ? "<span class='alert'>TESTING - changes won't be saved</span>" : "",
|
||||
this.confirmButton,
|
||||
this.cancelButton,
|
||||
this._confirmDescription,
|
||||
tagInfo
|
||||
|
||||
]).Render();
|
||||
|
@ -189,7 +192,7 @@ export class SimpleAddUI extends UIElement {
|
|||
}
|
||||
|
||||
if (State.state.locationControl.data.zoom < State.userJourney.minZoomLevelToAddNewPoints) {
|
||||
return new Combine([header, Translations.t.general.add.zoomInFurther]).Render()
|
||||
return new Combine([header, Translations.t.general.add.zoomInFurther.SetClass("alert")]).Render()
|
||||
}
|
||||
|
||||
if (State.state.layerUpdater.runningQuery.data) {
|
||||
|
|
|
@ -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>;
|
||||
|
@ -58,7 +62,7 @@ export class SubstitutedTranslation extends UIElement {
|
|||
|
||||
for (const knownSpecial of SpecialVisualizations.specialVisualizations) {
|
||||
|
||||
// NOte: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||
const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)}(.*)`);
|
||||
if (matched != null) {
|
||||
|
||||
|
@ -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, whenDone) => mangrove.AddReview(r, whenDone), 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'.",
|
||||
|
@ -200,16 +223,20 @@ export default class SpecialVisualizations {
|
|||
if (window.navigator.share) {
|
||||
const title = State.state.layoutToUse.data.title.txt;
|
||||
let name = tagSource.data.name;
|
||||
if(name){
|
||||
if (name) {
|
||||
name = `${name} (${title})`
|
||||
}else{
|
||||
} else {
|
||||
name = title;
|
||||
}
|
||||
return new ShareButton(Svg.share_svg(), {
|
||||
title: name,
|
||||
url: args[0] ?? window.location.href,
|
||||
text: State.state.layoutToUse.data.shortDescription.txt
|
||||
})
|
||||
let url = args[0] ?? ""
|
||||
if (url === "") {
|
||||
url = window.location.href
|
||||
}
|
||||
return new ShareButton(Svg.share_svg(), {
|
||||
title: name,
|
||||
url: url,
|
||||
text: State.state.layoutToUse.data.shortDescription.txt
|
||||
})
|
||||
} else {
|
||||
return new FixedUiElement("")
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {UIElement} from "./UIElement";
|
||||
import * as L from "leaflet";
|
||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import Translations from "./i18n/Translations";
|
||||
|
@ -9,7 +8,6 @@ import {UIEventSource} from "../Logic/UIEventSource";
|
|||
import Combine from "./Base/Combine";
|
||||
import Svg from "../Svg";
|
||||
import Link from "./Base/Link";
|
||||
import {Img} from "./Img";
|
||||
import LanguagePicker from "./LanguagePicker";
|
||||
|
||||
/**
|
||||
|
@ -61,7 +59,7 @@ export class UserBadge extends UIElement {
|
|||
if (home === undefined) {
|
||||
return;
|
||||
}
|
||||
State.state.bm.map.flyTo([home.lat, home.lon], 18);
|
||||
State.state.bm.map.setView([home.lat, home.lon], 16);
|
||||
});
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue