feat: enhance event detail and overview pages with improved UI and functionality

- Refactored event_detail.js to include better date formatting, time normalization, and diet label mapping.
- Improved the event detail rendering logic with a more structured layout, including a lightbox for gallery images.
- Updated event_overview.js to support dynamic filtering by location and date, with improved session storage handling.
- Added new SVG assets for location pin and invite logo to enhance visual elements.
This commit is contained in:
viiivo 2026-03-27 17:48:03 +01:00
parent c1b9bc85f5
commit edd601d330
8 changed files with 1633 additions and 150 deletions

13
assets/invite-logo.svg Normal file
View File

@ -0,0 +1,13 @@
<svg preserveAspectRatio="xMidYMid meet" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 100 37.6495" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 2">
<g id="Invit&#195;&#169;">
<path id="Vector" d="M81.0691 28.3941C81.0691 26.5407 81.4724 24.9101 82.2791 23.5025C83.1118 22.0948 84.2828 20.9921 85.7921 20.1945C87.3274 19.3968 89.1489 18.998 91.2567 18.998C92.896 18.998 94.3663 19.2678 95.6674 19.8074C96.9945 20.347 98.0353 21.0743 98.79 21.9892C99.5446 22.8808 99.9219 23.8661 99.9219 24.9453C99.9219 26.0715 99.5576 26.8691 98.829 27.3384C98.1004 27.8076 97.0205 28.0422 95.5893 28.0422H87.3924C87.3924 28.6522 87.6526 29.1566 88.1731 29.5554C88.7195 29.9543 89.6303 30.1537 90.9054 30.1537C91.5559 30.1537 92.1544 30.095 92.7009 29.9777C93.2733 29.8604 93.8458 29.7548 94.4183 29.661C95.0168 29.5437 95.6804 29.485 96.409 29.485C97.4499 29.485 98.3086 29.8252 98.9851 30.5056C99.6617 31.1625 100 32.0775 100 33.2505C100 34.6347 99.2063 35.714 97.619 36.4882C96.0577 37.2624 94.002 37.6495 91.4518 37.6495C89.6563 37.6495 87.9649 37.3328 86.3776 36.6993C84.8162 36.0659 83.5412 35.0688 82.5523 33.708C81.5635 32.3238 81.0691 30.5525 81.0691 28.3941ZM93.7157 26.8809C94.106 26.8809 94.2362 26.7166 94.106 26.3882C94.002 26.0597 93.6897 25.7195 93.1693 25.3676C92.6488 25.0157 91.8942 24.8398 90.9054 24.8398C89.7604 24.8398 88.9017 25.0626 88.3292 25.5084C87.7827 25.9542 87.5095 26.4116 87.5095 26.8809H93.7157ZM88.6024 13.7896C89.331 13.3908 90.0596 13.0154 90.7883 12.6635C91.5169 12.2881 92.3626 11.8541 93.3254 11.3614C94.3142 10.8687 95.264 10.7045 96.1748 10.8687C97.0855 11.0095 97.7491 11.5139 98.1655 12.3819C98.6078 13.25 98.6729 14.0359 98.3606 14.7398C98.0484 15.4201 97.3588 15.9597 96.2919 16.3586C95.3551 16.7105 94.3012 17.039 93.1302 17.3439C91.9853 17.6489 91.0094 17.8601 90.2028 17.9774C89.5782 18.0712 88.9928 18.0243 88.4463 17.8366C87.8998 17.6255 87.4965 17.2149 87.2363 16.6049C87.0021 15.9949 87.0151 15.4553 87.2753 14.9861C87.5355 14.5169 87.9779 14.1181 88.6024 13.7896Z" fill="var(--fill-0, #CD4918)"/>
<path id="Vector_2" d="M73.1805 37.6495C71.8013 37.6495 70.5523 37.368 69.4333 36.8049C68.3404 36.2418 67.4817 35.4442 66.8572 34.4119C66.2326 33.3561 65.9204 32.1244 65.9204 30.7168V24.523L64.4762 24.699C63.6955 24.7928 63.0059 24.5465 62.4074 23.96C61.8089 23.3734 61.5097 22.6344 61.5097 21.7429C61.5097 20.8514 61.8089 20.1241 62.4074 19.561C63.0059 18.9745 63.6955 18.7282 64.4762 18.822L65.9204 18.998L65.8813 16.6049C65.8553 15.6665 66.1546 14.9275 66.7791 14.3879C67.4036 13.8248 68.3144 13.5433 69.5114 13.5433C70.7084 13.5433 71.6322 13.8248 72.2827 14.3879C72.9333 14.9275 73.2325 15.6665 73.1805 16.6049L73.1024 18.998L75.1711 18.7516C76.5763 18.5639 77.6432 18.7985 78.3718 19.4554C79.1004 20.0889 79.4647 20.8514 79.4647 21.7429C79.4647 22.6344 79.1004 23.4086 78.3718 24.0655C77.6692 24.699 76.6414 24.9336 75.2882 24.7694L73.1024 24.523V29.7666C73.1024 30.0716 73.2195 30.3531 73.4537 30.6112C73.6879 30.8458 73.9741 30.9162 74.3124 30.8223C74.7027 30.7285 75.1061 30.5291 75.5224 30.2241C75.9648 29.9191 76.5243 29.7666 77.2008 29.7666C78.2157 29.7666 78.9964 30.095 79.5428 30.7519C80.0893 31.4089 80.3625 32.2183 80.3625 33.1802C80.3625 33.9309 80.0632 34.6582 79.4647 35.362C78.8923 36.0424 78.0726 36.5937 77.0057 37.016C75.9388 37.4383 74.6637 37.6495 73.1805 37.6495Z" fill="var(--fill-0, #CD4918)"/>
<path id="Vector_3" d="M52.709 15.0565C52.709 14.0476 53.0473 13.2382 53.7238 12.6283C54.4264 11.9948 55.3892 11.6781 56.6123 11.6781C57.9134 11.6781 58.8762 11.9831 59.5007 12.5931C60.1512 13.203 60.4765 14.0125 60.4765 15.0213C60.4765 16.0066 60.1512 16.8043 59.5007 17.4143C58.8762 18.0008 57.9134 18.2941 56.6123 18.2941C55.3372 18.2941 54.3614 18.0008 53.6848 17.4143C53.0343 16.8043 52.709 16.0184 52.709 15.0565ZM56.6123 37.5439C55.3892 37.5439 54.4785 37.2389 53.88 36.6289C53.2815 36.0189 52.9822 35.2564 52.9822 34.3415V22.3059C52.9822 21.2971 53.2815 20.4994 53.88 19.9129C54.4785 19.3264 55.3892 19.0331 56.6123 19.0331C57.8353 19.0331 58.7461 19.3264 59.3446 19.9129C59.9691 20.4994 60.2813 21.2736 60.2813 22.2356V34.3415C60.2813 35.2564 59.9561 36.0189 59.3055 36.6289C58.681 37.2389 57.7832 37.5439 56.6123 37.5439Z" fill="var(--fill-0, #CD4918)"/>
<path id="Vector_4" d="M42.0485 37.5439C40.3571 37.5439 38.9129 37.0982 37.7159 36.2066C36.5189 35.3151 35.53 33.8371 34.7494 31.7725L31.6658 23.7488C31.2494 22.6931 31.2494 21.7194 31.6658 20.8279C32.1081 19.9364 32.9148 19.3733 34.0858 19.1387C35.1267 18.9276 36.0374 19.0214 36.8181 19.4203C37.6248 19.8191 38.1973 20.4877 38.5355 21.4262L41.5801 29.485C41.9444 30.4235 42.3868 31.1156 42.9072 31.5614C43.4537 32.0071 44.1172 32.1127 44.8979 31.8781C45.6265 31.62 46.0168 31.1977 46.0689 30.6112C46.1469 30.0012 46.0038 29.2974 45.6395 28.4997C45.3273 27.7724 44.9109 27.1507 44.3905 26.6345C43.87 26.1184 43.4016 25.5788 42.9853 25.0157C42.5689 24.4527 42.3608 23.7488 42.3608 22.9042C42.3608 21.6843 42.7901 20.7341 43.6488 20.0537C44.5336 19.3499 45.6916 19.0214 47.1228 19.0683C48.3458 19.0918 49.3476 19.4789 50.1283 20.2297C50.9089 20.9804 51.2993 21.9306 51.2993 23.0802C51.2993 23.7136 51.1822 24.394 50.948 25.1213C50.7138 25.8486 50.4536 26.6111 50.1673 27.4087L48.606 31.8429C48.1897 33.0159 47.7213 34.0365 47.2008 34.9046C46.7064 35.7492 46.0559 36.4061 45.2492 36.8753C44.4685 37.321 43.4016 37.5439 42.0485 37.5439Z" fill="var(--fill-0, #CD4918)"/>
<path id="Vector_5" d="M14.3409 37.544C13.1959 37.544 12.2721 37.239 11.5695 36.629C10.893 35.9955 10.5547 35.2213 10.5547 34.3063V22.6579C10.5547 21.6726 10.88 20.8397 11.5305 20.1593C12.1811 19.479 13.1178 19.1388 14.3409 19.1388C15.5379 19.1388 16.4486 19.479 17.0732 20.1593C17.6977 20.8397 18.0099 21.696 18.0099 22.7283V24.4879H18.7125C18.7125 23.1506 18.9728 22.0949 19.4932 21.3206C20.0397 20.5464 20.7292 19.9951 21.5619 19.6666C22.3946 19.3147 23.2273 19.1388 24.06 19.1388C26.0637 19.1388 27.5339 19.7957 28.4707 21.1095C29.4075 22.3999 29.8759 24.2416 29.8759 26.6346V34.3063C29.8759 35.2213 29.5376 35.9955 28.8611 36.629C28.1845 37.239 27.2737 37.544 26.1288 37.544C24.9578 37.544 24.021 37.239 23.3184 36.629C22.6418 35.9955 22.3036 35.2213 22.3036 34.3063V28.0774C22.3036 27.2328 22.1214 26.5759 21.7571 26.1067C21.4188 25.6375 20.9114 25.4029 20.2348 25.4029C19.4542 25.4029 18.8817 25.6609 18.5174 26.1771C18.1791 26.6932 18.0099 27.3501 18.0099 28.1478V34.3063C18.0099 35.2213 17.6847 35.9955 17.0341 36.629C16.3836 37.239 15.4858 37.544 14.3409 37.544Z" fill="var(--fill-0, #CD4918)"/>
<path id="Vector_6" d="M4.05941 37.6143C2.8624 37.6143 1.88658 37.2859 1.13195 36.629C0.377317 35.9721 0 35.1392 0 34.1304V15.514C0 14.4817 0.364306 13.6137 1.09292 12.9098C1.84755 12.206 2.83638 11.8541 4.05941 11.8541C5.3605 11.8541 6.37535 12.206 7.10397 12.9098C7.8586 13.5902 8.23592 14.4348 8.23592 15.4436V34.1304C8.23592 35.1392 7.83258 35.9721 7.0259 36.629C6.24524 37.2859 5.25641 37.6143 4.05941 37.6143Z" fill="var(--fill-0, #CD4918)"/>
</g>
<path id="Vector_7" d="M17.9138 9.7901C18.0942 10.142 17.9272 10.5614 17.5406 10.7257C17.4345 10.7707 17.3231 10.7913 17.2149 10.7913C16.9232 10.7913 16.6449 10.6412 16.5129 10.385C15.8512 9.0957 15.5028 7.53236 15.5028 5.86486C15.5028 5.47638 15.8491 5.16108 16.2759 5.16108C16.7026 5.16108 17.049 5.47638 17.049 5.86486C17.049 7.30809 17.3551 8.70158 17.9138 9.7901ZM22.4596 5.16108C22.0328 5.16108 21.6865 5.47638 21.6865 5.86486C21.6865 7.30809 21.3793 8.70158 20.8206 9.7901C20.6402 10.142 20.8072 10.5614 21.1938 10.7257C21.3 10.7707 21.4113 10.7913 21.5195 10.7913C21.8112 10.7913 22.0895 10.6412 22.2215 10.385C22.8833 9.0957 23.2317 7.53236 23.2317 5.86486C23.2317 5.47638 22.8853 5.16108 22.4586 5.16108H22.4596ZM28.3867 6.33405C28.3867 7.38504 27.9723 8.40318 27.2188 9.2008C26.8106 9.63245 26.3251 10.3278 26.3251 11.1827V15.7178C26.3251 17.1414 25.0541 18.2984 23.4904 18.2984H15.244C13.6803 18.2984 12.4093 17.1414 12.4093 15.7178V11.1827C12.4093 10.3278 11.9228 9.63339 11.5156 9.2008C10.7632 8.40224 10.3478 7.3841 10.3478 6.33405C10.3478 4.11291 12.1053 2.28026 14.4905 1.94807C15.5966 0.722551 17.3912 0 19.3672 0C21.3432 0 23.1379 0.722551 24.2439 1.94807C26.6281 2.28026 28.3867 4.11291 28.3867 6.33405ZM24.7789 15.7178V14.5449H13.9555V15.7178C13.9555 16.3644 14.5338 16.8908 15.244 16.8908H23.4904C24.2006 16.8908 24.7789 16.3644 24.7789 15.7178ZM26.8405 6.33405C26.8405 4.74444 25.519 3.44479 23.7666 3.31341C23.5388 3.29558 23.3327 3.18861 23.1997 3.02064C22.4225 2.02596 20.9536 1.40851 19.3672 1.40851C17.7808 1.40851 16.3119 2.02596 15.5347 3.02064C15.4017 3.18955 15.1956 3.29652 14.9678 3.31341C13.2154 3.44572 11.8939 4.74444 11.8939 6.33405C11.8939 7.05848 12.1692 7.73317 12.6887 8.284C13.5175 9.16326 13.9555 10.1655 13.9555 11.1827V13.1373H24.7789V11.1827C24.7789 10.1655 25.217 9.16232 26.0457 8.284C26.5663 7.73317 26.8405 7.05848 26.8405 6.33405Z" fill="var(--fill-0, #CD4918)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

6
assets/location-pin.svg Normal file
View File

@ -0,0 +1,6 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 23 27.6667" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon">
<path d="M22 11.5C22 19.6667 11.5 26.6667 11.5 26.6667C11.5 26.6667 1 19.6667 1 11.5C1 8.71523 2.10625 6.04451 4.07538 4.07538C6.04451 2.10625 8.71523 1 11.5 1C14.2848 1 16.9555 2.10625 18.9246 4.07538C20.8938 6.04451 22 8.71523 22 11.5Z" stroke="var(--stroke-0, #D44B24)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.5 15C13.433 15 15 13.433 15 11.5C15 9.567 13.433 8 11.5 8C9.567 8 8 9.567 8 11.5C8 13.433 9.567 15 11.5 15Z" stroke="var(--stroke-0, #D44B24)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 762 B

File diff suppressed because it is too large Load Diff

View File

@ -2,26 +2,83 @@
{
"id": 1,
"title": "Italienische Tavolata",
"location": "LUZERN",
"location": "Luzern",
"date": "19. MÄR. 2026",
"time": "18:30 UHR",
"category": "DINNER",
"cuisine": "ITALIENISCH",
"diet": "VEGGIE",
"spots": 4,
"image": "https://via.placeholder.com/300x200"
"spots": 6,
"host": {
"name": "Ferdinando",
"initial": "F"
},
"hostMessage": [
"Ciao zusammen! Ich liebe die italienische Küche, nicht nur wegen des Essens, sondern wegen des Gefühls: Alle sitzen an einem langen Tisch, teilen sich grosse Platten und geniessen die Zeit.",
"Genau das möchte ich mit euch teilen. Ich bereite dafuer eine klassische Tavolata vor, bei der verschiedene Gerichte in die Mitte des Tisches kommen und sich jeder bedient."
],
"menu": [
"Bruschetta-Variationen und Antipasti",
"Hausgemachte Pasta mit saisonalem Gemüse",
"Optional mit Salsiccia-Ragu",
"Tiramisu als Dolce"
],
"specifications": [
"Glutenfrei"
],
"participants": [
"Ferdinando",
"Alina",
"Ramon",
"Franca"
],
"gallery": [
"https://i.pinimg.com/736x/f8/48/e2/f848e218a5bd6702c9fbb225c8eebb3e.jpg",
"https://i.pinimg.com/736x/b1/3a/00/b13a00d5ee7a93b0e14f757d27043370.jpg",
"https://i.pinimg.com/1200x/78/5b/05/785b052394a8337c9a3b152a9745a580.jpg",
""
]
},
{
"id": 2,
"title": "Noche Peruana",
"location": "LUZERN",
"location": "Chur",
"date": "11. APR. 2026",
"time": "19:00 UHR",
"category": "DINNER",
"cuisine": "PERUANISCH",
"diet": "FLEISCH",
"spots": 2,
"image": "https://via.placeholder.com/300x200"
"diet": "Omnivore",
"spots": 4,
"host": {
"name": "Camila",
"initial": "C"
},
"hostMessage": [
"¡Hola a todos! Ich lade euch ein auf eine kulinarische Reise nach Peru.",
"Ich koche für euch ein authentisches peruanisches Sharing-Menü, das vor Lebensfreude nur so sprüht. Freut euch auf eine Explosion aus leuchtenden Farben, fein abgestimmter Schärfe und der unverwechselbaren Frische verschiedenster Kräuter.",
"Wir genießen den Abend gemeinsam in mehreren kleinen Gängen, ganz nach dem Sharing-Prinzip. Dabei entdecken wir die klassischen Aromen meiner Heimatstadt Lima von traditionell bis modern interpretiert.",
"Es wird gesellig, aromatisch und ein echtes Erlebnis für alle Sinne. ¡Buen provecho!"
],
"menu": [
"Ceviche mit Limette und Koriander",
"Aji de Gallina",
"Lomo Saltado",
"Suspiro a la Limena"
],
"specifications": [
],
"participants": [
"Camila",
"Mara",
"Luis",
"Tobias"
],
"gallery": [
"https://i.pinimg.com/736x/f4/4c/59/f44c597ce62067eef2b03091f30d2279.jpg",
"https://i.pinimg.com/736x/1d/41/eb/1d41ebe23ce4dde7853ab93fce1cfdb2.jpg",
"https://i.pinimg.com/736x/8d/9e/2e/8d9e2e13651f11ef64f661cb1d959738.jpg",
"https://i.pinimg.com/1200x/ee/bf/f0/eebff08d0d4d20e1b4505495925fb1d8.jpg"
]
},
{
"id": 3,
@ -30,9 +87,36 @@
"date": "02. MAI. 2026",
"time": "12:30 UHR",
"category": "LUNCH",
"cuisine": "JAPANISCH",
"diet": "FISCH",
"diet": "Pescetarisch",
"spots": 8,
"image": "https://via.placeholder.com/300x200"
"host": {
"name": "Akiko",
"initial": "A"
},
"hostMessage": [
"Willkommen zu einem entspannten Lunch mit japanischen Klassikern und hausgemachten Beilagen.",
"Ich zeige euch, wie wir verschiedene kleine Teller kombinieren, damit jeder probieren kann."
],
"menu": [
"Miso Suppe",
"Sashimi Variation",
"Matcha Mochi"
],
"specifications": [
"Glutenfrei",
"Laktosefrei"
],
"participants": [
"Akiko",
"Jan",
"Mina"
],
"gallery": [
"https://i.pinimg.com/1200x/e2/6a/f5/e26af5c24b805081a3f304d240818302.jpg",
"https://i.pinimg.com/736x/21/77/4a/21774adee4ae0e4f7a1494e33ab3856b.jpg",
"https://i.pinimg.com/1200x/b1/fb/3a/b1fb3a7809f4046843904ac8800daacc.jpg",
"https://i.pinimg.com/1200x/c6/93/42/c69342ec621333e853c35bda891d8bc6.jpg"
]
}
]

View File

@ -4,25 +4,36 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event-Detail</title>
<link rel="stylesheet" href="css/event_overview.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bagel+Fat+One&family=Jost:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/event_overview.css">
</head>
<body>
<header class="navbar">
<div class="logo">SOCIAL COOKING</div>
<nav>
<a href="index.html">Events</a>
<div class="user-profile">M</div>
<!-- Top navigation: shared with overview, highlights current area -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/invite-logo.svg" alt="Invite Logo">
</a>
<nav class="top-nav-links" aria-label="Hauptnavigation">
<a class="nav-link active" href="event_overview.html" aria-current="page">Event finden</a>
<a class="nav-link" href="event_create.html">Event erstellen</a>
<a class="profile-pill" href="login.html" aria-label="Profil">M</a>
</nav>
</header>
</div>
</header>
<main class="container">
<div id="detail-view">
<p>Lädt Event-Details...</p>
</div>
</main>
<!-- Main content: detail page gets fully injected by JavaScript -->
<main class="container">
<!-- Render target: loading, error state or full detail layout -->
<div id="detail-view">
<p>Lädt Event-Details...</p>
</div>
</main>
<script src="js/event_overview.js"></script>
<!-- Page logic: fetch by URL id, compose detail UI, handle gallery lightbox -->
<script src="js/event_detail.js"></script>
</body>

