diff --git a/assets/layers/food/food.json b/assets/layers/food/food.json index 443cd8e36..bac347058 100644 --- a/assets/layers/food/food.json +++ b/assets/layers/food/food.json @@ -149,7 +149,6 @@ }, "tagRenderings": [ "images", - "level", { "question": { "nl": "Wat is de naam van deze eetgelegenheid?", @@ -213,6 +212,7 @@ "email", "phone", "payment-options", + "level", "wheelchair-access", { "question": { diff --git a/assets/layers/icons/icons.json b/assets/layers/icons/icons.json index 9f15ac5dd..dd1efb4e3 100644 --- a/assets/layers/icons/icons.json +++ b/assets/layers/icons/icons.json @@ -182,6 +182,16 @@ "then": "dogs are allowed" } ] + }, + { + "id": "rating", + "labels": [ + "defaults" + ], + "icon": { + "class": "w-20 mx-1 flex items-center" + }, + "render": "{rating()}" } ], "mapRendering": null diff --git a/assets/layers/questions/questions.json b/assets/layers/questions/questions.json index 748c2221e..af7f5742d 100644 --- a/assets/layers/questions/questions.json +++ b/assets/layers/questions/questions.json @@ -130,7 +130,7 @@ "id": "reviews", "description": "Shows the reviews module (including the possibility to leave a review)", "render": { - "*": "{reviews()}" + "*": "{create_review()}{list_reviews()}" } }, { diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json index f84818f4c..d6643c1c3 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -723,6 +723,16 @@ "authors": [], "sources": [] }, + { + "path": "mangrove_logo.svg", + "license": "LOGO", + "authors": [ + "Mangrove.reviews" + ], + "sources": [ + "https://mangrove.reviews/" + ] + }, { "path": "mapcomplete_logo.svg", "license": "LOGO AND CC-BY-SA-4.0", diff --git a/assets/svg/mangrove_logo.svg b/assets/svg/mangrove_logo.svg new file mode 100644 index 000000000..f79997020 --- /dev/null +++ b/assets/svg/mangrove_logo.svg @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/assets/svg/star.svg b/assets/svg/star.svg index 61301ada9..84cc67ff2 100644 --- a/assets/svg/star.svg +++ b/assets/svg/star.svg @@ -1,6 +1,52 @@ - - - - - + + + + + diff --git a/assets/svg/star_half.svg b/assets/svg/star_half.svg index 7765c2872..c52a80d87 100644 --- a/assets/svg/star_half.svg +++ b/assets/svg/star_half.svg @@ -1,6 +1,56 @@ - - - - - - \ No newline at end of file + + + + + + + diff --git a/assets/svg/star_outline.svg b/assets/svg/star_outline.svg index 859b7539c..fa23c3705 100644 --- a/assets/svg/star_outline.svg +++ b/assets/svg/star_outline.svg @@ -1,6 +1,52 @@ - - - - - - \ No newline at end of file + + + + + + diff --git a/langs/ca.json b/langs/ca.json index 21c6d7381..7e9cc090b 100644 --- a/langs/ca.json +++ b/langs/ca.json @@ -534,10 +534,7 @@ "attribution": "Les ressenyes funcionen gràcies a Mangrove Reviews i estan disponibles sota CC-BY 4.0.", "i_am_affiliated": "Tinc alguna filiació amb aquest objecte
Marca-ho si n'ets cap, creador, treballador, …", "name_required": "És requerit un nom per mostrar i crear revisions", - "no_rating": "Doneu una puntuació abans d'enviar…", "no_reviews_yet": "No hi ha revisions encara. Sigues el primer a escriure'n una i ajuda al negoci i a les dades lliures!", - "plz_login": "Entra per deixar una revisió", - "posting_as": "Enviat com", "save": "Desar", "saved": "Revisió compartida. Gràcies per compartir!", "saving_review": "Desant…", diff --git a/langs/cs.json b/langs/cs.json index e5229e6b9..fe833def6 100644 --- a/langs/cs.json +++ b/langs/cs.json @@ -448,10 +448,7 @@ "attribution": "Recenze jsou poskytovány službou Mangrove Reviews a jsou k dispozici pod licencí CC-BY 4.0.", "i_am_affiliated": "Jsem spojen/a s tímto objektem
Zaškrtněte, pokud jste vlastníkem, tvůrcem, zaměstnancem, …", "name_required": "Pro zobrazení a vytváření recenzí je vyžadováno jméno", - "no_rating": "Před odesláním udělte hodnocení…", "no_reviews_yet": "Zatím zde nejsou žádné recenze. Buďte první, kdo ji napíše, a pomozte otevřít data a podnikání!", - "plz_login": "Přihlaste se a zanechte recenzi", - "posting_as": "Přihlášeni jako", "save": "Uložit", "saved": "Recenze uložena. Díky za sdílení!", "saving_review": "Ukládání…", diff --git a/langs/da.json b/langs/da.json index 1350677b5..76bd60c63 100644 --- a/langs/da.json +++ b/langs/da.json @@ -383,10 +383,7 @@ "attribution": "Anmeldelserne er baseret på Mangrove Reviews og er tilgængelige under CC-BY 4.0.", "i_am_affiliated": "Jeg er tilknyttet dette objekt
Tjek, om du er ejer, skaber, ansat, ...", "name_required": "Der kræves et navn for at vise og oprette anmeldelser", - "no_rating": "Ingen vurdering givet", "no_reviews_yet": "Der er ingen anmeldelser endnu. Vær den første til at skrive en og hjælpe åbne data og forretningen!", - "plz_login": "Log ind for at give en anmeldelse", - "posting_as": "Anmelder som", "saved": "Anmeldelse gemt. Tak for at bidrage!", "saving_review": "Gemmer…", "title": "{count} Anmeldelser", diff --git a/langs/de.json b/langs/de.json index 671f97a02..2c18b8185 100644 --- a/langs/de.json +++ b/langs/de.json @@ -536,10 +536,7 @@ "attribution": "Rezensionen werden bereitgestellt von Mangrove Reviews und sind unter CC-BY 4.0 verfügbar.", "i_am_affiliated": "Ich bin an diesem Objekt beteiligt
Auswählen, wenn Sie Eigentümer, Ersteller, Angestellter … sind", "name_required": "Der Name des Objekts ist erforderlich, um Bewertungen zu erstellen und anzuzeigen", - "no_rating": "Vor dem Absenden eine Bewertung abgeben…", "no_reviews_yet": "Es gibt noch keine Bewertungen. Hilf mit der ersten Bewertung dem Geschäft und der Open Data Bewegung!", - "plz_login": "Anmelden, um eine Bewertung abzugeben", - "posting_as": "Veröffentlichen als", "save": "Speichern", "saved": "Bewertung gespeichert. Danke fürs Teilen!", "saving_review": "Speichern…", diff --git a/langs/en.json b/langs/en.json index 06e94de36..7167afb95 100644 --- a/langs/en.json +++ b/langs/en.json @@ -557,14 +557,16 @@ "reviews": { "affiliated_reviewer_warning": "(Affiliated review)", "attribution": "Reviews are powered by Mangrove Reviews and are available under CC-BY 4.0.", - "i_am_affiliated": "I am affiliated with this object
Check if you are an owner, creator, employee, …", + "i_am_affiliated": "I am affiliated with this object", + "i_am_affiliated_explanation": "Check if you are an owner, creator, employee, …", "name_required": "A name is required in order to display and create reviews", - "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", + "question": "How would you rate {title()}?", + "question_opinion": "How was your experience?", + "reviewing_as": "Reviewing as {nickname}", + "reviewing_as_anonymous": "Reviewing as anonymous", "save": "Save", - "saved": "Review saved. Thanks for sharing!", + "saved": "Review saved. Thanks for sharing!", "saving_review": "Saving…", "title": "{count} reviews", "title_singular": "One review", diff --git a/langs/es.json b/langs/es.json index 815880080..9522e2660 100644 --- a/langs/es.json +++ b/langs/es.json @@ -414,10 +414,7 @@ "reviews": { "affiliated_reviewer_warning": "(Revisión afiliada)", "name_required": "Se requiere un nombre para mostrar y crear comentarios", - "no_rating": "Da una calificación antes de enviar…", "no_reviews_yet": "Aún no hay reseñas. ¡Sé el primero en escribir una y ayuda a los datos abiertos y a los negocios!", - "plz_login": "Inicia sesión para dejar una reseña", - "posting_as": "Publicación como", "saved": "Reseña guardada. ¡Gracias por compartir!", "saving_review": "Guardando…", "title": "{count} comentarios", diff --git a/langs/fr.json b/langs/fr.json index 7d9422991..7bf0c5f9c 100644 --- a/langs/fr.json +++ b/langs/fr.json @@ -428,10 +428,7 @@ "attribution": "Les avis sont fournis par Mangrove Reviews et sont disponibles sous licence CC-BY 4.0.", "i_am_affiliated": "Je suis affilié à cet objet
Cochez si vous en êtes le propriétaire, créateur, employé, …", "name_required": "Un nom est requis pour afficher et créer des avis", - "no_rating": "Aucun score donné", "no_reviews_yet": "Il n'y a pas encore d'avis. Soyez le premier à en écrire un et aidez le lieu et les données ouvertes !", - "plz_login": "Connectez vous pour laisser un avis", - "posting_as": "Envoi en tant que", "saved": "Avis enregistré. Merci du partage !", "saving_review": "Enregistrement…", "title": "{count} avis", diff --git a/langs/gl.json b/langs/gl.json index 23648730b..8d9d04d00 100644 --- a/langs/gl.json +++ b/langs/gl.json @@ -163,10 +163,7 @@ "reviews": { "affiliated_reviewer_warning": "(Recensión de afiliado)", "name_required": "Requírese un nome para amosar e crear recensións", - "no_rating": "Sen puntuacións", "no_reviews_yet": "Non hai recensións aínda. Se o primeiro en escribir unha e axuda ao negocio e aos datos libres!", - "plz_login": "Inicia sesión para deixar unha recensión", - "posting_as": "Publicar como", "saved": "Recensión compartida. Grazas por compartir!", "saving_review": "Gardando…", "title": "{count} recensións", diff --git a/langs/hu.json b/langs/hu.json index de4faf548..625a81515 100644 --- a/langs/hu.json +++ b/langs/hu.json @@ -303,10 +303,7 @@ "attribution": "A véleményeket Mangrove Reviews tárolja, és a CC-BY 4.0 licenc szerint érhetők el.", "i_am_affiliated": "Kapcsolatban állok ezzel a létesítménnyel
Ellenőrizd, hogy tulajdonos, alkotó, alkalmazott vagy hasonló vagy-e.", "name_required": "Vélemények megjelenítéséhez és létrehozásához névre van szükség", - "no_rating": "Még nem kapott értékelést", "no_reviews_yet": "Még nincs vélemény. Légy Te az első, aki ír, és ezzel támogasd a nyílt adatokat és az üzletet!", - "plz_login": "Értékelés írásához jelentkezz be", - "posting_as": "Közzétéve mint", "saved": "Vélemény elmentve. Köszönjük a megosztást!", "saving_review": "Mentés…", "title": "{count} vélemény", diff --git a/langs/id.json b/langs/id.json index 69e86090a..419d2c101 100644 --- a/langs/id.json +++ b/langs/id.json @@ -162,9 +162,6 @@ }, "reviews": { "attribution": "Ulasan didukung oleh Mangrove Reviews dan tersedia di bawah CC-BY 4.0.", - "no_rating": "Tidak ada peringkat yang diberikan", - "plz_login": "Masuk untuk meninggalkan ulasan", - "posting_as": "Posting sebagai", "saved": " Ulasan disimpan. Terima kasih sudah berbagi! ", "saving_review": "Menyimpan…", "title": "{count} ulasan", diff --git a/langs/it.json b/langs/it.json index 1506a86b5..95464ae7a 100644 --- a/langs/it.json +++ b/langs/it.json @@ -307,10 +307,7 @@ "attribution": "Le recensioni sono fornite da Mangrove Reviews e sono disponibili con licenza CC-BY 4.0.", "i_am_affiliated": "Sono associato con questo oggetto
Spunta se sei il proprietario, creatore, dipendente, etc.", "name_required": "È richiesto un nome per poter mostrare e creare recensioni", - "no_rating": "Nessun voto ricevuto", "no_reviews_yet": "Non ci sono ancora recensioni. Sii il primo a scriverne una aiutando così i dati liberi e l’attività!", - "plz_login": "Accedi per lasciare una recensione", - "posting_as": "Pubblica come", "saved": "Recensione salvata. Grazie per averla condivisa!", "saving_review": "Salvataggio…", "title": "{count} recensioni", diff --git a/langs/ja.json b/langs/ja.json index afcfdd6ad..e09d9f1f9 100644 --- a/langs/ja.json +++ b/langs/ja.json @@ -165,10 +165,7 @@ "attribution": "レビューは、Mangrove Reviews and are available under CC-BY 4.0で公開されます。", "i_am_affiliated": "わたしは、この対象物の関係者です
所有者、作成者、従業員などの有無を確認します", "name_required": "レビューを表示および作成するには名前が必要です", - "no_rating": "評価が与えられていません", "no_reviews_yet": "まだレビューはありません。最初に書き込みを行い、データとビジネスのオープン化を支援しましょう!", - "plz_login": "ログインしてレビューを終了する", - "posting_as": "としての投稿", "saved": "レビューが保存されました。共有ありがとう!", "saving_review": "保存中…", "title": "{count}個のレビュー", diff --git a/langs/nb_NO.json b/langs/nb_NO.json index 3c56e2c00..af9d92472 100644 --- a/langs/nb_NO.json +++ b/langs/nb_NO.json @@ -401,10 +401,7 @@ "attribution": "Vurderinger er muliggjort av Mangrove Reviews og er tilgjengelige som CC-BY 4.0.", "i_am_affiliated": "Jeg har en tilknytning til dette objektet
Sjekk om du er eier, skaper, ansatt, …", "name_required": "Et navn kreves for å vise og opprette vurderinger", - "no_rating": "Ingen vurdering gitt", "no_reviews_yet": "Ingen vurderinger enda. Vær først til å skrive en og hjelp åpen data og bevegelsen.", - "plz_login": "Logg inn for å legge igjen en vurdering", - "posting_as": "Anmelder som", "saved": "Vurdering lagret. Takk for at du deler din mening.", "saving_review": "Lagrer …", "title": "{count} vurderinger", diff --git a/langs/nl.json b/langs/nl.json index 1a7e651cb..70101f0b2 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -530,10 +530,7 @@ "attribution": "De beoordelingen worden voorzien door Mangrove Reviews en zijn beschikbaar onder deCC-BY 4.0-licentie. ", "i_am_affiliated": "Ik ben persoonlijk betrokken
Vink aan indien je de oprichter, maker, werknemer, ... of dergelijke bent", "name_required": "De naam van dit object moet gekend zijn om een review te kunnen maken", - "no_rating": "Geef een beoordeling voordat je verzendt…", "no_reviews_yet": "Er zijn nog geen beoordelingen. Wees de eerste om een beoordeling te schrijven en help open data en het bedrijf!", - "plz_login": "Meld je aan om een beoordeling te geven", - "posting_as": "Ingelogd als", "save": "Opslaan", "saved": "Bedankt om je beoordeling te delen!", "saving_review": "Opslaan...", diff --git a/langs/pl.json b/langs/pl.json index d4d949a35..ca72241b4 100644 --- a/langs/pl.json +++ b/langs/pl.json @@ -331,10 +331,7 @@ "attribution": "Recenzje są obsługiwane przez Recenzje Mangrove i są dostępne na licencji CC-BY 4.0.", "i_am_affiliated": "Jestem powiązany z tym obiektem
Sprawdź czy jesteś właścicielem, twórcą, pracownikiem, ...", "name_required": "Nazwa jest wymagana do wyświetlania i tworzenia opinii", - "no_rating": "Nie podano oceny", "no_reviews_yet": "Nie ma jeszcze recenzji. Bądź pierwszym, który je napisze i pomóż otworzyć dane i biznes!", - "plz_login": "Zaloguj się, aby zostawić opinię", - "posting_as": "Publikowanie jako", "save": "Zapisz", "saved": "Opinia została zapisana. Dzięki za udostępnienie!", "saving_review": "Zapisywanie…", diff --git a/langs/pt.json b/langs/pt.json index 1b2767540..37f0f8c73 100644 --- a/langs/pt.json +++ b/langs/pt.json @@ -352,10 +352,7 @@ "attribution": "As avaliações são fornecidas por Mangrove Reviews e estão disponíveis sob a licença CC-BY 4.0.", "i_am_affiliated": "Eu sou afiliado a este objeto

Marque isto se for proprietário, criador, funcionário…
", "name_required": "É necessário um nome para mostrar e criar avaliações", - "no_rating": "Nenhuma classificação dada", "no_reviews_yet": "Ainda não existem avaliações. Seja o primeiro a escrever uma e ajude a abrir os dados e os negócios!", - "plz_login": "Inicie a sessão para deixar uma avaliação", - "posting_as": "Publicar como", "saved": "Avaliação guardada. Obrigado por partilhar!", "saving_review": "A guardar…", "title": "{count} avaliações", diff --git a/langs/pt_BR.json b/langs/pt_BR.json index d6af7cb4d..e84552603 100644 --- a/langs/pt_BR.json +++ b/langs/pt_BR.json @@ -162,10 +162,7 @@ "attribution": "As resenhas são fornecidas por Mangrove Reviews e estão disponíveis em CC-BY 4.0.", "i_am_affiliated": "Eu sou afiliado a este objeto

Verifique se você é proprietário, criador, funcionário, …
", "name_required": "É necessário um nome para exibir e criar comentários", - "no_rating": "Nenhuma classificação dada", "no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!", - "plz_login": "Entrar para deixar um comentário", - "posting_as": "Postando como", "saved": "Comentário salvo. Obrigado por compartilhar!", "saving_review": "Salvando…", "title": "{count} comentários", diff --git a/langs/ru.json b/langs/ru.json index aaa9683b7..ceb9b1378 100644 --- a/langs/ru.json +++ b/langs/ru.json @@ -176,10 +176,7 @@ "attribution": "Отзывы созданы на основе Mangrove Reviews и доступны под лицензией CC-BY 4.0.", "i_am_affiliated": "Я связан с этим объектом
Отметьте если вы создатель, владелец, работник, …", "name_required": "Необходимо название, чтобы просматривать и создавать отзывы", - "no_rating": "Нет рейтинга", "no_reviews_yet": "Пока нет отзывов. Оставьте первый отзыв и помогите открытым данным и бизнесу!", - "plz_login": "Войдите, чтобы оставить отзыв", - "posting_as": "Публикация от имени", "saved": " Отзыв сохранен. Спасибо, что поделились! ", "saving_review": "Сохранение…", "title": "{count} отзыв(-ов)", diff --git a/langs/zh_Hant.json b/langs/zh_Hant.json index 8772a0778..128b248a3 100644 --- a/langs/zh_Hant.json +++ b/langs/zh_Hant.json @@ -383,10 +383,7 @@ "attribution": "評審系統由Mangrove Reviews提供技術支援,採用CC-BY 4.0授權條款。", "i_am_affiliated": "我是這物件的相關關係者
確認你是否是擁有者、創造者、員工等等", "name_required": "需要有名稱才能顯示和創造審核", - "no_rating": "還沒有評分", "no_reviews_yet": "還沒有審核,當第一個撰寫者來幫助開放資料與商家吧!", - "plz_login": "登入來留下審核", - "posting_as": "以貼文", "saved": "已儲存審核,謝謝你的分享!", "saving_review": "儲存中…", "title": "{count} 審核次數", diff --git a/public/assets/mangrove_logo.png b/public/assets/mangrove_logo.png deleted file mode 100644 index 38f39f8ed..000000000 Binary files a/public/assets/mangrove_logo.png and /dev/null differ diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index da51a2a7f..7c9f0be3e 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -938,10 +938,6 @@ video { margin-bottom: 2rem; } -.ml-3 { - margin-left: 0.75rem; -} - .-ml-6 { margin-left: -1.5rem; } @@ -1210,11 +1206,6 @@ video { width: 2.5rem; } -.w-max { - width: -webkit-max-content; - width: max-content; -} - .w-48 { width: 12rem; } @@ -1404,6 +1395,12 @@ video { margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); } +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + .space-y-reverse > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 1; } @@ -1478,11 +1475,6 @@ video { text-overflow: clip; } -.break-normal { - overflow-wrap: normal; - word-break: normal; -} - .break-all { word-break: break-all; } @@ -1555,14 +1547,14 @@ video { border-width: 1px; } -.border-4 { - border-width: 4px; -} - .border-2 { border-width: 2px; } +.border-4 { + border-width: 4px; +} + .border-x { border-left-width: 1px; border-right-width: 1px; @@ -1669,10 +1661,6 @@ video { padding: 2rem; } -.p-1 { - padding: 0.25rem; -} - .p-2 { padding: 0.5rem; } @@ -1681,6 +1669,10 @@ video { padding: 1rem; } +.p-1 { + padding: 0.25rem; +} + .p-0\.5 { padding: 0.125rem; } @@ -1773,10 +1765,6 @@ video { text-align: justify; } -.align-middle { - vertical-align: middle; -} - .text-xl { font-size: 1.25rem; line-height: 1.75rem; @@ -1787,16 +1775,6 @@ video { line-height: 1.75rem; } -.text-4xl { - font-size: 2.25rem; - line-height: 2.5rem; -} - -.text-sm { - font-size: 0.875rem; - line-height: 1.25rem; -} - .text-3xl { font-size: 1.875rem; line-height: 2.25rem; @@ -1807,11 +1785,21 @@ video { line-height: 2rem; } +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + .text-base { font-size: 1rem; line-height: 1.5rem; } +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} + .font-bold { font-weight: 700; } @@ -1891,10 +1879,6 @@ video { font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction); } -.leading-none { - line-height: 1; -} - .tracking-tight { letter-spacing: -0.025em; } @@ -2662,26 +2646,6 @@ a.link-underline { opacity: 1; } -@media (prefers-reduced-motion: no-preference) { - @-webkit-keyframes spin { - to { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } - } - @keyframes spin { - to { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } - } - - .motion-safe\:animate-spin { - -webkit-animation: spin 1s linear infinite; - animation: spin 1s linear infinite; - } -} - @media (max-width: 480px) { .max-\[480px\]\:w-full { width: 100%; @@ -2816,10 +2780,6 @@ a.link-underline { height: 4rem; } - .md\:h-12 { - height: 3rem; - } - .md\:w-8 { width: 2rem; } diff --git a/src/Logic/Web/MangroveReviews.ts b/src/Logic/Web/MangroveReviews.ts index ec16f3445..15819a735 100644 --- a/src/Logic/Web/MangroveReviews.ts +++ b/src/Logic/Web/MangroveReviews.ts @@ -1,34 +1,35 @@ -import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" -import { MangroveReviews, Review } from "mangrove-reviews-typescript" -import { Utils } from "../../Utils" -import { Feature, Position } from "geojson" -import { GeoOperations } from "../GeoOperations" +import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"; +import { MangroveReviews, Review } from "mangrove-reviews-typescript"; +import { Utils } from "../../Utils"; +import { Feature, Position } from "geojson"; +import { GeoOperations } from "../GeoOperations"; export class MangroveIdentity { - public readonly keypair: Store - public readonly key_id: Store + public readonly keypair: Store; + public readonly key_id: Store; constructor(mangroveIdentity: UIEventSource) { - const key_id = new UIEventSource(undefined) - this.key_id = key_id - const keypairEventSource = new UIEventSource(undefined) - this.keypair = keypairEventSource + const key_id = new UIEventSource(undefined); + this.key_id = key_id; + const keypairEventSource = new UIEventSource(undefined); + this.keypair = keypairEventSource; mangroveIdentity.addCallbackAndRunD(async (data) => { if (data === "") { - return + return; } - const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data)) - keypairEventSource.setData(keypair) - const pem = await MangroveReviews.publicToPem(keypair.publicKey) - key_id.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 ?? "") === "") { - MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {}) + MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => { + }); } } catch (e) { - console.error("Could not create identity: ", e) + console.error("Could not create identity: ", e); } } @@ -38,13 +39,13 @@ export class MangroveIdentity { * @constructor */ private static async CreateIdentity(identity: UIEventSource): Promise { - const keypair = await MangroveReviews.generateKeypair() - const jwk = await MangroveReviews.keypairToJwk(keypair) + 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 + return; } - identity.setData(JSON.stringify(jwk)) + identity.setData(JSON.stringify(jwk)); } } @@ -52,17 +53,18 @@ export class MangroveIdentity { * Tracks all reviews of a given feature, allows to create a new review */ export default class FeatureReviews { - private static readonly _featureReviewsCache: Record = {} - public readonly subjectUri: Store + private static readonly _featureReviewsCache: Record = {}; + public readonly subjectUri: Store; + public readonly average: Store; private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store })[]> = - new UIEventSource([]) + new UIEventSource([]); public readonly reviews: Store<(Review & { madeByLoggedInUser: Store })[]> = - this._reviews - private readonly _lat: number - private readonly _lon: number - private readonly _uncertainty: number - private readonly _name: Store - private readonly _identity: MangroveIdentity + this._reviews; + private readonly _lat: number; + private readonly _lon: number; + private readonly _uncertainty: number; + private readonly _name: Store; + private readonly _identity: MangroveIdentity; private constructor( feature: Feature, @@ -75,55 +77,72 @@ export default class FeatureReviews { } ) { const centerLonLat = GeoOperations.centerpointCoordinates(feature) - ;[this._lon, this._lat] = centerLonLat + ;[this._lon, this._lat] = centerLonLat; this._identity = - mangroveIdentity ?? new MangroveIdentity(new UIEventSource(undefined)) - const nameKey = options?.nameKey ?? "name" + mangroveIdentity ?? new MangroveIdentity(new UIEventSource(undefined)); + const nameKey = options?.nameKey ?? "name"; if (feature.geometry.type === "Point") { - this._uncertainty = options?.uncertaintyRadius ?? 10 + this._uncertainty = options?.uncertaintyRadius ?? 10; } else { - let coordss: Position[][] + let coordss: Position[][]; if (feature.geometry.type === "LineString") { - coordss = [feature.geometry.coordinates] + coordss = [feature.geometry.coordinates]; } else if ( feature.geometry.type === "MultiLineString" || feature.geometry.type === "Polygon" ) { - coordss = feature.geometry.coordinates + coordss = feature.geometry.coordinates; } - let maxDistance = 0 + let maxDistance = 0; for (const coords of coordss) { for (const coord of coords) { maxDistance = Math.max( maxDistance, GeoOperations.distanceBetween(centerLonLat, coord) - ) + ); } } - this._uncertainty = options?.uncertaintyRadius ?? maxDistance + this._uncertainty = options?.uncertaintyRadius ?? maxDistance; } - this._name = tagsSource.map((tags) => tags[nameKey] ?? options?.fallbackName) + this._name = tagsSource.map((tags) => tags[nameKey] ?? options?.fallbackName); - this.subjectUri = this.ConstructSubjectUri() + this.subjectUri = this.ConstructSubjectUri(); - const self = this + const self = this; this.subjectUri.addCallbackAndRunD(async (sub) => { - const reviews = await MangroveReviews.getReviews({ sub }) - self.addReviews(reviews.reviews) - }) + 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) + const reviews = await MangroveReviews.getReviews({ sub }); + self.addReviews(reviews.reviews); } catch (e) { - console.log("Could not fetch reviews for partially incorrect query ", sub) + console.log("Could not fetch reviews for partially incorrect query ", sub); } - }) + }); + this.average = this._reviews.map(reviews => { + if (!reviews) { + return null; + } + if(reviews.length === 0){ + return null + } + let sum = 0; + let count = 0; + for (const review of reviews) { + if (review.rating !== undefined) { + count++; + sum += review.rating; + } + } + return Math.round(sum / count) + }); } /** @@ -139,14 +158,14 @@ export default class FeatureReviews { uncertaintyRadius?: number } ) { - const key = feature.properties.id - const cached = FeatureReviews._featureReviewsCache[key] + const key = feature.properties.id; + const cached = FeatureReviews._featureReviewsCache[key]; if (cached !== undefined) { - return cached + return cached; } - const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options) - FeatureReviews._featureReviewsCache[key] = featureReviews - return featureReviews + const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options); + FeatureReviews._featureReviewsCache[key] = featureReviews; + return featureReviews; } /** @@ -155,15 +174,15 @@ export default class FeatureReviews { public async createReview(review: Omit): Promise { 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() + ...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(); } /** @@ -172,46 +191,48 @@ export default class FeatureReviews { * @private */ private addReviews(reviews: { payload: Review; kid: string }[]) { - const self = this - const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion)) + const self = this; + const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion)); - let hasNew = false + let hasNew = false; for (const reviewData of reviews) { - const review = reviewData.payload + const review = reviewData.payload; try { - const url = new URL(review.sub) - console.log("URL is", url) + 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 + continue; } } } catch (e) { - console.warn(e) + console.warn(e); } - const key = review.rating + " " + review.opinion + const key = review.rating + " " + review.opinion; if (alreadyKnown.has(key)) { - continue + continue; } self._reviews.data.push({ ...review, madeByLoggedInUser: this._identity.key_id.map((user_key_id) => { - return reviewData.kid === user_key_id - }), - }) - hasNew = true + return reviewData.kid === user_key_id; + }) + }); + hasNew = true; } if (hasNew) { - self._reviews.ping() + self._reviews.data.sort((a, b) => b.iat - a.iat) // Sort with most recent first + + self._reviews.ping(); } } @@ -224,13 +245,13 @@ export default class FeatureReviews { private ConstructSubjectUri(dontEncodeName: boolean = false): Store { // 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 - return this._name.map(function (name) { - let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}` + const self = this; + return this._name.map(function(name) { + let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}`; if (name) { - uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name)) + uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name)); } - return uri - }) + return uri; + }); } } diff --git a/src/UI/BigComponents/SelectedElementTitle.svelte b/src/UI/BigComponents/SelectedElementTitle.svelte index 01c0bbaf9..600087dc7 100644 --- a/src/UI/BigComponents/SelectedElementTitle.svelte +++ b/src/UI/BigComponents/SelectedElementTitle.svelte @@ -46,7 +46,7 @@ > {#each layer.titleIcons as titleIconConfig} {#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ..._metatags, ..._tags } ) ?? true) && titleIconConfig.IsKnown(_tags)} -
+
+ import FeatureReviews from "../../Logic/Web/MangroveReviews"; + import SingleReview from "./SingleReview.svelte"; + import { Utils } from "../../Utils"; + import StarsBar from "./StarsBar.svelte"; + import ReviewForm from "./ReviewForm.svelte"; + import Translations from "../i18n/Translations"; + import Tr from "../Base/Tr.svelte"; + import type { SpecialVisualizationState } from "../SpecialVisualization"; + import { UIEventSource } from "../../Logic/UIEventSource"; + import type { Feature } from "geojson"; + import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; + import ToSvelte from "../Base/ToSvelte.svelte"; + import Svg from "../../Svg"; + + /** + * An element showing all reviews + */ + export let reviews: FeatureReviews; + export let state: SpecialVisualizationState; + export let tags: UIEventSource>; + export let feature: Feature; + export let layer: LayerConfig; + let average = reviews.average; + let _reviews = []; + reviews.reviews.addCallbackAndRunD(r => { + _reviews = Utils.NoNull(r); + }); + + + +
+ {#if _reviews.length > 1} + + {/if} + {#if _reviews.length > 0} + {#each _reviews as review} + + {/each} + {:else} + + {/if} +
+ + +
+
diff --git a/src/UI/Reviews/ReviewElement.ts b/src/UI/Reviews/ReviewElement.ts deleted file mode 100644 index efdf42fe8..000000000 --- a/src/UI/Reviews/ReviewElement.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Combine from "../Base/Combine" -import Translations from "../i18n/Translations" -import SingleReview from "./SingleReview" -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(reviews: FeatureReviews, middleElement: BaseUIElement) { - super( - 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(...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") - }, - [reviews.subjectUri] - ) - ) - } -} diff --git a/src/UI/Reviews/ReviewForm.svelte b/src/UI/Reviews/ReviewForm.svelte new file mode 100644 index 000000000..7949a457d --- /dev/null +++ b/src/UI/Reviews/ReviewForm.svelte @@ -0,0 +1,97 @@ + +{#if _state === "done"} + +{:else if _state === "saving"} + + + +{:else} +
+
+ +
+ {confirmedScore = e.detail.score}} on:hover={e => {score = e.detail.score}} + on:mouseout={e => {score = null}} score={score ?? confirmedScore ?? 0} + starSize="w-8 h-8"> + + {#if confirmedScore !== undefined} + +