added pubilc folder
This commit is contained in:
1 parent
34f463071f
commit
7f6b100506
9 files changed
+1440
-1
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
.env
|
||||
.env-frontend
|
||||
import_files/
|
||||
public
|
||||
/backend/public
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,183 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" viewBox="0 0 1024 1024" enable-background="new 0 0 1024 1024" xml:space="preserve">
|
||||
<path fill="#000000" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M795.999878,657.367310
|
||||
C760.167114,657.360779 724.834229,657.418213 689.501709,657.321106
|
||||
C670.976624,657.270203 657.227661,646.780029 652.388062,629.050171
|
||||
C651.357178,625.273621 651.202637,621.257812 650.576294,616.876770
|
||||
C657.482727,616.035034 663.884888,616.073242 670.260620,616.588074
|
||||
C670.547852,616.885864 670.906494,617.084229 670.980164,617.358948
|
||||
C674.983643,632.287720 680.651123,636.650940 696.163025,636.652344
|
||||
C765.328552,636.658569 834.494080,636.638977 903.659546,636.700073
|
||||
C907.869934,636.703796 912.004822,636.176758 915.987915,635.146790
|
||||
C924.782776,632.872986 930.628601,624.820740 930.633057,615.379333
|
||||
C930.661804,554.880310 930.603699,494.381226 930.737244,433.882477
|
||||
C930.747314,429.310455 929.062317,428.312653 924.881470,428.321350
|
||||
C875.049133,428.425018 825.216614,428.379333 775.384094,428.375854
|
||||
C743.051270,428.373596 710.718506,428.368530 678.385681,428.382355
|
||||
C670.385254,428.385773 670.400513,428.502502 670.277771,436.467804
|
||||
C670.044067,451.638245 672.689209,448.856934 657.284363,449.246796
|
||||
C652.653748,449.363953 650.602417,447.342773 650.624512,442.681427
|
||||
C650.706787,425.348785 650.489990,408.013336 650.713257,390.683197
|
||||
C650.872620,378.314575 663.611938,366.842316 677.007446,366.687500
|
||||
C687.672546,366.564270 698.341309,366.562439 709.005859,366.705078
|
||||
C712.399292,366.750458 713.831848,365.694122 713.704346,362.137573
|
||||
C713.495605,356.311951 713.589294,350.472198 713.675842,344.640198
|
||||
C713.750366,339.620636 715.621033,337.715820 720.650818,337.661316
|
||||
C728.649719,337.574646 736.651489,337.730652 744.649658,337.617584
|
||||
C749.221863,337.552979 751.560669,339.780548 751.741394,344.142334
|
||||
C751.968994,349.633331 752.067383,355.142029 751.899536,360.633209
|
||||
C751.767700,364.944031 752.969666,366.804321 757.817566,366.772186
|
||||
C787.315491,366.576630 816.815979,366.645477 846.314636,366.802856
|
||||
C849.960205,366.822296 851.001709,365.435333 850.936890,362.134735
|
||||
C850.835571,356.970978 850.838806,351.799805 850.993225,346.637634
|
||||
C851.197388,339.809082 853.414307,337.677887 860.121277,337.643799
|
||||
C867.120911,337.608246 874.121826,337.580017 881.120605,337.670807
|
||||
C886.823242,337.744781 889.363220,340.217010 889.585815,345.834656
|
||||
C889.776978,350.658508 889.892456,355.488403 889.898499,360.315796
|
||||
C889.906494,366.693604 889.846558,366.694550 896.477905,366.689728
|
||||
C905.644409,366.683044 914.816406,366.875366 923.976257,366.624054
|
||||
C941.124512,366.153534 953.648743,379.861237 953.548584,395.994293
|
||||
C953.094360,469.156952 953.355774,542.323975 953.346191,615.489441
|
||||
C953.343018,639.938416 935.945862,657.346497 911.492554,657.359192
|
||||
C873.161682,657.379028 834.830811,657.366089 795.999878,657.367310
|
||||
z"/>
|
||||
<path fill="#000000" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M528.409790,597.516602
|
||||
C497.075287,620.785706 444.226227,612.023682 429.015320,560.438843
|
||||
C421.740753,535.768616 424.170959,511.572845 439.374207,489.842865
|
||||
C455.550232,466.722412 487.366638,456.664978 514.018005,465.788971
|
||||
C521.454529,468.334839 527.421997,473.046326 533.668701,478.389252
|
||||
C534.868774,475.810791 534.631958,473.367340 534.635498,471.001099
|
||||
C534.661194,453.669586 534.632385,436.337952 534.665100,419.006439
|
||||
C534.677490,412.450287 536.433655,410.689545 542.900940,410.657166
|
||||
C550.733276,410.617950 558.566162,410.605804 566.398376,410.655701
|
||||
C572.320129,410.693420 574.311279,412.552216 574.314453,418.325104
|
||||
C574.347412,478.152313 574.348389,537.979553 574.320740,597.806763
|
||||
C574.318115,603.473328 571.764221,606.091187 566.124390,606.208923
|
||||
C559.794495,606.341064 553.459656,606.253235 547.127075,606.232300
|
||||
C538.384033,606.203369 535.937500,604.023315 534.892944,595.424866
|
||||
C534.815613,594.788635 534.606812,594.168396 534.276001,592.762329
|
||||
C532.176392,594.468384 530.433411,595.884583 528.409790,597.516602
|
||||
M536.676025,532.668274
|
||||
C536.117737,518.525208 529.938965,507.667847 517.228394,501.240967
|
||||
C504.691589,494.901886 492.378204,496.212128 480.863525,504.144287
|
||||
C464.767395,515.232483 460.265320,540.059570 470.983459,557.949646
|
||||
C483.140106,578.240845 509.116516,581.518616 525.439575,564.626892
|
||||
C533.729370,556.048279 537.213196,545.494995 536.676025,532.668274
|
||||
z"/>
|
||||
<path fill="#000000" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M411.343933,595.812073
|
||||
C411.222443,604.326233 409.331451,606.197693 401.241425,606.217773
|
||||
C394.744415,606.233887 388.247101,606.251282 381.750366,606.206177
|
||||
C373.682495,606.150146 371.660858,604.146240 371.656036,596.214417
|
||||
C371.641754,572.725098 371.718536,549.235413 371.617584,525.746521
|
||||
C371.573273,515.443726 368.031830,506.656311 358.522034,501.613953
|
||||
C349.308990,496.728912 339.763672,497.080200 330.970001,502.664642
|
||||
C322.013702,508.352356 317.891388,516.984375 317.918732,527.594727
|
||||
C317.949219,539.422546 317.961517,551.250427 317.969666,563.078247
|
||||
C317.977356,574.239746 318.019958,585.401611 317.951050,596.562683
|
||||
C317.902039,604.504211 316.014832,606.340088 308.294373,606.344788
|
||||
C301.130981,606.349121 293.966736,606.421570 286.804443,606.334900
|
||||
C281.836975,606.274841 279.176941,603.693298 278.743622,598.748047
|
||||
C278.627655,597.424377 278.685913,596.084290 278.685913,594.751770
|
||||
C278.685486,555.436279 278.684845,516.120789 278.691925,476.805298
|
||||
C278.693573,467.785950 279.816162,466.642700 288.617371,466.608826
|
||||
C295.114166,466.583832 301.611633,466.617676 308.107697,466.537506
|
||||
C316.139221,466.438385 316.798950,466.949768 318.007935,474.854065
|
||||
C318.179169,475.973694 318.305328,477.100281 318.543365,478.919159
|
||||
C321.681274,476.429321 324.312531,474.199677 327.085693,472.163269
|
||||
C350.631165,454.873627 400.239380,460.166534 409.304962,504.100464
|
||||
C410.547241,510.120819 411.408386,516.172302 411.384521,522.369873
|
||||
C411.290894,546.692017 411.345367,571.014587 411.343933,595.812073
|
||||
z"/>
|
||||
<path fill="#000000" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M589.636841,534.999878
|
||||
C589.640991,514.500916 589.623535,494.501953 589.661987,474.503082
|
||||
C589.674622,467.917572 591.091431,466.563690 597.555847,466.562927
|
||||
C604.555359,466.562073 611.555298,466.607178 618.554199,466.539062
|
||||
C626.701172,466.459778 627.335083,466.959503 628.288025,474.915466
|
||||
C628.406494,475.904846 628.405396,476.935425 629.628784,477.783325
|
||||
C631.940063,477.073456 633.330383,474.878784 635.192688,473.375671
|
||||
C642.799255,467.236206 651.144531,463.267914 661.215210,463.587219
|
||||
C668.189880,463.808289 675.188538,463.284485 682.074890,464.986420
|
||||
C701.912109,469.889099 717.578979,488.136353 719.345215,508.503510
|
||||
C721.450684,532.781555 719.795044,557.109436 720.289246,581.409241
|
||||
C720.401001,586.906616 720.365723,592.409302 720.281189,597.907776
|
||||
C720.191223,603.753479 717.549438,606.322144 711.613586,606.361084
|
||||
C703.947632,606.411316 696.280884,606.402100 688.614929,606.350281
|
||||
C682.504333,606.308899 680.674316,604.455383 680.666870,598.192566
|
||||
C680.638489,574.193787 680.665161,550.195068 680.654053,526.196289
|
||||
C680.651428,520.446228 679.617065,514.889343 676.558716,509.954407
|
||||
C666.466492,493.669189 642.606506,494.853424 632.738037,512.131897
|
||||
C629.813416,517.252625 628.230652,522.707031 628.274780,528.778320
|
||||
C628.443115,551.942566 628.346313,575.108765 628.326050,598.274231
|
||||
C628.320496,604.633179 626.664551,606.328613 620.405457,606.357910
|
||||
C612.739441,606.393738 605.072693,606.394409 597.406921,606.329346
|
||||
C591.545837,606.279663 589.680054,604.416931 589.668030,598.496948
|
||||
C589.625244,577.497986 589.642456,556.498962 589.636841,534.999878
|
||||
z"/>
|
||||
<path fill="#000000" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M849.782715,554.711548
|
||||
C835.896301,577.190186 819.183838,596.970520 801.475647,616.661133
|
||||
C795.724304,611.507751 791.442261,605.768860 786.949707,600.289978
|
||||
C771.314697,581.222717 755.695068,562.068115 745.512817,539.339111
|
||||
C735.929871,517.947571 735.117676,496.356903 747.230713,475.645508
|
||||
C758.418396,456.516327 775.896423,447.060944 797.821411,445.672852
|
||||
C813.106689,444.705078 827.347595,448.058899 839.726562,457.326752
|
||||
C853.845337,467.897247 862.064209,482.018188 864.414978,499.640503
|
||||
C867.123718,519.945801 860.191895,537.575745 849.782715,554.711548
|
||||
M817.940430,533.498657
|
||||
C832.330078,523.279175 835.928345,505.734222 826.468140,491.917389
|
||||
C817.192444,478.369873 799.224121,474.699524 785.513489,483.551544
|
||||
C771.904724,492.337830 767.687500,510.543579 776.234863,524.376282
|
||||
C784.100098,537.105225 801.930237,543.450989 817.940430,533.498657
|
||||
z"/>
|
||||
<path fill="#000000" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M164.230560,579.098877
|
||||
C161.430649,586.099060 158.689026,592.709106 156.132675,599.390076
|
||||
C154.364395,604.011475 151.112350,606.271240 146.240265,606.285217
|
||||
C137.410477,606.310486 128.580460,606.337769 119.750839,606.297485
|
||||
C115.130913,606.276428 112.085350,604.244324 110.266777,599.720703
|
||||
C93.696815,558.504150 76.976242,517.348206 60.334583,476.160461
|
||||
C57.220692,468.453644 58.487259,466.663300 66.792244,466.646271
|
||||
C75.622078,466.628174 84.452065,466.580109 93.281715,466.614502
|
||||
C100.139725,466.641205 102.243279,468.248596 104.441467,474.944489
|
||||
C112.995674,501.001221 121.532463,527.063721 130.106888,553.113770
|
||||
C130.921417,555.588440 131.374649,558.248901 133.512802,560.729980
|
||||
C134.596817,557.994141 135.651764,555.607788 136.499451,553.149902
|
||||
C145.609146,526.736633 154.680695,500.310089 163.777176,473.892181
|
||||
C165.401138,469.175842 168.337128,466.458221 173.742493,466.578461
|
||||
C183.067520,466.785980 192.400955,466.579773 201.729828,466.663879
|
||||
C207.445129,466.715363 208.961655,468.925934 206.816193,474.223358
|
||||
C192.700943,509.075775 178.524414,543.903381 164.230560,579.098877
|
||||
z"/>
|
||||
<path fill="#000000" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M260.338562,485.006592
|
||||
C260.337616,522.308533 260.340759,559.111755 260.332550,595.915039
|
||||
C260.330627,604.575378 258.503235,606.372253 249.785645,606.374512
|
||||
C242.957901,606.376221 236.129349,606.433655 229.302643,606.348267
|
||||
C223.933762,606.281006 221.690506,604.126282 221.684235,598.741943
|
||||
C221.635788,557.109314 221.635330,515.476685 221.680252,473.844055
|
||||
C221.686081,468.440796 223.561905,466.677368 228.907120,466.593689
|
||||
C236.564194,466.473724 244.222260,466.393372 251.880188,466.357727
|
||||
C258.492737,466.326935 260.264771,468.059448 260.335876,474.516052
|
||||
C260.372528,477.846313 260.339813,481.177277 260.338562,485.006592
|
||||
z"/>
|
||||
<path fill="#000000" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M220.625824,420.911560
|
||||
C231.295700,405.947174 251.704590,405.947052 260.492920,420.572662
|
||||
C265.935791,429.630768 264.519440,441.970642 257.211517,449.161285
|
||||
C249.742859,456.510193 237.582672,457.861633 228.613968,452.339539
|
||||
C219.633881,446.810394 215.476181,435.510529 218.742569,425.389709
|
||||
C219.199707,423.973267 219.878220,422.628296 220.625824,420.911560
|
||||
z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<svg baseProfile="full" height="1024" version="1.1" width="1024" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><path d="M 60,466 L 58,468 L 58,471 L 59,472 L 59,474 L 60,475 L 60,476 L 61,477 L 61,478 L 62,479 L 62,481 L 63,482 L 63,483 L 64,484 L 64,486 L 65,487 L 65,488 L 66,489 L 66,491 L 67,492 L 67,493 L 68,494 L 68,496 L 69,497 L 69,498 L 70,499 L 70,501 L 71,502 L 71,503 L 72,504 L 72,506 L 73,507 L 73,508 L 74,509 L 74,511 L 75,512 L 75,513 L 76,514 L 76,516 L 77,517 L 77,518 L 78,519 L 78,521 L 79,522 L 79,523 L 80,524 L 80,525 L 81,526 L 81,528 L 82,529 L 82,530 L 83,531 L 83,533 L 84,534 L 84,535 L 85,536 L 85,538 L 86,539 L 86,540 L 87,541 L 87,543 L 88,544 L 88,545 L 89,546 L 89,548 L 90,549 L 90,550 L 91,551 L 91,553 L 92,554 L 92,555 L 93,556 L 93,558 L 94,559 L 94,560 L 95,561 L 95,563 L 96,564 L 96,565 L 97,566 L 97,568 L 98,569 L 98,570 L 99,571 L 99,572 L 100,573 L 100,575 L 101,576 L 101,577 L 102,578 L 102,580 L 103,581 L 103,582 L 104,583 L 104,585 L 105,586 L 105,587 L 106,588 L 106,590 L 107,591 L 107,592 L 108,593 L 108,595 L 109,596 L 109,597 L 110,598 L 110,600 L 113,603 L 114,603 L 115,604 L 148,604 L 149,603 L 150,603 L 153,600 L 153,599 L 154,598 L 154,596 L 155,595 L 155,594 L 156,593 L 156,591 L 157,590 L 157,589 L 158,588 L 158,586 L 159,585 L 159,584 L 160,583 L 160,581 L 161,580 L 161,579 L 162,578 L 162,576 L 163,575 L 163,574 L 164,573 L 164,571 L 165,570 L 165,569 L 166,568 L 166,566 L 167,565 L 167,564 L 168,563 L 168,562 L 169,561 L 169,559 L 170,558 L 170,557 L 171,556 L 171,554 L 172,553 L 172,552 L 173,551 L 173,549 L 174,548 L 174,547 L 175,546 L 175,544 L 176,543 L 176,542 L 177,541 L 177,540 L 178,539 L 178,537 L 179,536 L 179,535 L 180,534 L 180,532 L 181,531 L 181,530 L 182,529 L 182,527 L 183,526 L 183,525 L 184,524 L 184,522 L 185,521 L 185,520 L 186,519 L 186,518 L 187,517 L 187,515 L 188,514 L 188,513 L 189,512 L 189,510 L 190,509 L 190,508 L 191,507 L 191,505 L 192,504 L 192,503 L 193,502 L 193,500 L 194,499 L 194,498 L 195,497 L 195,495 L 196,494 L 196,493 L 197,492 L 197,490 L 198,489 L 198,488 L 199,487 L 199,486 L 200,485 L 200,483 L 201,482 L 201,481 L 202,480 L 202,478 L 203,477 L 203,476 L 204,475 L 204,473 L 205,472 L 205,471 L 206,470 L 206,469 L 205,468 L 205,467 L 204,466 L 168,466 L 164,470 L 164,471 L 163,472 L 163,474 L 162,475 L 162,477 L 161,478 L 161,480 L 160,481 L 160,483 L 159,484 L 159,486 L 158,487 L 158,488 L 157,489 L 157,491 L 156,492 L 156,494 L 155,495 L 155,497 L 154,498 L 154,500 L 153,501 L 153,503 L 152,504 L 152,506 L 151,507 L 151,509 L 150,510 L 150,512 L 149,513 L 149,514 L 148,515 L 148,517 L 147,518 L 147,520 L 146,521 L 146,523 L 145,524 L 145,526 L 144,527 L 144,529 L 143,530 L 143,532 L 142,533 L 142,535 L 141,536 L 141,538 L 140,539 L 140,540 L 139,541 L 139,543 L 138,544 L 138,546 L 137,547 L 137,549 L 136,550 L 136,552 L 135,553 L 135,555 L 134,556 L 134,558 L 133,559 L 133,561 L 132,562 L 130,560 L 130,558 L 129,557 L 129,555 L 128,554 L 128,552 L 127,551 L 127,549 L 126,548 L 126,546 L 125,545 L 125,543 L 124,542 L 124,539 L 123,538 L 123,537 L 122,536 L 122,533 L 121,532 L 121,530 L 120,529 L 120,527 L 119,526 L 119,524 L 118,523 L 118,521 L 117,520 L 117,518 L 116,517 L 116,515 L 115,514 L 115,512 L 114,511 L 114,509 L 113,508 L 113,506 L 112,505 L 112,503 L 111,502 L 111,500 L 110,499 L 110,497 L 109,496 L 109,494 L 108,493 L 108,490 L 107,489 L 107,487 L 106,486 L 106,484 L 105,483 L 105,481 L 104,480 L 104,478 L 103,477 L 103,475 L 102,474 L 102,472 L 101,471 L 101,470 L 97,466 Z" fill="black" /><path d="M 253,465 L 252,466 L 223,466 L 222,467 L 222,468 L 221,469 L 221,600 L 222,601 L 222,602 L 224,604 L 254,604 L 255,603 L 256,603 L 257,602 L 257,601 L 258,600 L 258,469 L 255,466 L 254,466 Z" fill="black" /><path d="M 652,463 L 651,464 L 649,464 L 648,465 L 646,465 L 645,466 L 644,466 L 643,467 L 642,467 L 641,468 L 640,468 L 638,470 L 637,470 L 633,474 L 632,474 L 630,476 L 630,477 L 629,478 L 628,478 L 628,479 L 627,480 L 626,479 L 626,470 L 625,469 L 625,468 L 623,466 L 591,466 L 590,467 L 590,468 L 589,469 L 589,601 L 592,604 L 623,604 L 626,601 L 626,520 L 627,519 L 627,517 L 628,516 L 628,515 L 629,514 L 629,512 L 631,510 L 631,509 L 634,506 L 634,505 L 637,502 L 638,502 L 640,500 L 641,500 L 642,499 L 643,499 L 644,498 L 645,498 L 646,497 L 650,497 L 651,496 L 658,496 L 659,497 L 662,497 L 663,498 L 665,498 L 666,499 L 667,499 L 669,501 L 670,501 L 674,505 L 674,506 L 676,508 L 676,509 L 677,510 L 677,511 L 678,512 L 678,514 L 679,515 L 679,518 L 680,519 L 680,601 L 683,604 L 714,604 L 718,600 L 718,511 L 717,510 L 717,504 L 716,503 L 716,500 L 715,499 L 715,497 L 714,496 L 714,494 L 713,493 L 713,492 L 712,491 L 712,490 L 711,489 L 711,488 L 710,487 L 710,486 L 708,484 L 708,483 L 705,480 L 705,479 L 702,476 L 701,476 L 696,471 L 695,471 L 693,469 L 692,469 L 691,468 L 690,468 L 689,467 L 687,467 L 686,466 L 685,Line truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<svg baseProfile="tiny" height="1024" version="1.2" width="1024" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><text fill="black" font-family="Poppins" font-size="100px" x="10" y="130">vind</text><path d="M 60,466 L 58,468 L 58,471 L 59,472 L 59,474 L 60,475 L 60,476 L 61,477 L 61,478 L 62,479 L 62,481 L 63,482 L 63,483 L 64,484 L 64,486 L 65,487 L 65,488 L 66,489 L 66,491 L 67,492 L 67,493 L 68,494 L 68,496 L 69,497 L 69,498 L 70,499 L 70,501 L 71,502 L 71,503 L 72,504 L 72,506 L 73,507 L 73,508 L 74,509 L 74,511 L 75,512 L 75,513 L 76,514 L 76,516 L 77,517 L 77,518 L 78,519 L 78,521 L 79,522 L 79,523 L 80,524 L 80,525 L 81,526 L 81,528 L 82,529 L 82,530 L 83,531 L 83,533 L 84,534 L 84,535 L 85,536 L 85,538 L 86,539 L 86,540 L 87,541 L 87,543 L 88,544 L 88,545 L 89,546 L 89,548 L 90,549 L 90,550 L 91,551 L 91,553 L 92,554 L 92,555 L 93,556 L 93,558 L 94,559 L 94,560 L 95,561 L 95,563 L 96,564 L 96,565 L 97,566 L 97,568 L 98,569 L 98,570 L 99,571 L 99,572 L 100,573 L 100,575 L 101,576 L 101,577 L 102,578 L 102,580 L 103,581 L 103,582 L 104,583 L 104,585 L 105,586 L 105,587 L 106,588 L 106,590 L 107,591 L 107,592 L 108,593 L 108,595 L 109,596 L 109,597 L 110,598 L 110,600 L 113,603 L 114,603 L 115,604 L 148,604 L 149,603 L 150,603 L 153,600 L 153,599 L 154,598 L 154,596 L 155,595 L 155,594 L 156,593 L 156,591 L 157,590 L 157,589 L 158,588 L 158,586 L 159,585 L 159,584 L 160,583 L 160,581 L 161,580 L 161,579 L 162,578 L 162,576 L 163,575 L 163,574 L 164,573 L 164,571 L 165,570 L 165,569 L 166,568 L 166,566 L 167,565 L 167,564 L 168,563 L 168,562 L 169,561 L 169,559 L 170,558 L 170,557 L 171,556 L 171,554 L 172,553 L 172,552 L 173,551 L 173,549 L 174,548 L 174,547 L 175,546 L 175,544 L 176,543 L 176,542 L 177,541 L 177,540 L 178,539 L 178,537 L 179,536 L 179,535 L 180,534 L 180,532 L 181,531 L 181,530 L 182,529 L 182,527 L 183,526 L 183,525 L 184,524 L 184,522 L 185,521 L 185,520 L 186,519 L 186,518 L 187,517 L 187,515 L 188,514 L 188,513 L 189,512 L 189,510 L 190,509 L 190,508 L 191,507 L 191,505 L 192,504 L 192,503 L 193,502 L 193,500 L 194,499 L 194,498 L 195,497 L 195,495 L 196,494 L 196,493 L 197,492 L 197,490 L 198,489 L 198,488 L 199,487 L 199,486 L 200,485 L 200,483 L 201,482 L 201,481 L 202,480 L 202,478 L 203,477 L 203,476 L 204,475 L 204,473 L 205,472 L 205,471 L 206,470 L 206,469 L 205,468 L 205,467 L 204,466 L 168,466 L 164,470 L 164,471 L 163,472 L 163,474 L 162,475 L 162,477 L 161,478 L 161,480 L 160,481 L 160,483 L 159,484 L 159,486 L 158,487 L 158,488 L 157,489 L 157,491 L 156,492 L 156,494 L 155,495 L 155,497 L 154,498 L 154,500 L 153,501 L 153,503 L 152,504 L 152,506 L 151,507 L 151,509 L 150,510 L 150,512 L 149,513 L 149,514 L 148,515 L 148,517 L 147,518 L 147,520 L 146,521 L 146,523 L 145,524 L 145,526 L 144,527 L 144,529 L 143,530 L 143,532 L 142,533 L 142,535 L 141,536 L 141,538 L 140,539 L 140,540 L 139,541 L 139,543 L 138,544 L 138,546 L 137,547 L 137,549 L 136,550 L 136,552 L 135,553 L 135,555 L 134,556 L 134,558 L 133,559 L 133,561 L 132,562 L 130,560 L 130,558 L 129,557 L 129,555 L 128,554 L 128,552 L 127,551 L 127,549 L 126,548 L 126,546 L 125,545 L 125,543 L 124,542 L 124,539 L 123,538 L 123,537 L 122,536 L 122,533 L 121,532 L 121,530 L 120,529 L 120,527 L 119,526 L 119,524 L 118,523 L 118,521 L 117,520 L 117,518 L 116,517 L 116,515 L 115,514 L 115,512 L 114,511 L 114,509 L 113,508 L 113,506 L 112,505 L 112,503 L 111,502 L 111,500 L 110,499 L 110,497 L 109,496 L 109,494 L 108,493 L 108,490 L 107,489 L 107,487 L 106,486 L 106,484 L 105,483 L 105,481 L 104,480 L 104,478 L 103,477 L 103,475 L 102,474 L 102,472 L 101,471 L 101,470 L 97,466 Z" fill="black" /><path d="M 253,465 L 252,466 L 223,466 L 222,467 L 222,468 L 221,469 L 221,600 L 222,601 L 222,602 L 224,604 L 254,604 L 255,603 L 256,603 L 257,602 L 257,601 L 258,600 L 258,469 L 255,466 L 254,466 Z" fill="black" /><path d="M 652,463 L 651,464 L 649,464 L 648,465 L 646,465 L 645,466 L 644,466 L 643,467 L 642,467 L 641,468 L 640,468 L 638,470 L 637,470 L 633,474 L 632,474 L 630,476 L 630,477 L 629,478 L 628,478 L 628,479 L 627,480 L 626,479 L 626,470 L 625,469 L 625,468 L 623,466 L 591,466 L 590,467 L 590,468 L 589,469 L 589,601 L 592,604 L 623,604 L 626,601 L 626,520 L 627,519 L 627,517 L 628,516 L 628,515 L 629,514 L 629,512 L 631,510 L 631,509 L 634,506 L 634,505 L 637,502 L 638,502 L 640,500 L 641,500 L 642,499 L 643,499 L 644,498 L 645,498 L 646,497 L 650,497 L 651,496 L 658,496 L 659,497 L 662,497 L 663,498 L 665,498 L 666,499 L 667,499 L 669,501 L 670,501 L 674,505 L 674,506 L 676,508 L 676,509 L 677,510 L 677,511 L 678,512 L 678,514 L 679,515 L 679,518 L 680,519 L 680,601 L 683,604 L 714,604 L 718,600 L 718,511 L 717,510 L 717,504 L 716,503 L 716,500 L 715,499 L 715,497 L 714,496 L 714,494 L 713,493 L 713,492 L 712,491 L 712,490 L 711,489 L 711,488 L 710,487 L 710,486 L 708,484 L 708,483 L 705,480 L 705,479 L 702,476 L 701,476 L 696,471 LLine truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
||||
@@ -0,0 +1,762 @@
|
||||
(function (window) {
|
||||
// The leaflet-gesture-handling UMD build exposes its class as
|
||||
// window.leafletGestureHandling.GestureHandling instead of registering
|
||||
// itself on L (unlike the other Leaflet plugins loaded on this page), so
|
||||
// it has to be wired in manually before any L.map() call.
|
||||
if (window.L && window.leafletGestureHandling && !window.L.Map.prototype.options.gestureHandling) {
|
||||
window.L.Map.addInitHook('addHandler', 'gestureHandling', window.leafletGestureHandling.GestureHandling);
|
||||
}
|
||||
|
||||
// Köln — last-resort fallback if a caller doesn't pass its own center;
|
||||
// both controllers (Home/Map) explicitly pass the same point themselves,
|
||||
// so this only matters if that ever gets forgotten.
|
||||
var DEFAULT_CENTER = [50.9375, 6.9603];
|
||||
|
||||
// Leaflet.awesome-markers only ships a fixed palette of named colors (no
|
||||
// arbitrary hex/hsl) — curated to the ones that read well against the
|
||||
// grayscale-filtered basemap. Approximate hex values below are only used
|
||||
// for the legend swatches, so they match what's actually drawn on pins.
|
||||
var AWESOME_COLORS = ['red', 'darkred', 'orange', 'green', 'darkgreen', 'blue', 'darkblue', 'lightblue', 'purple', 'darkpurple', 'cadetblue', 'beige'];
|
||||
var AWESOME_COLOR_HEX = {
|
||||
red: '#d63e2a', darkred: '#a23336', orange: '#f69730', green: '#72b026',
|
||||
darkgreen: '#728224', blue: '#38aadd', darkblue: '#0067a3', lightblue: '#8adaff',
|
||||
purple: '#d252b9', darkpurple: '#5b396b', cadetblue: '#436978', beige: '#e0a458'
|
||||
};
|
||||
|
||||
// Real ODH category names are German tourism-taxonomy strings (80+ of
|
||||
// them) — this maps keywords found in a name to a Font Awesome 4 glyph,
|
||||
// first match wins, falling back to a generic calendar icon.
|
||||
var CATEGORY_ICON_RULES = [
|
||||
{ icon: 'tree', words: ['weihnacht'] },
|
||||
{ icon: 'music', words: ['musik', 'konzert', 'jazz', 'rock', 'pop', 'blues', 'soul', 'reggae', 'techno', 'house', 'elektro', 'punk', 'metal', 'folk', 'country', 'swing', 'gospel', 'klassisch', 'oper', 'musical', 'schlager', 'sakralmusik', 'volksmusik', 'dancehall', 'r\'n\'b'] },
|
||||
{ icon: 'comments', words: ['theater', 'bühne', 'schauspiel', 'kabarett', 'ballett', 'figurentheater'] },
|
||||
{ icon: 'film', words: ['film'] },
|
||||
{ icon: 'shopping-cart', words: ['markt', 'flohmarkt', 'messe'] },
|
||||
{ icon: 'flag', words: ['festival', 'fest', 'kirmes', 'jahrmarkt', 'stadtfest'] },
|
||||
{ icon: 'binoculars', words: ['führung', 'besichtigung', 'ausflug', 'exkursion'] },
|
||||
{ icon: 'bicycle', words: ['radtour', 'rad'] },
|
||||
{ icon: 'trophy', words: ['sport', 'turnier', 'wettbewerb', 'motorsport'] },
|
||||
{ icon: 'map-signs', words: ['wanderung'] },
|
||||
{ icon: 'cutlery', words: ['gastronomie', 'genuss', 'gourmet'] },
|
||||
{ icon: 'child', words: ['kinder', 'familie'] },
|
||||
{ icon: 'heartbeat', words: ['gesundheit', 'wellness'] },
|
||||
{ icon: 'university', words: ['kirche', 'sakral'] },
|
||||
{ icon: 'book', words: ['vortrag', 'lesung', 'kurs', 'seminar', 'workshop', 'hobby'] },
|
||||
{ icon: 'users', words: ['kongress', 'geselligkeit', 'spiele', 'treffen'] },
|
||||
{ icon: 'glass', words: ['party', 'nightlife'] },
|
||||
{ icon: 'picture-o', words: ['ausstellung'] }
|
||||
];
|
||||
var DEFAULT_ICON = 'calendar';
|
||||
|
||||
function hashHue(str) {
|
||||
var hash = 0;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
hash = (hash * 31 + str.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
function categoryColor(name) {
|
||||
if (!name) return 'hsl(220, 10%, 55%)';
|
||||
return 'hsl(' + (hashHue(name) % 360) + ', 62%, 42%)';
|
||||
}
|
||||
|
||||
function categoryMarkerColorName(name) {
|
||||
if (!name) return 'cadetblue';
|
||||
return AWESOME_COLORS[hashHue(name) % AWESOME_COLORS.length];
|
||||
}
|
||||
|
||||
function categoryMarkerHex(name) {
|
||||
return AWESOME_COLOR_HEX[categoryMarkerColorName(name)] || '#436978';
|
||||
}
|
||||
|
||||
function categoryIconName(name) {
|
||||
var lower = (name || '').toLowerCase();
|
||||
for (var i = 0; i < CATEGORY_ICON_RULES.length; i++) {
|
||||
var rule = CATEGORY_ICON_RULES[i];
|
||||
for (var j = 0; j < rule.words.length; j++) {
|
||||
if (lower.indexOf(rule.words[j]) !== -1) return rule.icon;
|
||||
}
|
||||
}
|
||||
return DEFAULT_ICON;
|
||||
}
|
||||
|
||||
function ensurePulseStyle() {
|
||||
if (document.getElementById('vindn-pulse-style')) return;
|
||||
var styleTag = document.createElement('style');
|
||||
styleTag.id = 'vindn-pulse-style';
|
||||
styleTag.textContent = '@keyframes vindnPulse{0%{transform:scale(.6);opacity:.45}100%{transform:scale(2.4);opacity:0}}';
|
||||
document.head.appendChild(styleTag);
|
||||
}
|
||||
|
||||
// Plain pulsing dot — used only for the "you are here" user-location
|
||||
// marker, kept visually distinct from the category pins below.
|
||||
function dotIcon(color, big) {
|
||||
var size = big ? 18 : 14;
|
||||
var ring = big
|
||||
? '<span style="position:absolute;inset:-8px;border-radius:50%;background:' + color + ';opacity:.25;animation:vindnPulse 2.2s ease-out infinite;"></span>'
|
||||
: '';
|
||||
return L.divIcon({
|
||||
className: 'pin-icon',
|
||||
html: '<span style="position:relative;display:block;width:' + size + 'px;height:' + size + 'px;">' + ring +
|
||||
'<span style="position:absolute;inset:0;border-radius:50%;background:' + color + ';border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.35);"></span></span>',
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [size / 2, size / 2]
|
||||
});
|
||||
}
|
||||
|
||||
// Category pin — Font Awesome glyph in a colored teardrop marker, falling
|
||||
// back to a plain dot if the awesome-markers plugin didn't load.
|
||||
function categoryIcon(name) {
|
||||
if (window.L.AwesomeMarkers) {
|
||||
return L.AwesomeMarkers.icon({
|
||||
icon: categoryIconName(name),
|
||||
prefix: 'fa',
|
||||
markerColor: categoryMarkerColorName(name)
|
||||
});
|
||||
}
|
||||
return dotIcon(categoryColor(name), false);
|
||||
}
|
||||
|
||||
// OpenFreeMap's "liberty" style labels everything with
|
||||
// coalesce(name_en, name) — English first — which shows "Cologne"
|
||||
// instead of "Köln" even though the whole map (and every event on it)
|
||||
// is German. Rewrites that one recurring sub-expression, wherever it
|
||||
// appears, to prefer name_de then the OSM-local `name` (which for
|
||||
// German places already *is* the German name) before falling back to
|
||||
// English. Layers that don't contain it (road-shield ref numbers, etc.)
|
||||
// are left alone.
|
||||
function preferGermanLabels(maplibreMap) {
|
||||
var EN_FIRST = '["coalesce",["get","name_en"],["get","name"]]';
|
||||
var DE_FIRST = '["coalesce",["get","name_de"],["get","name"],["get","name_en"]]';
|
||||
|
||||
var style = maplibreMap.getStyle();
|
||||
if (!style || !style.layers) return;
|
||||
|
||||
style.layers.forEach(function (layer) {
|
||||
var textField = layer.layout && layer.layout['text-field'];
|
||||
if (!textField) return;
|
||||
|
||||
var serialized = JSON.stringify(textField);
|
||||
if (serialized.indexOf(EN_FIRST) === -1) return;
|
||||
|
||||
maplibreMap.setLayoutProperty(layer.id, 'text-field', JSON.parse(serialized.split(EN_FIRST).join(DE_FIRST)));
|
||||
});
|
||||
}
|
||||
|
||||
function readEmbeddedJson(elementId) {
|
||||
var el = document.getElementById(elementId);
|
||||
if (!el) return [];
|
||||
try {
|
||||
return JSON.parse(el.textContent) || [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function toRad(d) { return d * Math.PI / 180; }
|
||||
|
||||
function distanceKm(lat1, lon1, lat2, lon2) {
|
||||
var R = 6371;
|
||||
var dLat = toRad(lat2 - lat1);
|
||||
var dLon = toRad(lon2 - lon1);
|
||||
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
|
||||
// Returns the `count` events closest to `center`, each tagged with `_dist` (km).
|
||||
function nearest(center, events, count) {
|
||||
return events
|
||||
.map(function (ev) {
|
||||
ev._dist = distanceKm(center[0], center[1], ev.lat, ev.lon);
|
||||
return ev;
|
||||
})
|
||||
.sort(function (a, b) { return a._dist - b._dist; })
|
||||
.slice(0, count);
|
||||
}
|
||||
|
||||
// Builds an (unattached) marker for an event — caller adds it to either
|
||||
// the map directly or a MarkerClusterGroup.
|
||||
function buildMarker(ev, onMarkerClick) {
|
||||
var hex = categoryMarkerHex(ev.category);
|
||||
var marker = L.marker([ev.lat, ev.lon], { icon: categoryIcon(ev.category) });
|
||||
var popupHtml = '<div class="pin-pop">' +
|
||||
(ev.category ? '<span class="cat" style="color:' + hex + ';">' + ev.category + '</span>' : '') +
|
||||
'<h4>' + ev.title + '</h4>' +
|
||||
(ev.venue ? '<p>' + ev.venue + '</p>' : '') +
|
||||
(ev.url ? '<a href="' + ev.url + '">View event →</a>' : '') +
|
||||
'</div>';
|
||||
marker.bindPopup(popupHtml);
|
||||
if (onMarkerClick) marker.on('click', function () { onMarkerClick(ev, marker); });
|
||||
return marker;
|
||||
}
|
||||
|
||||
function fitToMarkers(map, markers) {
|
||||
var list = Object.keys(markers).map(function (id) { return markers[id]; });
|
||||
if (!list.length) return;
|
||||
if (list.length === 1) { map.setView(list[0].getLatLng(), 13); return; }
|
||||
map.fitBounds(L.featureGroup(list).getBounds(), { padding: [40, 40], maxZoom: 14, animate: false });
|
||||
}
|
||||
|
||||
// Renders a Leaflet map of category pins (one per event) into
|
||||
// containerId, clustered via Leaflet.markercluster when it's loaded.
|
||||
// events: [{id, title, venue, category, lat, lon, url}]
|
||||
function renderPinMap(containerId, events, opts) {
|
||||
opts = opts || {};
|
||||
var el = document.getElementById(containerId);
|
||||
if (!el || !window.L) return null;
|
||||
ensurePulseStyle();
|
||||
|
||||
var center = opts.center || (events.length ? [events[0].lat, events[0].lon] : DEFAULT_CENTER);
|
||||
var map = L.map(containerId, {
|
||||
zoomControl: !!opts.zoomControl,
|
||||
attributionControl: !!opts.attributionControl,
|
||||
scrollWheelZoom: !!opts.scrollWheelZoom,
|
||||
// A raster L.tileLayer used to supply maxZoom implicitly; the
|
||||
// MapLibre GL bridge doesn't. minZoom/maxBounds are maplibre-gl-
|
||||
// leaflet's own documented recommendation, to avoid Leaflet/MapLibre
|
||||
// disagreeing about zoom 0 and latitudes MapLibre doesn't support.
|
||||
minZoom: 1,
|
||||
maxZoom: 19,
|
||||
maxBounds: [[-90, -Infinity], [90, Infinity]],
|
||||
maxBoundsViscosity: 1,
|
||||
// Requires two-finger drag / Ctrl+scroll to pan/zoom, with an
|
||||
// on-map hint, so an embedded map (home hero, mid-page /map) doesn't
|
||||
// trap page scroll/pinch-zoom on touch devices.
|
||||
gestureHandling: opts.gestureHandling !== false
|
||||
}).setView(center, opts.zoom || 13);
|
||||
|
||||
if (L.maplibreGL) {
|
||||
// OpenFreeMap serves vector tiles only, rendered by MapLibre GL and
|
||||
// bridged into this Leaflet map as an ordinary layer — everything
|
||||
// else (markers, clustering, fullscreen) is untouched by this.
|
||||
var glLayer = L.maplibreGL({
|
||||
style: 'https://tiles.openfreemap.org/styles/liberty',
|
||||
attribution: '© <a href="https://openfreemap.org" target="_blank">OpenFreeMap</a> <a href="https://www.openmaptiles.org/" target="_blank">© OpenMapTiles</a> Data from <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
var glMap = glLayer.getMaplibreMap && glLayer.getMaplibreMap();
|
||||
if (glMap) {
|
||||
// 'load' waits for full tile loading and can take a while;
|
||||
// 'styledata' fires as soon as the style/layers themselves are
|
||||
// parsed, which is all setLayoutProperty needs.
|
||||
glMap.once('styledata', function () { preferGermanLabels(glMap); });
|
||||
}
|
||||
} else {
|
||||
// Fallback if the MapLibre bridge failed to load for some reason.
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 18,
|
||||
attribution: opts.attributionControl ? '© OpenStreetMap contributors' : ''
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
if (opts.fullscreenControl !== false && L.control && L.control.fullscreen) {
|
||||
// Fullscreen the whole .stage (search bar, locate button, panel), not
|
||||
// just the bare map div — those overlay controls are DOM siblings of
|
||||
// #mapCanvas, not descendants, so they'd vanish in fullscreen otherwise.
|
||||
L.control.fullscreen({
|
||||
title: 'View fullscreen',
|
||||
titleCancel: 'Exit fullscreen',
|
||||
fullscreenElement: el.closest('.stage') || el
|
||||
}).addTo(map);
|
||||
|
||||
// Fullscreen means the map owns the whole viewport, so the two-
|
||||
// finger/Ctrl+scroll gating gesture handling exists for (avoiding
|
||||
// trapped page scroll) no longer applies — switch back to normal
|
||||
// single-finger/scroll interaction while fullscreen is active.
|
||||
if (map.gestureHandling) {
|
||||
map.on('enterFullscreen', function () { map.gestureHandling.disable(); });
|
||||
map.on('exitFullscreen', function () { map.gestureHandling.enable(); });
|
||||
}
|
||||
}
|
||||
|
||||
var clusterGroup = (opts.cluster !== false && L.markerClusterGroup)
|
||||
// animate:false — a fitBounds() called right after the initial
|
||||
// addLayer() (see fitToMarkers) otherwise races the cluster expand/
|
||||
// collapse animation and leaves stale icon elements in the DOM.
|
||||
? L.markerClusterGroup({ maxClusterRadius: 50, spiderfyOnMaxZoom: true, showCoverageOnHover: false, animate: false })
|
||||
: null;
|
||||
if (clusterGroup) map.addLayer(clusterGroup);
|
||||
|
||||
var markers = {};
|
||||
events.forEach(function (ev) {
|
||||
var marker = buildMarker(ev, opts.onMarkerClick);
|
||||
markers[ev.id] = marker;
|
||||
if (clusterGroup) clusterGroup.addLayer(marker);
|
||||
else marker.addTo(map);
|
||||
});
|
||||
|
||||
return { map: map, markers: markers, clusterGroup: clusterGroup };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires up the full interactive map "stage" used on both the home page
|
||||
* and /map: pin map, category chips (#catChips), legend (#legend),
|
||||
* nearby-events panel (#panelList/#resultCount), and a "use my location"
|
||||
* button (#locateBtn). All element ids are optional — missing
|
||||
* ones are just skipped, so the same call works whether or not a given
|
||||
* piece of UI is present on the page.
|
||||
*
|
||||
* opts.limit turns on the "light" mode used by the home page: instead of
|
||||
* plotting every event, it shows only the `limit` nearest to a default
|
||||
* center (opts.defaultCenter) until the user shares their real location,
|
||||
* at which point it re-picks the `limit` nearest to *that* from the full
|
||||
* event list and refits the map to them.
|
||||
*/
|
||||
function initMapStage(opts) {
|
||||
opts = opts || {};
|
||||
var allEvents = opts.events || [];
|
||||
var limit = opts.limit || null;
|
||||
var defaultCenter = opts.defaultCenter || DEFAULT_CENTER;
|
||||
var containerId = opts.containerId || 'mapCanvas';
|
||||
var state = { cat: 'all', userLoc: null };
|
||||
|
||||
var shown = limit ? nearest(defaultCenter, allEvents.slice(), limit) : allEvents;
|
||||
|
||||
var result = renderPinMap(containerId, shown, {
|
||||
zoomControl: true,
|
||||
attributionControl: true,
|
||||
zoom: opts.zoom || (limit ? 11 : (allEvents.length ? 9 : 7)),
|
||||
center: limit ? defaultCenter : undefined,
|
||||
onMarkerClick: function (ev) { highlightRow(ev.id); }
|
||||
});
|
||||
var map = result ? result.map : null;
|
||||
var markers = result ? result.markers : {};
|
||||
var clusterGroup = result ? result.clusterGroup : null;
|
||||
var userMarker = null;
|
||||
|
||||
if (limit && map) fitToMarkers(map, markers);
|
||||
|
||||
var legendEl = document.getElementById('legend');
|
||||
if (legendEl) {
|
||||
var seenCats = [];
|
||||
allEvents.forEach(function (ev) {
|
||||
if (ev.category && seenCats.indexOf(ev.category) === -1) seenCats.push(ev.category);
|
||||
});
|
||||
legendEl.innerHTML = seenCats.slice(0, 8).map(function (cat) {
|
||||
return '<span><span class="dot" style="background:' + categoryMarkerHex(cat) + ';"></span>' + cat + '</span>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function addMarker(marker) {
|
||||
if (clusterGroup) clusterGroup.addLayer(marker);
|
||||
else if (map) marker.addTo(map);
|
||||
}
|
||||
|
||||
function removeMarker(marker) {
|
||||
if (clusterGroup) clusterGroup.removeLayer(marker);
|
||||
else if (map) map.removeLayer(marker);
|
||||
}
|
||||
|
||||
function rebuildMarkers(list) {
|
||||
Object.keys(markers).forEach(function (id) { removeMarker(markers[id]); });
|
||||
markers = {};
|
||||
list.forEach(function (ev) {
|
||||
var marker = buildMarker(ev, function (clicked) { highlightRow(clicked.id); });
|
||||
markers[ev.id] = marker;
|
||||
addMarker(marker);
|
||||
});
|
||||
shown = list;
|
||||
}
|
||||
|
||||
function highlightRow(id) {
|
||||
document.querySelectorAll('.map-row').forEach(function (r) { r.classList.remove('is-highlighted'); });
|
||||
var row = document.querySelector('.map-row[data-id="' + id + '"]');
|
||||
if (row) { row.classList.add('is-highlighted'); row.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }
|
||||
}
|
||||
|
||||
function currentList() {
|
||||
if (limit) {
|
||||
var list = shown.slice();
|
||||
if (state.userLoc) {
|
||||
list = list.map(function (ev) { ev._dist = distanceKm(state.userLoc[0], state.userLoc[1], ev.lat, ev.lon); return ev; })
|
||||
.sort(function (a, b) { return a._dist - b._dist; });
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
var visible = allEvents.filter(function (ev) { return state.cat === 'all' || ev.category === state.cat; });
|
||||
if (state.userLoc) {
|
||||
visible = visible.map(function (ev) { ev._dist = distanceKm(state.userLoc[0], state.userLoc[1], ev.lat, ev.lon); return ev; })
|
||||
.sort(function (a, b) { return a._dist - b._dist; });
|
||||
}
|
||||
|
||||
Object.keys(markers).forEach(function (id) {
|
||||
var isVisible = visible.some(function (ev) { return String(ev.id) === String(id); });
|
||||
var m = markers[id];
|
||||
var hasLayer = clusterGroup ? clusterGroup.hasLayer(m) : (map && map.hasLayer(m));
|
||||
if (isVisible && !hasLayer) addMarker(m);
|
||||
if (!isVisible && hasLayer) removeMarker(m);
|
||||
});
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
var list = currentList();
|
||||
|
||||
var countEl = document.getElementById('resultCount');
|
||||
if (countEl) countEl.textContent = list.length + (list.length === 1 ? ' event' : ' events');
|
||||
|
||||
var panelEl = document.getElementById('panelList');
|
||||
if (!panelEl) return;
|
||||
|
||||
panelEl.innerHTML = list.map(function (ev) {
|
||||
var distHtml = '';
|
||||
if (ev._dist != null) {
|
||||
var distLabel = ev._dist < 1 ? Math.round(ev._dist * 1000) : ev._dist.toFixed(1);
|
||||
var distUnit = ev._dist < 1 ? 'm' : 'km';
|
||||
distHtml = '<div class="dist">' + distLabel + '<span class="u">' + distUnit + '</span></div>';
|
||||
}
|
||||
return (
|
||||
'<div class="map-row' + (ev._dist != null ? '' : ' no-dist') + '" data-id="' + ev.id + '" tabindex="0" role="button" aria-label="' + ev.title + '">' +
|
||||
distHtml +
|
||||
'<div><div class="name">' + ev.title + '</div><span class="when">' + (ev.venue || '') + '</span></div>' +
|
||||
'<div class="go">→</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}).join('');
|
||||
|
||||
panelEl.querySelectorAll('.map-row').forEach(function (row) {
|
||||
row.addEventListener('click', function () {
|
||||
var id = row.getAttribute('data-id');
|
||||
var ev = allEvents.filter(function (e) { return String(e.id) === String(id); })[0];
|
||||
if (!ev || !map) return;
|
||||
map.flyTo([ev.lat, ev.lon], 15);
|
||||
var marker = markers[id];
|
||||
if (marker) {
|
||||
if (clusterGroup && clusterGroup.zoomToShowLayer) {
|
||||
clusterGroup.zoomToShowLayer(marker, function () { marker.openPopup(); });
|
||||
} else {
|
||||
marker.openPopup();
|
||||
}
|
||||
}
|
||||
highlightRow(id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var chipsEl = document.getElementById('catChips');
|
||||
if (chipsEl) {
|
||||
chipsEl.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.chip-sm');
|
||||
if (!btn) return;
|
||||
chipsEl.querySelectorAll('.chip-sm').forEach(function (c) { c.classList.remove('is-active'); });
|
||||
btn.classList.add('is-active');
|
||||
state.cat = btn.getAttribute('data-cat');
|
||||
renderList();
|
||||
});
|
||||
}
|
||||
|
||||
function requestLocation() {
|
||||
var statusEl = document.getElementById('locateStatus');
|
||||
if (!navigator.geolocation) {
|
||||
if (statusEl) statusEl.textContent = 'Location not supported by your browser';
|
||||
return;
|
||||
}
|
||||
if (statusEl) statusEl.textContent = 'Finding you…';
|
||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
||||
var lat = pos.coords.latitude;
|
||||
var lon = pos.coords.longitude;
|
||||
state.userLoc = [lat, lon];
|
||||
|
||||
if (limit) {
|
||||
rebuildMarkers(nearest([lat, lon], allEvents.slice(), limit));
|
||||
if (statusEl) statusEl.textContent = 'Showing the ' + limit + ' nearest events';
|
||||
} else if (statusEl) {
|
||||
statusEl.textContent = 'Showing events near you';
|
||||
}
|
||||
|
||||
if (map) {
|
||||
if (userMarker) map.removeLayer(userMarker);
|
||||
userMarker = L.marker([lat, lon], { icon: dotIcon('#2440FF', true), zIndexOffset: 1000 }).addTo(map);
|
||||
if (limit) fitToMarkers(map, markers);
|
||||
else map.setView([lat, lon], 13);
|
||||
}
|
||||
|
||||
renderList();
|
||||
}, function () {
|
||||
if (statusEl) statusEl.textContent = "Couldn't get your location";
|
||||
}, { timeout: 8000 });
|
||||
}
|
||||
|
||||
var locateBtn = document.getElementById('locateBtn');
|
||||
if (locateBtn) locateBtn.addEventListener('click', requestLocation);
|
||||
|
||||
// If the user already granted location access (on this or an earlier
|
||||
// visit), center on it automatically — this only *reads* the current
|
||||
// permission state, it never triggers the browser's permission prompt
|
||||
// itself, so it's safe to check unconditionally on load.
|
||||
if (navigator.permissions && navigator.permissions.query) {
|
||||
navigator.permissions.query({ name: 'geolocation' }).then(function (status) {
|
||||
if (status.state === 'granted') requestLocation();
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
renderList();
|
||||
|
||||
return { map: map, markers: markers };
|
||||
}
|
||||
|
||||
/**
|
||||
* The /map page's full radius-based explorer — replaces a flat "first
|
||||
* N events" cap with "events within rangeKm of a source point", where
|
||||
* the source starts at the server-resolved center (Köln by default, or
|
||||
* ?lat/&lon) and can move via a map click/marker drag, a place search
|
||||
* (Nominatim), or "use my location". Assumes the full (non-compact)
|
||||
* _map_stage.html.twig markup — #catChips, #legend, #rangeSelect,
|
||||
* #locationSearchForm all present.
|
||||
*
|
||||
* opts.events is the full pool the server already limited to
|
||||
* opts.maxRangeKm around opts.center; moving the source further than
|
||||
* that away can't be served from the pool, so it triggers a fresh page
|
||||
* load centered on the new point instead of silently showing partial
|
||||
* results.
|
||||
*/
|
||||
function initRadiusMapStage(opts) {
|
||||
opts = opts || {};
|
||||
var allEvents = opts.events || [];
|
||||
var originalCenter = opts.center || DEFAULT_CENTER;
|
||||
var maxRangeKm = opts.maxRangeKm || 100;
|
||||
var containerId = opts.containerId || 'mapCanvas';
|
||||
|
||||
var state = { source: originalCenter.slice(), rangeKm: opts.rangeKm || 25, cat: 'all' };
|
||||
|
||||
var result = renderPinMap(containerId, [], {
|
||||
zoomControl: true,
|
||||
attributionControl: true,
|
||||
center: state.source,
|
||||
zoom: opts.zoom || 11,
|
||||
// This map already owns the whole viewport (no surrounding page to
|
||||
// protect from trapped scroll/pinch), so gesture handling would only
|
||||
// make one-finger dragging feel unresponsive for no benefit.
|
||||
gestureHandling: false,
|
||||
onMarkerClick: function (ev) { highlightRow(ev.id); }
|
||||
});
|
||||
var map = result ? result.map : null;
|
||||
var markers = result ? result.markers : {};
|
||||
var clusterGroup = result ? result.clusterGroup : null;
|
||||
|
||||
var sourceMarker = map
|
||||
? L.marker(state.source, { icon: dotIcon('#14140F', true), draggable: true, zIndexOffset: 1000 }).addTo(map)
|
||||
: null;
|
||||
|
||||
function addMarker(marker) {
|
||||
if (clusterGroup) clusterGroup.addLayer(marker);
|
||||
else if (map) marker.addTo(map);
|
||||
}
|
||||
|
||||
function removeMarker(marker) {
|
||||
if (clusterGroup) clusterGroup.removeLayer(marker);
|
||||
else if (map) map.removeLayer(marker);
|
||||
}
|
||||
|
||||
function rebuildMarkers(list) {
|
||||
Object.keys(markers).forEach(function (id) { removeMarker(markers[id]); });
|
||||
markers = {};
|
||||
list.forEach(function (ev) {
|
||||
var marker = buildMarker(ev, function (clicked) { highlightRow(clicked.id); });
|
||||
markers[ev.id] = marker;
|
||||
addMarker(marker);
|
||||
});
|
||||
}
|
||||
|
||||
function highlightRow(id) {
|
||||
document.querySelectorAll('.map-row').forEach(function (r) { r.classList.remove('is-highlighted'); });
|
||||
var row = document.querySelector('.map-row[data-id="' + id + '"]');
|
||||
if (row) { row.classList.add('is-highlighted'); row.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }
|
||||
}
|
||||
|
||||
function inRange() {
|
||||
return allEvents
|
||||
.filter(function (ev) { return state.cat === 'all' || ev.category === state.cat; })
|
||||
.map(function (ev) { ev._dist = distanceKm(state.source[0], state.source[1], ev.lat, ev.lon); return ev; })
|
||||
.filter(function (ev) { return ev._dist <= state.rangeKm; })
|
||||
.sort(function (a, b) { return a._dist - b._dist; });
|
||||
}
|
||||
|
||||
function updateUrl() {
|
||||
if (!window.history || !window.history.replaceState || !window.URL) return;
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set('lat', state.source[0].toFixed(5));
|
||||
url.searchParams.set('lon', state.source[1].toFixed(5));
|
||||
url.searchParams.set('range', state.rangeKm);
|
||||
window.history.replaceState(null, '', url.toString());
|
||||
}
|
||||
|
||||
function render() {
|
||||
var visible = inRange();
|
||||
rebuildMarkers(visible);
|
||||
|
||||
var legendEl = document.getElementById('legend');
|
||||
if (legendEl) {
|
||||
var seenCats = [];
|
||||
visible.forEach(function (ev) {
|
||||
if (ev.category && seenCats.indexOf(ev.category) === -1) seenCats.push(ev.category);
|
||||
});
|
||||
legendEl.innerHTML = seenCats.slice(0, 8).map(function (cat) {
|
||||
return '<span><span class="dot" style="background:' + categoryMarkerHex(cat) + ';"></span>' + cat + '</span>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
var statusEl = document.getElementById('locateStatus');
|
||||
if (statusEl) statusEl.textContent = 'Showing events within ' + state.rangeKm + ' km';
|
||||
|
||||
var countEl = document.getElementById('resultCount');
|
||||
if (countEl) countEl.textContent = visible.length + (visible.length === 1 ? ' event' : ' events');
|
||||
|
||||
var panelEl = document.getElementById('panelList');
|
||||
if (panelEl) {
|
||||
panelEl.innerHTML = visible.map(function (ev) {
|
||||
var distLabel = ev._dist < 1 ? Math.round(ev._dist * 1000) : ev._dist.toFixed(1);
|
||||
var distUnit = ev._dist < 1 ? 'm' : 'km';
|
||||
return (
|
||||
'<div class="map-row" data-id="' + ev.id + '" tabindex="0" role="button" aria-label="' + ev.title + '">' +
|
||||
'<div class="dist">' + distLabel + '<span class="u">' + distUnit + '</span></div>' +
|
||||
'<div><div class="name">' + ev.title + '</div><span class="when">' + (ev.venue || '') + '</span></div>' +
|
||||
'<div class="go">→</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}).join('');
|
||||
|
||||
panelEl.querySelectorAll('.map-row').forEach(function (row) {
|
||||
row.addEventListener('click', function () {
|
||||
var id = row.getAttribute('data-id');
|
||||
var ev = visible.filter(function (e) { return String(e.id) === String(id); })[0];
|
||||
if (!ev || !map) return;
|
||||
map.flyTo([ev.lat, ev.lon], 15);
|
||||
var marker = markers[id];
|
||||
if (marker) {
|
||||
if (clusterGroup && clusterGroup.zoomToShowLayer) {
|
||||
clusterGroup.zoomToShowLayer(marker, function () { marker.openPopup(); });
|
||||
} else {
|
||||
marker.openPopup();
|
||||
}
|
||||
}
|
||||
highlightRow(id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (map) fitToMarkers(map, markers);
|
||||
updateUrl();
|
||||
}
|
||||
|
||||
// Moving further than the pool's own coverage (maxRangeKm around the
|
||||
// server-resolved center) can't be answered from what's already
|
||||
// loaded — that combination might reach past maxRangeKm even if the
|
||||
// new point itself is inside it, so the check is deliberately
|
||||
// conservative: reload whenever full coverage isn't guaranteed,
|
||||
// rather than risk silently showing an incomplete result.
|
||||
function setSource(lat, lon) {
|
||||
var distFromOriginal = distanceKm(originalCenter[0], originalCenter[1], lat, lon);
|
||||
if (distFromOriginal + state.rangeKm > maxRangeKm) {
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set('lat', lat.toFixed(5));
|
||||
url.searchParams.set('lon', lon.toFixed(5));
|
||||
url.searchParams.set('range', state.rangeKm);
|
||||
window.location.href = url.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
state.source = [lat, lon];
|
||||
if (sourceMarker) sourceMarker.setLatLng(state.source);
|
||||
if (map) map.panTo(state.source);
|
||||
render();
|
||||
}
|
||||
|
||||
if (map) {
|
||||
map.on('click', function (e) { setSource(e.latlng.lat, e.latlng.lng); });
|
||||
}
|
||||
if (sourceMarker) {
|
||||
sourceMarker.on('dragend', function () {
|
||||
var ll = sourceMarker.getLatLng();
|
||||
setSource(ll.lat, ll.lng);
|
||||
});
|
||||
}
|
||||
|
||||
var rangeSelect = document.getElementById('rangeSelect');
|
||||
if (rangeSelect) {
|
||||
rangeSelect.addEventListener('change', function () {
|
||||
state.rangeKm = parseInt(rangeSelect.value, 10) || state.rangeKm;
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
var chipsEl = document.getElementById('catChips');
|
||||
if (chipsEl) {
|
||||
chipsEl.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.chip-sm');
|
||||
if (!btn) return;
|
||||
chipsEl.querySelectorAll('.chip-sm').forEach(function (c) { c.classList.remove('is-active'); });
|
||||
btn.classList.add('is-active');
|
||||
state.cat = btn.getAttribute('data-cat');
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
// Nominatim: explicit submit only, never as-you-type — required by
|
||||
// its usage policy (max ~1 req/s, no autocomplete-style querying).
|
||||
var locationForm = document.getElementById('locationSearchForm');
|
||||
if (locationForm) {
|
||||
locationForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var input = document.getElementById('locationSearch');
|
||||
var query = (input.value || '').trim();
|
||||
if (!query) return;
|
||||
var statusEl = document.getElementById('locateStatus');
|
||||
if (statusEl) statusEl.textContent = 'Looking up "' + query + '"…';
|
||||
fetch('https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' + encodeURIComponent(query))
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (results) {
|
||||
if (!results || !results.length) {
|
||||
if (statusEl) statusEl.textContent = 'Could not find "' + query + '"';
|
||||
return;
|
||||
}
|
||||
setSource(parseFloat(results[0].lat), parseFloat(results[0].lon));
|
||||
})
|
||||
.catch(function () {
|
||||
if (statusEl) statusEl.textContent = 'Location search failed';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function requestLocation() {
|
||||
var statusEl = document.getElementById('locateStatus');
|
||||
if (!navigator.geolocation) {
|
||||
if (statusEl) statusEl.textContent = 'Location not supported by your browser';
|
||||
return;
|
||||
}
|
||||
if (statusEl) statusEl.textContent = 'Finding you…';
|
||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
||||
setSource(pos.coords.latitude, pos.coords.longitude);
|
||||
}, function () {
|
||||
if (statusEl) statusEl.textContent = "Couldn't get your location";
|
||||
}, { timeout: 8000 });
|
||||
}
|
||||
|
||||
var locateBtn = document.getElementById('locateBtn');
|
||||
if (locateBtn) locateBtn.addEventListener('click', requestLocation);
|
||||
|
||||
if (navigator.permissions && navigator.permissions.query) {
|
||||
navigator.permissions.query({ name: 'geolocation' }).then(function (status) {
|
||||
if (status.state === 'granted') requestLocation();
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
return { map: map };
|
||||
}
|
||||
|
||||
window.Vindn = window.Vindn || {};
|
||||
window.Vindn.categoryColor = categoryColor;
|
||||
window.Vindn.categoryMarkerHex = categoryMarkerHex;
|
||||
window.Vindn.dotIcon = dotIcon;
|
||||
window.Vindn.distanceKm = distanceKm;
|
||||
window.Vindn.renderPinMap = renderPinMap;
|
||||
window.Vindn.readEmbeddedJson = readEmbeddedJson;
|
||||
window.Vindn.initMapStage = initMapStage;
|
||||
window.Vindn.initRadiusMapStage = initRadiusMapStage;
|
||||
})(window);
|
||||
@@ -0,0 +1,52 @@
|
||||
(function () {
|
||||
var burger = document.querySelector('.nav-burger');
|
||||
var menu = document.getElementById('mobileMenu');
|
||||
if (!burger || !menu) return;
|
||||
|
||||
function closeMenu() {
|
||||
menu.hidden = true;
|
||||
burger.classList.remove('is-open');
|
||||
burger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
menu.hidden = false;
|
||||
burger.classList.add('is-open');
|
||||
burger.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
|
||||
burger.addEventListener('click', function () {
|
||||
if (menu.hidden) {
|
||||
openMenu();
|
||||
} else {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
menu.addEventListener('click', function (e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!menu.hidden && !menu.contains(e.target) && !burger.contains(e.target)) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Desktop CSS hides .mobile-menu entirely, but if the menu was left open
|
||||
// on mobile and the viewport grows past the breakpoint (resize, rotate,
|
||||
// devtools), reset state so it doesn't reopen collapsed at mobile width.
|
||||
window.matchMedia('(min-width: 761px)').addEventListener('change', function (e) {
|
||||
if (e.matches) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,429 @@
|
||||
/* ============================================================
|
||||
VINDN — Events platform
|
||||
Design concept: the "board" — every event is treated like an
|
||||
entry on a departure board (date / time / place rendered in
|
||||
tabular mono type on a dark strip). It is the one motif that
|
||||
repeats across hero, listings, search results and detail page,
|
||||
tying "find an event" to the universal feeling of checking
|
||||
what's on, right now, near you.
|
||||
============================================================ */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
|
||||
|
||||
:root{
|
||||
/* ---- color ---- */
|
||||
--paper: #F7F7F4;
|
||||
--paper-dim: #EFEFE9;
|
||||
--ink: #14140F;
|
||||
--ink-soft: #3C3C34;
|
||||
--muted: #74746A;
|
||||
--line: #DEDED5;
|
||||
--line-strong: #C7C7BC;
|
||||
|
||||
--accent: #2440FF;
|
||||
--accent-deep: #1530CC;
|
||||
--accent-soft: #E7EAFF;
|
||||
|
||||
--warn: #C97A00;
|
||||
--warn-soft: #FFEFD2;
|
||||
|
||||
--board-bg: #14140F;
|
||||
--board-bg-2: #1D1D16;
|
||||
--board-text: #F4F3EC;
|
||||
--board-muted: #9A9A8C;
|
||||
--board-accent: #6E86FF;
|
||||
--board-warn: #FFC04D;
|
||||
|
||||
/* ---- type ---- */
|
||||
--f-display: 'Space Grotesk', sans-serif;
|
||||
--f-body: 'Inter', sans-serif;
|
||||
--f-mono: 'IBM Plex Mono', monospace;
|
||||
|
||||
/* ---- shape / space ---- */
|
||||
--r-card: 10px;
|
||||
--r-pill: 999px;
|
||||
--r-flat: 2px;
|
||||
--gutter: clamp(20px, 5vw, 64px);
|
||||
--maxw: 1240px;
|
||||
}
|
||||
|
||||
*{ box-sizing: border-box; }
|
||||
html{ scroll-behavior: smooth; }
|
||||
body{
|
||||
margin:0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--f-body);
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
img,svg{ max-width:100%; display:block; }
|
||||
a{ color:inherit; text-decoration:none; }
|
||||
ul{ margin:0; padding:0; list-style:none; }
|
||||
button{ font-family:inherit; cursor:pointer; }
|
||||
.wrap{ max-width: var(--maxw); margin:0 auto; padding:0 var(--gutter); }
|
||||
|
||||
/* focus visibility — accessibility floor */
|
||||
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible{
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
*{ animation-duration: .001ms !important; transition-duration: .001ms !important; }
|
||||
}
|
||||
|
||||
/* ============ type scale ============ */
|
||||
h1,h2,h3{ font-family: var(--f-display); font-weight:700; letter-spacing:-0.01em; margin:0; }
|
||||
.eyebrow{
|
||||
font-family: var(--f-mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.mono{ font-family: var(--f-mono); font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* ============ top navigation ============ */
|
||||
.site-header{
|
||||
position: sticky; top:0; z-index: 50;
|
||||
background: rgba(247, 247, 244, 0.85);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.site-header .wrap{
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
height: 76px;
|
||||
}
|
||||
.brand{ display:flex; align-items:center; gap:10px; }
|
||||
.brand img{ height: 76px; width:auto; }
|
||||
.main-nav{ display:flex; align-items:center; gap: 30px; }
|
||||
.main-nav a{
|
||||
font-size: 14px; font-weight:600; color: var(--ink-soft);
|
||||
padding: 8px 2px; position:relative;
|
||||
}
|
||||
.main-nav a.is-active{ color: var(--ink); }
|
||||
.main-nav a.is-active::after{
|
||||
content:""; position:absolute; left:0; right:0; bottom:-2px; height:2px; background: var(--accent);
|
||||
}
|
||||
.nav-search{
|
||||
display:flex; align-items:center; gap:8px;
|
||||
border:1px solid var(--line-strong); border-radius: var(--r-pill);
|
||||
padding: 8px 14px; font-size:13px; color:var(--muted);
|
||||
min-width: 220px;
|
||||
}
|
||||
.nav-search svg{ width:15px; height:15px; flex:none; }
|
||||
.nav-search:focus-within{ border-color:var(--accent); }
|
||||
.nav-search button{ background:none; border:none; padding:0; display:flex; color:inherit; flex:none; }
|
||||
.nav-search input{ border:none; background:none; outline:none; padding:0; font-family:inherit; font-size:13px; color:var(--ink); flex:1; min-width:0; }
|
||||
.nav-search input::placeholder{ color:var(--muted); }
|
||||
.nav-cta{
|
||||
background: var(--ink); color: var(--paper);
|
||||
font-size: 13px; font-weight:600;
|
||||
padding: 11px 18px; border-radius: var(--r-pill);
|
||||
border:none;
|
||||
}
|
||||
.nav-actions{ display:flex; align-items:center; gap:16px; }
|
||||
.nav-burger{ display:none; background:none; border:none; padding:6px; }
|
||||
.nav-burger span{ display:block; width:22px; height:2px; background:var(--ink); margin:5px 0; transition:transform .2s ease, opacity .2s ease; }
|
||||
.nav-burger.is-open span:nth-child(1){ transform:translateY(7px) rotate(45deg); }
|
||||
.nav-burger.is-open span:nth-child(2){ opacity:0; }
|
||||
.nav-burger.is-open span:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }
|
||||
|
||||
.mobile-menu{ display:none; }
|
||||
|
||||
/* ============ buttons ============ */
|
||||
.btn{
|
||||
display:inline-flex; align-items:center; gap:8px;
|
||||
font-family: var(--f-body); font-weight:600; font-size:14px;
|
||||
padding: 13px 22px; border-radius: var(--r-pill); border:1px solid transparent;
|
||||
transition: transform .15s ease, background .15s ease;
|
||||
}
|
||||
.btn-primary{ background: var(--ink); color:var(--paper); }
|
||||
.btn-primary:hover{ background: var(--accent-deep); }
|
||||
.btn-ghost{ background: transparent; color:var(--ink); border-color: var(--line-strong); }
|
||||
.btn-ghost:hover{ border-color: var(--ink); }
|
||||
.btn-accent{ background: var(--accent); color:#fff; }
|
||||
.btn-accent:hover{ background: var(--accent-deep); }
|
||||
.btn:active{ transform: translateY(1px); }
|
||||
.btn-sm{ padding:9px 16px; font-size:13px; }
|
||||
|
||||
/* ============ chips / tags ============ */
|
||||
.chip-row{ display:flex; flex-wrap:wrap; gap:10px; }
|
||||
|
||||
/* collapsible chip-row: CSS-only checkbox toggle, no JS needed */
|
||||
.chip-row-collapsible{ max-height:100px; overflow:hidden; }
|
||||
.chip-toggle-checkbox{ position:absolute; opacity:0; pointer-events:none; }
|
||||
.chip-toggle-checkbox:checked ~ .chip-row-collapsible{ max-height:none; }
|
||||
.chip-toggle-btn{
|
||||
display:inline-flex; margin-top:12px; cursor:pointer;
|
||||
font-family:var(--f-mono); font-size:12px; font-weight:600; color:var(--accent);
|
||||
}
|
||||
.chip-toggle-btn:hover{ color:var(--accent-deep); }
|
||||
.chip-toggle-btn .show-less{ display:none; }
|
||||
.chip-toggle-checkbox:checked ~ .chip-toggle-btn .show-more{ display:none; }
|
||||
.chip-toggle-checkbox:checked ~ .chip-toggle-btn .show-less{ display:inline; }
|
||||
|
||||
.chip{
|
||||
font-family: var(--f-mono); font-size: 12px; letter-spacing:.02em;
|
||||
padding: 7px 14px; border-radius: var(--r-pill);
|
||||
border:1px solid var(--line-strong); color: var(--ink-soft);
|
||||
background: var(--paper); white-space:nowrap;
|
||||
}
|
||||
.chip.is-active{ background: var(--ink); color: var(--paper); border-color: var(--ink); }
|
||||
|
||||
/* ============ pagination (first/prev/current/next/last) ============ */
|
||||
.pager{ display:flex; justify-content:center; align-items:center; gap:8px; margin-top:36px; flex-wrap:wrap; }
|
||||
.pager a, .pager span{
|
||||
padding:9px 16px; border:1px solid var(--line); border-radius:8px;
|
||||
font-family:var(--f-mono); font-size:13px; white-space:nowrap;
|
||||
}
|
||||
.pager a:hover{ border-color:var(--line-strong); }
|
||||
.pager span.is-active{ background:var(--ink); color:var(--paper); border-color:var(--ink); font-weight:600; }
|
||||
|
||||
.tag{
|
||||
font-family: var(--f-mono); font-size:11px; letter-spacing:.04em; text-transform:uppercase;
|
||||
padding: 4px 9px; border-radius: var(--r-flat); background: var(--accent-soft); color: var(--accent-deep);
|
||||
}
|
||||
.tag.tag-warn{ background: var(--warn-soft); color: var(--warn); }
|
||||
.tag.tag-dark{ background: var(--ink); color: var(--paper); }
|
||||
|
||||
/* ============ category glyph (replaces stock photography) ============
|
||||
Small abstract line-glyphs per category drawn with currentColor,
|
||||
set on a soft tinted tile — keeps the system illustrative and
|
||||
consistent instead of relying on photography. */
|
||||
.glyph-tile{
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
border-radius: var(--r-card);
|
||||
aspect-ratio: 4/3;
|
||||
position:relative; overflow:hidden;
|
||||
}
|
||||
.glyph-tile svg{ width:34%; height:34%; }
|
||||
.glyph-tile::after{
|
||||
content:""; position:absolute; inset:0;
|
||||
background-image: radial-gradient(circle at 20% 20%, rgba(255,255,255,.5), transparent 45%);
|
||||
}
|
||||
.t-music{ background:#E7EAFF; color:#2440FF; }
|
||||
.t-tech{ background:#E4F3EC; color:#1F8A56; }
|
||||
.t-community{ background:#FFEFD2; color:#C97A00; }
|
||||
.t-sports{ background:#FDE3E0; color:#D14B3A; }
|
||||
.t-arts{ background:#F1E6FB; color:#7A3FC9; }
|
||||
.t-food{ background:#FFF1DC; color:#B9740B; }
|
||||
|
||||
/* ============ departure board — the signature motif ============ */
|
||||
.board{
|
||||
background: var(--board-bg);
|
||||
border-radius: var(--r-card);
|
||||
color: var(--board-text);
|
||||
overflow:hidden;
|
||||
}
|
||||
.board-head{
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
padding: 16px 22px; border-bottom:1px solid #2B2B22;
|
||||
}
|
||||
.board-head .eyebrow{ color: var(--board-muted); }
|
||||
.board-clock{ font-family:var(--f-mono); font-size:13px; color: var(--board-accent); }
|
||||
.board-row{
|
||||
display:grid;
|
||||
grid-template-columns: 96px 1fr 140px 90px;
|
||||
align-items:center;
|
||||
gap: 18px;
|
||||
padding: 16px 22px;
|
||||
border-bottom: 1px solid #232319;
|
||||
font-family: var(--f-mono);
|
||||
}
|
||||
.board-row:last-child{ border-bottom:none; }
|
||||
.board-row:hover{ background: var(--board-bg-2); }
|
||||
.board-date{ color: var(--board-accent); font-size:13px; letter-spacing:.03em; }
|
||||
.board-date .dow{ display:block; color: var(--board-muted); font-size:11px; margin-top:2px; }
|
||||
.board-name{ font-family: var(--f-body); font-weight:600; font-size:15px; color: var(--board-text); }
|
||||
.board-name .where{ display:block; font-family:var(--f-mono); font-weight:400; font-size:12px; color: var(--board-muted); margin-top:3px; letter-spacing:.02em; }
|
||||
.board-status{ font-size:12px; text-align:right; color: var(--board-muted); }
|
||||
.board-status.is-soon{ color: var(--board-warn); }
|
||||
.board-status.is-live{ color: #6CE3A0; }
|
||||
.board-go{ text-align:right; font-size:12px; color: var(--board-accent); }
|
||||
|
||||
/* ============ section scaffolding ============ */
|
||||
.section{ padding: 64px 0; }
|
||||
.section-head{
|
||||
display:flex; align-items:flex-end; justify-content:space-between;
|
||||
gap: 20px; margin-bottom: 28px; flex-wrap:wrap;
|
||||
}
|
||||
.section-head h2{ font-size: clamp(24px,3vw,32px); }
|
||||
.section-link{ font-size:13px; font-weight:600; color:var(--accent); display:flex; gap:6px; align-items:center; }
|
||||
|
||||
/* ============ card grid ============ */
|
||||
.card-grid{ display:grid; grid-template-columns: repeat(3,1fr); gap: 22px; }
|
||||
.event-card{
|
||||
border:1px solid var(--line); border-radius: var(--r-card); background:#fff;
|
||||
overflow:hidden; display:flex; flex-direction:column;
|
||||
transition: border-color .15s ease, transform .15s ease;
|
||||
}
|
||||
.event-card:hover{ border-color: var(--line-strong); transform: translateY(-2px); }
|
||||
.event-card .body{ padding: 16px 18px 18px; display:flex; flex-direction:column; gap:8px; flex:1; }
|
||||
.event-card .meta-row{ display:flex; align-items:center; gap:10px; }
|
||||
.event-card h3{ font-size:16px; font-family: var(--f-body); font-weight:600; line-height:1.3; }
|
||||
.event-card .when{ font-family: var(--f-mono); font-size:12px; color: var(--muted); }
|
||||
.event-card .place{ font-size:13px; color: var(--muted); margin-top:auto; }
|
||||
.event-card .price{ font-family:var(--f-mono); font-weight:600; font-size:13px; }
|
||||
|
||||
/* ============ footer ============ */
|
||||
.site-footer{ border-top:1px solid var(--line); padding: 48px 0 28px; margin-top:40px; }
|
||||
.footer-grid{ display:grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: 32px; }
|
||||
.footer-grid h4{ font-family:var(--f-mono); font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:0 0 14px; }
|
||||
.footer-grid li{ margin-bottom:10px; font-size:14px; }
|
||||
.footer-grid li a:hover{ color: var(--accent); }
|
||||
.footer-bottom{ display:flex; justify-content:space-between; align-items:center; margin-top:40px; padding-top:20px; border-top:1px solid var(--line); font-size:12px; color:var(--muted); font-family:var(--f-mono); }
|
||||
|
||||
/* ============ misc utility ============ */
|
||||
.divider{ height:1px; background:var(--line); border:none; margin: 0; }
|
||||
.skip-link{ position:absolute; left:-999px; }
|
||||
.skip-link:focus{ left: 12px; top: 12px; background: var(--ink); color:#fff; padding:8px 14px; border-radius:6px; z-index:100; }
|
||||
|
||||
/* ============ responsive ============ */
|
||||
@media (max-width: 980px){
|
||||
.card-grid{ grid-template-columns: repeat(2,1fr); }
|
||||
.footer-grid{ grid-template-columns: 1.4fr 1fr 1fr; }
|
||||
.board-row{ grid-template-columns: 80px 1fr 76px; }
|
||||
.board-go{ display:none; }
|
||||
}
|
||||
@media (max-width: 760px){
|
||||
.main-nav, .nav-search{ display:none; }
|
||||
.nav-burger{ display:block; }
|
||||
.card-grid{ grid-template-columns: 1fr 1fr; }
|
||||
.footer-grid{ grid-template-columns: 1fr 1fr; }
|
||||
.section{ padding: 44px 0; }
|
||||
|
||||
.mobile-menu{
|
||||
display:block;
|
||||
position:absolute; top:100%; left:0; right:0; z-index:49;
|
||||
background:var(--paper); border-bottom:1px solid var(--line);
|
||||
padding:8px 20px 20px;
|
||||
box-shadow:0 10px 30px rgba(20,20,15,.08);
|
||||
}
|
||||
.mobile-menu[hidden]{ display:none; }
|
||||
.mobile-menu nav{ display:flex; flex-direction:column; }
|
||||
.mobile-menu nav a{
|
||||
padding:14px 0; font-size:15px; font-weight:600; color:var(--ink-soft);
|
||||
border-bottom:1px solid var(--line);
|
||||
}
|
||||
.mobile-menu nav a.is-active{ color:var(--ink); }
|
||||
.mobile-menu .mobile-search{ display:flex; gap:8px; margin-top:14px; }
|
||||
.mobile-menu .mobile-search input{
|
||||
flex:1; min-width:0; border:1px solid var(--line-strong); border-radius:var(--r-pill);
|
||||
padding:10px 14px; font-size:14px; font-family:inherit; color:var(--ink); background:none;
|
||||
}
|
||||
.mobile-menu .mobile-search button{
|
||||
background:var(--ink); color:var(--paper); border:none; border-radius:var(--r-pill);
|
||||
padding:10px 16px; font-size:13px; font-weight:600;
|
||||
}
|
||||
}
|
||||
@media (max-width: 520px){
|
||||
.card-grid{ grid-template-columns: 1fr; }
|
||||
.board-row{ grid-template-columns: 70px 1fr; }
|
||||
.board-status{ display:none; }
|
||||
}
|
||||
|
||||
/* ============ interactive map stage (home hero + /map) ============ */
|
||||
.stage{ position:relative; height: calc(100vh - 76px); min-height:520px; }
|
||||
.stage-compact{ height:560px; min-height:420px; border-radius:var(--r-card); overflow:hidden; box-shadow:0 10px 30px rgba(20,20,15,.08); }
|
||||
.stage-compact .panel{ width:300px; }
|
||||
.stage:fullscreen{ height:100vh; border-radius:0; }
|
||||
.stage:-webkit-full-screen{ height:100vh; border-radius:0; }
|
||||
#mapCanvas{ position:absolute; inset:0; background: var(--paper-dim); z-index: 10;}
|
||||
.leaflet-tile-pane, .maplibregl-map{ filter: grayscale(0.85) brightness(1.08) contrast(0.92); }
|
||||
.leaflet-popup-content-wrapper{ border-radius:10px; box-shadow:0 10px 30px rgba(20,20,15,.18); }
|
||||
.leaflet-popup-content{ margin:14px 16px; font-family:var(--f-body); }
|
||||
.leaflet-control-attribution{ font-size:10px; }
|
||||
|
||||
.map-overlay-top{
|
||||
position:absolute; top:5px; left:50px; right:10px; z-index:30;
|
||||
display:flex; align-items:flex-start; justify-content:space-between; gap:14px; flex-wrap:wrap;
|
||||
pointer-events:none;
|
||||
}
|
||||
.float-card{
|
||||
background: rgba(247,247,244,0.85);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
border:1px solid var(--line); border-radius:14px;
|
||||
box-shadow: 0 8px 24px rgba(20,20,15,.10);
|
||||
pointer-events:auto;
|
||||
}
|
||||
.search-card{ padding:14px 16px; flex:1; max-width:560px; }
|
||||
.search-card form{ display:flex; gap:10px; margin-bottom:12px; }
|
||||
.search-card input{ flex:1; padding:11px 14px; border:1px solid var(--line-strong); border-radius:var(--r-pill); font-size:14px; }
|
||||
.chip-row-tight{ display:flex; gap:8px; flex-wrap:wrap; max-height:64px; overflow-y:auto; }
|
||||
.chip-sm{ font-family:var(--f-mono); font-size:11px; padding:6px 12px; border-radius:var(--r-pill); border:1px solid var(--line-strong); background:#fff; color:var(--ink-soft); }
|
||||
.chip-sm.is-active{ background:var(--ink); color:var(--paper); border-color:var(--ink); }
|
||||
.locate-card{ padding:10px; flex:none; width:220px; }
|
||||
#locationSearchForm{ display:flex; gap:6px; margin-bottom:8px; }
|
||||
#locationSearchForm input{ flex:1; min-width:0; padding:9px 12px; border:1px solid var(--line-strong); border-radius:var(--r-pill); font-size:12px; }
|
||||
.locate-btn{
|
||||
display:flex; align-items:center; gap:8px; background:var(--ink); color:var(--paper);
|
||||
border:none; border-radius:var(--r-pill); padding:11px 16px; font-size:13px; font-weight:600; width:100%;
|
||||
}
|
||||
.range-label{
|
||||
display:flex; align-items:center; gap:8px; margin-top:10px;
|
||||
font-family:var(--f-mono); font-size:11px; color:var(--muted);
|
||||
}
|
||||
.range-label select{
|
||||
flex:1; padding:6px 10px; border:1px solid var(--line-strong); border-radius:6px;
|
||||
font-family:var(--f-mono); font-size:12px; background:#fff; color:var(--ink);
|
||||
}
|
||||
.locate-status{ font-family:var(--f-mono); font-size:11px; color:var(--muted); margin-top:8px; max-width:200px; }
|
||||
|
||||
.legend{
|
||||
position:absolute; left:18px; bottom:18px; z-index:30; padding:10px 14px;
|
||||
display:flex; gap:14px; flex-wrap:wrap; max-width:30vw; flex-direction: column;
|
||||
}
|
||||
.legend span{ display:flex; align-items:center; gap:6px; font-family:var(--f-mono); font-size:11px; color:var(--ink-soft); }
|
||||
.legend .dot{ width:9px; height:9px; border-radius:50%; }
|
||||
|
||||
.panel{
|
||||
position:absolute; top:200px; right:10px; bottom:18px; width:360px; z-index:25;
|
||||
display:flex; flex-direction:column; overflow:hidden;
|
||||
}
|
||||
/* Overrides .board's solid background just for the map's floating events
|
||||
panel, to match the other map overlay elements — other .board usages
|
||||
(event cards, home hero board) keep their solid background. */
|
||||
.panel.board{
|
||||
background: rgba(20,20,15,0.85);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
}
|
||||
.panel-head{ padding:16px 18px; border-bottom:1px solid var(--line); display:flex; justify-content:space-between; align-items:center; }
|
||||
.panel-head .count{ font-family:var(--f-mono); font-size:12px; color:var(--muted); }
|
||||
.panel-list{ overflow-y:auto; flex:1; }
|
||||
.map-row{ display:grid; grid-template-columns: 58px 1fr 22px; gap:12px; align-items:center; padding:13px 18px; border-bottom:1px solid #232319; cursor:pointer; }
|
||||
.map-row.no-dist{ grid-template-columns: 1fr 22px; }
|
||||
.map-row:last-child{ border-bottom:none; }
|
||||
.map-row:hover, .map-row.is-highlighted{ background:var(--board-bg-2); }
|
||||
.map-row .dist{ font-family:var(--f-mono); font-size:13px; color:var(--board-accent); }
|
||||
.map-row .dist .u{ display:block; font-size:10px; color:var(--board-muted); }
|
||||
.map-row .name{ font-family:var(--f-body); font-weight:600; font-size:13.5px; color:var(--board-text); line-height:1.3; }
|
||||
.map-row .when{ display:block; font-family:var(--f-mono); font-size:11px; color:var(--board-muted); margin-top:3px; }
|
||||
.map-row .go{ color:var(--board-accent); text-align:right; }
|
||||
.pin-pop{ min-width:170px; }
|
||||
.pin-pop .cat{ font-family:var(--f-mono); font-size:10px; text-transform:uppercase; letter-spacing:.05em; }
|
||||
.pin-pop h4{ font-size:14px; margin:6px 0 4px; font-family:var(--f-body); font-weight:600; }
|
||||
.pin-pop p{ font-family:var(--f-mono); font-size:11.5px; color:var(--muted); margin:0 0 8px; }
|
||||
.pin-pop a{ font-size:12px; font-weight:600; color:var(--accent); }
|
||||
|
||||
/* reskin markercluster's default green/orange bubbles to the site's ink tone */
|
||||
/* !important: MarkerCluster.Default.css loads after this file (in each
|
||||
page's head_extra block), so an equal-specificity override would lose
|
||||
the tie without it. */
|
||||
.marker-cluster-small, .marker-cluster-medium, .marker-cluster-large{ background-color: rgba(20,20,15,.18) !important; }
|
||||
.marker-cluster-small div, .marker-cluster-medium div, .marker-cluster-large div{ background-color: var(--ink) !important; }
|
||||
.marker-cluster span{ color:#fff !important; font-family:var(--f-mono); font-weight:600; }
|
||||
|
||||
@media (max-width: 900px){
|
||||
.stage{ height:auto; }
|
||||
.stage-compact{ height:auto; }
|
||||
#mapCanvas{ position:static; height:62vh; }
|
||||
.panel, .stage-compact .panel{ position:static; width:auto; height:auto; max-height:50vh; margin-top:14px; }
|
||||
.map-overlay-top{ position:absolute; }
|
||||
.legend{ display:none; }
|
||||
}
|
||||
Reference in new issue
Block a user