View File

@ -4,33 +4,66 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event-Overview</title>
<link rel="stylesheet" href="css/event_overview.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bagel+Fat+One&family=Jost:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/event_overview.css">
</head>
<body>
<header class="navbar">
<div class="logo">SOCIAL COOKING</div>
<nav>
<span>Events</span>
<div class="user-profile">M</div>
</nav>
<!-- Top navigation: global entry points and current page indicator -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/invite-logo.svg" alt="Invite Logo">
</a>
<nav class="top-nav-links" aria-label="Hauptnavigation">
<a class="nav-link active" href="event_overview.html" aria-current="page">Event finden</a>
<a class="nav-link" href="event_create.html">Event erstellen</a>
<a class="profile-pill" href="login.html" aria-label="Profil">M</a>
</nav>
</div>
</header>
<!-- Main content: page headline, filter controls and dynamic event list -->
<main class="container">
<h1>Invité Events</h1>
<!-- Page headline -->
<h1 class="overview-title">Events</h1>
<!-- Filter section: category chips + location/date filters -->
<section class="filter-section">
<p class="filter-label">WORAUF HAST DU LUST</p>
<div class="category-group">
<div class="category-item" data-cat="BRUNCH"><div class="square"></div><span>BRUNCH</span></div>
<div class="category-item" data-cat="LUNCH"><div class="square"></div><span>LUNCH</span></div>
<div class="category-item" data-cat="DINNER"><div class="square"></div><span>DINNER</span></div>
<div class="category-item" data-cat="COFFEE"><div class="square"></div><span>COFFEE</span></div>
<div class="category-item active" data-cat="ALLE"><div class="square"></div><span>ALLE</span></div>
<p class="filter-label">Was darf es sein?</p>
<div class="filter-row">
<!-- Primary category filter buttons -->
<div class="category-group">
<button class="category-item" type="button" data-cat="BRUNCH">BRUNCH</button>
<button class="category-item" type="button" data-cat="LUNCH">LUNCH</button>
<button class="category-item" type="button" data-cat="DINNER">DINNER</button>
<button class="category-item" type="button" data-cat="COFFEE">COFFEE</button>
<button class="category-item active" type="button" data-cat="ALLE">ALLE</button>
</div>
<!-- Secondary filters populated/handled by JavaScript -->
<div class="meta-filter-group" aria-label="Weitere Filter">
<label class="meta-filter" for="location-filter">
<span>Ort</span>
<select id="location-filter">
<option value="ALLE_ORTE">Alle Orte</option>
</select>
</label>
<label class="meta-filter" for="date-filter">
<span>Datum</span>
<input id="date-filter" type="date">
</label>
</div>
</div>
</section>
<!-- Render target: event cards or empty state -->
<section id="event-grid" class="event-list"></section>
</main>
<script src="js/event_overview.js"></script>
<!-- Page logic: data loading, filtering and card rendering -->
<script src="js/event_overview.js"></script>
</body>
</html>

