forked from MapComplete/MapComplete
Use mangrove-reviews-typescript, rework reviews modules
This commit is contained in:
parent
93961e553f
commit
888d4e95a3
15 changed files with 768 additions and 375 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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
263
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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
108
test.ts
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue