Use mangrove-reviews-typescript, rework reviews modules

This commit is contained in:
Pieter Vander Vennet 2023-01-21 23:58:14 +01:00
parent 93961e553f
commit 888d4e95a3
15 changed files with 768 additions and 375 deletions

View file

@ -22,7 +22,7 @@ export class ElementStorage {
*
* Note: it will cleverly merge the tags, if needed
*/
addOrGetElement(feature: any): UIEventSource<any> {
addOrGetElement(feature: Feature<Geometry, OsmTags>): UIEventSource<any> {
const elementId = feature.properties.id
const newProperties = feature.properties

View file

@ -143,7 +143,6 @@ export default class OsmFeatureSource {
try {
const osmJson = await Utils.downloadJson(url)
try {
console.log("Got tile", z, x, y, "from the osm api")
this.rawDataHandlers.forEach((handler) =>
handler(osmJson, Tiles.tile_index(z, x, y))
)

View file

@ -371,12 +371,12 @@ class ListenerTracker<T> {
* It'll fuse
*/
class MappedStore<TIn, T> extends Store<T> {
private _upstream: Store<TIn>
private _upstreamCallbackHandler: ListenerTracker<TIn> | undefined
private readonly _upstream: Store<TIn>
private readonly _upstreamCallbackHandler: ListenerTracker<TIn> | undefined
private _upstreamPingCount: number = -1
private _unregisterFromUpstream: () => void
private _f: (t: TIn) => T
private readonly _f: (t: TIn) => T
private readonly _extraStores: Store<any>[] | undefined
private _unregisterFromExtraStores: (() => void)[] | undefined

View file

@ -1,36 +1,37 @@
import { UIEventSource } from "../UIEventSource"
import { Review } from "./Review"
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
import { MangroveReviews, Review } from "mangrove-reviews-typescript"
import { Utils } from "../../Utils"
import { Feature, Geometry, Position } from "geojson"
import { GeoOperations } from "../GeoOperations"
import { OsmTags } from "../../Models/OsmFeature"
import { ElementStorage } from "../ElementStorage"
export class MangroveIdentity {
public keypair: any = undefined
public readonly kid: UIEventSource<string> = new UIEventSource<string>(undefined)
private readonly _mangroveIdentity: UIEventSource<string>
public readonly keypair: Store<CryptoKeyPair>
public readonly key_id: Store<string>
constructor(mangroveIdentity: UIEventSource<string>) {
const self = this
/*
this._mangroveIdentity = mangroveIdentity
mangroveIdentity.addCallbackAndRunD((str) => {
if (str === "") {
const key_id = new UIEventSource<string>(undefined)
this.key_id = key_id
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
this.keypair = keypairEventSource
mangroveIdentity.addCallbackAndRunD(async (data) => {
if (data === "") {
return
}
mangrove.jwkToKeypair(JSON.parse(str)).then((keypair) => {
self.keypair = keypair
mangrove.publicToPem(keypair.publicKey).then((pem) => {
console.log("Identity loaded")
self.kid.setData(pem)
})
})
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))
keypairEventSource.setData(keypair)
const pem = await MangroveReviews.publicToPem(keypair.publicKey)
key_id.setData(pem)
})
try {
if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") {
this.CreateIdentity()
MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {})
}
} catch (e) {
console.error("Could not create identity: ", e)
}
// */
}
/**
@ -38,170 +39,204 @@ export class MangroveIdentity {
* 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"
private static async CreateIdentity(identity: UIEventSource<string>): Promise<void> {
const keypair = await MangroveReviews.generateKeypair()
const jwk = await MangroveReviews.keypairToJwk(keypair)
if ((identity.data ?? "") !== "") {
// Identity has been loaded via osmPreferences by now - we don't overwrite
return
}
const self = this
/*mangrove.generateKeypair().then((keypair) => {
self.keypair = keypair
mangrove.keypairToJwk(keypair).then((jwk) => {
self._mangroveIdentity.setData(JSON.stringify(jwk))
})
})//*/
identity.setData(JSON.stringify(jwk))
}
}
export default class MangroveReviews {
private static _reviewsCache = {}
private static didWarn = false
private readonly _lon: number
/**
* Tracks all reviews of a given feature, allows to create a new review
*/
export default class FeatureReviews {
private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {}
public readonly subjectUri: Store<string>
private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
new UIEventSource([])
public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
this._reviews
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
private readonly _lon: number
private readonly _uncertainty: number
private readonly _name: Store<string>
private readonly _identity: MangroveIdentity
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
if (dryRun && !MangroveReviews.didWarn) {
MangroveReviews.didWarn = true
console.warn("Mangrove reviews will _not_ be saved as dryrun is specified")
feature: Feature<Geometry, OsmTags>,
state: {
allElements: ElementStorage
mangroveIdentity?: MangroveIdentity
},
options?: {
nameKey?: "name" | string
fallbackName?: string
uncertaintyRadius?: number
}
) {
const centerLonLat = GeoOperations.centerpointCoordinates(feature)
;[this._lon, this._lat] = centerLonLat
this._identity =
state?.mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined))
const nameKey = options?.nameKey ?? "name"
if (feature.geometry.type === "Point") {
this._uncertainty = options?.uncertaintyRadius ?? 10
} else {
let coords: Position[][]
if (feature.geometry.type === "LineString") {
coords = [feature.geometry.coordinates]
} else if (
feature.geometry.type === "MultiLineString" ||
feature.geometry.type === "Polygon"
) {
coords = feature.geometry.coordinates
}
let maxDistance = 0
for (const coord of coords) {
maxDistance = Math.max(
maxDistance,
GeoOperations.distanceBetween(centerLonLat, <any>coord)
)
}
this._uncertainty = options?.uncertaintyRadius ?? maxDistance
}
this._name = state.allElements
.getEventSourceById(feature.properties.id)
.map((tags) => tags[nameKey] ?? options?.fallbackName)
this.subjectUri = this.ConstructSubjectUri()
const self = this
this.subjectUri.addCallbackAndRunD(async (sub) => {
const reviews = await MangroveReviews.getReviews({ sub })
self.addReviews(reviews.reviews)
})
/* We also construct all subject queries _without_ encoding the name to work around a previous bug
* See https://github.com/giggls/opencampsitemap/issues/30
*/
this.ConstructSubjectUri(true).addCallbackAndRunD(async (sub) => {
try {
const reviews = await MangroveReviews.getReviews({ sub })
self.addReviews(reviews.reviews)
} catch (e) {
console.log("Could not fetch reviews for partially incorrect query ", sub)
}
})
}
public static Get(
lon: number,
lat: number,
name: string,
identity: MangroveIdentity,
dryRun?: boolean
/**
* Construct a featureReviewsFor or fetches it from the cache
*/
public static construct(
feature: Feature<Geometry, OsmTags>,
state: {
allElements: ElementStorage
mangroveIdentity?: MangroveIdentity
},
options?: {
nameKey?: "name" | string
fallbackName?: string
uncertaintyRadius?: number
}
) {
const newReviews = new MangroveReviews(lon, lat, name, identity, dryRun)
const uri = newReviews.GetSubjectUri()
const cached = MangroveReviews._reviewsCache[uri]
const key = feature.properties.id
const cached = FeatureReviews._featureReviewsCache[key]
if (cached !== undefined) {
return cached
}
MangroveReviews._reviewsCache[uri] = newReviews
const featureReviews = new FeatureReviews(feature, state, options)
FeatureReviews._featureReviewsCache[key] = featureReviews
return featureReviews
}
return newReviews
/**
* The given review is uploaded to mangrove.reviews and added to the list of known reviews
*/
public async createReview(review: Omit<Review, "sub">): Promise<void> {
const r: Review = {
sub: this.subjectUri.data,
...review,
}
const keypair: CryptoKeyPair = this._identity.keypair.data
console.log(r)
const jwt = await MangroveReviews.signReview(keypair, r)
console.log("Signed:", jwt)
await MangroveReviews.submitReview(jwt)
this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) })
this._reviews.ping()
}
/**
* Adds given reviews to the 'reviews'-UI-eventsource
* @param reviews
* @private
*/
private addReviews(reviews: { payload: Review; kid: string }[]) {
const self = this
const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion))
let hasNew = false
for (const reviewData of reviews) {
const review = reviewData.payload
try {
const url = new URL(review.sub)
console.log("URL is", url)
if (url.protocol === "geo:") {
const coordinate = <[number, number]>(
url.pathname.split(",").map((n) => Number(n))
)
const distance = GeoOperations.distanceBetween(
[this._lat, this._lon],
coordinate
)
if (distance > this._uncertainty) {
continue
}
}
} catch (e) {
console.warn(e)
}
const key = review.rating + " " + review.opinion
if (alreadyKnown.has(key)) {
continue
}
self._reviews.data.push({
...review,
madeByLoggedInUser: this._identity.key_id.map((user_key_id) => {
return reviewData.kid === user_key_id
}),
})
hasNew = true
}
if (hasNew) {
self._reviews.ping()
}
}
/**
* Gets an URI which represents the item in a mangrove-compatible way
*
* See https://mangrove.reviews/standard#mangrove-core-uri-schemes
* @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 GetReviews(): UIEventSource<Review[]> {
/*
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()
private ConstructSubjectUri(dontEncodeName: boolean = false): Store<string> {
// https://www.rfc-editor.org/rfc/rfc5870#section-3.4.2
// `u` stands for `uncertainty`, https://www.rfc-editor.org/rfc/rfc5870#section-3.4.3
const self = this
mangrove
.getReviews({ sub: this.GetSubjectUri() })
.then((data) => {
const reviews = []
const reviewsByUser = []
for (const review of data.reviews) {
const r = review.payload
console.log(
"PublicKey is ",
self._mangroveIdentity.kid.data,
"reviews.kid is",
review.kid
)
const byUser = self._mangroveIdentity.kid.map(
(data) => data === review.signature
)
const rev: Review = {
made_by_user: byUser,
date: new Date(r.iat * 1000),
comment: r.opinion,
author: r.metadata.nickname,
affiliated: r.metadata.is_affiliated,
rating: r.rating, // percentage points
}
;(rev.made_by_user ? reviewsByUser : reviews).push(rev)
}
self._reviews.setData(reviewsByUser.concat(reviews))
})
.catch((e) => {
console.error("Could not download review for ", e)
})
//*/
return this._reviews
}
AddReview(r: Review, callback?: () => void) {
callback =
callback ??
(() => {
return undefined
})
const payload = {
sub: this.GetSubjectUri(),
rating: r.rating,
opinion: r.comment,
metadata: {
nickname: r.author,
},
}
if (r.affiliated) {
// @ts-ignore
payload.metadata.is_affiliated = true
}
if (this._dryRun) {
console.warn("DRYRUNNING mangrove reviews: ", payload)
if (callback) {
if (callback) {
callback()
}
this._reviews.data.push(r)
this._reviews.ping()
return this._name.map(function (name) {
let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}`
if (name) {
uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name))
}
} else {
/*mangrove.signAndSubmitReview(this._mangroveIdentity.keypair, payload).then(() => {
if (callback) {
callback()
}
this._reviews.data.push(r)
this._reviews.ping()
})//*/
}
return uri
})
}
}

View file

@ -1,13 +0,0 @@
import { Store } from "../UIEventSource"
export interface Review {
comment?: string
author: string
date: Date
rating: number
affiliated: boolean
/**
* True if the current logged in user is the creator of this comment
*/
made_by_user: Store<boolean>
}

View file

@ -12,7 +12,12 @@ import Loading from "./Loading"
export class SubtleButton extends UIElement {
private readonly imageUrl: string | BaseUIElement
private readonly message: string | BaseUIElement
private readonly options: { url?: string | Store<string>; newTab?: boolean; imgSize?: string }
private readonly options: {
url?: string | Store<string>
newTab?: boolean
imgSize?: string
extraClasses?: string
}
constructor(
imageUrl: string | BaseUIElement,
@ -21,6 +26,7 @@ export class SubtleButton extends UIElement {
url?: string | Store<string>
newTab?: boolean
imgSize?: "h-11 w-11" | string
extraClasses?: string
} = undefined
) {
super()
@ -31,7 +37,8 @@ export class SubtleButton extends UIElement {
protected InnerRender(): string | BaseUIElement {
const classes =
"block flex p-3 my-2 bg-subtle rounded-lg hover:shadow-xl hover:bg-unsubtle transition-colors transition-shadow link-no-underline"
"block flex p-3 my-2 bg-subtle rounded-lg hover:shadow-xl hover:bg-unsubtle transition-colors transition-shadow link-no-underline " +
(this?.options?.extraClasses ?? "")
const message = Translations.W(this.message)?.SetClass(
"block text-ellipsis no-images flex-shrink"
)

View file

@ -1,5 +1,3 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import { Review } from "../../Logic/Web/Review"
import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import SingleReview from "./SingleReview"
@ -7,44 +5,52 @@ import BaseUIElement from "../BaseUIElement"
import Img from "../Base/Img"
import { VariableUiElement } from "../Base/VariableUIElement"
import Link from "../Base/Link"
import FeatureReviews from "../../Logic/Web/MangroveReviews"
/**
* Shows the reviews and scoring base on mangrove.reviews
* The middle element is some other component shown in the middle, e.g. the review input element
*/
export default class ReviewElement extends VariableUiElement {
constructor(subject: string, reviews: UIEventSource<Review[]>, middleElement: BaseUIElement) {
constructor(reviews: FeatureReviews, middleElement: BaseUIElement) {
super(
reviews.map((revs) => {
const elements = []
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),
new Link(
revs.length === 1
? Translations.t.reviews.title_singular.Clone()
: Translations.t.reviews.title.Subs({ count: "" + revs.length }),
`https://mangrove.reviews/search?sub=${encodeURIComponent(subject)}`,
true
),
]).SetClass("font-2xl flex justify-between items-center pl-2 pr-2")
)
reviews.reviews.map(
(revs) => {
const elements = []
revs.sort((a, b) => b.iat - a.iat) // 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),
new Link(
revs.length === 1
? Translations.t.reviews.title_singular.Clone()
: Translations.t.reviews.title.Subs({
count: "" + revs.length,
}),
`https://mangrove.reviews/search?sub=${encodeURIComponent(
reviews.subjectUri.data
)}`,
true
),
]).SetClass("font-2xl flex justify-between items-center pl-2 pr-2")
)
elements.push(middleElement)
elements.push(middleElement)
elements.push(...revs.map((review) => new SingleReview(review)))
elements.push(
new Combine([
Translations.t.reviews.attribution.Clone(),
new Img("./assets/mangrove_logo.png"),
]).SetClass("review-attribution")
)
elements.push(...revs.map((review) => new SingleReview(review)))
elements.push(
new Combine([
Translations.t.reviews.attribution.Clone(),
new Img("./assets/mangrove_logo.png"),
]).SetClass("review-attribution")
)
return new Combine(elements).SetClass("block")
})
return new Combine(elements).SetClass("block")
},
[reviews.subjectUri]
)
)
}
}