View File

@ -1,7 +1,11 @@
document.addEventListener('DOMContentLoaded', async () => {
// -------------------------------------------------------------
// DOM entry point and shared asset path.
// -------------------------------------------------------------
const detailContainer = document.getElementById('detail-view');
const locationIconPath = 'assets/location-pin.svg';
// 1. ID aus der URL lesen (z.B. detail.html?id=1)
// Read event id from query string (detail page deep-link support).
const params = new URLSearchParams(window.location.search);
const eventId = parseInt(params.get('id'));
@ -10,7 +14,7 @@ document.addEventListener('DOMContentLoaded', async () => {
return;
}
// 2. Daten laden und das richtige Event suchen
// Fetch data source and resolve the matching event record.
try {
const response = await fetch('data/events.json');
const allEvents = await response.json();
@ -25,23 +29,232 @@ document.addEventListener('DOMContentLoaded', async () => {
console.error("Fehler beim Laden der Details:", error);
}
// Format localized date token into full readable date.
function formatEventDate(dateString) {
const labels = {
JAN: 'Januar',
FEB: 'Februar',
'MÄR': 'März',
MRZ: 'März',
APR: 'April',
MAI: 'Mai',
JUN: 'Juni',
JUL: 'Juli',
AUG: 'August',
SEP: 'September',
OKT: 'Oktober',
NOV: 'November',
DEZ: 'Dezember'
};
const match = dateString.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
if (!match) {
return dateString;
}
const day = Number(match[1]);
const monthLabel = labels[match[2]];
const year = match[3];
return monthLabel ? `${day}. ${monthLabel} ${year}` : dateString;
}
// Normalize time casing for UI consistency.
function formatEventTime(timeString) {
return timeString.replace('UHR', 'Uhr').trim();
}
// Map diet keys to readable labels while keeping unknown values untouched.
function getDietLabel(diet) {
const labels = {
VEGGIE: 'Vegetarisch',
VEGAN: 'Vegan',
FLEISCH: 'Fleisch',
FISCH: 'Fisch'
};
return labels[diet] || diet;
}
// Compose and inject the full detail UI for a single event.
function renderDetailPage(event) {
//Layout Deatilseite der Events mit Rücklink zur Übersicht, Eventtitel, Infos und Bild
// Core display values and resilient fallbacks for optional data fields.
const displayDate = formatEventDate(event.date);
const displayTime = formatEventTime(event.time);
const dietLabel = getDietLabel(event.diet);
const eventCategory = event.category || 'EVENT';
const hostName = event.host?.name || 'Host';
const hostInitial = (event.host?.initial || hostName.charAt(0) || 'H').charAt(0).toUpperCase();
const hostMessage = Array.isArray(event.hostMessage) && event.hostMessage.length > 0
? event.hostMessage
: ['Der Host hat für dieses Event noch keine Nachricht hinterlegt.'];
const menuItems = Array.isArray(event.menu) && event.menu.length > 0
? event.menu
: ['Menü wird in Kuerze bekannt gegeben.'];
const specifications = Array.isArray(event.specifications) && event.specifications.length > 0
? event.specifications
: [];
const participants = Array.isArray(event.participants) ? event.participants : [];
const galleryImages = Array.isArray(event.gallery) && event.gallery.length > 0
? event.gallery
: [event.image, event.image, event.image];
const visibleParticipants = participants.slice(0, 6);
const remainingParticipants = Math.max(0, participants.length - visibleParticipants.length);
const totalGuests = Number.isFinite(event.spots) ? event.spots : 0;
const confirmedGuests = participants.length;
const freePlaces = Math.max(0, totalGuests - confirmedGuests);
const isFull = freePlaces === 0;
const detailChips = [
`<span class="event-tag">${eventCategory}</span>`,
`<span class="event-tag">${dietLabel}</span>`,
...specifications.map(item => `<span class="event-tag">${item}</span>`)
].join('');
// Render complete detail page layout including:
// hero metadata, host card, menu, participants, gallery and sticky action bar.
detailContainer.innerHTML = `
<div class="detail-header">
<a href="event_overview.html" style="text-decoration:none; color:black; font-size:24px;"></a>
<h1>${event.title}</h1>
</div>
<div class="detail-grid">
<div class="info-section">
<p>📍 ${event.location} | 📅 ${event.date} | 👤 Max. ${event.spots} Personen</p>
<hr>
<p>Hier kommen die detaillierten Infos zu ${event.title} hin...</p>
</div>
<div class="image-section">
<img src="${event.image}" alt="${event.title}" style="width:100%;">
<div class="detail-page">
<a class="detail-back" href="event_overview.html">
<span aria-hidden="true">&lsaquo;</span>
Alle Events
</a>
<section class="detail-hero">
<div class="detail-top-row">
<span class="event-location">
<img src="${locationIconPath}" alt="">
${event.location}
</span>
<p class="event-date-time">${displayDate} | ${displayTime} | ${confirmedGuests}/${totalGuests} Gaeste</p>
</div>
<h1 class="detail-title">${event.title}</h1>
<div class="event-meta-row detail-chip-row">
${detailChips}
</div>
</section>
<section class="detail-content-grid">
<div class="detail-side-stack">
<article class="host-card detail-panel">
<header class="host-header">
<span class="host-avatar">${hostInitial}</span>
<span class="host-name">${hostName}</span>
<span class="host-role">Host</span>
</header>
${hostMessage.map(paragraph => `<p>${paragraph}</p>`).join('')}
</article>
<article class="detail-panel detail-panel-compact">
<h2 class="detail-section-title">Menue</h2>
<ul class="detail-menu-list">
${menuItems.map(item => `<li>${item}</li>`).join('')}
</ul>
</article>
<article class="detail-panel detail-panel-compact">
<div class="detail-participants-head">
<h2 class="detail-section-title">Teilnehmer</h2>
<a href="#" class="detail-participants-link">Alle ansehen</a>
</div>
<div class="detail-avatar-row">
${visibleParticipants.map(name => `<span class="participant-avatar">${name.charAt(0).toUpperCase()}</span>`).join('')}
${remainingParticipants > 0 ? `<span class="participant-more">+${remainingParticipants}</span>` : ''}
</div>
</article>
</div>
<div class="detail-gallery detail-gallery-large">
${galleryImages.slice(0, 9).map((img, index) => `
<button class="detail-gallery-item" type="button" aria-label="Bild ${index + 1} gross anzeigen" data-fullsrc="${img}">
<img src="${img}" alt="${event.title} Bild ${index + 1}" class="detail-gallery-image">
</button>
`).join('')}
</div>
</section>
<section class="detail-action-bar">
<div class="detail-action-summary">
<small class="detail-action-meta">
<span class="event-location detail-action-location">
<img src="${locationIconPath}" alt="">
${event.location}
</span>
<span class="detail-action-meta-text">| ${displayDate} | ${displayTime} | ${confirmedGuests}/${totalGuests} Gaeste</span>
</small>
<strong>${event.title}</strong>
</div>
<div class="detail-action-buttons">
<span class="detail-spots-pill${isFull ? ' detail-spots-pill-full' : ''}">
${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plaetze frei`}
</span>
<button class="detail-primary-btn" type="button" ${isFull ? 'disabled' : ''}>
Anmelden
</button>
</div>
</section>
<div class="detail-lightbox" aria-hidden="true">
<div class="detail-lightbox-backdrop" data-close-lightbox="true"></div>
<figure class="detail-lightbox-content" role="dialog" aria-modal="true" aria-label="Bildansicht">
<button class="detail-lightbox-close" type="button" aria-label="Schliessen">&times;</button>
<img class="detail-lightbox-image" src="" alt="Grossansicht Eventbild">
</figure>
</div>
</div>
`;
// ---------------------------------------------------------
// Lightbox behavior for gallery images:
// open on image click, close via backdrop, close button or ESC.
// ---------------------------------------------------------
const lightbox = detailContainer.querySelector('.detail-lightbox');
const lightboxImage = detailContainer.querySelector('.detail-lightbox-image');
const lightboxClose = detailContainer.querySelector('.detail-lightbox-close');
const galleryButtons = detailContainer.querySelectorAll('.detail-gallery-item');
// Central close helper to keep all close paths consistent.
function closeLightbox() {
if (!lightbox) {
return;
}
lightbox.classList.remove('is-open');
lightbox.setAttribute('aria-hidden', 'true');
}
if (lightbox && lightboxImage) {
// Open with selected image source.
galleryButtons.forEach(button => {
button.addEventListener('click', () => {
const imageSrc = button.getAttribute('data-fullsrc');
if (!imageSrc) {
return;
}
lightboxImage.src = imageSrc;
lightbox.classList.add('is-open');
lightbox.setAttribute('aria-hidden', 'false');
});
});
// Close when user clicks on backdrop.
lightbox.addEventListener('click', event => {
const target = event.target;
if (target instanceof HTMLElement && target.hasAttribute('data-close-lightbox')) {
closeLightbox();
}
});
// Close via dedicated icon/button.
lightboxClose?.addEventListener('click', closeLightbox);
// Close with keyboard for accessibility.
document.addEventListener('keydown', event => {
if (event.key === 'Escape') {
closeLightbox();
}
});
}
}
});

View File

@ -1,111 +1,251 @@
document.addEventListener('DOMContentLoaded', () => {
// -------------------------------------------------------------
// DOM references used throughout the page lifecycle.
// -------------------------------------------------------------
const eventGrid = document.getElementById('event-grid');
const filterButtons = document.querySelectorAll('.category-item');
let allEvents = [];
const locationFilter = document.getElementById('location-filter');
const dateFilter = document.getElementById('date-filter');
const locationIconPath = 'assets/location-pin.svg';
// 1. Daten laden aus JSOn file
// -------------------------------------------------------------
// In-memory state for fetched events and currently active category.
// -------------------------------------------------------------
let allEvents = [];
let activeCategory = 'ALLE';
// -------------------------------------------------------------
// Initial data bootstrap:
// 1) fetch JSON,
// 2) populate select options,
// 3) restore filter state from sessionStorage,
// 4) render filtered list.
// -------------------------------------------------------------
async function fetchEvents() {
try {
// Pfad zu JSON File angepasst an lokale Ordnerstruktur
const response = await fetch('data/events.json');
allEvents = await response.json();
renderEvents(allEvents);
populateMetaFilters();
// Beim Laden prüfen, ob ein Filter gespeichert war
const savedFilter = sessionStorage.getItem('activeFilter') || 'ALLE';
applyFilter(savedFilter);
const savedCategory = sessionStorage.getItem('activeFilter') || 'ALLE';
const savedLocation = sessionStorage.getItem('activeLocation') || 'ALLE_ORTE';
const savedDate = sessionStorage.getItem('activeDate') || '';
//checked ob Fehler beim Laden oder Parsen der Daten auftreten
activeCategory = savedCategory;
if (locationFilter) {
locationFilter.value = hasOption(locationFilter, savedLocation) ? savedLocation : 'ALLE_ORTE';
}
if (dateFilter) {
dateFilter.value = savedDate;
}
applyFilters();
} catch (error) {
console.error("Fehler:", error);
eventGrid.innerHTML = "<p>Events konnten nicht geladen werden.</p>";
console.error('Fehler:', error);
eventGrid.innerHTML = '<p>Events konnten nicht geladen werden.</p>';
}
}
// Funktion um Filter anzuwenden und gleichzeitig UI zu aktualisieren
function applyFilter(category) {
// UI: Aktiven Button stylen
// Build location options dynamically from loaded events.
function populateMetaFilters() {
const locations = [...new Set(allEvents.map(event => event.location))].sort();
if (locationFilter) {
locations.forEach(location => {
const option = document.createElement('option');
option.value = location;
option.textContent = location;
locationFilter.appendChild(option);
});
}
}
// Convert localized event date (e.g. 19. MÄR. 2026) into ISO format for date input comparison.
function parseEventDateToIso(dateString) {
const months = {
JAN: '01',
FEB: '02',
'MÄR': '03',
MRZ: '03',
APR: '04',
MAI: '05',
JUN: '06',
JUL: '07',
AUG: '08',
SEP: '09',
OKT: '10',
NOV: '11',
DEZ: '12'
};
const match = dateString.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
if (!match) {
return '';
}
const day = String(match[1]).padStart(2, '0');
const month = months[match[2]];
const year = match[3];
return month ? `${year}-${month}-${day}` : '';
}
// Convert short month notation into full German month label for UI display.
function formatEventDate(dateString) {
const labels = {
JAN: 'Januar',
FEB: 'Februar',
'MÄR': 'März',
MRZ: 'März',
APR: 'April',
MAI: 'Mai',
JUN: 'Juni',
JUL: 'Juli',
AUG: 'August',
SEP: 'September',
OKT: 'Oktober',
NOV: 'November',
DEZ: 'Dezember'
};
const match = dateString.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
if (!match) {
return dateString;
}
const day = Number(match[1]);
const monthLabel = labels[match[2]];
const year = match[3];
return monthLabel ? `${day}. ${monthLabel} ${year}` : dateString;
}
// Normalize time label from UHR to Uhr for consistent typography.
function formatEventTime(timeString) {
return timeString.replace('UHR', 'Uhr').trim();
}
// Safely verify whether a value exists in the given select element.
function hasOption(selectElement, value) {
return Array.from(selectElement.options).some(option => option.value === value);
}
// Apply all filters together (category, location, date), update button state, render and persist.
function applyFilters() {
const selectedLocation = locationFilter ? locationFilter.value : 'ALLE_ORTE';
const selectedDate = dateFilter ? dateFilter.value : '';
filterButtons.forEach(btn => {
if (btn.getAttribute('data-cat') === category) {
if (btn.getAttribute('data-cat') === activeCategory) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// Daten filtern
const filtered = category === 'ALLE'
? allEvents
: allEvents.filter(e => e.category === category);
const filtered = allEvents.filter(event => {
const categoryMatch = activeCategory === 'ALLE' || event.category === activeCategory;
const locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation;
const eventDateIso = parseEventDateToIso(event.date);
const dateMatch = !selectedDate || eventDateIso === selectedDate;
return categoryMatch && locationMatch && dateMatch;
});
renderEvents(filtered);
//Filter im Browser merken
sessionStorage.setItem('activeFilter', category);
sessionStorage.setItem('activeFilter', activeCategory);
sessionStorage.setItem('activeLocation', selectedLocation);
sessionStorage.setItem('activeDate', selectedDate);
}
// 2. Events rendern + "Empty State" Logik
function renderEvents(events) {
eventGrid.innerHTML = '';
// PRÜFUNG: Wenn keine Events vorhanden sind zeigt folgende Nachricht
// Render either:
// - empty state call-to-action when no results match,
// - or event cards with status and metadata.
function renderEvents(events) {
eventGrid.innerHTML = '';
if (events.length === 0) {
eventGrid.innerHTML = `
<div class="empty-state">
<h3>Schade! Aktuell gibt es hier keine Events.</h3>
<p>Möchtest du vielleicht selbst Gastgeber sein?</p>
<a href="event_create.html" style="text-decoration: none;">
<button class="btn-outline">Eigenes Event erstellen</button>
<p class="empty-state-kicker">Keine Treffer</p>
<h3>Schade, aktuell gibt es hier keine Events.</h3>
<p>Starte dein eigenes Dinner und bringe die Community an deinen Tisch.</p>
<a class="empty-state-link" href="event_create.html">
<button class="empty-state-btn" type="button">Event erstellen</button>
</a>
</div>
`;
return;
}
// Wenn Events da sind, Karten bauen
events.forEach(event => {
// Card shell and click-through navigation to detail page.
const card = document.createElement('article');
card.className = 'event-card';
//Klick auf die gesamte Karte leitet zur Detailseite weiter
card.style.cursor = "pointer";
card.style.cursor = 'pointer';
card.onclick = () => {
window.location.href = `event_detail.html?id=${event.id}`;
};
//internes HTML im Js zur Styling der Event-Karte (HIER CHECKEN OB SO OK NACH CLEAN CODE)
const displayDate = formatEventDate(event.date);
const displayTime = formatEventTime(event.time);
// Capacity logic:
// spots = total capacity, participants.length = booked seats.
const bookedSeats = event.participants ? event.participants.length : 0;
const totalCapacity = event.spots;
const freePlaces = Math.max(0, totalCapacity - bookedSeats);
const isFull = freePlaces === 0;
// Build optional specification chips only when data exists.
const specsChips = event.specifications && event.specifications.length > 0
? event.specifications.map(spec => `<span class="event-tag">${spec}</span>`).join('')
: '';
card.innerHTML = `
<div class="event-image" style="background-image: url('${event.image}')"></div>
<div class="event-content">
<small>📍 ${event.location}</small>
<h2 style="margin: 5px 0;">${event.title}</h2>
<div>
<span class="tag">${event.cuisine}</span>
<span class="tag">${event.category}</span>
<div class="event-main">
<div class="event-top-row">
<span class="event-location">
<img src="${locationIconPath}" alt="">
${event.location}
</span>
<p class="event-date-time">${displayDate} | ${displayTime} | ${bookedSeats}/${totalCapacity} Gaeste</p>
</div>
<h2 class="event-title">${event.title}</h2>
<div class="event-meta-row">
<span class="event-tag">${event.category}</span>
<span class="event-tag">${event.diet}</span>
${specsChips}
</div>
</div>
<div class="event-info">
<strong>${event.date}</strong><br>
<span>🕒 ${event.time}</span>
<div class="event-side${isFull ? ' event-side-full' : ''}">
<span class="event-spots${isFull ? ' event-spots-full' : ''}">${isFull ? 'AUSGEBUCHT' : `${freePlaces} Plätze FREI`}</span>
${isFull ? '' : '<button class="btn-primary" type="button">Anmelden</button>'}
</div>
<div class="event-cta">
<div style="font-size: 12px; margin-bottom: 8px;">🥗 ${event.diet}</div>
<button class="btn-primary">Anmelden</button>
<p style="font-size: 10px; margin-top: 8px;">${event.spots} PLÄTZE FREI</p>
</div>
<div style="text-align: center;"></div>
`;
eventGrid.appendChild(card);
});
}
// 3. Filter-Logik basic anhand der Kategorien im JSON File
// Category filter interactions.
filterButtons.forEach(button => {
button.addEventListener('click', () => {
const selectedCat = button.getAttribute('data-cat');
applyFilter(selectedCat);
activeCategory = button.getAttribute('data-cat');
applyFilters();
});
});
// Secondary filter interactions.
if (locationFilter) {
locationFilter.addEventListener('change', applyFilters);
}
if (dateFilter) {
dateFilter.addEventListener('change', applyFilters);
}
// Kick off initial load/render cycle.
fetchEvents();
});
});