Refactoring: port reviews to svelte
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
"id": "reviews",
|
||||
"description": "Shows the reviews module (including the possibility to leave a review)",
|
||||
"render": {
|
||||
"*": "{reviews()}"
|
||||
"*": "{create_review()}{list_reviews()}"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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",
|
||||
|
|
47
assets/svg/mangrove_logo.svg
Normal file
After Width: | Height: | Size: 21 KiB |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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…",
|
||||
|
|
|
@ -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í…",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 l’attività!",
|
||||
"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",
|
||||
|
|
|
@ -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}個のレビュー",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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} отзыв(-ов)",
|
||||
|
|
|
@ -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} 審核次數",
|
||||
|
|
Before Width: | Height: | Size: 14 KiB |
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
47
src/UI/Reviews/AllReviews.svelte
Normal 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>
|
|
@ -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]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
97
src/UI/Reviews/ReviewForm.svelte
Normal 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}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
38
src/UI/Reviews/SingleReview.svelte
Normal 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>
|
|
@ -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")
|
||||
}
|
||||
}
|
32
src/UI/Reviews/StarElement.svelte
Normal 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>
|
21
src/UI/Reviews/StarsBar.svelte
Normal 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}
|
11
src/UI/Reviews/StarsBarIcon.svelte
Normal 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}
|
|
@ -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})
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|