View file

@ -1,118 +1,89 @@
import { InputElement } from "../Input/InputElement"
import { Review } from "../../Logic/Web/Review"
import { Review } from "mangrove-reviews-typescript"
import { Store, 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 { CheckBox } from "../Input/Checkboxes"
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
import BaseUIElement from "../BaseUIElement"
import Toggle from "../Input/Toggle"
import { LoginToggle } from "../Popup/LoginButton"
import { SubtleButton } from "../Base/SubtleButton"
export default class ReviewForm extends InputElement<Review> {
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false)
private readonly _value: UIEventSource<Review>
private readonly _comment: BaseUIElement
private readonly _stars: BaseUIElement
private _saveButton: BaseUIElement
private readonly _isAffiliated: BaseUIElement
private readonly _postingAs: BaseUIElement
private readonly _state: {
readonly osmConnection: OsmConnection
readonly featureSwitchUserbadge: Store<boolean>
}
export default class ReviewForm extends LoginToggle {
constructor(
onSave: (r: Review, doneSaving: () => void) => void,
onSave: (r: Omit<Review, "sub">) => Promise<void>,
state: {
readonly osmConnection: OsmConnection
readonly featureSwitchUserbadge: Store<boolean>
}
) {
super()
this._state = state
const osmConnection = state.osmConnection
this._value = new UIEventSource({
made_by_user: new UIEventSource<boolean>(true),
/* made_by_user: new UIEventSource<boolean>(true),
rating: undefined,
comment: undefined,
author: osmConnection.userDetails.data.name,
affiliated: false,
date: new Date(),
})
const comment = new TextField({
date: new Date(),*/
const commentForm = new TextField({
placeholder: Translations.t.reviews.write_a_comment.Clone(),
htmlType: "area",
textAreaRows: 5,
})
comment.GetValue().addCallback((comment) => {
self._value.data.comment = comment
self._value.ping()
})
const self = this
this._postingAs = new Combine([
const rating = new UIEventSource<number>(undefined)
const isAffiliated = new CheckBox(Translations.t.reviews.i_am_affiliated)
const reviewMade = new UIEventSource(false)
const postingAs = new Combine([
Translations.t.reviews.posting_as.Clone(),
new VariableUiElement(
osmConnection.userDetails.map((ud: UserDetails) => ud.name)
state.osmConnection.userDetails.map((ud: UserDetails) => ud.name)
).SetClass("review-author"),
]).SetStyle("display:flex;flex-direction: column;align-items: flex-end;margin-left: auto;")
const reviewIsSaved = new UIEventSource<boolean>(false)
const reviewIsSaving = new UIEventSource<boolean>(false)
this._saveButton = new Toggle(
Translations.t.reviews.saved.Clone().SetClass("thanks"),
new Toggle(
Translations.t.reviews.saving_review.Clone(),
new SaveButton(
this._value.map((r) => self.IsValid(r)),
osmConnection
).onClick(() => {
reviewIsSaving.setData(true)
onSave(this._value.data, () => {
reviewIsSaved.setData(true)
})
}),
reviewIsSaving
),
reviewIsSaved
).SetClass("break-normal")
const saveButton = new Toggle(
Translations.t.reviews.no_rating.SetClass("block alert"),
new SubtleButton(Svg.confirm_svg(), Translations.t.reviews.save, {
extraClasses: "border-attention-catch",
})
.OnClickWithLoading(
Translations.t.reviews.saving_review.SetClass("alert"),
async () => {
const review: Omit<Review, "sub"> = {
rating: rating.data,
opinion: commentForm.GetValue().data,
metadata: { nickname: state.osmConnection.userDetails.data.name },
}
await onSave(review)
}
)
.SetClass("break-normal"),
rating.map((r) => r === undefined, [commentForm.GetValue()])
)
this._isAffiliated = new CheckBoxes([Translations.t.reviews.i_am_affiliated.Clone()])
this._comment = comment
const stars = []
for (let i = 1; i <= 5; i++) {
stars.push(
new VariableUiElement(
this._value.map((review) => {
if (review.rating === undefined) {
rating.map((score) => {
if (score === undefined) {
return Svg.star_outline.replace(/#000000/g, "#ccc")
}
return review.rating < i * 20 ? Svg.star_outline : Svg.star
return score < i * 20 ? Svg.star_outline : Svg.star
})
).onClick(() => {
self._value.data.rating = i * 20
self._value.ping()
rating.setData(i * 20)
})
)
}
this._stars = new Combine(stars).SetClass("review-form-rating")
}
GetValue(): UIEventSource<Review> {
return this._value
}
InnerConstructElement(): HTMLElement {
const form = new Combine([
new Combine([this._stars, this._postingAs]).SetClass("flex"),
this._comment,
new Combine([this._isAffiliated, this._saveButton]).SetClass("review-form-bottom"),
new Combine([new Combine(stars).SetClass("review-form-rating"), postingAs]).SetClass(
"flex"
),
commentForm,
new Combine([isAffiliated, saveButton]),
Translations.t.reviews.tos.Clone().SetClass("subtle"),
])
.SetClass("flex flex-col p-4")
@ -123,22 +94,10 @@ export default class ReviewForm extends InputElement<Review> {
" border: 2px solid var(--subtle-detail-color-contrast)"
)
return new LoginToggle(
form,
Translations.t.reviews.plz_login.Clone(),
this._state
).ConstructElement()
}
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
super(
new Toggle(Translations.t.reviews.saved.Clone().SetClass("thanks"), form, reviewMade),
Translations.t.reviews.plz_login,
state
)
}
}

View file

@ -1,33 +1,47 @@
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 BaseUIElement from "../BaseUIElement"
import Img from "../Base/Img"
import { Review } from "mangrove-reviews-typescript"
import { Store } from "../../Logic/UIEventSource"
import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"
export default class SingleReview extends Combine {
constructor(review: Review) {
const d = review.date
constructor(review: Review & { madeByLoggedInUser: Store<boolean> }) {
const d = review
const date = new Date(review.iat * 1000)
const reviewAuthor =
review.metadata.nickname ??
(review.metadata.given_name ?? "") + (review.metadata.family_name ?? "")
super([
new Combine([SingleReview.GenStars(review.rating)]),
new FixedUiElement(review.comment),
new FixedUiElement(review.opinion),
new Combine([
new Combine([
new FixedUiElement(review.author).SetClass("font-bold"),
review.affiliated ? Translations.t.reviews.affiliated_reviewer_warning : "",
new FixedUiElement(reviewAuthor).SetClass("font-bold"),
review.metadata.is_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())}`
`${date.getFullYear()}-${Utils.TwoDigits(
date.getMonth() + 1
)}-${Utils.TwoDigits(date.getDate())} ${Utils.TwoDigits(
date.getHours()
)}:${Utils.TwoDigits(date.getMinutes())}`
).SetClass("subtle-lighter"),
]).SetClass("flex mb-4 justify-end"),
])
this.SetClass("block p-2 m-4 rounded-xl subtle-background review-element")
if (review.made_by_user.data) {
this.SetClass("border-attention-catch")
}
review.madeByLoggedInUser.addCallbackAndRun((madeByUser) => {
if (madeByUser) {
this.SetClass("border-attention-catch")
} else {
this.RemoveClass("border-attention-catch")
}
})
}
public static GenStars(rating: number): BaseUIElement {

View file

@ -52,6 +52,7 @@ import { GeoOperations } from "../Logic/GeoOperations"
import StatisticsPanel from "./BigComponents/StatisticsPanel"
import AutoApplyButton from "./Popup/AutoApplyButton"
import { LanguageElement } from "./Popup/LanguageElement"
import FeatureReviews from "../Logic/Web/MangroveReviews"
export default class SpecialVisualizations {
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
@ -204,24 +205,16 @@ export default class SpecialVisualizations {
},
],
constr: (state, tags, args) => {
const tgs = tags.data
const key = args[0] ?? "name"
let subject = tgs[key] ?? args[1]
if (subject === undefined || subject === "") {
return Translations.t.reviews.name_required
}
const mangrove = MangroveReviews.Get(
Number(tgs._lon),
Number(tgs._lat),
encodeURIComponent(subject),
state.mangroveIdentity,
state.featureSwitchIsTesting.data
)
const form = new ReviewForm(
(r, whenDone) => mangrove.AddReview(r, whenDone),
state
)
return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form)
const nameKey = args[0] ?? "name"
let fallbackName = args[1]
const feature = state.allElements.ContainingFeatures.get(tags.data.id)
const mangrove = FeatureReviews.construct(feature, state, {
nameKey: nameKey,
fallbackName,
})
const form = new ReviewForm((r) => mangrove.createReview(r), state)
return new ReviewElement(mangrove, form)
},
},
{

View file

@ -1,13 +1,21 @@
{
"id": "mapcomplete-changes",
"title": {
"en": "Changes made with MapComplete"
"en": "Changes made with MapComplete",
"de": "Mit MapComplete vorgenommene Änderungen",
"nl": "Wijzigingen gemaakt met MapComplete"
},
"shortDescription": {
"en": "Shows changes made by MapComplete"
"en": "Shows changes made by MapComplete",
"de": "Zeigt Änderungen, die von MapComplete vorgenommen wurden",
"nl": "Toont wijzigingen gemaakt met MapComplete"
},
"description": {
"en": "This maps shows all the changes made with MapComplete"
"en": "This map shows all the changes made with MapComplete",
"ca": "Aquest mapa mostra tots els canvis fets amb MapComplete",
"de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen",
"fr": "Cette carte montre tous les changements faits avec MapComplete",
"nl": "Deze kaart toont alle wijzigingen gemaakt met MapComplete"
},
"icon": "./assets/svg/logo.svg",
"hideFromOverview": true,
@ -20,7 +28,10 @@
{
"id": "mapcomplete-changes",
"name": {
"en": "Changeset centers"
"en": "Changeset centers",
"de": "Zentrum der Änderungssätze",
"fr": "Centres de modifications de paramètres",
"nl": "Middelpunt van de wijzigingenset"
},
"minzoom": 0,
"source": {
@ -31,41 +42,58 @@
},
"title": {
"render": {
"en": "Changeset for {theme}"
"en": "Changeset for {theme}",
"de": "Änderungssatz für {theme}",
"nl": "Changeset voor {theme}"
}
},
"description": {
"en": "Shows all MapComplete changes"
"en": "Shows all MapComplete changes",
"de": "Zeigt alle MapComplete-Änderungen",
"fr": "Montre tous les changements de MapComplete",
"nl": "Toon alle MapComplete wijzigingen"
},
"tagRenderings": [
{
"id": "show_changeset_id",
"render": {
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"de": "Änderungssatz <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
"nl": "Wijzigingenset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
}
},
{
"id": "contributor",
"question": {
"en": "What contributor did make this change?"
"en": "What contributor did make this change?",
"de": "Welcher Mitwirkende hat diese Änderung vorgenommen?",
"fr": "Quel contributeur a fait ce changement ?",
"nl": "Welke bijdrager maakte deze wijziging?"
},
"freeform": {
"key": "user"
},
"render": {
"en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
"en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"de": "Änderung vorgenommen von <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"fr": "Modification faite par <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
"nl": "Wijziging gemaakt door <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
}
},
{
"id": "theme-id",
"question": {
"en": "What theme was used to make this change?"
"en": "What theme was used to make this change?",
"de": "Welches Thema wurde für diese Änderung verwendet?",
"nl": "Welk thema is gebruikt voor deze wijziging?"
},
"freeform": {
"key": "theme"
},
"render": {
"en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
"en": "Change with theme <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"de": "Geändert mit Thema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>",
"nl": "Wijziging met thema <a href='https://mapcomplete.osm.be/{theme}'>{theme}</a>"
}
},
{
@ -74,19 +102,32 @@
"key": "locale"
},
"question": {
"en": "What locale (language) was this change made in?"
"en": "What locale (language) was this change made in?",
"de": "In welchem Gebietsschema (Sprache) wurde diese Änderung vorgenommen?",
"fr": "En quelle langue est-ce que ce changement a été fait ?",
"nl": "In welke taal (en cultuur) werd deze wijziging gemaakt?"
},
"render": {
"en": "User locale is {locale}"
"en": "User locale is {locale}",
"de": "Benutzergebietsschema ist {locale}",
"fr": "La langue de l'utilisateur est {locale}",
"nl": "De locale van de bijdrager is {locale}"
}
},
{
"id": "host",
"render": {
"en": "Change with with <a href='{host}'>{host}</a>"
"en": "Change with <a href='{host}'>{host}</a>",
"ca": "Canvi amb <a href='{host}'>{host}</a>",
"de": "Geändert über <a href='{host}'>{host}</a>",
"fr": "Changement avec <a href='{host}'>{host}</a>",
"nl": "Wijziging met <a href='{host}'>{host}</a>"
},
"question": {
"en": "What host (website) was this change made with?"
"en": "What host (website) was this change made with?",
"de": "Über welchen Host (Webseite) wurde diese Änderung vorgenommen?",
"fr": "Depuis quel serveur (site web) ce changement a-t-il été fait ?",
"nl": "Op welk webadres werd deze wijziging gemaakt?"
},
"freeform": {
"key": "host"
@ -427,7 +468,10 @@
}
],
"question": {
"en": "Themename contains {search}"
"en": "Themename contains {search}",
"de": "Themename enthält {search}",
"fr": "Nom de thème contenant {search}",
"nl": "Themanaam bevat {search}"
}
}
]
@ -443,7 +487,10 @@
}
],
"question": {
"en": "Made by contributor {search}"
"en": "Made by contributor {search}",
"de": "Erstellt vom Mitwirkenden {search}",
"fr": "Fait par le contributeur {search}",
"nl": "Gemaakt door {search}"
}
}
]
@ -459,7 +506,10 @@
}
],
"question": {
"en": "<b>Not</b> made by contributor {search}"
"en": "<b>Not</b> made by contributor {search}",
"de": "<b>Nicht</b> von Mitwirkendem {search}",
"fr": "<b>Non</b> réalisé par le contributeur{search}",
"nl": "<b>Niet</b> gemaakt door {search}"
}
}
]
@ -476,7 +526,10 @@
}
],
"question": {
"en": "Made before {search}"
"en": "Made before {search}",
"de": "Erstellt vor {search}",
"fr": "Fait avant {search}",
"nl": "Gemaakt voor {search}"
}
}
]
@ -493,7 +546,10 @@
}
],
"question": {
"en": "Made after {search}"
"en": "Made after {search}",
"de": "Erstellt nach {search}",
"fr": "Fait après {search}",
"nl": "Gemaakt na {search}"
}
}
]
@ -509,7 +565,10 @@
}
],
"question": {
"en": "User language (iso-code) {search}"
"en": "User language (iso-code) {search}",
"de": "Benutzersprache (ISO-Code) {search}",
"fr": "Langage utilisateur (code-iso) {search}",
"nl": "Gebruikerstaal (iso-code) {search}"
}
}
]
@ -525,7 +584,10 @@
}
],
"question": {
"en": "Made with host {search}"
"en": "Made with host {search}",
"de": "Erstellt mit host {search}",
"fr": "Fait par le serveur {search}",
"nl": "Gemaakt met host {search}"
}
}
]
@ -551,7 +613,9 @@
{
"id": "link_to_more",
"render": {
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>"
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>",
"de": "Weitere Statistiken <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a>",
"nl": "Meer statistieken zijn <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a> te vinden"
}
},
{

View file

@ -905,10 +905,11 @@
"attribution": "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>.",
"i_am_affiliated": "<span>I am affiliated with this object</span><br/><span class='subtle'>Check if you are an owner, creator, employee, …</span>",
"name_required": "A name is required in order to display and create reviews",
"no_rating": "No rating given",
"no_rating": "Give a rating before submitting…",
"no_reviews_yet": "There are no reviews yet. Be the first to write one and help open data and the business!",
"plz_login": "Log in to leave a review",
"posting_as": "Posting as",
"save": "Save",
"saved": "<span class='thanks'>Review saved. Thanks for sharing!</span>",
"saving_review": "Saving…",
"title": "{count} reviews",

263
package-lock.json generated
View file

@ -34,6 +34,7 @@
"leaflet-simple-map-screenshoter": "^0.4.5",
"libphonenumber-js": "^1.10.8",
"lz-string": "^1.4.4",
"mangrove-reviews-typescript": "^0.0.6",
"opening_hours": "^3.6.0",
"osm-auth": "^1.0.2",
"osmtogeojson": "^3.0.0-beta.5",
@ -4055,6 +4056,17 @@
"safer-buffer": "~2.1.0"
}
},
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/assert": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz",
@ -4124,6 +4136,29 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg=="
},
"node_modules/axios": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.3.tgz",
"integrity": "sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.10.tgz",
@ -4229,6 +4264,11 @@
"readable-stream": "^3.4.0"
}
},
"node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"node_modules/bops": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz",
@ -4266,6 +4306,11 @@
"node": ">=8"
}
},
"node_modules/brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="
},
"node_modules/browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
@ -5188,6 +5233,20 @@
"integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
"dev": true
},
"node_modules/elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"dependencies": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/email-validator": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz",
@ -5472,6 +5531,25 @@
"flat": "cli.js"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -5882,6 +5960,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@ -5890,6 +5977,16 @@
"he": "bin/he"
}
},
"node_modules/hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
"dependencies": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
@ -6357,6 +6454,14 @@
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="
},
"node_modules/jose": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz",
"integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -6561,6 +6666,16 @@
"node": ">= 4"
}
},
"node_modules/jwk-to-pem": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz",
"integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==",
"dependencies": {
"asn1.js": "^5.3.0",
"elliptic": "^6.5.4",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jxon": {
"version": "2.0.0-beta.5",
"resolved": "https://registry.npmjs.org/jxon/-/jxon-2.0.0-beta.5.tgz",
@ -6764,6 +6879,17 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"devOptional": true
},
"node_modules/mangrove-reviews-typescript": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/mangrove-reviews-typescript/-/mangrove-reviews-typescript-0.0.6.tgz",
"integrity": "sha512-31wF20PdaKUhxP5lek7YouF50QbNk4U571I86e0lG5U/khP96wbVToZB2P4Anb0OPoQ2alHfpqJPuc491ptw2Q==",
"dependencies": {
"axios": "^1.2.3",
"jose": "^4.11.2",
"jwk-to-pem": "^2.0.5",
"typescript": "^4.9.4"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -6820,6 +6946,16 @@
"dom-walk": "^0.1.0"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"node_modules/minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -7575,6 +7711,11 @@
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@ -13439,6 +13580,17 @@
"safer-buffer": "~2.1.0"
}
},
"asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"requires": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"assert": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz",
@ -13487,6 +13639,28 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg=="
},
"axios": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.3.tgz",
"integrity": "sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
},
"dependencies": {
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"babel-plugin-polyfill-corejs2": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.10.tgz",
@ -13563,6 +13737,11 @@
"readable-stream": "^3.4.0"
}
},
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"bops": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz",
@ -13596,6 +13775,11 @@
"fill-range": "^7.0.1"
}
},
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="
},
"browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
@ -14289,6 +14473,20 @@
"integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==",
"dev": true
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"email-validator": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz",
@ -14502,6 +14700,11 @@
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="
},
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
},
"for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -14816,11 +15019,30 @@
"has-symbols": "^1.0.2"
}
},
"hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"requires": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
"requires": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
@ -15154,6 +15376,11 @@
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="
},
"jose": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz",
"integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -15312,6 +15539,16 @@
"resolved": "https://registry.npmjs.org/jsts/-/jsts-1.1.2.tgz",
"integrity": "sha512-4qWAI9gR72HcGWCl7bej9/2dCM6Nv6dh5Zn1G+wzJYW9wsFL/2bPA3kdR8IAPObmF4gb56l5EGlXxErmB+9GOw=="
},
"jwk-to-pem": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz",
"integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==",
"requires": {
"asn1.js": "^5.3.0",
"elliptic": "^6.5.4",
"safe-buffer": "^5.0.1"
}
},
"jxon": {
"version": "2.0.0-beta.5",
"resolved": "https://registry.npmjs.org/jxon/-/jxon-2.0.0-beta.5.tgz",
@ -15475,6 +15712,17 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"devOptional": true
},
"mangrove-reviews-typescript": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/mangrove-reviews-typescript/-/mangrove-reviews-typescript-0.0.6.tgz",
"integrity": "sha512-31wF20PdaKUhxP5lek7YouF50QbNk4U571I86e0lG5U/khP96wbVToZB2P4Anb0OPoQ2alHfpqJPuc491ptw2Q==",
"requires": {
"axios": "^1.2.3",
"jose": "^4.11.2",
"jwk-to-pem": "^2.0.5",
"typescript": "^4.9.4"
}
},
"merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -15513,6 +15761,16 @@
"dom-walk": "^0.1.0"
}
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -16072,6 +16330,11 @@
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",

View file

@ -89,6 +89,7 @@
"leaflet-simple-map-screenshoter": "^0.4.5",
"libphonenumber-js": "^1.10.8",
"lz-string": "^1.4.4",
"mangrove-reviews-typescript": "^0.0.6",
"opening_hours": "^3.6.0",
"osm-auth": "^1.0.2",
"osmtogeojson": "^3.0.0-beta.5",

108
test.ts
View file

@ -1,26 +1,90 @@
import { LanguageElement } from "./UI/Popup/LanguageElement"
import { ImmutableStore, UIEventSource } from "./Logic/UIEventSource"
import MangroveReviewsOfFeature, { MangroveIdentity } from "./Logic/Web/MangroveReviews"
import { Feature, Point } from "geojson"
import { OsmTags } from "./Models/OsmFeature"
import { VariableUiElement } from "./UI/Base/VariableUIElement"
import Locale from "./UI/i18n/Locale"
import { OsmConnection } from "./Logic/Osm/OsmConnection"
import List from "./UI/Base/List"
import { UIEventSource } from "./Logic/UIEventSource"
import UserRelatedState from "./Logic/State/UserRelatedState"
const tgs = new UIEventSource({
name: "xyz",
id: "node/1234",
_country: "BE",
const feature: Feature<Point, OsmTags> = {
type: "Feature",
id: "node/6739848322",
properties: {
"addr:city": "San Diego",
"addr:housenumber": "2816",
"addr:postcode": "92106",
"addr:street": "Historic Decatur Road",
"addr:unit": "116",
amenity: "restaurant",
cuisine: "burger",
delivery: "yes",
"diet:halal": "no",
"diet:vegetarian": "yes",
dog: "yes",
image: "https://i.imgur.com/AQlGNHQ.jpg",
internet_access: "wlan",
"internet_access:fee": "no",
"internet_access:ssid": "Public-stinebrewingCo",
microbrewery: "yes",
name: "Stone Brewing World Bistro & Gardens",
opening_hours: "Mo-Fr, Su 11:30-21:00; Sa 11:30-22:00",
organic: "no",
"payment:cards": "yes",
"payment:cash": "yes",
"service:electricity": "ask",
takeaway: "yes",
website: "https://www.stonebrewing.com/visit/bistros/liberty-station",
wheelchair: "designated",
"_last_edit:contributor": "Drew Dowling",
"_last_edit:timestamp": "2023-01-11T23:22:28Z",
id: "node/6739848322",
timestamp: "2023-01-11T23:22:28Z",
user: "Drew Dowling",
_backend: "https://www.openstreetmap.org",
_lat: "32.7404614",
_lon: "-117.211684",
_layer: "food",
_length: "0",
"_length:km": "0.0",
"_now:date": "2023-01-20",
"_now:datetime": "2023-01-20 17:46:54",
"_loaded:date": "2023-01-20",
"_loaded:datetime": "2023-01-20 17:46:54",
"_geometry:type": "Point",
_surface: "0",
"_surface:ha": "0",
_country: "us",
},
geometry: {
type: "Point",
coordinates: [0, 0],
},
}
const state = new UserRelatedState(undefined)
state.allElements.addOrGetElement(feature)
const reviews = MangroveReviewsOfFeature.construct(feature, state)
reviews.reviews.addCallbackAndRun((r) => {
console.log("Reviews are:", r)
})
Locale.language.setData("nl")
console.log(tgs)
console.log("Locale", Locale.language)
const conn = new OsmConnection({})
new LanguageElement()
.constr(<any>{ osmConnection: conn, featureSwitchIsTesting: new ImmutableStore(true) }, tgs, [
"language",
"What languages are spoken here?",
"{language()} is spoken here",
"{language()} is the only language spoken here",
"The following languages are spoken here: {list()}",
])
.AttachTo("maindiv")
window.setTimeout(async () => {
await reviews.createReview({
opinion: "Cool bar",
rating: 90,
metadata: {
nickname: "Pietervdvn",
},
})
console.log("Submitted review")
}, 1000)
new VariableUiElement(tgs.map(JSON.stringify)).AttachTo("extradiv")
new VariableUiElement(
reviews.reviews.map(
(reviews) =>
new List(
reviews.map((r) => r.rating + "% " + r.opinion + " (" + r.metadata.nickname + ")")
)
)
).AttachTo("maindiv")