Refactoring: port reviews to svelte

This commit is contained in:
Pieter Vander Vennet 2023-09-28 04:02:42 +02:00
parent 6edcd7d73c
commit 6d5f5d54f8
41 changed files with 683 additions and 462 deletions

View file

@ -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": {

View file

@ -182,6 +182,16 @@
"then": "<img textmode='\uD83D\uDC15' alt='dogs are allowed' src='./assets/layers/questions/dogs_allowed.svg'>"
}
]
},
{
"id": "rating",
"labels": [
"defaults"
],
"icon": {
"class": "w-20 mx-1 flex items-center"
},
"render": "{rating()}"
}
],
"mapRendering": null

View file

@ -130,7 +130,7 @@
"id": "reviews",
"description": "Shows the reviews module (including the possibility to leave a review)",
"render": {
"*": "{reviews()}"
"*": "{create_review()}{list_reviews()}"
}
},
{

View file

@ -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",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,6 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" width="374px" height="374px" viewBox="0 0 374 374" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:#000000;fill-opacity:1;" class="selectable" d="M 197.824219 10.429688 C 193.816406 11.191406 188.988281 15.195312 184.570312 21.386719 C 178.515625 29.949219 173.335938 40.234375 161.109375 68.078125 C 150.957031 91.164062 146.890625 99.34375 141.597656 107.144531 C 135.421875 116.230469 131.152344 118.042969 115.910156 118.074219 C 107.046875 118.074219 99.144531 117.429688 77.4375 114.976562 C 68.371094 113.925781 57.953125 112.84375 50.230469 112.140625 C 44.644531 111.644531 27.0625 111.644531 23.753906 112.140625 C 13.253906 113.777344 8.394531 117.226562 8.015625 123.304688 C 7.8125 127.042969 9.273438 131.046875 12.726562 136.21875 C 18.488281 144.808594 27.761719 154.101562 51.253906 174.875 C 79.195312 199.5625 87.738281 209.175781 88.996094 217.300781 C 90.078125 224.136719 86.214844 236.46875 75.742188 259.902344 C 65.09375 283.742188 63.164062 288.039062 61.40625 292.332031 C 54.035156 310.1875 51.109375 321.492188 52.105469 328.449219 C 53.011719 334.933594 56.347656 338.179688 62.546875 338.558594 C 71.644531 339.171875 85.015625 333.621094 112.253906 317.988281 C 134.164062 305.394531 138.144531 303.144531 143 300.574219 C 156.195312 293.5625 164.003906 290.699219 170.03125 290.609375 C 173.570312 290.582031 174.742188 290.902344 179.101562 293.0625 C 186.855469 296.949219 195.894531 304.167969 217.132812 323.453125 C 246.5625 350.214844 258.238281 358.6875 268.386719 360.734375 C 271.371094 361.347656 273.449219 361.113281 275.847656 359.945312 C 278.863281 358.484375 280.527344 356 281.816406 351.0625 C 282.429688 348.695312 282.488281 347.820312 282.488281 341.859375 C 282.488281 338.207031 282.34375 334.058594 282.136719 332.507812 C 280.644531 321.433594 279.007812 312.816406 274.738281 293.503906 C 268.039062 263.289062 266.574219 254.730469 266.574219 245.378906 C 266.574219 240.644531 267.042969 237.78125 268.183594 235.269531 C 271.136719 228.8125 281.699219 220.953125 305.484375 207.453125 C 324.820312 196.496094 329.796875 193.632812 334.886719 190.566406 C 356.503906 177.53125 366.042969 168.328125 366.042969 160.558594 C 366.042969 156.027344 363.234375 152.582031 357.089844 149.511719 C 348.3125 145.128906 335.207031 142.617188 305.484375 139.519531 C 274.707031 136.363281 266.457031 135.195312 257.59375 132.945312 C 246.269531 130.023438 242.554688 127.074219 238.636719 117.898438 C 235.210938 109.832031 232.460938 99.519531 227.195312 74.59375 C 223.246094 55.777344 220.996094 46.164062 218.742188 38.453125 C 215.378906 26.824219 211.722656 18.933594 207.625 14.375 C 204.875 11.308594 201.160156 9.816406 197.824219 10.429688 Z M 197.824219 10.429688 "/>
</g>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="374px"
height="374px"
viewBox="0 0 374 374"
version="1.1"
id="svg5"
sodipodi:docname="star.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs9" />
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="1.0896688"
inkscape:cx="229.42751"
inkscape:cy="291.37294"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg5" />
<path
sodipodi:type="star"
style="fill:#000000;stroke:#000000;stroke-width:5.7519;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.57519, 0.57519;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:1"
id="path891"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="-20.180035"
sodipodi:cy="177.65573"
sodipodi:r1="44.653515"
sodipodi:r2="111.63379"
sodipodi:arg1="0.74973014"
sodipodi:arg2="1.3780487"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 12.500658,208.08448 -11.2965304,79.13776 -40.2247326,-69.08232 -78.755305,13.71127 53.27107,-59.6036 -37.37692,-70.66373 73.148059,32.24527 55.655095,-57.383864 -8.06308,79.532284 71.773666,35.19855 z"
inkscape:transform-center-x="10.914421"
inkscape:transform-center-y="5.8173361"
transform="matrix(-0.19478496,-1.6320507,1.6320507,-0.19478496,-107.17071,207.1864)" />
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,6 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" width="374px" height="374px" viewBox="0 0 374 374" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 197.824219 10.429688 C 193.816406 11.191406 188.988281 15.195312 184.570312 21.386719 C 178.515625 29.949219 173.335938 40.234375 161.109375 68.078125 C 150.957031 91.164062 146.890625 99.34375 141.597656 107.144531 C 135.421875 116.230469 131.152344 118.042969 115.910156 118.074219 C 107.046875 118.074219 99.144531 117.429688 77.4375 114.976562 C 68.371094 113.925781 57.953125 112.84375 50.230469 112.140625 C 44.644531 111.644531 27.0625 111.644531 23.753906 112.140625 C 13.253906 113.777344 8.394531 117.226562 8.015625 123.304688 C 7.8125 127.042969 9.273438 131.046875 12.726562 136.21875 C 18.488281 144.808594 27.761719 154.101562 51.253906 174.875 C 79.195312 199.5625 87.738281 209.175781 88.996094 217.300781 C 90.078125 224.136719 86.214844 236.46875 75.742188 259.902344 C 65.09375 283.742188 63.164062 288.039062 61.40625 292.332031 C 54.035156 310.1875 51.109375 321.492188 52.105469 328.449219 C 53.011719 334.933594 56.347656 338.179688 62.546875 338.558594 C 71.644531 339.171875 85.015625 333.621094 112.253906 317.988281 C 134.164062 305.394531 138.144531 303.144531 143 300.574219 C 156.195312 293.5625 164.003906 290.699219 170.03125 290.609375 Z M 197.824219 10.429688 "/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="374px"
height="374px"
viewBox="0 0 374 374"
version="1.1"
id="svg5"
sodipodi:docname="star_half.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs9" />
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="0.72869754"
inkscape:cx="148.20964"
inkscape:cy="-18.526205"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg5" />
<path
sodipodi:type="star"
style="fill:none;stroke:#000000;stroke-width:5.7519;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.57519, 0.57519;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:1"
id="path1499"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="-20.180035"
sodipodi:cy="177.65573"
sodipodi:r1="44.653515"
sodipodi:r2="111.63379"
sodipodi:arg1="0.74973014"
sodipodi:arg2="1.3780487"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 12.500658,208.08448 -11.2965304,79.13776 -40.2247326,-69.08232 -78.755305,13.71127 53.27107,-59.6036 -37.37692,-70.66373 73.148059,32.24527 55.655095,-57.383864 -8.06308,79.532284 71.773666,35.19855 z"
inkscape:transform-center-x="10.914421"
inkscape:transform-center-y="5.8173361"
transform="matrix(-0.19478496,-1.6320507,1.6320507,-0.19478496,-107.17071,207.1864)" />
<path
id="rect1847"
style="stroke-width:10;stroke-linecap:round;stroke-dasharray:1, 1"
d="M 187.18463,22.032185 144.39557,144.09468 13.024475,146.42672 117.51862,226.08101 79.141663,351.74312 187.18463,276.97945 Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -1,6 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" width="374px" height="374px" viewBox="0 0 374 374" version="1.1">
<g id="surface1">
<path style="fill:none;stroke-width:107.38591;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:4;" d="M 674.316761 62.954545 C 661.244886 65.427807 645.489205 78.502674 631.082102 98.743316 C 611.320739 126.71123 594.430114 160.307487 554.52017 251.283422 C 521.406534 326.684492 508.134375 353.42246 490.856534 378.903743 C 470.721307 408.582888 456.781534 414.505348 407.044318 414.59893 C 378.123295 414.59893 352.353409 412.5 281.532955 404.47861 C 251.930966 401.042781 217.949432 397.513369 192.753693 395.227273 C 174.527841 393.596257 117.153125 393.596257 106.364489 395.227273 C 72.102557 400.574866 56.253409 411.831551 55.011648 431.684492 C 54.344034 443.903743 59.124148 456.97861 70.380114 473.877005 C 89.193466 501.938503 119.449716 532.299465 196.091761 600.160428 C 287.26108 680.828877 315.127273 712.23262 319.22642 738.770053 C 322.764773 761.096257 310.160227 801.377005 275.991761 877.941176 C 241.249148 955.828877 234.946875 969.852941 229.21875 983.890374 C 205.157955 1042.219251 195.624432 1079.157754 198.869034 1101.871658 C 201.819886 1123.061497 212.701989 1133.663102 232.944034 1134.893048 C 262.626136 1136.898396 306.248011 1118.770053 395.120739 1067.700535 C 466.608807 1026.564171 479.600568 1019.21123 495.436364 1010.802139 C 538.484091 987.90107 563.97358 978.542781 583.641477 978.262032 C 595.191193 978.168449 599.009943 979.21123 613.230114 986.283422 C 638.519318 998.970588 668.027841 1022.553476 737.326136 1085.548128 C 833.34233 1172.981283 871.436364 1200.668449 904.55 1207.352941 C 914.297159 1209.358289 921.066761 1208.596257 928.891193 1204.772727 C 938.731818 1200 944.166193 1191.885027 948.372159 1175.748663 C 950.375 1168.02139 950.561932 1165.160428 950.561932 1145.681818 C 950.561932 1133.756684 950.094602 1120.200535 949.413636 1115.13369 C 944.553409 1078.957219 939.2125 1050.802139 925.272727 987.713904 C 903.415057 889.010695 898.634943 861.042781 898.634943 830.494652 C 898.634943 815.026738 900.170455 805.681818 903.882386 797.473262 C 913.522727 776.377005 947.984943 750.695187 1025.588352 706.590909 C 1088.691193 670.802139 1104.914205 661.44385 1121.524432 651.417112 C 1192.064489 608.850267 1223.188636 578.783422 1223.188636 553.395722 C 1223.188636 538.596257 1214.015625 527.339572 1193.973864 517.312834 C 1165.333239 502.994652 1122.579261 494.786096 1025.588352 484.665775 C 925.179261 474.358289 898.26108 470.534759 869.340057 463.181818 C 832.394318 453.636364 820.270455 443.997326 807.478977 414.024064 C 796.316477 387.687166 787.34375 353.983957 770.159375 272.566845 C 757.274432 211.096257 749.91733 179.692513 742.57358 154.491979 C 731.598011 116.497326 719.66108 90.721925 706.295455 75.828877 C 697.322727 65.815508 685.198864 60.949198 674.316761 62.954545 Z M 674.316761 62.954545 " transform="matrix(0.292553,0,0,0.292188,0.0585106,0)"/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="374px"
height="374px"
viewBox="0 0 374 374"
version="1.1"
id="svg5"
sodipodi:docname="star_outline.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs9" />
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="1.0896688"
inkscape:cx="241.35774"
inkscape:cy="291.37294"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg5" />
<path
sodipodi:type="star"
style="fill:none;stroke:#000000;stroke-width:5.7519;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.57519, 0.57519;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:1"
id="path891"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="-20.180035"
sodipodi:cy="177.65573"
sodipodi:r1="44.653515"
sodipodi:r2="111.63379"
sodipodi:arg1="0.74973014"
sodipodi:arg2="1.3780487"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 12.500658,208.08448 -11.2965304,79.13776 -40.2247326,-69.08232 -78.755305,13.71127 53.27107,-59.6036 -37.37692,-70.66373 73.148059,32.24527 55.655095,-57.383864 -8.06308,79.532284 71.773666,35.19855 z"
inkscape:transform-center-x="10.914421"
inkscape:transform-center-y="5.8173361"
transform="matrix(-0.19478496,-1.6320507,1.6320507,-0.19478496,-107.17071,207.1864)" />
</svg>

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -534,10 +534,7 @@
"attribution": "Les ressenyes funcionen gràcies a <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> i estan disponibles sota <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Tinc alguna filiació amb aquest objecte</span><br/><span class='subtle'>Marca-ho si n'ets cap, creador, treballador, …</span>",
"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": "<span class=\"thanks\">Revisió compartida. Gràcies per compartir!</span>",
"saving_review": "Desant…",

View file

@ -448,10 +448,7 @@
"attribution": "Recenze jsou poskytovány službou <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> a jsou k dispozici pod licencí <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Jsem spojen/a s tímto objektem</span><br/><span class='subtle'>Zaškrtněte, pokud jste vlastníkem, tvůrcem, zaměstnancem, …</span>",
"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": "<span class='thanks'>Recenze uložena. Díky za sdílení!</span>",
"saving_review": "Ukládání…",

View file

@ -383,10 +383,7 @@
"attribution": "Anmeldelserne er baseret på <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> og er tilgængelige under <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Jeg er tilknyttet dette objekt</span><br><span class=\"subtle\">Tjek, om du er ejer, skaber, ansat, ...</span>",
"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": "<span class=\"thanks\">Anmeldelse gemt. Tak for at bidrage!</span>",
"saving_review": "Gemmer…",
"title": "{count} Anmeldelser",

View file

@ -536,10 +536,7 @@
"attribution": "Rezensionen werden bereitgestellt von <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> und sind unter <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a> verfügbar.",
"i_am_affiliated": "<span>Ich bin an diesem Objekt beteiligt</span><br/><span class='subtle'>Auswählen, wenn Sie Eigentümer, Ersteller, Angestellter … sind</span>",
"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": "<span class=\"thanks\">Bewertung gespeichert. Danke fürs Teilen!</span>",
"saving_review": "Speichern…",

View file

@ -557,14 +557,16 @@
"reviews": {
"affiliated_reviewer_warning": "(Affiliated review)",
"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>",
"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": "<span class='thanks'>Review saved. Thanks for sharing!</span>",
"saved": "Review saved. Thanks for sharing!",
"saving_review": "Saving…",
"title": "{count} reviews",
"title_singular": "One review",

View file

@ -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": "<span class=\"thanks\">Reseña guardada. ¡Gracias por compartir!</span>",
"saving_review": "Guardando…",
"title": "{count} comentarios",

View file

@ -428,10 +428,7 @@
"attribution": "Les avis sont fournis par <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> et sont disponibles sous licence <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Je suis affilié à cet objet</span><br><span class=\"subtle\">Cochez si vous en êtes le propriétaire, créateur, employé, …</span>",
"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": "<span class=\"thanks\">Avis enregistré. Merci du partage !</span>",
"saving_review": "Enregistrement…",
"title": "{count} avis",

View file

@ -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": "<span class=\"thanks\">Recensión compartida. Grazas por compartir!</span>",
"saving_review": "Gardando…",
"title": "{count} recensións",

View file

@ -303,10 +303,7 @@
"attribution": "A véleményeket <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> tárolja, és a <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0 licenc</a> szerint érhetők el.",
"i_am_affiliated": "<span>Kapcsolatban állok ezzel a létesítménnyel</span><br><span class=\"subtle\">Ellenőrizd, hogy tulajdonos, alkotó, alkalmazott vagy hasonló vagy-e.</span>",
"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": "<span class=\"thanks\">Vélemény elmentve. Köszönjük a megosztást!</span>",
"saving_review": "Mentés…",
"title": "{count} vélemény",

View file

@ -162,9 +162,6 @@
},
"reviews": {
"attribution": "Ulasan didukung oleh <a href=\"https://mangrove.reviews/\" target=\"_blank\"> Mangrove Reviews</a> dan tersedia di bawah <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"no_rating": "Tidak ada peringkat yang diberikan",
"plz_login": "Masuk untuk meninggalkan ulasan",
"posting_as": "Posting sebagai",
"saved": "<span class=\"thanks\"> Ulasan disimpan. Terima kasih sudah berbagi! </span>",
"saving_review": "Menyimpan…",
"title": "{count} ulasan",

View file

@ -307,10 +307,7 @@
"attribution": "Le recensioni sono fornite da <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> e sono disponibili con licenza <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Sono associato con questo oggetto</span><br><span class=\"subtle\">Spunta se sei il proprietario, creatore, dipendente, etc.</span>",
"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 lattività!",
"plz_login": "Accedi per lasciare una recensione",
"posting_as": "Pubblica come",
"saved": "<span class=\"thanks\">Recensione salvata. Grazie per averla condivisa!</span>",
"saving_review": "Salvataggio…",
"title": "{count} recensioni",

View file

@ -165,10 +165,7 @@
"attribution": "レビューは、<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>わたしは、この対象物の関係者です</span><br><span class=\"subtle\">所有者、作成者、従業員などの有無を確認します</span>",
"name_required": "レビューを表示および作成するには名前が必要です",
"no_rating": "評価が与えられていません",
"no_reviews_yet": "まだレビューはありません。最初に書き込みを行い、データとビジネスのオープン化を支援しましょう!",
"plz_login": "ログインしてレビューを終了する",
"posting_as": "としての投稿",
"saved": "<span class=\"thanks\">レビューが保存されました。共有ありがとう!</span>",
"saving_review": "保存中…",
"title": "{count}個のレビュー",

View file

@ -401,10 +401,7 @@
"attribution": "Vurderinger er muliggjort av <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> og er tilgjengelige som <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Jeg har en tilknytning til dette objektet</span><br/><span class='subtle'>Sjekk om du er eier, skaper, ansatt, …</span>",
"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": "<span class=\"thanks\">Vurdering lagret. Takk for at du deler din mening.</span>",
"saving_review": "Lagrer …",
"title": "{count} vurderinger",

View file

@ -530,10 +530,7 @@
"attribution": "De beoordelingen worden voorzien door <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> en zijn beschikbaar onder de<a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0-licentie</a>. ",
"i_am_affiliated": "<span>Ik ben persoonlijk betrokken</span><br/><span class='subtle'>Vink aan indien je de oprichter, maker, werknemer, ... of dergelijke bent</span>",
"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": "<span class='thanks'>Bedankt om je beoordeling te delen!</span>",
"saving_review": "Opslaan...",

View file

@ -331,10 +331,7 @@
"attribution": "Recenzje są obsługiwane przez <a href=\"https://mangrove.reviews/\" target=\"_blank\">Recenzje Mangrove</a> i są dostępne na licencji <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Jestem powiązany z tym obiektem</span><br><span class=\"subtle\">Sprawdź czy jesteś właścicielem, twórcą, pracownikiem, ...</span>",
"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": "<span class=\"thanks\">Opinia została zapisana. Dzięki za udostępnienie!</span>",
"saving_review": "Zapisywanie…",

View file

@ -352,10 +352,7 @@
"attribution": "As avaliações são fornecidas por <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> e estão disponíveis sob a licença <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Eu sou afiliado a este objeto</span><br><span class=\"subtle\"><br><span class=\"subtle\">Marque isto se for proprietário, criador, funcionário…</span></span>",
"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": "<span class=\"thanks\">Avaliação guardada. Obrigado por partilhar!</span>",
"saving_review": "A guardar…",
"title": "{count} avaliações",

View file

@ -162,10 +162,7 @@
"attribution": "As resenhas são fornecidas por <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> e estão disponíveis em <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Eu sou afiliado a este objeto</span><br><span class=\"subtle\"><br><span class=\"subtle\">Verifique se você é proprietário, criador, funcionário, …</span></span>",
"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": "<span class=\"thanks\">Comentário salvo. Obrigado por compartilhar!</span>",
"saving_review": "Salvando…",
"title": "{count} comentários",

View file

@ -176,10 +176,7 @@
"attribution": "Отзывы созданы на основе <a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a> и доступны под лицензией <a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>.",
"i_am_affiliated": "<span>Я связан с этим объектом</span><br><span class=\"subtle\"> Отметьте если вы создатель, владелец, работник, …</span>",
"name_required": "Необходимо название, чтобы просматривать и создавать отзывы",
"no_rating": "Нет рейтинга",
"no_reviews_yet": "Пока нет отзывов. Оставьте первый отзыв и помогите открытым данным и бизнесу!",
"plz_login": "Войдите, чтобы оставить отзыв",
"posting_as": "Публикация от имени",
"saved": "<span class=\"thanks\"> Отзыв сохранен. Спасибо, что поделились! </span>",
"saving_review": "Сохранение…",
"title": "{count} отзыв(-ов)",

View file

@ -383,10 +383,7 @@
"attribution": "評審系統由<a href=\"https://mangrove.reviews/\" target=\"_blank\">Mangrove Reviews</a>提供技術支援,採用<a href=\"https://mangrove.reviews/terms#8-licensing-of-content\" target=\"_blank\">CC-BY 4.0</a>授權條款。",
"i_am_affiliated": "<span>我是這物件的相關關係者</span><br><span class=\"subtle\">確認你是否是擁有者、創造者、員工等等</span>",
"name_required": "需要有名稱才能顯示和創造審核",
"no_rating": "還沒有評分",
"no_reviews_yet": "還沒有審核,當第一個撰寫者來幫助開放資料與商家吧!",
"plz_login": "登入來留下審核",
"posting_as": "以貼文",
"saved": "<span class=\"thanks\">已儲存審核,謝謝你的分享!</span>",
"saving_review": "儲存中…",
"title": "{count} 審核次數",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -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;
}

View file

@ -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<CryptoKeyPair>
public readonly key_id: Store<string>
public readonly keypair: Store<CryptoKeyPair>;
public readonly key_id: Store<string>;
constructor(mangroveIdentity: UIEventSource<string>) {
const key_id = new UIEventSource<string>(undefined)
this.key_id = key_id
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
this.keypair = keypairEventSource
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
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<string>): Promise<void> {
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<string, FeatureReviews> = {}
public readonly subjectUri: Store<string>
private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {};
public readonly subjectUri: Store<string>;
public readonly average: Store<number | null>;
private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
new UIEventSource([])
new UIEventSource([]);
public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
this._reviews
private readonly _lat: number
private readonly _lon: number
private readonly _uncertainty: number
private readonly _name: Store<string>
private readonly _identity: MangroveIdentity
this._reviews;
private readonly _lat: number;
private readonly _lon: number;
private readonly _uncertainty: number;
private readonly _name: Store<string>;
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<string>(undefined))
const nameKey = options?.nameKey ?? "name"
mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(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<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()
...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<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
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;
});
}
}

