From 6d5f5d54f8d754b9148e5e7b9f5c32435e596d1a Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 28 Sep 2023 04:02:42 +0200 Subject: [PATCH] Refactoring: port reviews to svelte --- assets/layers/food/food.json | 2 +- assets/layers/icons/icons.json | 10 + assets/layers/questions/questions.json | 2 +- assets/svg/license_info.json | 10 + assets/svg/mangrove_logo.svg | 47 ++++ assets/svg/star.svg | 56 ++++- assets/svg/star_half.svg | 62 +++++- assets/svg/star_outline.svg | 58 ++++- langs/ca.json | 3 - langs/cs.json | 3 - langs/da.json | 3 - langs/de.json | 3 - langs/en.json | 12 +- langs/es.json | 3 - langs/fr.json | 3 - langs/gl.json | 3 - langs/hu.json | 3 - langs/id.json | 3 - langs/it.json | 3 - langs/ja.json | 3 - langs/nb_NO.json | 3 - langs/nl.json | 3 - langs/pl.json | 3 - langs/pt.json | 3 - langs/pt_BR.json | 3 - langs/ru.json | 3 - langs/zh_Hant.json | 3 - public/assets/mangrove_logo.png | Bin 14110 -> 0 bytes public/css/index-tailwind-output.css | 88 +++----- src/Logic/Web/MangroveReviews.ts | 201 ++++++++++-------- .../BigComponents/SelectedElementTitle.svelte | 2 +- src/UI/Reviews/AllReviews.svelte | 47 ++++ src/UI/Reviews/ReviewElement.ts | 56 ----- src/UI/Reviews/ReviewForm.svelte | 97 +++++++++ src/UI/Reviews/ReviewForm.ts | 101 --------- src/UI/Reviews/SingleReview.svelte | 38 ++++ src/UI/Reviews/SingleReview.ts | 64 ------ src/UI/Reviews/StarElement.svelte | 32 +++ src/UI/Reviews/StarsBar.svelte | 21 ++ src/UI/Reviews/StarsBarIcon.svelte | 11 + src/UI/SpecialVisualizations.ts | 74 ++++++- 41 files changed, 683 insertions(+), 462 deletions(-) create mode 100644 assets/svg/mangrove_logo.svg delete mode 100644 public/assets/mangrove_logo.png create mode 100644 src/UI/Reviews/AllReviews.svelte delete mode 100644 src/UI/Reviews/ReviewElement.ts create mode 100644 src/UI/Reviews/ReviewForm.svelte delete mode 100644 src/UI/Reviews/ReviewForm.ts create mode 100644 src/UI/Reviews/SingleReview.svelte delete mode 100644 src/UI/Reviews/SingleReview.ts create mode 100644 src/UI/Reviews/StarElement.svelte create mode 100644 src/UI/Reviews/StarsBar.svelte create mode 100644 src/UI/Reviews/StarsBarIcon.svelte 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 38f39f8eda52574e21720c1925792308a68669cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14110 zcmY+LRa9I}u!V7Vch}(V?!hI&Ex1D$V+R0-=qH?2yozMl}>dY1Ox?yg0zH|r_q%mVy4Ox zu~<>0^dm3MyCjzP>eA9QQI!J(7X+3B!<^}Lu^hvq&2u^8as|%@NwzlC+d{$B4wK{DGi>6)v&l^Z2I}Vfrt*bCh?U^?!_(nlnxG$YlC)+3A}oT4PG{)$MdBa(Wl*z zVuQBN#fBa3tbWS#d>tsRe6#y1jgDK{)y6$}a~#yvA=g_S8IxID^yc@6V{WIj-z1cG zynYi|lz~j`x>6BnB`Nf(c-L%5+QmBys$$AJ0e}5?e>KdAD(?8) zh&=vf`dC@e;eJJ>n9hudU_{j_G)J$^@-ZP$`73Wpz@KM<>BCT2C0d-b~|ww>0~; z`k|P-I3k|@Z=6a8**tdh;1dYR%34I{cEA-BZrV=B?$@mHy_`=MzhiGT+F-YN-hJx5 z#Twd~_HVPT^A6SAhVXuSzG>O<7xMeLq^*#}X)zj#h~0?>zE_2t4G9^U%1zfVqvX`o z!QXJ%5?I*SXZ02nqqET|!=;Px9L@FiZ5=A0|G;#6?AYqkW%BQ6w8T zUVq)#$nDP1>S=2J!?Ek<$47Q@j!!U%$jCo>^F(}j{jZ#pI*LemW`e*lzkZ?+Ht(Y{I%(dUZq)v76e80?p^eB7FPrg zn^8`VSb)*bgXH97$#`-xGPRr`ZTfAl5th-Fd8_Fh1x-!OEF|||*IS2mBV^QU-A`1{ z2MJ=M#7>M+tY3~)UxI|v)$#hfXnnTamC076$$Qs5L-178Xm2DpXbvc!tbk= zo#dqoZFn53gNjA+qyxM|yD@6H_~Ls$sNcMfo0e?*?bAIVYhGF_om{~()o!DwiHUK z@I;kd!6ad?dkl3QQX%)^tgNiV;W%R5<6)-~#SA*lQYG~vyUkV?wX$T7ZLbI%R)b7& z(#;Y@d`_GB;zY}->^TrL_4;FYP+2ga!E&6}cWevG5%Edin`?H^|Glj5IZqr7>O;ERJ-4vPm2;blc4lyt)29mY}1Zp zzw5WKzU_vfuV#?fKa2f+m7<^n%OEzDMIY~mSXMidM!Uv{NBY2YAY6U)ZKc|fI_=#D zs-XIaR0j3nKaU$uMoNOF{V+&Grd?oZeQL=yG(NvXqL*W?)v5dR&>x0ey3FbQV4aapI?hrP3>GDb{UL~0h~b_6m@D8K9)(M*m`($} zFwN6To!Lm|?>#1=&xi7lP9Wo2@S*-%n*U1NGoV9=i77$xD6!e``3Py7!;?B|n`t$G z0*?_FQSYg@2!iUO(PlyT{MK=+eGXi&HemqF$Zns^xg8N7W;RT{>MR?IUO7mC8_QpC zSoBhtYmD=E=>ngcO!}ZSy1c(cs^@vV)2>#!>3(q(uJ@Z}@%s{XTk)X+kJ~n{`^U*l znJ4rvW@RbDtPnpk9&-U0Xe5y0LNK)u;Cnld()Ag(CGQ1(9F9qSYd30l`<`K}8-|3- zuz2u9ki+j1%xuuA=1C^<+Awgl=Fc%(Cso;(w$tW%k~kEDFO?JA7p?1M>X8^QcS&LE z?JelDAWjYKB}7RH`A65NF<9c}>F(gLEZ_fLVc_MaC*S|qmWS@|T~krBVU*U->{sZG zx}FanXY)f9PS(~8hHal|UCs8^Y36wLFZ>@))q{WGHoEQY?X|z;CMJr1y9OiFbaGr_ z+#?zW^$oH_}ye`s}BZ~W%e{Y)oKk^kc}EJVO8Rl#{tf+!V6SMM%Jx^%TV zQf`pc;)a5Lzunj}!7LXW`#;m#%oT<)>O(sZ+8SXJC0Z2+yZo44?+W z{{pEUkp$~5{5zO8E+xs_N;k&7DNgXo_mf;RCd$rSRzE^a@5q8C_xzZ2YAIg( zBLiQMu}RxDP`8bGyUa%7b(xPa5Q(|116!g^eJAsv0}>`Mk(sp0F??1*hFhxssYbG^ zn89k(Pt0kKFm$k%p(vj(>X$kGc$8smc|+*txK>Zw<^LwwA@XlSxCb*73hN52$4-xU}o?v+6?6jWkfnyn5Gt84z+BOC^`z}K6Azdwi~ zzOu8hu*NmK~sgz;mAcua67j!1QlW`UIMH%r5Dus6eoUcrVzL)kjZb@Y$Lh>7G9B zIk>+4{*v!p?5y-Lz}`w6ym>#7JT_a~7N zW(*G8r}Mu*X2eQY26<#fydMIs_ps6UN*SFnBGn>0FE-m6PTqFyib^suK?yT}l9Ypa zYg`qALJQ07Wo=?fz$e2kk8s(K3K7-|b|i}Jx)#jEme>U+(Ls^Zele6AvL^leEUUb4 zwU6Zd&b{1S4baf1dx=GO+TFfhdR2Nn^`--GnirD0DL4c)f_bpIcNC0_gLpS^oKD-q z>I4{B41+|zB}b@*D83WD{I@f~7Q|T^Ai)piz#J*7qWX@IdZ>+I_lDsAiHp&6UQCq} zTdp?z`u_TAvdX+K7*=;VWY)kJ!E!Q_vh!mT^je_|G7_!0wY4>ZwC1Kpx1&~bYy=RFN)&@2ti7)2(U|#K{}-a%S&87SMbYe1i8O_I4ZH_s2t)A z#;TWzT;Ms;AMHT;V2`%XM>-_oo!`F)e;e$ra@ik5L^r6$9at6h`)%%GDwhmL;?Hfp z&1&3T8I2tq{koj4)i2q~v=YIG!TrfOn45|>dUotRxn@iq=eHEIy&%DWKfW5p@|cga zp}cVSuF}$>*MfI=f%XpUYEk_VEv_dj$Cc$HB%oF-+>Brp5XkBehjvkzSv`%hG#ieU zIHcm%&vT626Q{#vhjpZ#4Hi4kU^AZnBk=(slsx?vX6 zE?5MCkAQ?H>3;N*?0}A^$d_Fxf0TbA1{2b;8GRp(lZz|NnHaG1biNu zpFt>!EKrw1!U^~qw&kai@SW{aZGKq!B3(xpT_d^;eXFmbw{hfWnmZ!VPfh%Z!jYIn zt7HCDo|Vt&wC_4NB*xjK7&ACV)SmKnio^X49^)FbRs z@j^-50Y!E1^6|E%k)Z*Ff)g#l(D#VKgDE8bc1ijb&VYONy63OIrJb6n)*SlXek4IE z9}TJXdLv_pA;khdC`0?d4DDV)YU3cu4pt2{1VmO=DP?g^(wTawVM;4)THlX|kZn6> zL7Mg6j^Pnv^&7m%NDC;0?oViluzA)}@2i<#>G|yzN1;bhJq{wt>ksScrLqu|Lj?K2 zp>Yf*Nrb@l5hjh5Vh~e|7f8^f^ZZwii|=^x6x{P`+lP5Mm@kr=eUq?>d=(_Dk#!0s= zbN%(PN-s@-sc_FQTqj;3g)Yk_*Q(t(^1qLZr+ST{%82j0S!Q4(6V&oxs8DTZ+2uhg zMBoaF{k3RU&?4!~!*DaEYT=7+iWfXf9Oy9@p`6$J8E;WAFvy7%OUL)#)W!OXa08jQ zI~~un>i}08X4P1Kmq~nhBDt&c@_6;-*DzYnA^WT7w=CF)o1J_MGLDo*WpmLn>a;Jo z0^*ybGQp%M+z&c`O^^k?oTpOen1+(_J5%Vh1`kc=3Rziq)`;vnksQAB`SX0z)h?L@ zF(KxdARhK>y;&_O#JV}5*wGC|TOr)?Gzx~^g&2w~?f$jNa^>WKA11KRmenrY2GPkx zXJ>9#ByN?Y!t)1u%4U`(s$~Qk=_%;@m}jiub^?{&MKfSYQKLaQ?x<{$k>E!*q2>PTXnYx^KkB?jl2zUj;sql6<0h`n+I{Vy1(IfRX5q|f1#O$yN6C*nsg}zCehwo|7K;g8-gJ#e$ z{c1a&FM+VV4W&?$$MK+7)!F27rG~r+yPh3v+&lzB70jQ~IZx8o_mXxp*x*2~QPfZv+d_%fktt;A@so0_C9vJ#VHsC65 zx-A)o)K?QTceWQnURrxf-OftD#L`!xeO+`s(IguqkVvd(1TE9XH;RHd69JlaFWr0f zeaLI!Q9YZ%7>MT}Ots+&sizm_R+K`CyO-;Zgdy4j9=qjO?FNheG>Ht)fSd2fzMx4f zBP1lu+u1|s1syH36~rCkt!WD1CW-gtw;WHz#DA0^(@#-VjTwF^iPQ{zyyE3dyA6PU--#!)K zD*Dcqsgk2pf3146wiF}jk^mW$amIy74(<9VF^E!gpVE>pE-q_`h?F|b({g|Yy3p*8 zysQh?hOy&qiiziRpy4Ek?~^kUALfx2B(Ff1(20%3bKPy$vh=}`P>yzg zAo)~&u8xC{@I@Wn`p{5Tkbdxv!iRd_SB(_qM7E+b6v0vUH~)B;Ja)8P6{oJQxj9YB z4WGmM;pwX^!nY-lxk4En1rkr}yQO+FqcW39qps|sfFI$UZjr)TKCYP-XuY!b0$Pou z(KBC*w=Du>J~?(t%G7R&(?}VJZmPHPVjjGuac|T?M{hPcZ;2bofs7NmnE^M3i?NE0 z@XLY=9(|bQ@KomM9q%P*`M<rj&T~0XdQmtgNZbq@X(SjBYDrv;>!l5$6dKE64zCaV&8s62U zN9@m&*-}FoF$$p;hGG$ON$;Hm6nv=KN|ipoKzyQlOjUybubEhZE^IBmThmCt7)-ZeV}496x$6W z=xik}Pt;FP!=_$(v`c5VXG`!XcEaR=HZAR+o2~bJNen{weX9uh?+ZFxxOv z1xdlDWJRV07;&lB0)EOeN|qF?>CCAwhZl>i>EtSL5|iza|u<1!OrL z6|OPXegpMe-a4V1K-^6Sk>dHZGi&b?$0;uo;k!g7? z_rbX;Gc$YCB^Wq-!XlVTP|j%)2=SGfb#-;kd8%9&^|+8pFrR?LP$=(V!>#Z&YiLa% zT?GMZ!8N%Zo|m$+C6WI=b?y(S_1|QkXlRr`x?sOX^13`_OgE zlRD1nW#!?kz(dFGaa3wP=F0NEVTylk|B|DZ=0~5VdPGQquVE5Ub>ySy;e%VF3UE@) z2J(sPj4Uiu%dR$I-OW_>=3`hN@eL^Ii0icqK37%!(5DRD)+sNCM-Yi^|&{rJX}41h{-1F(z-MiiVIr{C@vf>0tu> z&~ajNR`MSrhJX74t3hjG;X~v5hM&x*RFVj`vx0MZr&>jZ_A%fFC?v|t%0HpZ zxLsO4kfa_QS>WyNuJ!~3$W4c3{iZI(lz@W9QR1j47=#Jb1-izF#sf7TrO;XDPhKOV z^hp~RadiBKo{BUlDY)b=pI_Oc$d)d92ZxlfDhhZAb>e&K#y{2U(}ao&3(MDRCI5*F z;8k>=#YifoA4fcm*0W|{nVcy&9dvXrQdC71e%Qtn%K_I&)U*ENp{ z`C?lT4ja9wh^r4B9+5Oa*RWS>S|pc12V^RVAR%LV)lE8MzIT_U#lxmUrC%dzLpJ$ew@ zTI0FdKCT8Kg#WwVlF;I3az3xg_GSJ<>F{XMY`#D6S+2VZ@p?%NKLV9vJrAObKc)#s zPsQl`k)KEbVhH6-$U%)kTe1Usj^3EXAZPFC*#%Rvhy3fSPjhYI#02B$%HOAhgoeUO zdeuDh08eEs79z?lUJ^ESdw4#9J%M^`SAYxiwV-vCwcvzO@!4pbgBRN64sn7qh+bRHQ}r98CEimkl%7|5=*zX^zS; zv8?JXeUBxx?syTq?S;OiU?hYgLoNKWFtr;>qKhGSgv#gW=Ej}HW0!u^keZXDv}bq~ z3SDV>_#PM{Ne`>m4$icud!q5P`o$2zDtgfWBanzGt??}`syW*V$!h(vw#e)I}K#G+U2AC9H$!NmM3VD+bJ^$D|8{UTzSS?G2)50fBkY|L*Ise#;b z*-Imw7C&lQyAJP!Xr5<3UyS&2yGL1N-%Yk}c``DB_j4nud-R$wjS>z>=uso)=+tPf zcyNS=J7FE1S`M9*23Qd&@K2*j)JAp@K8VTUeAct#Pp08^IFt2OGh|CdUcOMfu(Bc0 zWQPtq=Q2)6zJ#Wve4no>a7ym<>?o2cA%a*9+tp}-H0>~S_n6PMQhYBvdy7snxcz|m z!tQn)yNlqH&B;@_gf*W%+OLf?XXI=Vu_O9EL84Lp``SrUcx)`n$96(^!ew=XA&hUS ze4$BU6+dMVm>mgpNhH!8p9=R%e=Ld59#4I}pwoDSl`0W0)&4a)D*sPW89@v8<=jk{ zpDrt>u}IqtAO4|%>6=K3?jixx8W%C=y;g65b3>Bk9jSNwg%6d7v%o{P^?N!(5e%Bk zJK*M6l=1sM)n1bX$E;iq54HTWJV;R#mZvz;_*P@sx~-!aQ^nIPuu^-d)ke;AF67Al zQkSL?+){Sb^qxIP`;*cZA*8~?!YE4^ec(FpuEC)rwyiV9`MLD?QnSMbCvMXm94?zE-e!>MjI9HX=nZ<(FJR)h zKQN0=L_6y-c-F3z(?ZP=%_Y|!nC2!1CbAF8cVSZ&)AHmx8k_$V^?hOwV(GQ|z z&tM}zYUl%tk&YS9AfOm?5zYl$x6w@e2JoN=@**p zko=T;bbo9c72<%lzC>c@qcye&6vF--$|la~mww|6aRTNjbkP?o3fP1P*OO_)J{-#b zvP3l~@QKe)F1A8gwf2B=w;)Y!Ea=sR!*x-qQG!&_^QrM$VMbO6l_2^!aRHei+2Qyx zO^Ucad>H1SZD&T>nxclLCZi)}QI=aMS_4j4>3T*F-+v#gqUU=gZ>6IgO7lD6^0C9I z)pHi-ea1(!CxaxB&S+kBdc|{*;rZQ>FDWS!W3{?BC4#^5OhGa zD`vwrvDByUPQ?DD!i473csO5Dq`^-o6eK31M*lN|mK1%jF>i~^f#nSAf&@>3<}FJ! z$VgM_et%UGmXJCrJTX(7()n{)U(!m|sfvSx z+&`1nl8=DPG8TiRXq52E7-(DLRre|aHH1UoL5M0DRxcGOFEB+JHrajvt(kg}rVv)Q zEe>K?R_KajUj#&9h2f1*x5`#X94ys`1U)@+9_m;v8V&qvb;*mLCx7@);XmvE1@bjM z1TVhRHhK9*?+7PYCTD(wm?Z<-Q%pzsZP1nOKq_v2pGrc*`)NF&#Vyjg>qCuEE)aXC zpygZoc=hW>C98V-bqz zyZ6Me`YEBBF!cx6g0abgiBq94Q%=^lM@@Gtm}g;XmtOMcC%FBH z5b8dTwa>@2VW|*+l*xYHuQI^%MT3C_+3w>XV$soZ{hfLX#d@0sx{>K@+Fqanmatzj zaWVH-e4fq`pyA}4$MQ{zOSJQxT*s3>uIo^3vR%@-x8y=C%oOw8oZO`ol+_i`!HScK z@G+3z&DuSb>raP9F*60yCOn>S5D+y`KGd?@ekrG$ncquz4&_s?p*C<$#F1*Gbb~mz z3m|-vzT@0T;EIxOCqE)VI1Ns~9rU5E=eU0+5mKwQrskUiemKcjDI>h8ViY@fCkMhZ zJg5pd{>&jp5y>%JX0;h=A`G-}6A13EU_J&WvFL!ov{xr#@)h-76%oSHw(aD2zGRkJuP4_B{H(R z+3w2XA#I7O;chmOW#EnteIb&SHQ$Shic(5I*L?<_f1bRQFRlqmu=+^k1V^Ss9yVf9 ztaI$(QtSvZnY%>l#)_0ACY(g*A@I^k*65SE9^}YVp(JWHfEfZ*`9% zJo-JXGRE@tP31m8_7cfdu8@c2bdEK`YRFgg%><0K=t|*k z&bEr^7I!kJLxHV&3*=Uqfc)M+{fx>>E9vbSLxJIy=Z>dpiR%mRrgkd zT_-d%r5AZHX+znUp)9ozvIEPr`!!A@anmp|fTQBEyB~n7d-;qZk9)VvokOvf8TBI>$#;_|)lAQXzy?a9wWLQ*8k$f^pP){8FkH>mBaFkl&W3Q$3H{E# z$_fgme@*0me~i2zwN9>b#OQ*?1vG?qwZT~X+L1|kXXR|_8g0jURp0c^zqe#N>)Y;h zi@dy7N#U6az935sp46k-&0$hG&`8e>sB(%u`HqS&SI@6chW zh7*hMk08g>m`2EU%lULV+#Qg_80jZye+V~PN;!b9#W2pKvxbo&F{#$;GZ_d8ok=Y0 zTURhThV~v`pTK;!K;ja)I$Z(tIogEwYOSG+#r3;G2oAi{Z&J=L=UPY;{%&rKX8ix{ zU2RkRRj1|RN-2Cqp?a6}pgMkYvGS(2L9LIGXaK8as&K~dkTtTxcMjl%5@)kMAFJcZ ztnlLyAJO??9pz)o2jX+i(Eyx@uY7D_a@W9um-0+TE=$>ry3F_%LIb6?Cif2G=1ExI z5&p(>P-$0@+hQE&o4{8dyJ4sdBkwcXU;@0u#B5=&`dJ;Whr|RI!b3V96Pb~zyPm&q zswy6(oaD0=w4EKjmC5T6X=?{4Y+ zpO+h-KcqKF78B_L@oX5AXDGp63CUKvIVTTi^7Bh7KeRu%ekI(dZv9Z{A3GWE;kGXm zgEz;hS;}5Fa(1h)o5QD%Z7bFL1v0={%9-tx!2y6J!jT0g1~am=&lymC9h6k8Ftm`T zM@LN(976>dL~Jm%B7TK`M6{~TmHt)&%ujgpe3INoqb_eZkf$g$3N7SLs6y3Rx&J;a zsAm6A$`j6#WQz~{3s3Hg0V|a|Ja!VnRK|sHX;!WkdyjOnQWHVVq{tcv-Q&0=_(;&> zau9w@Rtfq$$wL|aR_AX;(PUL|pC1C5jk|NG*lmHX5_$O9o>fQLMKS6a0jy`yjF)tkPKmUqI}2mqmL*{l5d16H+Do`RgkCutCa&Bn zp{*xvi2BgA^)~1;79vxvI`%D~T*7}eESIZ?kO_RIfoW^nP@Xwbdh#kNj~k*d4pc?bNxu@};!4zRNCiHJ z+OWW(k({RW>S86S<)aDI2Y*u5$)BM3s1g=13^!!@US8e=U696VcHWyJZZ?a{ig|?d zCE}9L%7t(+f036@Ta6tdeHpm8<_-^0Z08VE&d4`do@Zpjp6vQHGZq-G5zF~S^*&#Y;Szr_iUH<(e;hHHePrdXPIh&^qg{r_@QGEaEqaX`BmDydTYgBhqHUq*7<<&z)MuSuKisTt zfJc-ihv7j7UUn@eTSrQA|e1^efgyjs)P36f_MX=?iVEe@%Q4Q zmQ=$dFbF5Jc_y})q_i5!8Wi1pP8ncIz>e<*c792}qul$G8P#fow*Big({Ww$U92rZ z)G!KZ8JRf&*JJC)sT6wjX8_pn1SCX70SlS&+8Oo=l6hN&zlw+Fo=zQK{SD%P9v+FTms}A|S<}5pqg5IN94X zXYo2@cUYm_WRY|#+QUbQ*srpG%=%EVcf32<_mcYVLv)(6nq6&@LAUSP78m?4CB${w zW~o6u(|ABnCDzum*NA|)D0Ojh$=2Bm2gGP}hZEWrSU&y~_67zd${=Q*Dd1h*uGT;N zEXZKiP0kbYQ2F8uK!!2U+8Dsy4Tye1%9!QF_6BLzZGSLIG|TL6e@Jvo4v>`$WS4rV ze6eE4y6Z}X1yCoPk#QUUAjj;~)M7*qP~r4;6|3fpw!3*aVG{EVLosZ{)k6qrqk^!c z$z?YURy_`c72)L6h&%>$s~f0KvTvOSXlQ8>`^G;IoBsG@2(=+T3jo2n!9nSb%n;~z zA1evEocDs?Z3ELL#309BX+H?_hR+>JF5g=7gwaQt!XBO!fugGirqXxf>z>FLT@L zB-cdAgIxKtXcQx(mP#x6=Xb*nUJ>Wrz~gejeH5bboNct!I5fs3B$OSDB&_6M$H$BK zJT^LrhR}@7Z5b9KIo^O9W9&C(GwLko>&MLgZ;J{R35VinJdJuJp48P`r7P?>e8;dK z>GJdcf@LL+s`R;tX|khKGSL{U?jnG+J_O3wwbA)?;vE{R`WI`uxEKO-4@4O zGw!ad=8aR}%zWTrHR|LS2>8=!=1MH4Ctwz;s21n<@DpPsafHk#mPFKd)$Rc>$Mv2k z>tB=}_TRIk&%;nY4o?3{Fnoe%BFT`@a=-q^<^Ydgb4zpd%~{qLP{d;Z1{U^n=6Bj` z-6If58_y?QDTb zyV^6+Zm}G5t;sHf`oSK+7h83pv!e6_;0rF(JX_BzCvs9YpOZ?npu-eCn4jrdLG;U9}m)yQwqfQ zq#4ndNyZ$g0PnZJAVuM*QP9ei0;HJI=kfB6%ICXA$!JZ_pK}@dl8?wJ(u`k1)4@a( z?fAb`;j7#teeC9HBPs^7(dGV#7uNAS#03Q6oi#|0A`Go|ZOjzf`jnT~ZbY#^%fU;+Q)Lw0|(EEQK~U zGaX$x=#Ep`!bge8HUWG|3p5hFhdUyOmJ|?6Ybq1Lu*ma(d}B-qycYfkdJ04GlE>;h z0^qoMCtV_Zp0-)SQ(^&_QRkps6Z@jKT66 zJ71~@JAMa%dThe9D&UfvPi6B+vIkpokxFx~U?P1Qtu-A;#8{VF_POcGcm*{f5T#z}cM9>pyT82F-mzsD^6Jj*jf9pv`j&3a;C| z0I^OLl-3sBWL`>$zxFi#i-rZx?uBLcse~+JjoeQ#g>oQxNsrb={C4#~C6ph=3(%Fq zeL2X1Sj!HGe=043!L91r*>S*3TOqN37* z)!!Hv4`lk%*}Sn0K-H;NZNr5y_nBV6nh0^+>CUC7pxWOcy}h~L zOir$dh7SXAmHhA0BYE&#FF_mv$rSJLzlf;I(|#zAV2#0JDFn*$?QDzMH_39sNCZ*U zR{^qa;C{47Py8dq9X6W1j)S%eop!q2SLjn?vMK)^r2yuctfW0J+j6t@9Mycap^%~- zhYvx+K`V(2LR*X=*`B9e=iM-IF{qwyyO#_q-cO0E?)X06tLIfoPa`apDi<-x_3HmS z6agS_heH%3MgZeI<=gQc_=7YYn%v9s?4BZ6%DWgIS5GPp7VH@q z1OkIU2q$+3;6l{EJju3m0O`*Vq(2h+Wo^Ra=*DmNlacSu@(2@*N+fQsJ8tN}7Qi)7 zVz*Ll4mNG~>!f!fCe;9Yngax=r?py-zu!~Inn25y&oz3WKrjTjfkI%f^ZA5QztOLZ zkCK&$uISbniGaf2hz(Ai%%B)jSxIRUstkac4*>AYtGmr55^}G^Fudvfp%Sen(McH( z?F8A0KAZUdbhFJ>vK9tChAzZJ*wMuL_DQN6H0Ot)eRrQ`i?n<^!&{7rNdFJ$!Nt~d zg)o$5_lK~FHFsYlPY0?FFxX7Ud$nJIe13bW0Wz_ItZX=4)uE`SMwLXqn_;~%n0#utj%@;W*M zGO=9`)t}y3G0@T}XPe`*LY=~OE2^>JwSA|erp}6GF8NE(UhJ^lsp~+xnBm>>QN>9#KhXI$53$h@DVd zOYJvXG5H`vjLTF=?*Yp6ySS?}vrNOs(v(e!3*p#)t`JtH5Xlp-NG&nJlMtD~vgVsa z9|iv`hVpJHi`RBC6fBZjCweMo0BB9PSyqfYk|Xi@hp2_q@8I3h z$#ktb&6k9r6zkb~)iLhOviYUQt^YinW19VVrySSZz j8QE$g8066Z@>gtif!}DbK$QlZdxKDrQIW2eGztD623Ha@ 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} + +