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}) | ||||
|                 }, | ||||
|             }, | ||||
|             { | ||||
|  |  | |||