View file

@ -46,7 +46,7 @@
>
{#each layer.titleIcons as titleIconConfig}
{#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ..._metatags, ..._tags } ) ?? true) && titleIconConfig.IsKnown(_tags)}
<div class="flex h-8 w-8 items-center">
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
<TagRenderingAnswer
config={titleIconConfig}
{tags}

View file

@ -0,0 +1,47 @@
<script lang="ts">
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<Record<string, string>>;
export let feature: Feature;
export let layer: LayerConfig;
let average = reviews.average;
let _reviews = [];
reviews.reviews.addCallbackAndRunD(r => {
_reviews = Utils.NoNull(r);
});
</script>
<div class="border-gray-300 border-dashed border-2">
{#if _reviews.length > 1}
<StarsBar score={$average}></StarsBar>
{/if}
{#if _reviews.length > 0}
{#each _reviews as review}
<SingleReview {review}></SingleReview>
{/each}
{:else}
<Tr t={Translations.t.reviews.no_reviews_yet} />
{/if}
<div class="flex justify-end">
<ToSvelte construct={Svg.mangrove_logo_svg().SetClass("w-12 h-12")} />
<Tr t={Translations.t.reviews.attribution} />
</div>
</div>

View file

@ -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]
)
)
}
}

View file

@ -0,0 +1,97 @@
<script lang="ts">
import FeatureReviews from "../../Logic/Web/MangroveReviews";
import StarsBar from "./StarsBar.svelte";
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import { UIEventSource } from "../../Logic/UIEventSource";
import type { Feature } from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import Translations from "../i18n/Translations";
import Checkbox from "../Base/Checkbox.svelte";
import Tr from "../Base/Tr.svelte";
import If from "../Base/If.svelte";
import Loading from "../Base/Loading.svelte";
import { Review } from "mangrove-reviews-typescript";
import { Utils } from "../../Utils";
export let state: SpecialVisualizationState;
export let tags: UIEventSource<Record<string, string>>;
export let feature: Feature;
export let layer: LayerConfig;
/**
* The form to create a new review.
* This is multi-stepped.
*/
export let reviews: FeatureReviews;
let score = 0;
let confirmedScore = undefined;
let isAffiliated = new UIEventSource(false);
let opinion = new UIEventSource<string>(undefined);
const t = Translations.t.reviews;
let _state: "ask" | "saving" | "done" = "ask";
const connection = state.osmConnection;
async function save() {
_state = "saving";
let nickname = undefined;
if (connection.isLoggedIn.data) {
nickname = connection.userDetails.data.name;
}
const review: Omit<Review, "sub"> = {
rating: confirmedScore,
opinion: opinion.data,
metadata: { nickname, is_affiliated: isAffiliated.data }
};
if (state.featureSwitchIsTesting.data) {
console.log("Testing - not actually saving review", review);
await Utils.waitFor(1000);
} else {
await reviews.createReview(review);
}
_state = "done";
}
</script>
{#if _state === "done"}
<Tr cls="thanks w-full" t={t.saved} />
{:else if _state === "saving"}
<Loading>
<Tr t={t.saving_review} />
</Loading>
{:else}
<div class="interactive border-interactive p-1">
<div class="font-bold">
<SpecialTranslation {feature} {layer} {state} t={Translations.t.reviews.question} {tags}></SpecialTranslation>
</div>
<StarsBar on:click={e => {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"></StarsBar>
{#if confirmedScore !== undefined}
<Tr cls="font-bold mt-2" t={t.question_opinion} />
<textarea bind:value={$opinion} inputmode="text" rows="3" class="w-full mb-1" />
<Checkbox selected={isAffiliated}>
<div class="flex flex-col">
<Tr t={t.i_am_affiliated} />
<Tr cls="subtle" t={t.i_am_affiliated_explanation} />
</div>
</Checkbox>
<div class="flex w-full justify-between flex-wrap items-center">
<If condition={state.osmConnection.isLoggedIn}>
<Tr t={t.reviewing_as.Subs({nickname: state.osmConnection.userDetails.data.name})} />
<Tr slot="else" t={t.reviewing_as_anonymous} />
</If>
<button class="primary" on:click={save}>
<Tr t={t.save} />
</button>
</div>
<Tr cls="subtle mt-4" t={t.tos} />
{/if}
</div>
{/if}

View file

@ -1,101 +0,0 @@
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 { CheckBox } from "../Input/Checkboxes"
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Toggle from "../Input/Toggle"
import { LoginToggle } from "../Popup/LoginButton"
import { SubtleButton } from "../Base/SubtleButton"
export default class ReviewForm extends LoginToggle {
constructor(
onSave: (r: Omit<Review, "sub">) => Promise<void>,
state: {
readonly osmConnection: OsmConnection
readonly featureSwitchUserbadge: Store<boolean>
}
) {
/* made_by_user: new UIEventSource<boolean>(true),
rating: undefined,
comment: undefined,
author: osmConnection.userDetails.data.name,
affiliated: false,
date: new Date(),*/
const commentForm = new TextField({
placeholder: Translations.t.reviews.write_a_comment.Clone(),
htmlType: "area",
textAreaRows: 5,
})
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(
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 saveButton = new Toggle(
Translations.t.reviews.no_rating.SetClass("block alert"),
new SubtleButton(Svg.confirm_svg(), Translations.t.reviews.save)
.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()])
)
const stars = []
for (let i = 1; i <= 5; i++) {
stars.push(
new VariableUiElement(
rating.map((score) => {
if (score === undefined) {
return Svg.star_outline.replace(/#000000/g, "#ccc")
}
return score < i * 20 ? Svg.star_outline : Svg.star
})
).onClick(() => {
rating.setData(i * 20)
})
)
}
const form = new Combine([
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")
.SetStyle(
"border-radius: 1em;" +
" background-color: var(--subtle-detail-color);" +
" color: var(--subtle-detail-color-contrast);" +
" border: 2px solid var(--subtle-detail-color-contrast)"
)
super(
new Toggle(Translations.t.reviews.saved.Clone().SetClass("thanks"), form, reviewMade),
Translations.t.reviews.plz_login,
state
)
}
}

View file

@ -0,0 +1,38 @@
<script lang="ts">
import { Review } from "mangrove-reviews-typescript";
import { Store } from "../../Logic/UIEventSource";
import StarsBar from "./StarsBar.svelte";
import Translations from "../i18n/Translations";
import Tr from "../Base/Tr.svelte";
export let review: Review & { madeByLoggedInUser: Store<boolean> };
let name = review.metadata.nickname;
name ??= (review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "").trim();
if (name.length === 0) {
name = "Anonymous";
}
let d = new Date();
d.setTime(review.iat * 1000);
let date = d.toDateString();
let byLoggedInUser = review.madeByLoggedInUser;
</script>
<div class={"low-interaction p-1 px-2 rounded-lg "+ ($byLoggedInUser ? "border-interactive" : "")}>
<div class="flex justify-between items-center">
<StarsBar score={review.rating}></StarsBar>
<div class="flex flex-wrap space-x-2">
<div class="font-bold">
{name}
</div>
<span class="subtle">
{date}
</span>
</div>
</div>
{#if review.opinion}
{review.opinion}
{/if}
{#if review.metadata.is_affiliated}
<Tr t={Translations.t.reviews.affiliated_reviewer_warning} />
{/if}
</div>

View file

@ -1,64 +0,0 @@
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"
export default class SingleReview extends Combine {
constructor(review: Review & { madeByLoggedInUser: Store<boolean> }) {
const date = new Date(review.iat * 1000)
const reviewAuthor =
review.metadata.nickname ??
(review.metadata.given_name ?? "") + (review.metadata.family_name ?? "")
const authorElement = new FixedUiElement(reviewAuthor).SetClass("font-bold")
super([
new Combine([SingleReview.GenStars(review.rating)]),
new FixedUiElement(review.opinion),
new Combine([
new Combine([
authorElement,
review.metadata.is_affiliated
? Translations.t.reviews.affiliated_reviewer_warning
: "",
]).SetStyle("margin-right: 0.5em"),
new FixedUiElement(
`${date.getFullYear()}-${Utils.TwoDigits(
date.getMonth() + 1
)}-${Utils.TwoDigits(date.getDate())} ${Utils.TwoDigits(
date.getHours()
)}:${Utils.TwoDigits(date.getMinutes())}`
).SetClass("subtle"),
]).SetClass("flex mb-4 justify-end"),
])
this.SetClass("block p-2 m-4 rounded-xl subtle-background review-element")
review.madeByLoggedInUser.addCallbackAndRun((madeByUser) => {
if (madeByUser) {
authorElement.SetClass("thanks")
} else {
authorElement.RemoveClass("thanks")
}
})
}
public static GenStars(rating: number): BaseUIElement {
if (rating === undefined) {
return Translations.t.reviews.no_rating
}
if (rating < 10) {
rating = 10
}
const scoreTen = Math.round(rating / 10)
return new Combine([
...Utils.TimesT(scoreTen / 2, (_) =>
new Img("./assets/svg/star.svg").SetClass("'h-8 w-8 md:h-12")
),
scoreTen % 2 == 1
? new Img("./assets/svg/star_half.svg").SetClass("h-8 w-8 md:h-12")
: undefined,
]).SetClass("flex w-max")
}
}

View file

@ -0,0 +1,32 @@
<script lang="ts">
import ToSvelte from "../Base/ToSvelte.svelte";
import Svg from "../../Svg";
import { createEventDispatcher } from "svelte";
export let score: number;
export let cutoff: number;
export let starSize = "w-h h-4";
let dispatch = createEventDispatcher<{ hover: { score: number } }>();
let container: HTMLElement;
function getScore(e: MouseEvent): number {
const x = e.clientX - e.target.getBoundingClientRect().x;
const w = container.getClientRects()[0]?.width;
return (x / w) < 0.5 ? cutoff - 10 : cutoff;
}
</script>
<div bind:this={container} on:click={(e) => dispatch("click", {score: getScore(e)})}
on:mousemove={(e) => dispatch("hover", { score: getScore(e) })}>
{#if score >= cutoff}
<ToSvelte construct={Svg.star_svg().SetClass(starSize)} />
{:else if score + 10 >= cutoff}
<ToSvelte construct={Svg.star_half_svg().SetClass(starSize)} />
{:else}
<ToSvelte construct={Svg.star_outline_svg().SetClass(starSize)} />
{/if}
</div>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import StarElement from "./StarElement.svelte";
/**
* Number between 0 and 100. Every 10 points, another half star is added
*/
export let score: number;
let dispatch = createEventDispatcher<{ hover: number, click: number }>();
let cutoffs = [20,40,60,80,100]
export let starSize = "w-h h-4"
</script>
{#if score !== undefined}
<div class="flex" on:mouseout>
{#each cutoffs as cutoff}
<StarElement {score} {cutoff} {starSize} on:hover on:click/>
{/each}
</div>
{/if}

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { Store } from "../../Logic/UIEventSource";
import StarsBar from "./StarsBar.svelte";
export let score: Store<number>;
</script>
{#if $score !== undefined && $score !== null}
<StarsBar score={$score} />
{/if}

View file

@ -23,8 +23,6 @@ import { Utils } from "../Utils";
import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata";
import { Translation } from "./i18n/Translation";
import Translations from "./i18n/Translations";
import ReviewForm from "./Reviews/ReviewForm";
import ReviewElement from "./Reviews/ReviewElement";
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization";
import LiveQueryHandler from "../Logic/Web/LiveQueryHandler";
import { SubtleButton } from "./Base/SubtleButton";
@ -66,6 +64,9 @@ import SendEmail from "./Popup/SendEmail.svelte";
import NearbyImages from "./Popup/NearbyImages.svelte";
import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte";
import UploadImage from "./Image/UploadImage.svelte";
import AllReviews from "./Reviews/AllReviews.svelte";
import StarsBarIcon from "./Reviews/StarsBarIcon.svelte";
import ReviewForm from "./Reviews/ReviewForm.svelte";
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -624,7 +625,66 @@ export default class SpecialVisualizations {
},
},
{
funcName: "reviews",
funcName: "rating",
docs: "Shows stars which represent the avarage rating on mangrove.reviews",
args: [
{
name: "subjectKey",
defaultValue: "name",
doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>",
},
{
name: "fallback",
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value",
},
],
constr: (state, tags, args, feature, layer) => {
const nameKey = args[0] ?? "name"
let fallbackName = args[1]
const reviews = FeatureReviews.construct(
feature,
tags,
state.userRelatedState.mangroveIdentity,
{
nameKey: nameKey,
fallbackName,
}
)
return new SvelteUIElement(StarsBarIcon, {score:reviews.average, reviews, state, tags, feature, layer})
},
},
{
funcName: "create_review",
docs: "Invites the contributor to leave a review. Somewhat small UI-element until interacted",
args: [
{
name: "subjectKey",
defaultValue: "name",
doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>",
},
{
name: "fallback",
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value",
},
],
constr: (state, tags, args, feature, layer) => {
const nameKey = args[0] ?? "name"
let fallbackName = args[1]
const reviews = FeatureReviews.construct(
feature,
tags,
state.userRelatedState.mangroveIdentity,
{
nameKey: nameKey,
fallbackName,
}
)
return new SvelteUIElement(ReviewForm, {reviews, state, tags, feature, layer})
},
},
{
funcName: "list_reviews",
docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten",
example:
"`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used",
@ -639,10 +699,10 @@ export default class SpecialVisualizations {
doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value",
},
],
constr: (state, tags, args, feature) => {
constr: (state, tags, args, feature, layer) => {
const nameKey = args[0] ?? "name"
let fallbackName = args[1]
const mangrove = FeatureReviews.construct(
const reviews = FeatureReviews.construct(
feature,
tags,
state.userRelatedState.mangroveIdentity,
@ -651,9 +711,7 @@ export default class SpecialVisualizations {
fallbackName,
}
)
const form = new ReviewForm((r) => mangrove.createReview(r), state)
return new ReviewElement(mangrove, form)
return new SvelteUIElement(AllReviews, {reviews, state, tags, feature, layer})
},
},
{