Compare commits

...

No commits in common. "main" and "feature-estelle-köhler" have entirely different histories.

58 changed files with 271 additions and 9680 deletions

160
.gitignore vendored
View File

@ -52,20 +52,6 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
@ -468,3 +454,149 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -1,8 +0,0 @@
{
"chat.tools.terminal.autoApprove": {
"git remote": true,
"git push": true,
"ssh": true,
"git add": true
}
}

View File

@ -1,3 +1,3 @@
# Social_Cooking_Team
# msc-uxd-fs26-test
Hier im Readme schreiben wir etwas dazu, wie/ob wir KI benutzen.
Test-Repository für Unterrichtszwecke

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 24 24">
<!-- Generator: Adobe Illustrator 30.3.0, SVG Export Plug-In . SVG Version: 2.1.3 Build 182) -->
<defs>
<style>
.st0 {
fill: #6b6b05;
}
</style>
</defs>
<path class="st0" d="M9,1v2h6V1h2v2h4c.55,0,1,.45,1,1v16c0,.55-.45,1-1,1H3c-.55,0-1-.45-1-1V4c0-.55.45-1,1-1h4V1h2ZM20,11H4v8h16v-8ZM7,5h-3v4h16v-4h-3v2h-2v-2h-6v2h-2v-2Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 497 B

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 36 36">
<!-- Generator: Adobe Illustrator 30.3.0, SVG Export Plug-In . SVG Version: 2.1.3 Build 182) -->
<defs>
<style>
.st0 {
fill: #6b6b05;
}
</style>
</defs>
<path class="st0" d="M6,33c0-6.63,5.37-12,12-12s12,5.37,12,12h-3c0-4.97-4.03-9-9-9s-9,4.03-9,9h-3ZM18,19.5c-4.97,0-9-4.03-9-9S13.03,1.5,18,1.5s9,4.03,9,9-4.03,9-9,9ZM18,16.5c3.32,0,6-2.68,6-6s-2.68-6-6-6-6,2.68-6,6,2.68,6,6,6Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 553 B

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 24 24">
<!-- Generator: Adobe Illustrator 30.3.0, SVG Export Plug-In . SVG Version: 2.1.3 Build 182) -->
<defs>
<style>
.st0 {
fill: #6b6b05;
}
</style>
</defs>
<path class="st0" d="M12,20.9l4.95-4.95c2.73-2.73,2.73-7.17,0-9.9-2.73-2.73-7.17-2.73-9.9,0-2.73,2.73-2.73,7.17,0,9.9l4.95,4.95ZM12,23.73l-6.36-6.36c-3.51-3.51-3.51-9.21,0-12.73,3.51-3.51,9.21-3.51,12.73,0,3.51,3.51,3.51,9.21,0,12.73l-6.36,6.36ZM12,13c1.1,0,2-.9,2-2s-.9-2-2-2-2,.9-2,2,.9,2,2,2ZM12,15c-2.21,0-4-1.79-4-4s1.79-4,4-4,4,1.79,4,4-1.79,4-4,4Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

View File

@ -1,9 +0,0 @@
<svg width="104" height="50" viewBox="0 0 104 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M83.0693 30.3941C83.0693 28.5407 83.4727 26.9102 84.2794 25.5025C85.1121 24.0948 86.283 22.9922 87.7923 22.1945C89.3276 21.3968 91.1491 20.998 93.2569 20.998C94.8963 20.998 96.3665 21.2678 97.6676 21.8074C98.9947 22.347 100.036 23.0743 100.79 23.9893C101.545 24.8808 101.922 25.8661 101.922 26.9453C101.922 28.0715 101.558 28.8692 100.829 29.3384C100.101 29.8076 99.0207 30.0422 97.5895 30.0422H89.3926C89.3926 30.6522 89.6529 31.1566 90.1733 31.5554C90.7198 31.9543 91.6305 32.1537 92.9056 32.1537C93.5561 32.1537 94.1546 32.095 94.7011 31.9777C95.2736 31.8604 95.8461 31.7549 96.4185 31.661C97.017 31.5437 97.6806 31.4851 98.4092 31.4851C99.4501 31.4851 100.309 31.8252 100.985 32.5056C101.662 33.1625 102 34.0775 102 35.2506C102 36.6348 101.207 37.714 99.6192 38.4882C98.0579 39.2624 96.0022 39.6495 93.4521 39.6495C91.6565 39.6495 89.9651 39.3328 88.3778 38.6993C86.8165 38.0659 85.5414 37.0688 84.5526 35.7081C83.5637 34.3239 83.0693 32.5525 83.0693 30.3941ZM95.716 28.8809C96.1063 28.8809 96.2364 28.7167 96.1063 28.3882C96.0022 28.0597 95.6899 27.7196 95.1695 27.3676C94.6491 27.0157 93.8944 26.8398 92.9056 26.8398C91.7606 26.8398 90.9019 27.0627 90.3294 27.5084C89.783 27.9542 89.5097 28.4117 89.5097 28.8809H95.716ZM90.6027 15.7896C91.3313 15.3908 92.0599 15.0154 92.7885 14.6635C93.5171 14.2881 94.3628 13.8541 95.3256 13.3614C96.3145 12.8687 97.2643 12.7045 98.175 12.8687C99.0858 13.0095 99.7493 13.5139 100.166 14.382C100.608 15.25 100.673 16.036 100.361 16.7398C100.049 17.4202 99.359 17.9598 98.2921 18.3586C97.3553 18.7105 96.3014 19.039 95.1305 19.344C93.9855 19.649 93.0097 19.8601 92.203 19.9774C91.5785 20.0713 90.993 20.0243 90.4465 19.8366C89.9001 19.6255 89.4967 19.2149 89.2365 18.6049C89.0023 17.995 89.0153 17.4553 89.2755 16.9861C89.5358 16.5169 89.9781 16.1181 90.6027 15.7896Z" fill="#CD4918"/>
<path d="M75.1806 39.6495C73.8014 39.6495 72.5524 39.368 71.4334 38.8049C70.3405 38.2418 69.4818 37.4442 68.8573 36.4119C68.2327 35.3561 67.9205 34.1244 67.9205 32.7168V26.5231L66.4763 26.699C65.6956 26.7929 65.006 26.5465 64.4075 25.96C63.809 25.3735 63.5098 24.6344 63.5098 23.7429C63.5098 22.8514 63.809 22.1241 64.4075 21.561C65.006 20.9745 65.6956 20.7282 66.4763 20.822L67.9205 20.998L67.8814 18.6049C67.8554 17.6665 68.1547 16.9275 68.7792 16.3879C69.4037 15.8248 70.3145 15.5433 71.5115 15.5433C72.7085 15.5433 73.6323 15.8248 74.2828 16.3879C74.9334 16.9275 75.2326 17.6665 75.1806 18.6049L75.1025 20.998L77.1712 20.7516C78.5764 20.5639 79.6433 20.7986 80.3719 21.4555C81.1005 22.0889 81.4648 22.8514 81.4648 23.7429C81.4648 24.6344 81.1005 25.4086 80.3719 26.0656C79.6693 26.699 78.6415 26.9336 77.2883 26.7694L75.1025 26.5231V31.7666C75.1025 32.0716 75.2196 32.3531 75.4538 32.6112C75.688 32.8458 75.9742 32.9162 76.3125 32.8223C76.7028 32.7285 77.1062 32.5291 77.5225 32.2241C77.9649 31.9191 78.5244 31.7666 79.2009 31.7666C80.2158 31.7666 80.9964 32.095 81.5429 32.752C82.0894 33.4089 82.3626 34.2183 82.3626 35.1802C82.3626 35.9309 82.0633 36.6582 81.4648 37.3621C80.8924 38.0424 80.0727 38.5938 79.0058 39.0161C77.9389 39.4384 76.6638 39.6495 75.1806 39.6495Z" fill="#CD4918"/>
<path d="M54.709 17.0565C54.709 16.0477 55.0473 15.2383 55.7238 14.6283C56.4264 13.9948 57.3892 13.6781 58.6123 13.6781C59.9134 13.6781 60.8762 13.9831 61.5007 14.5931C62.1512 15.2031 62.4765 16.0125 62.4765 17.0213C62.4765 18.0067 62.1512 18.8043 61.5007 19.4143C60.8762 20.0009 59.9134 20.2941 58.6123 20.2941C57.3372 20.2941 56.3614 20.0009 55.6848 19.4143C55.0343 18.8043 54.709 18.0184 54.709 17.0565ZM58.6123 39.5439C57.3892 39.5439 56.4785 39.2389 55.88 38.6289C55.2815 38.0189 54.9822 37.2565 54.9822 36.3415V24.306C54.9822 23.2971 55.2815 22.4995 55.88 21.9129C56.4785 21.3264 57.3892 21.0331 58.6123 21.0331C59.8353 21.0331 60.7461 21.3264 61.3446 21.9129C61.9691 22.4995 62.2813 23.2737 62.2813 24.2356V36.3415C62.2813 37.2565 61.9561 38.0189 61.3055 38.6289C60.681 39.2389 59.7832 39.5439 58.6123 39.5439Z" fill="#CD4918"/>
<path d="M44.0485 39.5439C42.3571 39.5439 40.9129 39.0982 39.7159 38.2067C38.5189 37.3151 37.53 35.8371 36.7494 33.7725L33.6658 25.7488C33.2494 24.6931 33.2494 23.7195 33.6658 22.8279C34.1081 21.9364 34.9148 21.3734 36.0858 21.1387C37.1267 20.9276 38.0374 21.0214 38.8181 21.4203C39.6248 21.8191 40.1973 22.4878 40.5355 23.4262L43.5801 31.4851C43.9444 32.4235 44.3868 33.1156 44.9072 33.5614C45.4537 34.0071 46.1172 34.1127 46.8979 33.8781C47.6265 33.62 48.0168 33.1977 48.0689 32.6112C48.1469 32.0012 48.0038 31.2974 47.6395 30.4997C47.3273 29.7724 46.9109 29.1507 46.3905 28.6346C45.87 28.1184 45.4016 27.5788 44.9853 27.0157C44.5689 26.4527 44.3608 25.7488 44.3608 24.9042C44.3608 23.6843 44.7901 22.7341 45.6488 22.0537C46.5336 21.3499 47.6916 21.0214 49.1228 21.0684C50.3458 21.0918 51.3476 21.4789 52.1283 22.2297C52.9089 22.9804 53.2993 23.9306 53.2993 25.0802C53.2993 25.7137 53.1822 26.394 52.948 27.1213C52.7138 27.8486 52.4536 28.6111 52.1673 29.4088L50.606 33.8429C50.1897 35.016 49.7213 36.0365 49.2008 36.9046C48.7064 37.7492 48.0559 38.4061 47.2492 38.8753C46.4685 39.3211 45.4016 39.5439 44.0485 39.5439Z" fill="#CD4918"/>
<path d="M16.3409 39.544C15.1959 39.544 14.2721 39.239 13.5695 38.629C12.893 37.9956 12.5547 37.2213 12.5547 36.3064V24.658C12.5547 23.6726 12.88 22.8397 13.5305 22.1593C14.1811 21.479 15.1178 21.1388 16.3409 21.1388C17.5379 21.1388 18.4486 21.479 19.0732 22.1593C19.6977 22.8397 20.0099 23.696 20.0099 24.7283V26.4879H20.7125C20.7125 25.1506 20.9728 24.0949 21.4932 23.3207C22.0397 22.5465 22.7292 21.9951 23.5619 21.6667C24.3946 21.3148 25.2273 21.1388 26.06 21.1388C28.0637 21.1388 29.5339 21.7957 30.4707 23.1095C31.4075 24.3999 31.8759 26.2416 31.8759 28.6346V36.3064C31.8759 37.2213 31.5376 37.9956 30.8611 38.629C30.1845 39.239 29.2737 39.544 28.1288 39.544C26.9578 39.544 26.021 39.239 25.3184 38.629C24.6418 37.9956 24.3036 37.2213 24.3036 36.3064V30.0775C24.3036 29.2329 24.1214 28.5759 23.7571 28.1067C23.4188 27.6375 22.9114 27.4029 22.2348 27.4029C21.4542 27.4029 20.8817 27.661 20.5174 28.1771C20.1791 28.6933 20.0099 29.3502 20.0099 30.1478V36.3064C20.0099 37.2213 19.6847 37.9956 19.0341 38.629C18.3836 39.239 17.4858 39.544 16.3409 39.544Z" fill="#CD4918"/>
<path d="M6.05941 39.6144C4.8624 39.6144 3.88658 39.2859 3.13195 38.629C2.37732 37.9721 2 37.1392 2 36.1304V17.5141C2 16.4818 2.36431 15.6137 3.09292 14.9099C3.84755 14.206 4.83638 13.8541 6.05941 13.8541C7.3605 13.8541 8.37535 14.206 9.10397 14.9099C9.8586 15.5902 10.2359 16.4348 10.2359 17.4437V36.1304C10.2359 37.1392 9.83258 37.9721 9.0259 38.629C8.24524 39.2859 7.25641 39.6144 6.05941 39.6144Z" fill="#CD4918"/>
<path d="M19.9137 11.7901C20.0941 12.142 19.9271 12.5614 19.5405 12.7257C19.4344 12.7707 19.323 12.7913 19.2148 12.7913C18.9231 12.7913 18.6448 12.6412 18.5128 12.385C17.8511 11.0957 17.5027 9.53236 17.5027 7.86486C17.5027 7.47638 17.849 7.16108 18.2758 7.16108C18.7025 7.16108 19.0489 7.47638 19.0489 7.86486C19.0489 9.30809 19.355 10.7016 19.9137 11.7901ZM24.4595 7.16108C24.0327 7.16108 23.6864 7.47638 23.6864 7.86486C23.6864 9.30809 23.3792 10.7016 22.8205 11.7901C22.6401 12.142 22.8071 12.5614 23.1937 12.7257C23.2999 12.7707 23.4112 12.7913 23.5194 12.7913C23.8111 12.7913 24.0894 12.6412 24.2214 12.385C24.8832 11.0957 25.2316 9.53236 25.2316 7.86486C25.2316 7.47638 24.8852 7.16108 24.4585 7.16108H24.4595ZM30.3866 8.33405C30.3866 9.38504 29.9722 10.4032 29.2187 11.2008C28.8105 11.6325 28.325 12.3278 28.325 13.1827V17.7178C28.325 19.1414 27.054 20.2984 25.4903 20.2984H17.2439C15.6802 20.2984 14.4092 19.1414 14.4092 17.7178V13.1827C14.4092 12.3278 13.9227 11.6334 13.5155 11.2008C12.7631 10.4022 12.3477 9.3841 12.3477 8.33405C12.3477 6.11291 14.1052 4.28026 16.4904 3.94807C17.5965 2.72255 19.3911 2 21.3671 2C23.3431 2 25.1378 2.72255 26.2438 3.94807C28.628 4.28026 30.3866 6.11291 30.3866 8.33405ZM26.7788 17.7178V16.5449H15.9554V17.7178C15.9554 18.3644 16.5337 18.8908 17.2439 18.8908H25.4903C26.2005 18.8908 26.7788 18.3644 26.7788 17.7178ZM28.8404 8.33405C28.8404 6.74444 27.5189 5.44479 25.7665 5.31341C25.5387 5.29558 25.3326 5.18861 25.1996 5.02064C24.4224 4.02596 22.9535 3.40851 21.3671 3.40851C19.7807 3.40851 18.3118 4.02596 17.5346 5.02064C17.4016 5.18955 17.1955 5.29652 16.9677 5.31341C15.2153 5.44572 13.8938 6.74444 13.8938 8.33405C13.8938 9.05848 14.1691 9.73317 14.6886 10.284C15.5174 11.1633 15.9554 12.1655 15.9554 13.1827V15.1373H26.7788V13.1827C26.7788 12.1655 27.2169 11.1623 28.0456 10.284C28.5662 9.73317 28.8404 9.05848 28.8404 8.33405Z" fill="#CD4918"/>
</svg>

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 604 KiB

View File

@ -1,615 +0,0 @@
:root {
--max-width: 1120px;
--content-width: 760px;
--header-height: 4.5rem;
--control-min-height: 3rem;
--input-min-height: 3.5rem;
--card-min-height: 6rem;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
.event-flow-header {
display: flex;
justify-content: flex-start;
}
.event-form {
min-height: calc(100vh - var(--header-height) - 9rem);
display: flex;
flex-direction: column;
}
.step {
display: none;
}
/*
.submission-success {
padding: var(--space-24) 0 var(--space-48);
}*/
.submission-success-title-row {
display: flex;
align-items: center;
gap: var(--space-32);
width: 100%;
}
.submission-success-title-row h2 {
margin-bottom: 0;
}
.submission-success-icon {
width: 2.5rem;
height: 2.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--brown);
font-size: 3rem;
line-height: 1;
transform: translateY(0.4rem);
}
.submission-success-image {
object-position: 42% center;
}
.submission-success .intro-card--image {
width: min(100%, 40rem);
aspect-ratio: 4 / 5;
align-self: flex-start;
justify-self: center;
}
.step--active {
display: block;
}
.step-layout {
gap: 80px;
}
.startseite {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 80px;
}
.step-layout--intro {
min-height: 60vh;
align-content: center;
grid-template-columns: 1fr;
gap: var(--space-48);
}
.step-copy,
.step-fields,
.form-field,
fieldset {
display: grid;
}
.step-copy {
gap: var(--space-24);
align-content: start;
}
.step-fields {
gap: var(--space-32);
}
.form-field,
fieldset {
margin: 0;
padding: 0;
border: 0;
}
.step-text {
/* definiert Breite des Beschriebtexts der einzelnen Schritte*/
max-width: 100%;
}
.intro-card,
.review-card {
border: 1px solid var(--color-border);
background: var(--color-surface);
box-shadow: var(--shadow-interaction);
}
.intro-card {
padding: var(--space-40);
border-radius: 1.75rem;
background: linear-gradient(135deg, var(--color-surface), var(--color-surface-soft));
}
.intro-image {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
border-radius: var(--radius-lg);
}
.field-hint {
color: var(--olive);
font-size: 1rem;
margin-bottom: var(--space-8);
}
input[type="text"],
input[type="date"],
input[type="time"],
input[type="number"],
textarea {
font-family: var(--font-main);
font-weight: 400;
font-size: 1.125rem;
padding: var(--space-16) var(--space-20);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-md);
background: var(--butter-light);
transition: border-color 0.2s ease;
}
/* Blendet die Standard-Buttons für number inputs aus */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
textarea {
min-height: 9rem;
resize: vertical;
}
input[type="text"]:hover,
input[type="date"]:hover,
input[type="time"]:hover,
input[type="number"]:hover,
textarea:hover {
border: 2px solid var(--olive);
}
input[type="text"]:focus,
input[type="date"]:focus,
input[type="time"]:focus,
input[type="number"]:focus,
textarea:focus {
border: 2px solid var(--olive);
}
.field-invalid {
border-color: var(--error) !important;
box-shadow: var(--shadow-error);
}
.field-row {
display: grid;
gap: var(--space-24);
}
.option-grid {
display: grid;
gap: var(--space-16);
}
.option-card {
position: relative;
display: grid;
padding: var(--space-20);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-md);
background: var(--butter-light);
transition: box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
}
.option-card--with-icon {
justify-items: center;
align-content: center;
gap: var(--space-12);
color: var(--black);
}
.option-card__icon {
color: var(--black);
font-size: 1.25rem;
pointer-events: none;
}
.option-card--with-icon span {
pointer-events: none;
}
.option-card:hover {
background: var(--olive-light);
box-shadow: var(--shadow-interaction);
transform: translateY(-3px);
}
.option-grid--tomato-choices .option-card:hover,
.option-grid--tomato-choices .option-card:has(input:focus-visible),
.option-grid--tomato-choices .option-card:has(input:checked) {
background: var(--olive-light);
}
.option-grid--icon-choices .option-card--with-icon:has(input:disabled) {
opacity: 0.6;
}
.option-grid--icon-choices .option-card--with-icon:has(input:disabled):hover {
border-color: var(--olive-light);
background: var(--butter-light);
color: var(--black);
box-shadow: none;
transform: none;
}
.option-grid--icon-choices .option-card--with-icon:has(input:disabled):hover .option-card__icon {
color: var(--black);
}
.option-card input {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
.option-card:has(input:checked) {
border: 1.5px solid var(--olive-light);
background: var(--olive-light);
}
.option-card--invalid {
border-color: var(--error) !important;
box-shadow: var(--shadow-error);
}
.guest-count-icon {
display: block;
text-align: center;
width: 100%;
color: var(--black);
font-size: 1.5rem;
line-height: 1;
}
.counter {
display: inline-flex;
align-items: center;
gap: var(--space-16);
}
.counter-value-group {
display: grid;
justify-items: center;
row-gap: var(--space-8);
width: 6rem;
}
.counter input {
width: 100%;
height: 3rem;
padding-block: 0.75rem;
text-align: center;
}
.review-card {
display: grid;
gap: var(--space-24);
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.review-card--success {
display: grid;
gap: var(--space-32);
padding: var(--space-16) 0 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.review-list {
display: grid;
gap: var(--space-12);
margin: 0;
}
.review-item {
display: grid;
padding: 1rem 1.1rem;
border: 1.5px solid var(--olive-light);
border-radius: 1.125rem;
background: var(--butter-light);
cursor: pointer;
transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease, background-color 0.3s ease;
}
.review-item:hover,
.review-item:focus-visible {
border: 2px solid var(--olive);
box-shadow: var(--shadow-interaction);
transform: translateY(-3px);
}
.review-item dt {
font-weight: 600;
}
.review-item dd {
margin: 0;
}
.review-gallery {
display: flex;
flex-wrap: wrap;
gap: var(--space-8);
}
.review-gallery__thumb {
width: 4.5rem;
height: 4.5rem;
border-radius: var(--radius-sm);
object-fit: cover;
box-shadow: var(--shadow-interaction);
}
.submission-success-actions {
display: flex;
justify-content: flex-start;
width: 100%;
}
.flow-footer {
padding-top: var(--space-80);
backdrop-filter: none;
padding-bottom: env(safe-area-inset-bottom);
}
.flow-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.progress-wrap {
flex: 1;
position: relative;
display: flex;
align-items: center;
align-self: center;
min-height: 2.75rem;
}
.progress-label {
position: absolute;
top: -1.1rem;
left: 50%;
transform: translateX(-50%);
font-size: 1rem;
font-weight: 400;
color: var(--black);
white-space: nowrap;
text-align: center;
}
.progress {
flex: 1;
width: 100%;
height: 0.45rem;
background: var(--olive-light);
border-radius: var(--radius-sm);
overflow: hidden;
}
.progress-bar {
display: block;
width: 0;
height: 100%;
background: var(--olive);
border-radius: 999px;
transition: width 0.25s ease;
}
.flow-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-24);
width: 100%;
}
.flow-actions-right {
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
}
.error-message--callout {
position: absolute;
right: 0;
bottom: calc(100% + 1.25rem);
}
.button--ghost:hover {
background: rgba(0, 0, 0, 0.03);
}
.button--intro {
justify-self: start;
margin-top: var(--space-12);
}
.gallery-preview {
display: contents;
}
.gallery-add-button {
display: grid;
place-items: center;
align-content: center;
gap: var(--space-8);
padding: var(--space-20);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-md);
background: var(--butter-light);
font-family: var(--font-main);
cursor: pointer;
transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.gallery-add-button__icon {
color: var(--black);
font-size: 1.5rem;
line-height: 1;
}
.gallery-add-button__text {
color: var(--black);
font-size: 0.95rem;
line-height: 1.15;
text-align: center;
overflow-wrap: anywhere;
}
.gallery-add-button:hover {
background: var(--olive-light);
box-shadow: var(--shadow-interaction);
transform: translateY(-3px);
}
.gallery-thumb {
position: relative;
width: 7rem;
height: 7rem;
border-radius: var(--radius-sm, 0.5rem);
overflow: hidden;
flex-shrink: 0;
}
.gallery-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.gallery-thumb-remove {
position: absolute;
top: 0.2rem;
right: 0.2rem;
width: 1.4rem;
height: 1.4rem;
border: 0;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 0.85rem;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.site-footer {
width: min(100% - 2rem, var(--max-width));
margin: 0 auto;
padding: var(--space-32) 0 var(--space-40);
color: var(--color-muted);
text-align: center;
}
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: 3px solid var(--color-focus);
outline-offset: 3px;
}
@media (max-width: 767px) {
.site-nav {
flex-wrap: wrap;
padding: var(--space-16) 0;
}
.site-nav-links {
gap: var(--space-16);
}
.flow-actions,
.flow-actions-right {
flex-direction: column;
align-items: stretch;
}
.error-message--callout {
position: static;
width: 100%;
max-width: 100%;
margin-bottom: var(--space-12);
}
.progress-wrap {
min-height: auto;
}
.progress-label {
position: static;
transform: none;
}
.error-message--callout::after {
display: none;
}
.event-flow-header {
justify-content: flex-start;
}
}
@media (min-width: 768px) {
.step-layout--intro {
grid-template-columns: 1fr 1fr;
align-items: stretch;
gap: var(--space-8);
}
.field-row {
grid-template-columns: 1fr 1fr;
}
.option-grid--3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.option-grid--4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}

View File

@ -1,938 +0,0 @@
/* ---------------------------------------------------------
Shared Typography Tokens
Reuse common text styles across nav, controls and buttons
--------------------------------------------------------- */
.meta-filter select,
.meta-filter input[type="date"],
.detail-primary-btn {
font-family: var(--font-main);
font-size: 1.125rem;
line-height: 1;
color: var(--black);
}
/* Heading hierarchy: page title > detail title > card title > section title */
.overview-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-24);
}
.overview-title-row .overview-title {
margin-bottom: var(--space-24);
}
.detail-title {
margin-bottom: var(--space-24);
}
.detail-section-title {
margin: 0 0 10px;
color: var(--brown);
font-family: "Bagel Fat One", cursive;
font-size: 28px;
font-weight: 400;
line-height: 1.15;
}
/* ---------------------------------------------------------
Overview Header + Filters
--------------------------------------------------------- */
.filter-row {
display: flex;
justify-content: flex-start;
}
.category-group {
display: flex;
gap: var(--space-6);
margin-bottom: 0;
flex-wrap: wrap;
flex: 1;
}
.event-tag {
border: 1.5px solid var(--tomato);
color: var(--tomato);
border-radius: var(--radius-pill);
font-family: var(--font-main);
font-weight: 400;
font-size: 1rem;
line-height: 1;
padding: var(--space-8) var(--space-20);
}
.meta-filter-group {
display: flex;
flex-wrap: nowrap;
gap: var(--space-12);
margin-bottom: 0;
}
.meta-filter {
display: block;
}
.meta-filter span {
display: none;
}
.meta-filter select,
.meta-filter input[type="date"] {
border: 1.5px solid var(--olive);
border-radius: var(--radius-sm);
background: var(--butter-light);
height: 37px;
padding: 0 var(--space-24);
box-sizing: border-box;
}
.meta-filter select:focus,
.meta-filter input[type="date"]:focus {
border: 1.5px solid var(--olive-dark);
outline: 2px solid var(--butter);
}
.meta-filter input[type="date"] {
color-scheme: light;
accent-color: var(--olive-dark);
}
.meta-filter input[type="date"]::-webkit-calendar-picker-indicator {
cursor: pointer;
filter: invert(8%) sepia(5%) saturate(300%) hue-rotate(10deg) brightness(10%) contrast(95%);
transition: background-color 0.2s ease;
}
.meta-filter input[type="date"]:hover::-webkit-calendar-picker-indicator {
filter: brightness(0.8);
}
.meta-filter select {
cursor: pointer;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='currentColor' stroke-width='1.5' fill='none' stroke-linecap='butt' stroke-linejoin='miter'/%3E%3C/svg%3E");
background-repeat: no-repeat;
color: var(--black);
background-position: right var(--space-24) center;
}
/* ---------------------------------------------------------
Overview Event Cards
--------------------------------------------------------- */
.event-list {
display: flex;
flex-direction: column;
gap: var(--space-16);
}
.event-card {
/* Core card container for every overview event entry. */
background: var(--butter-light);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
padding: var(--space-32) var(--space-40);
display: flex;
gap: var(--space-40);
cursor: pointer;
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.event-card:hover {
box-shadow: var(--shadow-interaction);
transform: translateY(-3px);
}
.event-main {
min-width: 0;
flex: 1;
}
.event-location,
.event-date-time,
.event-gast {
display: inline-flex;
gap: var(--space-0);
}
.event-top-row {
/* Primary metadata line: location + date/time/guest counters. */
display: flex;
align-items: center;
gap: var(--space-24);
margin-bottom: var(--space-0);
flex-wrap: wrap;
}
.event-location {
display: inline-flex;
gap: var(--space-0);
}
.event-location .icon path{
width: 20px;
height: 20px;
flex: 0 0 20px;
display: block;
}
.event-meta-row {
display: flex;
align-items: center;
gap: var(--space-8);
flex-wrap: wrap;
}
.event-tag {
border: 1.5px solid var(--tomato);
color: var(--tomato);
border-radius: var(--radius-pill);
font-family: var(--font-main);
font-weight: 400;
font-size: 1rem;
line-height: 1;
padding: var(--space-8) var(--space-20);
}
.event-spec-chip {
display: none;
}
.event-specs {
display: none;
}
.event-capacity {
display: none;
}
.event-hints {
display: none;
}
.event-side {
/* Right-side action area: availability status and optional signup button. */
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
gap: var(--space-16);
padding-top: 36px;
flex-shrink: 0;
}
.button-plaetze {
border: none;
padding: var(--space-8) var(--space-24);
font-family: var(--font-main);
font-weight: 400;
font-size: 1.125rem;
color: var(--olive);
}
.event-spots {
color: var(--olive);
font-size: 18px;
white-space: nowrap;
}
.event-spots-full, .detail-spots-pill-full {
/* Sold-out visual state, intentionally high-contrast and always filled. */
border: 1.5px solid var(--tomato-light);
padding: var(--space-8) var(--space-24);
border-radius: var(--radius-pill);
color: var(--butter-light);
background: var(--tomato-light);
font-family: var(--font-main);
font-weight: 400;
font-size: 1.25rem;
cursor: not-allowed;
}
.btn-primary-register {
background: var(--olive);
}
.btn-primary-own,
.btn-primary-own:disabled {
background: var(--olive-light);
color: var(--black);
opacity: 1;
cursor: not-allowed;
}
/* ---------------------------------------------------------
Detail Page
--------------------------------------------------------- */
.detail-page {
display: grid;
gap: 14px;
}
.detail-hero {
display: grid;
margin-bottom: var(--space-40);
}
.detail-top-row {
display: inline-flex;
align-items: center;
gap: var(--space-32);
flex-wrap: wrap;
}
.detail-meta {
margin: 0;
font-size: 26px;
color: var(--brown);
}
.detail-chip-row {
margin-bottom: 0;
}
.detail-content-grid {
/* Two-column detail layout: content stack (left) + gallery (right). */
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-areas: "side gallery";
gap: var(--space-40);
align-items: stretch;
}
.detail-side-stack {
grid-area: side;
display: grid;
gap: var(--space-16);
align-content: start;
}
.host-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 30px;
}
.host-name {
margin-left: 12px;
font-weight: 600;
letter-spacing: var(--ls-sm);
}
/*
.host-role {
border: 1.5px solid var(--olive-light);
background: var(--olive-light);;
color: var(--olive);
border-radius: var(--radius-pill);
font-family: var(--font-main);
font-weight: 400;
font-size: 1rem;
line-height: 1;
letter-spacing: var(--ls-lg);
padding: var(--space-8) var(--space-20);
}*/
.detail-gallery {
grid-area: gallery;
display: grid;
}
.detail-gallery-large {
/* Editorial mosaic: first image spans two rows, side images stack vertically. */
grid-template-columns: 1fr 1fr;
grid-template-rows: repeat(2, minmax(220px, 1fr));
gap: var(--space-16);
min-height: 520px;
}
.detail-gallery-large img {
width: 100%;
height: 100%;
min-height: 220px;
object-fit: cover;
border-radius: var(--radius-md);
box-shadow: var(--shadow-interaction);
}
.detail-gallery-item {
display: block;
border: 0;
background: transparent;
padding: 0;
cursor: zoom-in;
}
.detail-gallery-image {
transition: transform 0.2s ease;
}
.detail-gallery-item:hover .detail-gallery-image,
.detail-gallery-item:focus-visible .detail-gallery-image {
transform: translateY(-3px);
}
.detail-gallery-large img:first-child {
grid-row: 1 / 3;
}
.detail-gallery-large--single {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
align-items: start;
}
.detail-gallery-large--single .detail-gallery-item {
grid-column: 1 / -1;
grid-row: auto;
align-self: start;
}
.detail-gallery-large--single .detail-gallery-image {
height: auto;
min-height: 0;
object-fit: contain;
object-position: top center;
background: var(--butter-light);
}
.detail-gallery-large--single img:first-child {
grid-row: auto;
}
.detail-lightbox {
/* Full-screen overlay for enlarged gallery image view. */
position: fixed;
inset: 0;
z-index: 200;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
}
.detail-lightbox.is-open {
display: flex;
}
.detail-lightbox-backdrop {
position: absolute;
z-index: 200;
inset: 0;
background: var(--black);
opacity: 0.6;
}
.detail-lightbox-content {
position: relative;
z-index: 300;
margin: 0;
max-width: min(96vw, 1100px);
max-height: 90vh;
}
.detail-lightbox-image {
display: block;
width: 100%;
max-height: 80vh;
object-fit: contain;
border-radius: var(--radius-md);
background: var(--black);
}
.detail-lightbox-close {
position: absolute;
top: -40px;
right: -40px;
border: 0;
background: transparent;
color: var(--butter-light);
font-size: 40px;
line-height: 1;
cursor: pointer;
}
.detail-panel {
background: var(--butter-light);
border-radius: var(--radius-md);
padding: var(--space-24) var(--space-32);
}
.detail-panel-compact {
padding: 12px 14px;
}
.detail-menu-list {
margin: 20px;
list-style: disc;
font-size: 1.125rem;
line-height: 1.45;
}
.detail-participants-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.detail-avatar-row {
display: flex;
align-items: center;
gap: 10px;
}
.participant-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--tomato);
color: var(--butter-light);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 500;
}
.participant-more {
font-size: 12px;
opacity: 0.7;
}
.detail-participants-full {
display: flex;
flex-direction: column;
}
.detail-participant-item {
display: flex;
align-items: center;
}
.participant-name {
font-size: 1.125rem;
font-weight: 400;
color: var(--black);
}
.detail-participants-link {
background: none;
border: none;
font-family: var(--font-main);
color: var(--olive);
font-size: 1rem;
font-weight: 400;
cursor: pointer;
padding: 0;
text-decoration: underline;
text-underline-offset: 3px;
}
.detail-participants-link:hover {
color: var(--olive-dark);
}
.detail-action-bar {
/* Sticky bottom CTA bar with summary and booking controls. */
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-16);
background: var(--white);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-md);
box-shadow: var(--shadow-interaction);
padding: var(--space-24) var(--space-32);
margin-top: var(--space-40);
position: sticky;
bottom: var(--space-40);
z-index: 100;
}
.detail-action-summary {
display: grid;
gap: 2px;
}
.detail-action-summary small {
font-size: 17px;
opacity: 0.75;
color: var(--brown);
}
.detail-action-meta {
display: inline-flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.detail-action-location,
.detail-action-meta-text {
color: var(--olive);
font-size: 1rem;
}
.detail-action-location img {
width: 18px;
height: 18px;
}
.detail-action-summary strong {
font-size: 40px;
font-family: "Bagel Fat One", cursive;
line-height: 1.2;
font-weight: 400;
color: var(--brown);
}
.detail-action-buttons {
display: flex;
align-items: center;
gap: var(--space-8);
}
.detail-spots-pill {
border: none;
padding: var(--space-8) var(--space-24);
font-family: var(--font-main);
font-weight: 400;
font-size: 1rem;
color: var(--olive);
}
.detail-spots-pill-full {
border: 1.5px solid var(--tomato-light);
color: var(--butter-light);
background: var(--tomato-light);
font-family: var(--font-main);
font-weight: 400;
font-size: 1.25rem;
cursor: not-allowed;
}
.detail-action-btn-wrap {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.detail-action-row {
display: flex;
gap: 12px;
align-items: center;
}
.detail-dereg-column {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
}
.detail-action-btn-wrap .button-plaetze,
.detail-action-btn-wrap .event-spots-full,
.detail-action-btn-wrap .button-primary,
.detail-action-btn-wrap .button-primary-abmelden,
.detail-action-btn-wrap .button-primary-eigener-event {
/* Force identical sizing and vertical alignment for action buttons in detail bar */
display: inline-flex;
align-items: center;
justify-content: center;
height: 52px;
padding: 0 22px;
font-size: 1.25rem;
border-radius: var(--radius-pill);
}
.detail-dereg-hint {
display: block;
font-size: 14px;
font-weight: 400;
color: var(--olive);
}
.detail-dereg-hint--placeholder {
visibility: hidden;
}
.detail-primary-btn {
border-radius: var(--radius-pill);
color: var(--white);
padding: 10px 22px;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.detail-primary-btn-register {
border: 2px solid var(--olive);
background: var(--olive);
}
.detail-primary-btn-register:not(:disabled):hover,
.detail-primary-btn-register:not(:disabled):focus-visible {
background: #575704;
border-color: #575704;
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(107, 107, 5, 0.28);
}
.detail-primary-btn-danger {
border: 2px solid var(--tomato);
background: var(--tomato);
}
.detail-primary-btn-danger:not(:disabled):hover,
.detail-primary-btn-danger:not(:disabled):focus-visible {
background: var(--tomato-dark);
border-color: var(--tomato-dark);
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(188, 74, 52, 0.28);
}
.detail-primary-btn:not(:disabled):active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(102, 52, 13, 0.22);
}
.detail-primary-btn-own,
.detail-primary-btn-own:disabled {
border-color: var(--olive-light);
background: var(--olive-light);
color: var(--black);
opacity: 1;
cursor: not-allowed;
}
/* ---------------------------------------------------------
Responsive: Tablet (<= 850px)
--------------------------------------------------------- */
@media (max-width: 850px) {
.top-nav {
padding-left: 16px;
min-height: 64px;
}
.brand {
height: 40px;
}
.brand img {
width: auto;
height: 100%;
max-width: 84px;
}
.overview-title {
font-size: 34px;
}
.overview-info-button {
width: 44px;
height: 44px;
flex-basis: 44px;
font-size: 1.5rem;
}
.top-nav-links {
gap: 8px;
}
.nav-link,
.login-pill {
font-size: 15px;
padding: 8px 14px;
}
.profile-pill {
width: 34px;
height: 34px;
border-radius: 17px;
font-size: 15px;
}
.meta-filter {
min-width: 155px;
}
.meta-filter select,
.meta-filter input[type="date"] {
font-size: 15px;
padding: 8px 14px;
min-height: 38px;
height: 38px;
}
.event-side-full {
justify-content: flex-end;
}
.btn-primary {
font-size: 17px;
padding: 8px 18px;
}
.detail-title {
font-size: 40px;
}
.detail-meta {
font-size: 18px;
}
.detail-top-row {
gap: 12px;
}
.detail-content-grid {
grid-template-columns: minmax(230px, 0.8fr) minmax(0, 1.2fr);
grid-template-areas: "side gallery";
gap: 12px;
}
.detail-side-stack {
gap: 10px;
}
.detail-gallery-large {
grid-template-columns: 1fr;
grid-template-rows: repeat(3, minmax(180px, auto));
min-height: auto;
}
.detail-gallery-large img {
min-height: 180px;
}
.detail-gallery-large img:first-child {
grid-row: auto;
}
.detail-gallery-large--single {
grid-template-columns: 1fr;
grid-template-rows: auto;
}
.detail-lightbox {
padding: 12px;
}
.detail-section-title {
font-size: 24px;
}
.detail-action-bar {
flex-direction: column;
align-items: stretch;
bottom: 10px;
}
.detail-action-summary strong {
font-size: 32px;
}
.detail-action-buttons {
flex-wrap: wrap;
}
}
/* ---------------------------------------------------------
Responsive: Mobile (<= 640px)
--------------------------------------------------------- */
@media (max-width: 640px) {
.top-nav {
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 10px;
padding-top: 8px;
padding-bottom: 8px;
}
.top-nav-wrap {
padding: 6px 12px;
}
.overview-title {
font-size: 30px;
}
.overview-title-row {
align-items: flex-start;
gap: 12px;
}
.overview-info-button {
width: 40px;
height: 40px;
flex-basis: 40px;
font-size: 1.35rem;
margin-top: 2px;
}
.top-nav-links {
width: auto;
justify-content: flex-end;
gap: 8px;
flex-wrap: nowrap;
}
.top-nav-links .nav-link.active {
display: none;
}
.filter-row {
flex-direction: row;
align-items: flex-end;
flex-wrap: wrap;
gap: 12px;
}
.detail-content-grid {
grid-template-columns: 1fr;
grid-template-areas:
"side"
"gallery";
}
.meta-filter-group {
width: auto;
flex-wrap: wrap;
}
.meta-filter {
flex: 1 1 160px;
min-width: 140px;
}
}
.filter-section {
display: flex;
flex-direction: column;
}
.filter-box summary {
cursor: pointer;
margin-bottom: var(--space-8);
}

View File

@ -1,575 +0,0 @@
/* ===========================================
INDEX.CSS Styles specific to index.html
Global styles (reset, variables, body, nav,
brand, typography) are in stylesheet_global.css
=========================================== */
/* --- Navigation overrides (index-specific) --- */
.nav-link {
border: 2px solid var(--olive-light);
transition: background-color 0.2s ease, color 0.2s ease;
}
.nav-link:hover,
.nav-link.active,
.nav-link:focus-visible {
background: var(--olive);
color: var(--white);
border-color: var(--olive);
}
.nav-link--login {
background: var(--olive);
color: var(--white);
border-color: var(--olive);
}
.nav-link--login:hover,
.nav-link--login:focus-visible {
background: var(--white);
color: var(--olive);
border-color: var(--olive);
}
/* --- Page layout --- */
.container {
width: min(100% - 4rem, 1200px);
margin: 0 auto;
}
/* --- Hero section --- */
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 80px;
margin-bottom: 100px;
}
.hero__right {
display: flex;
align-items: center;
justify-content: center;
}
.image-card {
width: 100%;
max-width: 436px;
overflow: hidden;
border-radius: var(--radius-lg);
}
.hero-image {
width: 100%;
max-width: 436px;
max-height: 510px;
height: auto;
border-radius: var(--radius-lg);
object-fit: cover;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12);
}
@media (max-width: 900px) {
.hero {
grid-template-columns: 1fr;
padding: 30px 0;
}
.hero__right {
order: -1;
}
.hero-image {
min-height: 320px;
}
}
/* --- "So funktioniert's" steps --- */
.how-it-works {
margin-bottom: 100px;
}
.how-it-works__header {
text-align: center;
margin-bottom: 32px;
}
.how-it-works__header h2 {
font-size: 2rem;
margin: 0;
color: var(--black);
}
.how-it-works__steps {
display: grid;
grid-template-columns: repeat(3, minmax(180px, 1fr));
gap: 20px;
}
.how-step {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space-40);
background: var(--butter-light);
border-radius: var(--radius-lg);
}
.how-step-number-numbered {
position: relative;
}
.how-step_corner-number {
position: absolute;
color: var(--butter-light);
background: var(--tomato);
border-radius: var(--radius-lg);
top: var(--space-20);
left: var(--space-20);
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Bagel Fat One';
font-size: 1.5rem;
font-weight: 400;
color: var(--butter-light);
}
.how-step_corner-number--brown {
color: var(--brown);
}
.how-step_icon {
font-size: 3.5rem;
color: var(--brown);
margin: var(--space-24) 0;
}
.how-step__png {
width: 192px;
height: 192px;
object-fit: contain;
}
.how-step__png--brown {
filter: brightness(0) saturate(100%) invert(18%) sepia(56%) saturate(2800%) hue-rotate(16deg) brightness(92%) contrast(95%);
}
.how-step_text {
margin-bottom: var(--space-24);
text-align: center;
}
.text-left{
text-align: left;
}
.how-step__footer-pill {
margin-bottom: 4px;
}
.how-step__footer-badges {
display: flex;
gap: var(--space-16);
justify-content: center;
}
.how-step__footer-banner {
width: 100%;
padding: 10px 16px;
background: var(--butter);
border-radius: var(--radius-sm);
text-align: center;
font-family: 'Bagel Fat One', sans-serif;
font-size: 0.85rem;
letter-spacing: 0.12rem;
color: var(--brown);
}
@media (max-width: 900px) {
.how-it-works__steps {
grid-template-columns: 1fr;
}
}
/* --- Carousel gallery --- */
.gallery {
margin-bottom: 100px;
}
.gallery__carousel {
display: grid;
grid-template-columns: 46px minmax(0, 1fr) 46px;
align-items: center;
column-gap: 6px;
margin-bottom: 30px;
}
.gallery__viewport {
overflow: hidden;
}
.gallery__track {
display: flex;
gap: 20px;
margin-bottom: 0;
}
.gallery__item {
flex: 0 0 calc((100% - 40px) / 3);
min-width: calc((100% - 40px) / 3);
border-radius: 24px;
overflow: hidden;
background: var(--white);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.06);
aspect-ratio: 2 / 3;
cursor: pointer;
}
.gallery__item img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
/* =========================================
NEW INSTAGRAM HOVER STYLES START HERE
========================================= */
.ig-post-wrapper {
position: relative;
width: 100%;
height: 100%; /* Ensures it fills the existing gallery__item */
aspect-ratio: 1 / 1;
overflow: hidden;
cursor: pointer;
}
.ig-post-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.ig-overlay {
position: absolute;
inset: 0;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-weight: 600;
font-size: 1.1rem;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.ig-post-wrapper:hover .ig-overlay {
opacity: 1;
}
.ig-overlay span {
display: flex;
align-items: center;
gap: 8px;
}
.gallery__arrow {
display: grid;
width: 46px;
height: 46px;
border: 0;
background: transparent;
border-radius: 999px;
font-family: var(--font-main);
font-weight: 400;
font-size: 2.45rem;
place-items: center;
z-index: 2;
color: var(--olive);
cursor: pointer;
text-decoration: none;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.gallery__arrow:hover,
.gallery__arrow:focus-visible {
color: var(--olive-light);
background: transparent;
transform: scale(1.1);
}
.gallery__arrow--prev {
justify-self: start;
}
.gallery__arrow--next {
justify-self: end;
}
/* --- Carousel dot indicators --- */
.gallery_dots {
display: flex;
justify-content: center;
gap: 10px;
padding: 12px 0 8px;
}
.gallery_dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid var(--olive);
background: transparent;
cursor: pointer;
padding: 0;
transition: background 0.25s ease, transform 0.2s ease;
}
.gallery_dot:not(.gallery_dot--active):hover {
background: var(--olive-light);
border-color: var(--olive-light);
transform: scale(1.2);
}
.gallery_dot--active {
background: var(--olive);
border-color: var(--olive);
transform: scale(1.2);
}
.gallery_dot:focus-visible {
outline: 2px solid var(--olive);
outline-offset: 3px;
}
@media (max-width: 900px) {
.gallery__carousel {
grid-template-columns: 38px minmax(0, 1fr) 38px;
column-gap: 5px;
}
.gallery__arrow {
width: 38px;
height: 38px;
font-size: 1.95rem;
}
.gallery__track {
gap: 16px;
}
.gallery__item {
flex: 0 0 100%;
min-width: 100%;
}
}
/* --- Gallery info (Instagram link) --- */
.gallery__info {
display: flex;
align-items: center;
gap: 10px;
}
.gallery__icon--instagram {
height: 32px;
width: 32px;
object-fit: contain;
border-radius: 8px;
background: none;
filter: brightness(0) saturate(100%) invert(27%) sepia(81%) saturate(749%) hue-rotate(24deg) brightness(90%) contrast(90%);
}
.gallery__icon--invite {
height: 56px;
width: 56px;
object-fit: contain;
margin-left: 0;
transform: translate(-4%, -1%);
position: relative;
}
/* --- CTA button --- */
.btn {
border: none;
background: var(--olive);
color: var(--white);
padding: 12px 22px;
font-weight: 700;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-pill);
font-size: 0.95rem;
transition: background-color 0.2s ease, transform 0.2s ease;
}
.btn:hover {
background-color: var(--olive-dark);
transform: translateY(-1px);
}
/* --- Footer --- */
.footer {
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
padding: 16px 24px;
border: none;
margin-top: 40px;
}
.footer-link {
color: var(--black);
text-decoration: underline;
font-size: 0.8rem;
font-weight: 400;
}
/* --- FAQ Section: Akkordion --- */
.faq-section {
margin-bottom: 0px;
}
.faq-accordion {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--space-8);
}
.faq-item {
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--butter-light);
padding: var(--space-12) var(--space-24) ;
transition: background-color 0.2s ease;
}
.faq-item:hover {
background: var(--olive-light);
}
.faq-trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
background: transparent;
border: none;
cursor: pointer;
font-family: var(--font-main);
font-size: 1.25rem;
font-weight: 400;;
text-align: left;
transition: background-color 0.2s ease;
}
.faq-title {
flex: 1;
font-weight: 400;
font-size: 1.25rem;
}
.faq-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 1.5rem;
font-weight: 400;
color: var(--black);
transition: transform 0.3s ease;
flex-shrink: 0;
}
.faq-trigger[aria-expanded="true"] .faq-icon {
transform: rotate(45deg);
}
.faq-content {
padding: 0;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.faq-content p {
margin: 0;
padding: var(--space-12) var(--space-40) var(--space-12) 0;
}
.faq-trigger[aria-expanded="true"] + .faq-content {
display: block;
max-height: 500px;
padding: var(--space-3) var(--space-24);
}
.faq-list {
padding-left: var(--space-24);
margin: var(--space-12) var(--space-24) var(--space-12) 0;
}
.faq-list li {
font-size: 1.125rem;
margin-bottom: var(--space-12)
}
/* --- Responsive: FAQ Section --- */
@media (max-width: 768px) {
.faq-section {
padding: var(--space-40) var(--space-24);
margin: var(--space-40) 0 var(--space-32);
}
.faq-trigger {
padding: var(--space-2) var(--space-3);
font-size: 1.125rem;
}
.faq-content {
padding: 0 var(--space-3);
font-size: 1rem;
}
.faq-content p {
padding: var(--space-2) 0;
}
}

View File

@ -1,220 +0,0 @@
/* ===========================================
LOGIN_SIGNUP.CSS Styles for login & signup
Global styles (reset, variables, body, nav,
typography) are in stylesheet_global.css
=========================================== */
.container-login {
background-color: var(--butter-light);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
padding: var(--space-40) var(--space-80) var(--space-80) var(--space-80);
}
.container-registration {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-64);
background-color: var(--butter-light);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
overflow: hidden;
}
.text-section {
padding: var(--space-40) var(--space-80) var(--space-80) var(--space-80);
}
/* --- Image section --- */
.image-section {
height: 100%;
display: flex;
}
.image-section img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.form-group.has-error {
margin-bottom: 0;
}
input[type="text"],
input[type="email"],
input[type="password"] {
font-size: 1.125rem;
font-family: var(--font-main);
width: 100%;
padding: var(--space-8) var(--space-16);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-md);
background: transparent;
transition: border-color 0.3s ease;
}
input:focus {
outline: none;
border: 2px solid var(--olive);
}
/* --- Info box --- */
.info-box {
background-color: var(--olive-light);
padding: var(--space-16);
margin-bottom: var(--space-40);
border-radius: var(--radius-md);
font-size: 1rem;
color: var(--black);
line-height: 1.4;
}
/* --- Hints & errors --- */
.error-message--field-callout {
display: none;
margin-top: 0.65rem;
margin-left: auto;
}
.form-group.has-error input {
border-color: var(--error);
box-shadow: var(--shadow-error);
}
.form-group.has-error .error-message--field-callout {
display: block;
}
*/
/* --- Modal / Popup --- */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal.show {
display: flex;
justify-content: center;
align-items: center;
}
@keyframes slideIn {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.close-btn {
position: absolute;
right: 0;
top: 0;
font-size: 28px;
color: var(--black);
background: none;
border: none;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-primary {
padding: var(--space-2) var(--space-32);
background-color: var(--olive);
color: var(--white);
border: none;
border-radius: var(--radius-pill);
font-size: 1rem;
font-weight: 700;
font-family: var(--font-main);
cursor: pointer;
transition: background-color 0.3s ease;
}
.btn-primary:hover {
background-color: var(--olive-dark);
}
/* --- Footer --- */
.footer {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: var(--space-16) var(--space-32);
border: none;
margin-top: 40px;
}
.footer_link {
color: var(--black);
text-decoration: underline;
font-size: 0.8rem;
font-weight: 400;
}
/* Left aligned */
.footer-left {
justify-self: start;
}
/* Center aligned */
.footer-center {
justify-self: center;
}
/* Right aligned */
.footer-right {
justify-self: end;
display: flex;
gap: var(--space-24);
}
/* --- Responsive --- */
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.image-section {
min-height: 300px;
}
.error-message--field-callout {
margin-top: var(--space-1);
max-width: 100%;
white-space: normal;
}
.error-message--field-callout::after {
display: none;
}
}

View File

@ -1,251 +0,0 @@

/* Kopfbereich mit Titel und Logout-Aktion. */
.profile-hero {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-24);
margin-bottom: var(--space-32);
}
.profile-kicker {
margin: 0;
color: var(--olive);
font-size: 1rem;
font-weight: 500;
letter-spacing: var(--ls-lg);
}
#headline {
color: var(--brown);
}
.profile-subline {
margin: 0;
max-width: 48rem;
}
.profile-logout {
border: none;
cursor: pointer;
}
.profile-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-24);
}
.btn-count {
color: var(--black);
background: var(--tomato-light);
height: 32px;
width: 32px;
margin-right: -18px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-pill);
}
/* Konsistentes Karten-Layout für alle Profilsektionen. */
.profile-panel {
background: var(--butter-light);
border-radius: var(--radius-lg);
padding: var(--space-32);
}
.panel-head {
display: none;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-16);
}
.profile-card-list {
display: grid;
gap: var(--space-16);
}
/* Einzelne Eventkarte für "Meine Events" und "Meine Anmeldungen". */
.profile-event-card {
background: var(--butter-light);
border: 1.5px solid var(--olive-light);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-interaction);
padding: var(--space-32) var(--space-40);
display: flex;
justify-content: space-between;
gap: var(--space-40);
cursor: pointer;
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.profile-event-card-clickable {
cursor: pointer;
}
.profile-event-card-clickable:hover {
transform: translateY(-3px);
}
.profile-event-title h3{
margin: 0;
}
/*
.profile-event-meta {
}*/
.profile-event-address-block {
margin-top: var(--space-24);
background-color: var(--olive-light);
padding: var(--space-16);
border-radius: var(--radius-md);
color: var(--black);
line-height: 1.4;
}
.profile-event-address-label {
font-size: 1rem;
font-weight: 400;
color: var(--olive-dark);
}
.profile-event-address {
font-size: 1.125rem;
font-weight: 400;
line-height: 1.4;
color: var(--black);
}
.profile-event-link {
flex-shrink: 0;
color: var(--blue);
font-weight: 500;
text-decoration: none;
}
.profile-event-link:hover,
.profile-event-link:focus-visible {
text-decoration: underline;
text-underline-offset: 3px;
}
.profile-event-actions {
display: flex;
align-items: center;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-16);
}
/*
.form-group label {
display: block;
margin-bottom: 0.35rem;
font-size: 0.95rem;
font-weight: 500;
}*/
.form-group input {
width: 100%;
border: 1px solid #d8d8d8;
border-radius: var(--radius-sm);
background: var(--white);
padding: 0.7rem 0.85rem;
font-size: 1rem;
}
.form-group input:focus {
outline: 2px solid rgba(107, 107, 5, 0.35);
outline-offset: 1px;
}
.input-hint {
margin: 0.4rem 0 0;
font-size: 0.9rem;
color: #535353;
}
.input-error {
margin-top: 0.35rem;
color: var(--error);
font-size: 0.85rem;
display: none;
}
.form-group.has-error .input-error {
display: block;
}
.form-group.has-error input {
border-color: var(--error);
box-shadow: var(--shadow-error);
}
.info-abmeldung {
font-size: 1rem;
color: var(--olive);
margin-bottom: 16px;
display: flex; align-items:
flex-start;
gap: 8px;
}
.profile-feedback {
margin: 0.75rem 0 0;
font-size: 0.95rem;
color: var(--olive);
min-height: 1.3rem;
}
.profile-cta-row {
display: flex;
gap: var();
margin-top: var(--space-16);
}
.profile-button-secondary {
background: var(--tomato);
}
.profile-button-secondary:hover {
background: var(--tomato-dark);
}
@media (max-width: 48rem) {
.container {
padding-top: 5.5rem;
}
.profile-hero {
flex-direction: column;
align-items: stretch;
}
.profile-logout {
width: max-content;
}
.form-grid {
grid-template-columns: 1fr;
}
.profile-event-card {
flex-direction: column;
align-items: flex-start;
}
.profile-event-actions {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,266 +0,0 @@
[
{
"id": 1,
"title": "Italienische Tavolata",
"location": "Luzern",
"address": "Pilatusstrasse 18, 6003 Luzern",
"date": "30. April 2026",
"time": "15:30 UHR",
"category": "Dinner",
"diet": "Vegetarisch",
"spots": 8,
"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 dafür eine klassische Tavolata vor, bei der verschiedene Gerichte in die Mitte des Tisches kommen und sich jeder bedient.",
"Wenn es das Wetter erlaubt sind wir draussen."
],
"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",
"https://i.pinimg.com/1200x/82/5e/9c/825e9c735e6fccdf6e7a4ac5dc2b2b7b.jpg"
]
},
{
"id": 2,
"title": "Noche Peruana",
"location": "Chur",
"address": "Obere Gasse 41, 7000 Chur",
"date": "8. Mai 2026",
"time": "19:00 UHR",
"category": "Dinner",
"diet": "Fleisch, Fisch",
"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 geniessen 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,
"title": "Japanese Delight",
"location": "Zürich",
"address": "Limmatquai 92, 8001 Zürich",
"date": "12. Mai 2026",
"time": "12:30 UHR",
"category": "Lunch",
"diet": "Fisch",
"spots": 8,
"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"
]
},
{
"id": 4,
"title": "Cucina Brasileira",
"location": "Basel",
"address": "Fredy Kübler Weg 5, 8134 Adliswil",
"date": "15. Mai. 2026",
"time": "19:00 UHR",
"category": "Dinner",
"diet": "Fleisch",
"spots": 8,
"host": {
"name": "Mia",
"initial": "M"
},
"hostMessage": [
"Ihr seit herzlich eingeladen zu meinem Brasilianischen Abendessen! Lasst euch überraschen."
],
"menu": [
"Feijoada Brasileira com Farofa",
"Arroz",
"Vinagrette",
"Salada de Couve",
"Salada de batata",
"Bolo de Mandioca"
],
"specifications": [],
"participants": [
"Carlos",
"Vivien",
"Estelle",
"Simona",
"Ysabelle"
],
"gallery": [
"https://i.pinimg.com/736x/62/39/4b/62394bb73b986dfb89f41e809e2c8dd4.jpg",
"https://i.pinimg.com/1200x/68/fe/bd/68febdd512a00f0a345e51ebed7ddd63.jpg",
"https://i.pinimg.com/1200x/0a/8d/67/0a8d674a7923c6e9bfe3665bc63522d0.jpg"
]
},
{
"id": 5,
"title": "Mexican Fiesta",
"location": "Basel",
"address": "Münsterplatz 10, 4051 Basel",
"date": "28. April. 2026",
"time": "18:00 UHR",
"category": "Dinner",
"diet": "Omnivore",
"spots": 6,
"host": {
"name": "Carlos",
"initial": "C"
},
"hostMessage": [
"Hallo zusammen! Leider muss ich dieses Event absagen, da mir etwas Wichtiges dazwischengekommen ist.",
"Ich hoffe, wir können das bald nachholen!"
],
"menu": [
"Guacamole & Nachos",
"Tacos al Pastor",
"Churros"
],
"specifications": [],
"participants": [
"Carlos",
"Vanessa",
"Christina",
"Julian"
],
"gallery": [
"https://i.pinimg.com/736x/7d/5c/29/7d5c29117ef6f974b1a6f77b22408ae7.jpg",
"https://i.pinimg.com/1200x/4e/4e/5d/4e4e5d57576d475316f25f84e5afb38f.jpg",
"https://i.pinimg.com/webp/1200x/d6/c2/4c/d6c24c1582d944229d271d8948b53dbb.webp",
"https://i.pinimg.com/webp/1200x/24/51/8e/24518e6e7bd9a68befcd9a98bba72a23.webp"
]
}, {
"id": 6,
"title": "Schwedentorte Schlemmern",
"location": "Zürich",
"address": "Münsterplatz 10, 8009 Zürich",
"date": "9. Mai 2026",
"time": "14:00 UHR",
"category": "Kaffee + Kuchen",
"diet": "Vegan",
"spots": 5,
"host": {
"name": "Annalea",
"initial": "A"
},
"hostMessage": [
"Hallo :) Ich suche Personen die Lust haben meine Vegane Schwedentorten Kreation zu probieren. Es ist eine Schwedentorte, die ich mit einer veganen Buttercreme und frischen Früchten zubereite. Es wird ein süsser Genuss, den ihr nicht verpassen solltet!"
],
"menu": [
"Schwedentorte",
"Diverse Teesorten"
],
"specifications": [],
"participants": [
"Annalea",
"Andi",
"Leah"
],
"gallery": [
"https://i.pinimg.com/736x/0e/44/78/0e4478e4e3389c77e3e859b2663e6d47.jpg"
]
}, {
"id": 7,
"title": "Mexican Fiesta",
"location": "Basel",
"address": "Münsterplatz 10, 4051 Basel",
"date": "29. Mai. 2026",
"time": "18:00 UHR",
"category": "Dinner",
"diet": "Omnivore",
"spots": 6,
"status": "canceled",
"host": {
"name": "Carlos",
"initial": "C"
},
"hostMessage": [
"Hallo zusammen! Leider muss ich dieses Event absagen, da mir etwas Wichtiges dazwischengekommen ist.",
"Ich hoffe, wir können das bald nachholen!"
],
"menu": [
"Guacamole & Nachos",
"Tacos al Pastor",
"Churros"
],
"specifications": [],
"participants": [
"Carlos",
"Vivien",
"Test"
],
"gallery": [
"https://i.pinimg.com/1200x/e2/6a/f5/e26af5c24b805081a3f304d240818302.jpg"
]
}
]

View File

@ -1,107 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invité | Datenschutz</title>
<link rel="stylesheet" href="css/stylesheet_global.css" />
<script src="js/navigation.js" defer></script>
</head>
<body>
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invité">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<main class="layout-wide">
<h1>Datenschutzerklärung</h1>
<h3>1. Verantwortliche Stelle</h3>
<p>
Invité GmbH<br>
Musterstrasse 12<br>
7000 Chur<br>
Schweiz<br>
E-Mail: datenschutz@invite-cooking.ch
</p>
<h3>2. Erhebung und Verarbeitung personenbezogener Daten</h3>
<p>
Beim Besuch unserer Website werden automatisch Informationen allgemeiner Natur erfasst.
Diese Informationen (Server-Logfiles) beinhalten die Art des Webbrowsers, das verwendete
Betriebssystem, den Domainnamen Ihres Internet-Service-Providers, Ihre IP-Adresse und
Ähnliches. Sie werden ausschliesslich zur technischen Bereitstellung und Verbesserung
unserer Website verwendet.
</p>
<h3>3. Registrierung und Nutzerkonto</h3>
<p>
Bei der Erstellung eines Nutzerkontos erheben wir folgende Daten: Name, E-Mail-Adresse
und Passwort. Diese Daten werden ausschliesslich zur Bereitstellung unserer Dienste
verwendet und nicht an Dritte weitergegeben.
</p>
<h3>4. Cookies</h3>
<p>
Unsere Website verwendet Cookies, um die Nutzererfahrung zu verbessern. Cookies sind
kleine Textdateien, die auf Ihrem Endgerät gespeichert werden. Sie können die Verwendung
von Cookies in Ihren Browsereinstellungen deaktivieren. Bitte beachten Sie, dass dadurch
die Funktionalität der Website eingeschränkt sein kann.
</p>
<h3>5. Datenweitergabe an Dritte</h3>
<p>
Eine Übermittlung Ihrer persönlichen Daten an Dritte findet nicht statt, es sei denn,
wir sind gesetzlich dazu verpflichtet oder Sie haben Ihre ausdrückliche Einwilligung
erteilt.
</p>
<h3>6. Datensicherheit</h3>
<p>
Wir setzen technische und organisatorische Sicherheitsmassnahmen ein, um Ihre Daten
gegen zufällige oder vorsätzliche Manipulation, Verlust, Zerstörung oder den Zugriff
unberechtigter Personen zu schützen. Unsere Sicherheitsmassnahmen werden entsprechend
der technologischen Entwicklung fortlaufend verbessert.
</p>
<h3>7. Ihre Rechte</h3>
<p>
Sie haben jederzeit das Recht auf Auskunft über die bei uns gespeicherten
personenbezogenen Daten. Ebenso haben Sie das Recht auf Berichtigung, Löschung
oder Einschränkung der Verarbeitung Ihrer Daten. Bitte wenden Sie sich dazu an:
datenschutz@invite-cooking.ch
</p>
<h3>8. Änderungen dieser Datenschutzerklärung</h3>
<p>
Wir behalten uns vor, diese Datenschutzerklärung gelegentlich anzupassen, damit sie
stets den aktuellen rechtlichen Anforderungen entspricht oder um Änderungen unserer
Leistungen umzusetzen. Für Ihren erneuten Besuch gilt dann die neue Datenschutzerklärung.
</p>
</main>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>

122
estelle-köhler.html Normal file
View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Estelle's Comedy Show</title>
<style>
:root {
--bg-color: #111;
--text-color: #eee;
--accent-color: #ff69b4; /* pink accent */
--accent2-color: #9370db; /* lila accent */
--font-stack: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
margin: 0;
font-family: var(--font-stack);
color: var(--text-color);
background-color: var(--bg-color);
text-align: center;
overflow-x: hidden;
}
header {
background-color: #222;
padding: 1rem;
text-align: center;
}
header h1 {
margin: 0;
font-size: 2.5rem;
}
.hero {
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.hero img {
max-height: 400px;
width: auto;
border-radius: 8px;
border: 4px solid var(--accent-color);
}
.tour {
padding: 2rem;
}
.tour table {
width: 100%;
max-width: 600px;
margin: 0 auto;
border-collapse: collapse;
}
.tour th, .tour td {
border: 1px solid #444;
padding: 0.5rem;
text-align: center;
}
.tour th {
background-color: #222;
}
@media (max-width: 600px) {
.hero img {
max-width: 200px;
}
}
</style>
</head>
<body>
<header>
<h1>Estelle's Comedy Show</h1>
</header>
<section class="hero">
<h2>Live on stage!</h2>
<img src="image_2e567a.png" alt="Estelle auf der Bühne">
</section>
<section class="tour">
<h2>Tour-Daten</h2>
<table>
<thead>
<tr><th>Datum</th><th>Ort</th><th>Status</th></tr>
</thead>
<tbody>
<tr><td>15. April 2026</td><td>Zürich</td><td>Tickets verfügbar</td></tr>
<tr><td>22. April 2026</td><td>Bern</td><td>Ausverkauft</td></tr>
<tr><td>30. April 2026</td><td>Basel</td><td>Tickets verfügbar</td></tr>
<tr><td>5. Mai 2026</td><td>Lausanne</td><td>Vorverkauf</td></tr>
</tbody>
</table>
</section>
<footer>
<p>&copy; 2026 Estelle's Comedy Show</p>
</footer>
<script>
// cursor laugh effect
document.addEventListener('mousemove', function(e) {
const span = document.createElement('span');
span.textContent = 'Ha Ha Ha';
span.style.position = 'fixed';
span.style.left = e.pageX + 'px';
span.style.top = e.pageY + 'px';
span.style.color = 'var(--accent-color)';
span.style.pointerEvents = 'none';
span.style.opacity = '1';
span.style.transition = 'transform 1s ease-out, opacity 1s ease-out';
const size = Math.random() * 10 + 14;
span.style.fontSize = size + 'px';
span.style.transform = `translate(-50%, -50%) rotate(${(Math.random()*40-20)}deg)`;
document.body.appendChild(span);
requestAnimationFrame(() => {
span.style.transform += ' translateY(-30px)';
span.style.opacity = '0';
});
setTimeout(() => document.body.removeChild(span), 1000);
});
</script>
</body>
</html>

1
estelle-köhler.txt Normal file
View File

@ -0,0 +1 @@
Welcome to the chaos

BIN
estelle.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,489 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invité | Event erstellen</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/event_create.css" />
<script src="js/navigation.js" defer></script>
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<main class="event-create-page layout-narrow">
<section class="event-flow-header" aria-label="Event erstellen Aktionen">
</section>
<form id="eventForm" class="event-form" novalidate>
<section
class="step step--active step--intro"
data-step="0"
aria-labelledby="intro-title"
>
<div class="step-layout hero startseite">
<div>
<p class="badge margin-bottom-40">Event erstellen</p>
<h1 id="intro-title">Hey <span id="username">{{username}}</span>, was hast du vor?</h1>
<p class="step-text margin-bottom-40">
Erzähl uns von deiner Idee, vom Essen bis zur Stimmung. Ob Dinner, Brunch
oder etwas ganz Eigenes wir helfen dir dabei, dein Event in sieben Schritten aufzubauen.
</p>
<button type="button" class="button-primary" data-start-flow>
Los gehts!
</button>
</div>
<div class="hero__right" aria-label="Stimmungsbild zur Event-Erstellung">
<img
class="intro-image"
src="assets/eventcreate_foodtable-new.jpg"
alt="Ein gedeckter Tisch mit gemeinsamem Essen"
/>
</div>
</div>
</section>
<section class="step" data-step="1" aria-labelledby="step1-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 1</p>
<h2 id="step1-title">Was hast du vor?</h2>
<p class="step-text margin-bottom-40">
Erzähl uns, was für ein Event du planst. Ist es ein gemütlicher Brunch,
ein Dinner mit Wow-Effekt oder einfach ein entspanntes Mittagessen mit gutem Essen?
</p>
</div>
<div class="step-fields">
<fieldset class="form-field">
<label>Art des Essens / Eventtyp</label>
<div class="option-grid option-grid--4 option-grid--event-type option-grid--icon-choices option-grid--tomato-choices">
<label class="option-card option option-card--with-icon">
<input type="radio" name="eventType" value="Brunch" required />
<i class="fa-solid fa-bread-slice option-card__icon" aria-hidden="true"></i>
<span>Brunch</span>
</label>
<label class="option-card option option-card--with-icon">
<input type="radio" name="eventType" value="Lunch" />
<i class="fa-solid fa-pizza-slice option-card__icon" aria-hidden="true"></i>
<span>Lunch</span>
</label>
<label class="option-card option option-card--with-icon">
<input type="radio" name="eventType" value="Kaffee + Kuchen" />
<i class="fa-solid fa-mug-hot option-card__icon" aria-hidden="true"></i>
<span>Kaffee + Kuchen</span>
</label>
<label class="option-card option option-card--with-icon">
<input type="radio" name="eventType" value="Dinner" />
<i class="fa-solid fa-martini-glass option-card__icon" aria-hidden="true"></i>
<span>Dinner</span>
</label>
</div>
</fieldset>
<fieldset class="form-field">
<label>Maximale Gästeanzahl</label>
<div class="counter" data-counter>
<button
type="button"
class="counter-button"
data-counter-action="decrease"
aria-label="Personenzahl verringern"
>
</button>
<div class="counter-value-group">
<i class="fa-solid fa-user-group guest-count-icon" aria-hidden="true"></i>
<input
type="number"
id="maxGuests"
name="maxGuests"
min="1"
step="1"
value="0"
required
/>
</div>
<button
type="button"
class="counter-button"
data-counter-action="increase"
aria-label="Personenzahl erhöhen"
>
+
</button>
</div>
</fieldset>
</div>
</div>
</section>
<section class="step" data-step="2" aria-labelledby="step2-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 2</p>
<h2 id="step2-title">Was kommt auf den Tisch?</h2>
<p class="step-text margin-bottom-40">
Mach uns neugierig. Was gibt es zu essen? Gibt es eine bestimmte Ernährungsform oder ein Motto? Je mehr du verrätst, desto besser können sich deine Gäste auf dein Event freuen.
</p>
</div>
<div class="step-fields">
<fieldset class="form-field">
<label>Ernährungsform</label>
<div class="option-grid option-grid--4 option-grid--icon-choices option-grid--tomato-choices">
<label class="option-card option option-card--with-icon">
<input type="checkbox" name="dietType" value="Fleisch" />
<i class="fa-solid fa-drumstick-bite option-card__icon" aria-hidden="true"></i>
<span>Fleisch</span>
</label>
<label class="option-card option option-card--with-icon">
<input type="checkbox" name="dietType" value="Fisch" />
<i class="fa-solid fa-fish option-card__icon" aria-hidden="true"></i>
<span>Fisch</span>
</label>
<label class="option-card option option-card--with-icon">
<input type="checkbox" name="dietType" value="Vegetarisch" />
<i class="fa-solid fa-seedling option-card__icon" aria-hidden="true"></i>
<span>Vegetarisch</span>
</label>
<label class="option-card option option-card--with-icon">
<input type="checkbox" name="dietType" value="Vegan" />
<i class="fa-solid fa-leaf option-card__icon" aria-hidden="true"></i>
<span>Vegan</span>
</label>
</div>
</fieldset>
<div class="form-field">
<label for="menuDescription">Was ist das Menü?</label>
<textarea id="menuDescription" name="menuDescription" rows="5" required></textarea>
</div>
</div>
</div>
</section>
<section class="step" data-step="3" aria-labelledby="step3-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 3</p>
<h2 id="step3-title">Gibt es etwas zu beachten?</h2>
<p class="step-text margin-bottom-40">
Gibt es Allergien, Unverträglichkeiten oder andere Hinweise, die für dein Event wichtig sind? So wissen deine Gäste gleich, worauf sie sich einstellen können.
</p>
</div>
<div class="step-fields">
<fieldset class="form-field">
<label>Allergene / Unverträglichkeiten</label>
<p class="field-hint">Optional nur auswählen, wenn es für dein Event relevant ist.</p>
<div class="option-grid option-grid--3 option-grid--tomato-choices">
<label class="option-card option">
<input type="checkbox" name="allergies" value="glutenfrei" />
<span>Glutenfrei</span>
</label>
<label class="option-card option">
<input type="checkbox" name="allergies" value="laktosefrei" />
<span>Laktosefrei</span>
</label>
<label class="option-card option">
<input type="checkbox" name="allergies" value="ohne Nüsse" />
<span>Ohne Nüsse</span>
</label>
</div>
</fieldset>
<div class="form-field">
<label for="allergiesOther">Weitere Unverträglichkeiten oder Hinweise</label>
<p class="field-hint">Optional nur auswählen, wenn es für dein Event relevant ist.</p>
<textarea id="allergiesOther" name="allergiesOther" rows="3"></textarea>
</div>
</div>
</div>
</section>
<section class="step" data-step="4" aria-labelledby="step4-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 4</p>
<h2 id="step4-title">Wann findet dein Event statt?</h2>
<p class="step-text margin-bottom-40">
Wähle Datum und Uhrzeit für dein Event. So können deine Gäste direkt einschätzen, ob der Termin für sie passt.
</p>
</div>
<div class="step-fields">
<div class="field-row">
<div class="form-field">
<label for="eventDate">Datum</label>
<input type="date" id="eventDate" name="eventDate" required />
</div>
<div class="form-field">
<label for="eventTime">Uhrzeit</label>
<input type="time" id="eventTime" name="eventTime" required />
</div>
</div>
</div>
</div>
</section>
<section class="step" data-step="5" aria-labelledby="step5-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 5</p>
<h2 id="step5-title">Wo findet dein Event statt?</h2>
<p class="step-text margin-bottom-40">
Sag uns, wo dein Event stattfindet. Keine Sorge: Die genaue Adresse sehen Gäste erst nach der Buchung.
</p>
</div>
<div class="step-fields">
<div class="form-field">
<label for="eventAddress">Adresse</label>
<input type="text" id="eventAddress" name="eventAddress" autocomplete="street-address" required />
</div>
<div class="form-field">
<label for="eventCity">Ort</label>
<input type="text" id="eventCity" name="eventCity" autocomplete="address-level2" required />
</div>
</div>
</div>
</section>
<section class="step" data-step="6" aria-labelledby="step6-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 6</p>
<h2 id="step6-title">Gib deinem Event den letzten Schliff.</h2>
<p class="step-text margin-bottom-40">
Jetzt bekommt dein Event seinen Namen und die Atmosphäre, die Lust aufs Dabeisein macht.
Ein klarer Titel (z.B. "Italienische Tavolata") und ein guter Beschreibungstext (Ablauf etc.) machen den Unterschied.
</p>
</div>
<div class="step-fields">
<div class="form-field">
<label for="eventTitle">Wie soll dein Event heissen?</label>
<input type="text" id="eventTitle" name="eventTitle" required />
</div>
<div class="form-field">
<label for="eventDescription">Beschreibung des Events</label>
<textarea id="eventDescription" name="eventDescription" rows="6" required></textarea>
</div>
<div class="form-field">
<label>Wie wird dein Event aussehen?</label>
<p class="field-hint">Optional füge Bilder zu deinem Event hinzu.</p>
<div class= "option-grid option-grid--4">
<div class="gallery-preview" id="galleryPreview"></div>
<button type="button" class="gallery-add-button" id="galleryAddBtn" aria-label="Foto hinzufügen">
<i class="fa-solid fa-plus gallery-add-button__icon" aria-hidden="true"></i>
<span class="option">Foto hinzufügen</span>
</button>
<input type="file" id="galleryFileInput" accept="image/*" multiple hidden />
</div>
</div>
</div>
</div>
</section>
<section class="step" data-step="7" aria-labelledby="step7-title">
<div class="step-layout">
<div class="step-copy">
<p class="badge">Schritt 7</p>
<h2 id="step7-title">Dein Event auf einen Blick</h2>
<p class="step-text margin-bottom-40">
Schau dir alle Details nochmal in Ruhe an. Wenn alles passt,
kannst du dein Event jetzt veröffentlichen und Gäste einladen.
</p>
</div>
<div class="review-card" aria-live="polite">
<dl class="review-list">
<div class="review-item" data-edit-step="1" data-edit-field="eventType" role="button" tabindex="0" aria-label="Eventtyp bearbeiten">
<dt>Eventtyp</dt>
<dd data-review="eventType"></dd>
</div>
<div class="review-item" data-edit-step="1" data-edit-field="maxGuests" role="button" tabindex="0" aria-label="Maximale Gästeanzahl bearbeiten">
<dt>Maximale Gästeanzahl</dt>
<dd data-review="maxGuests"></dd>
</div>
<div class="review-item" data-edit-step="2" data-edit-field="dietType" role="button" tabindex="0" aria-label="Ernährungsform bearbeiten">
<dt>Ernährungsform</dt>
<dd data-review="dietType"></dd>
</div>
<div class="review-item" data-edit-step="2" data-edit-field="menuDescription" role="button" tabindex="0" aria-label="Menü bearbeiten">
<dt>Menü</dt>
<dd data-review="menuDescription"></dd>
</div>
<div class="review-item" data-edit-step="3" data-edit-field="allergiesOther" role="button" tabindex="0" aria-label="Allergene und Unverträglichkeiten bearbeiten">
<dt>Allergene / Unverträglichkeiten</dt>
<dd data-review="allergies">Keine Angabe</dd>
</div>
<div class="review-item" data-edit-step="4" data-edit-field="eventDate" role="button" tabindex="0" aria-label="Datum bearbeiten">
<dt>Datum</dt>
<dd data-review="eventDate"></dd>
</div>
<div class="review-item" data-edit-step="4" data-edit-field="eventTime" role="button" tabindex="0" aria-label="Uhrzeit bearbeiten">
<dt>Uhrzeit</dt>
<dd data-review="eventTime"></dd>
</div>
<div class="review-item" data-edit-step="5" data-edit-field="eventAddress" role="button" tabindex="0" aria-label="Adresse bearbeiten">
<dt>Adresse</dt>
<dd data-review="eventAddress"></dd>
</div>
<div class="review-item" data-edit-step="5" data-edit-field="eventCity" role="button" tabindex="0" aria-label="Ort bearbeiten">
<dt>Ort</dt>
<dd data-review="eventCity"></dd>
</div>
<div class="review-item" data-edit-step="6" data-edit-field="eventTitle" role="button" tabindex="0" aria-label="Eventtitel bearbeiten">
<dt>Eventtitel</dt>
<dd data-review="eventTitle"></dd>
</div>
<div class="review-item" data-edit-step="6" data-edit-field="eventDescription" role="button" tabindex="0" aria-label="Event-Abend bearbeiten">
<dt>Event-Abend</dt>
<dd data-review="eventDescription"></dd>
</div>
<div class="review-item" data-edit-step="6" data-edit-field="galleryAddBtn" role="button" tabindex="0" aria-label="Fotos bearbeiten">
<dt>Fotos</dt>
<dd>
<div class="review-gallery" data-review-gallery>
<span>Keine Fotos hinzugefügt</span>
</div>
</dd>
</div>
</dl>
</div>
</div>
</section>
<div class="flow-footer" id="flowFooter" hidden>
<div class="flow-actions">
<button type="button" id="backButton" class="button-secondary">
Zurück
</button>
<div class="progress-wrap">
<span class="progress-label" id="progressMarkerLabel">
Schritt 1 von 7
</span>
<div class="progress">
<span id="progressBar" class="progress-bar"></span>
</div>
</div>
<div class="flow-actions-right">
<p
id="errorMessage"
class="error-message error-message--callout"
role="alert"
aria-live="assertive"
></p>
<button type="button" id="nextButton" class="button-primary">
Weiter
</button>
</div>
</div>
</div>
<section
id="submissionSuccess"
class="submission-success"
aria-labelledby="success-title"
aria-live="polite"
hidden
>
<div class="step-layout hero startseite">
<div class="step-layout">
<p class="badge margin-bottom-40">Event veröffentlicht</p>
<h1 id="success-title">Dein Event ist ready <i class="fa-solid fa-champagne-glasses"></i> </h1>
<p class="step-text margin-bottom-40">
Sieht gut aus: Deine Idee ist jetzt live und bereit für Gäste.
Im Profil kannst du dein Event anschauen, verwalten oder direkt das nächste planen.
</p>
<div class="submission-success-actions">
<a class="button-primary button--intro" href="my_profil.html">Weiter zu deinem Profil</a>
</div>
</div>
<div class="hero__right" aria-label="Stimmungsbild zur Event-Erstellung">
<img
class="intro-image"
src="assets/eventcreate_foodtable with friends.jpg"
alt="Gemeinsames Essen an einem gedeckten Tisch"
/>
</div>
</div>
</section>
</form>
</main>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
<script src="js/event_create.js"></script>
</body>
</html>

View File

@ -1,92 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invité | Event-Detail</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/event_overview.css">
<script src="js/navigation.js" defer></script>
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<!-- Main content: detail page gets fully injected by JavaScript -->
<main class="container layout-wide">
<!-- Render target: loading, error state or full detail layout -->
<div id="detail-view">
<p>Lädt Event-Details...</p>
</div>
</main>
<!-- Page logic: fetch by URL id, compose detail UI, handle gallery lightbox -->
<script src="js/event_detail.js"></script>
<div id="register-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Ein Platz für dich am Tisch!</h2>
</div>
<div class="modal-body">
<p>Schön, dass du dich dazu gesellen möchtest! Da dein:e Gastgeber:in extra für dich einkauft und mit viel Liebe kocht, ist deine Anmeldung ein festes Versprechen. Bitte sag nur zu, wenn du an dem Tag wirklich Zeit hast. So zeigst du echte Wertschätzung für die Mühe und wir lassen niemanden auf vollen Töpfen sitzen.</p>
</div>
<div class="modal-footer">
<button class="button-primary button--outline" type="button" id="unregister-modal-cancel" onclick="closeUnregisterModal()">Abbrechen</button>
<button class="button-primary" type="button" id="confirm-register-btn">Ja, ich bin dabei</button>
</div>
</div>
</div>
<div id="unregister-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Pläne haben sich geändert?</h2>
</div>
<div class="modal-body">
<p>Schade, dass du nicht dabei sein kannst! Aber manchmal kommt einfach etwas dazwischen. Wenn du dich jetzt abmeldest, gibst du deinen Stuhl am Tisch für jemand anderen aus der Community frei. So hilfst du bei der Planung und ein anderer Feinschmecker freut sich über den freien Platz.</p>
</div>
<div class="modal-footer">
<button class="button-primary button--outline" type="button" id="unregister-modal-cancel" onclick="closeUnregisterModal()">Abbrechen</button>
<button class="button-primary button-primary-abmelden" type="button" id="confirm-unregister-btn">Abmelden</button>
</div>
</div>
</div>
<!-- Snackbar: Feedback bei An-/Abmeldung -->
<div class="snackbar" id="snackbar"></div>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>

View File

@ -1,166 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invité | Event-Übersicht</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/event_overview.css">
<script src="js/navigation.js" defer></script>
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<!-- Main content: page headline, filter controls and dynamic event list -->
<main class="container layout-wide">
<!-- Page headline -->
<p class="badge margin-bottom-40">Event finden</p>
<div class="overview-title-row">
<h1 class="overview-title">Was darf es sein?</h1>
<button type="button" id="info-button" class="btn-info" aria-label="Informationen zu kostenlosen Events">?</button>
</div>
<!-- Filter section: category chips + location/date filters -->
<section class="filter-section margin-bottom-24">
<p class="filter-label">Art des Essens / Eventtyp</p>
<div class="filter-row margin-bottom-24">
<!-- Primary category filter buttons -->
<div class="category-group">
<button class="category-item active" type="button" data-cat="ALLE">Alle</button>
<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="Kaffee + Kuchen">Kaffee + Kuchen</button>
<button class="category-item" type="button" data-cat="Dinner">Dinner</button>
</div>
<!-- Secondary filters populated/handled by JavaScript -->
<div class="meta-filter-group" aria-label="Weitere Filter">
<div class="meta-filter" for="location-filter">
<span>Ort</span>
<select id="location-filter">
<option value="ALLE_ORTE">Alle Orte</option>
</select>
</div>
<div class="meta-filter" for="date-filter">
<span>Datum</span>
<div class="date-input-wrapper">
<input id="date-filter" type="date">
</div>
</div>
</div>
</div>
<details class="filter-box">
<summary>Nach Ernährungform filtern</summary>
<div class="filter-row margin-bottom-16">
<div class="category-group">
<button class="category-item" type="button" data-diet="Fleisch">Fleisch</button>
<button class="category-item" type="button" data-diet="Fisch">Fisch</button>
<button class="category-item" type="button" data-diet="Vegetarisch">Vegetarisch</button>
<button class="category-item" type="button" data-diet="Vegan">Vegan</button>
<button class="category-item filter-delete" type="button" id="clear-all-filters">Alle Filter löschen</button>
</div>
</div>
</details>
<details class="filter-box margin-bottom-24">
<summary>Nach Allergenen filtern</summary>
<div class="filter-row margin-bottom-16">
<div class="category-group">
<button class="category-item" type="button" data-allergie="Glutenfrei">Glutenfrei</button>
<button class="category-item" type="button" data-allergie="Laktosefrei">Laktosefrei</button>
<button class="category-item" type="button" data-allergie="Ohne Nüsse">Ohne Nüsse</button>
</div>
</div>
</details>
<!-- Render target: event cards or empty state -->
<section id="event-grid" class="event-list"></section>
</main>
<!-- Seitenlogik: Daten laden, filtern und Event-Karten rendern -->
<script src="js/event_overview.js"></script>
<!-- Info Modal: Kostenlose Events Info -->
<div id="info-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Warum Invité kostenlos ist</h2>
</div>
<div class="modal-body">
<p>Alle Events bei uns sind komplett kostenlos. Invité basiert rein auf Freiwilligkeit und der Freude am Teilen. Kein Geldfluss, keine versteckten Kosten nur die pure Absicht, die Community zu stärken und den sozialen Zusammenhalt in unserer Nachbarschaft zu fördern. Egal ob du den Kochlöffel schwingst oder dich als Gast dazu gesellst: Bei uns zählt nur die menschliche Begegnung.</p>
</div>
<div class="modal-footer">
<button class="button-primary button--outline" type="button" onclick="closeInfoModal()">Okay</button>
</div>
</div>
</div>
<div id="register-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Ein Platz für dich am Tisch!</h2>
</div>
<div class="modal-body">
<p>Schön, dass du dich dazu gesellen möchtest! Da dein:e Gastgeber:in extra für dich einkauft und mit viel Liebe kocht, ist deine Anmeldung ein festes Versprechen. Bitte sag nur zu, wenn du an dem Tag wirklich Zeit hast. So zeigst du echte Wertschätzung für die Mühe und wir lassen niemanden auf vollen Töpfen sitzen.</p>
</div>
<div class="modal-footer">
<button class="button-primary button--outline" type="button" onclick="closeRegisterModal()">Abbrechen</button>
<button class="button-primary" type="button" id="confirm-register-btn">Ja, ich bin dabei</button>
</div>
</div>
</div>
<div id="unregister-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Pläne haben sich geändert?</h2>
</div>
<div class="modal-body">
<p>Schade, dass du nicht dabei sein kannst! Aber manchmal kommt einfach etwas dazwischen. Wenn du dich jetzt abmeldest, gibst du deinen Stuhl am Tisch für jemand anderen aus der Community frei. So hilfst du bei der Planung und ein anderer Feinschmecker freut sich über den freien Platz.</p>
</div>
<div class="modal-footer">
<button class="button-primary button--outline" type="button" onclick="closeUnregisterModal()">Abbrechen</button>
<button class="button-primary button-primary-abmelden" type="button" id="confirm-unregister-btn">Abmelden</button>
</div>
</div>
</div>
<!-- Snackbar: Feedback bei An-/Abmeldung -->
<div class="snackbar" id="snackbar"></div>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>

View File

@ -1,80 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invité | Impressum</title>
<link rel="stylesheet" href="css/stylesheet_global.css" />
<script src="js/navigation.js" defer></script>
</head>
<body>
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invité">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<main class="layout-wide">
<h1>Impressum</h1>
<h3>Angaben gemäss § 5 TMG</h3>
<p>
Invité GmbH<br>
Musterstrasse 12<br>
7000 Chur<br>
Schweiz
</p>
<h3>Kontakt</h3>
<p>
Telefon: +41 81 123 45 67<br>
E-Mail: info@invite-cooking.ch
</p>
<h3>Vertretungsberechtigte Person</h3>
<p>Max Mustermann, Geschäftsführer</p>
<h3>Handelsregistereintrag</h3>
<p>
Eingetragen im Handelsregister des Kantons Graubünden<br>
Firmennummer: CHE-123.456.789
</p>
<h3>Haftungsausschluss</h3>
<p>
Die Inhalte dieser Website wurden mit grösster Sorgfalt erstellt. Für die Richtigkeit,
Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen.
</p>
<h3>Urheberrecht</h3>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen
dem schweizerischen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede
Art der Verwertung ausserhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen
Zustimmung des jeweiligen Autors bzw. Erstellers.
</p>
</main>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>

View File

@ -1,359 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invité</title>
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/index.css">
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="js/navigation.js" defer></script>
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invité">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<main class="container layout-wide">
<!-- Hero: uses .hero, .btn, .image-card, and .hero-image for a polished first impression -->
<section class="hero">
<div class="hero__left">
<span class="badge margin-bottom-40">einfach. lecker. gemeinsam.</span>
<h1>Teile deine Leidenschaft, geniesse gemeinsam.</h1>
<p class="margin-bottom-40">Ob du als leidenschaftlicher Hobbykoch Gastgeber sein möchtest oder als Feinschmecker einen Platz an einem lokalen Tisch suchst Invité verbindet Menschen durch die Kraft einer gemeinsamen Mahlzeit.</p>
<a class="button-primary" href="signup.html">Registrieren</a>
</div>
<div class="hero__right">
<div class="image-card">
<img class="hero-image" src="assets/index_round table friends.jpeg" alt="Round table friends" />
</div>
</div>
</section>
<section class="how-it-works">
<h2>So funktioniert's</h2>
<div class="how-it-works__steps">
<article class="how-step how-step-number-numbered">
<span class="how-step_corner-number">1</span>
<i class="fa-solid fa-id-card how-step_icon"></i>
<h3 class="how-step_text">Anmelden und Dabeisein</h3>
<p class="how-step_text text-left">Erstelle kurz dein Profil und zeig uns deinen Geschmack. Bei uns zählt der Mensch am Tisch, nicht der Lebenslauf.</p>
<div class="badge margin-bottom-24">
<span>Quick Setup in 2 Min</span>
</div>
</article>
<article class="how-step how-step-number-numbered">
<span class="how-step_corner-number">2</span>
<i class="fa-solid fa-magnifying-glass-location how-step_icon"></i>
<h3 class="how-step_text">Tisch finden oder decken</h3>
<p class="how-step_text text-left">Entdecke spontane Events in deiner Nähe oder öffne deine eigene Küche. Du entscheidest, ob du Gast oder Gastgeber:in bist.</p>
<div class="badge margin-bottom-24">
<span>Gast Gastgeber:in</span>
</div>
</article>
<article class="how-step how-step-number-numbered">
<span class="how-step_corner-number">3</span>
<i class="fa-solid fa-utensils how-step_icon"></i>
<h3 class="how-step_text">Teile den Tisch</h3>
<p class="how-step_text text-left">Triff neue Leute in entspannter Atmosphäre. Geniesse gutes Essen in Gesellschaft und mach aus einer Mahlzeit eine echte Begegnung.</p>
<div class="badge margin-bottom-24">
<span>Gemeinsam geniessen</span>
</div>
</article>
</div>
</section>
<!-- Main Content: uses .gallery, .gallery__carousel, .gallery__track, .gallery__item, and .gallery__info to present event carousel content -->
<section class="gallery" aria-label="Bildergalerie" aria-roledescription="Karussell">
<h2>Einblick in Cooking-Erlebnisse</h2>
<p class="margin-bottom-16">#gemeinsam_invité auf Instagram</p>
<div class="gallery__carousel">
<button type="button" class="gallery__arrow gallery__arrow--prev" aria-label="Vorheriges Bild">
<i class="fa-solid fa-angle-left"></i>
</button>
<div class="gallery__viewport">
<div class="gallery__track" aria-live="polite">
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 1 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_Red checkered social eating.jpg" alt="Red checkered social eating">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 142</span>
<span><i class="fa-solid fa-comment"></i> 18</span>
</div>
</div>
</article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 2 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_Sharing food table.jpg" alt="Sharing food table">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 89</span>
<span><i class="fa-solid fa-comment"></i> 5</span>
</div>
</div>
</article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 3 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_Zoomed in asian eating.jpg" alt="Zoomed in asian eating">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 215</span>
<span><i class="fa-solid fa-comment"></i> 32</span>
</div>
</div>
</article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 4 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_Burger eating together.jpg" alt="Burger eating together">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 304</span>
<span><i class="fa-solid fa-comment"></i> 41</span>
</div>
</div>
</article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 5 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_Cake cutting figs.jpg" alt="Cake cutting figs">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 178</span>
<span><i class="fa-solid fa-comment"></i> 12</span>
</div>
</div>
</article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 6 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_Cooking woman at home.jpg" alt="Cooking woman at home">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 95</span>
<span><i class="fa-solid fa-comment"></i> 8</span>
</div>
</div>
</article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 7 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_Eating and laughing girls.jpg" alt="Eating and laughing girls">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 420</span>
<span><i class="fa-solid fa-comment"></i> 55</span>
</div>
</div>
</article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 8 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_Pasta in cheese.jpg" alt="Pasta in cheese">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 267</span>
<span><i class="fa-solid fa-comment"></i> 29</span>
</div>
</div>
</article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 9 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_Salad roommates.jpg" alt="Salad roommates">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 112</span>
<span><i class="fa-solid fa-comment"></i> 4</span>
</div>
</div>
</article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 10 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_Pasta and many forks.jpg" alt="Pasta and many forks">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 389</span>
<span><i class="fa-solid fa-comment"></i> 47</span>
</div>
</div>
</article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 11 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_Spicy food zoomed.jpg" alt="Spicy food zoomed">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 156</span>
<span><i class="fa-solid fa-comment"></i> 11</span>
</div>
</div>
</article>
<article class="gallery__item" role="group" aria-roledescription="Folie" aria-label="Bild 12 von 12">
<div class="ig-post-wrapper">
<img src="assets/index_cooking.jpg" alt="Cooking">
<div class="ig-overlay">
<span><i class="fa-solid fa-heart"></i> 234</span>
<span><i class="fa-solid fa-comment"></i> 21</span>
</div>
</div>
</article>
</div>
</div>
<button type="button" class="gallery__arrow gallery__arrow--next" aria-label="Nächstes Bild">
<i class="fa-solid fa-angle-right"></i>
</button>
</div>
<div class="gallery_dots" role="tablist" aria-label="Seite auswählen"></div>
</section>
<!-- Lightbox: Bildansicht vergrössert -->
<div class="lightbox" id="gallery-lightbox" aria-hidden="true">
<div class="lightbox__backdrop" data-close-lightbox></div>
<figure class="lightbox__content" role="dialog" aria-modal="true" aria-label="Bildansicht">
<button class="lightbox__close" type="button" aria-label="Schliessen">&times;</button>
<img class="lightbox__image" src="" alt="Grossansicht">
</figure>
</div>
<script src="js/index-carousel.js"></script>
<!-- FAQ Section: Akkordion mit häufig gestellten Fragen -->
<section class="faq-section">
<h2>Häufig gestellte Fragen</h2>
<div class="faq-accordion">
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Wie kann ich bei Invité anfangen?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<ol class="faq-list">
<li>
<strong>Kostenloses Konto erstellen</strong><br>
Gehe auf Invité, klicke auf «Jetzt beitreten» und fülle das Anmeldeformular aus. Du benötigst nur deine E-Mail und ein Passwort.
</li>
<li>
<strong>Finde passende Events</strong><br>
Erkunde unsere Events, filtere nach Diät- oder Allergie-Einstellungen und melde dich zu den Events an, die dich interessieren!
</li>
<li>
<strong>Erstelle dein eigenes Event</strong><br>
Du kannst auch selbst ein Kochevent hosten! Klick auf «Event erstellen», beschreib dein Menü und lade Gäste ein.
</li>
</ol>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Fallen bei Invité Kosten an?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Nein, Invité ist komplett kostenlos. Alle Events basieren auf Freiwilligkeit und der Freude am Teilen. Es gibt keine versteckten Kosten nur die pure Absicht, die Community zu stärken.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Kann ich ein eigenes Event erstellen?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Ja, absolut! Du kannst dein eigenes Kochevent erstellen und Gäste einladen. Beschreibe dein Menü, die Teilnehmerzahl und weitere Details. Es ist deine Küche, dein Event, deine Regeln.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Wie funktioniert die An-/Abmeldung?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Bei jedem Event sehen dich die verfügbaren Plätze. Du kannst dich mit einem Klick anmelden. Eine Abmeldung ist bis 24 Stunden vor dem Event möglich so respektieren wir den Aufwand des Gastgebers.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Was ist mit Allergien und Diäten?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Ich kann Informationen zu Allergien und Ernährungseinstellungen in der Event-Beschreibung hinzufügen oder beim Anmelden angeben. So können Gastgeber und Gäste besser zusammenkommen und Überraschungen vermeiden.</p>
</div>
</div>
<div class="faq-item">
<button type="button" class="faq-trigger" aria-expanded="false">
<span class="faq-title">Ist Invité sicher und vertrauenswürdig?</span>
<span class="faq-icon">+</span>
</button>
<div class="faq-content">
<p>Ja, dein Profil hilft anderen, dich besser kennenzulernen. Wir ermutigen zu Offenheit und gegenseitigem Vertrauen. Allerdings bleibt es deine Entscheidung, wem du deine Adresse mitteilst die erfolgt nur 12 Stunden vor dem Event.</p>
</div>
</div>
</div>
</section>
</main>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
<!-- FAQ Akkordion Toggle Script -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const faqTriggers = document.querySelectorAll('.faq-trigger');
faqTriggers.forEach((trigger) => {
trigger.addEventListener('click', function(e) {
e.preventDefault();
const isExpanded = this.getAttribute('aria-expanded') === 'true';
// Close all other items (optional: comment out to allow multiple open)
faqTriggers.forEach((otherTrigger) => {
if (otherTrigger !== trigger) {
otherTrigger.setAttribute('aria-expanded', 'false');
}
});
// Toggle current item
this.setAttribute('aria-expanded', !isExpanded);
});
});
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,611 +0,0 @@
document.addEventListener('DOMContentLoaded', async () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const USERS_STORAGE_KEY = 'socialCookingUsers';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
const detailcontainer = document.getElementById('detail-view');
const locationIconPath = 'assets/icon_location.svg';
const calendarIconPath = 'assets/icon_calendar.svg';
const gastIconPath = 'assets/icon_gast.svg';
const currentUser = getCurrentUser();
const params = new URLSearchParams(window.location.search);
const eventId = parseInt(params.get('id'));
if (!eventId) {
window.location.href = 'event_overview.html';
return;
}
function getStoredEvents() {
try {
const stored = localStorage.getItem(EVENTS_STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error('Lokale Events konnten nicht gelesen werden.', error);
return [];
}
}
function getCurrentUser() {
try {
const stored = localStorage.getItem(CURRENT_USER_KEY);
return stored ? JSON.parse(stored) : null;
} catch (error) {
console.error('Aktueller Benutzer konnte nicht gelesen werden.', error);
return null;
}
}
function getRegistrationMap() {
try {
const stored = localStorage.getItem(REGISTRATION_STORAGE_KEY);
return stored ? JSON.parse(stored) : {};
} catch (error) {
console.error('Anmeldedaten konnten nicht gelesen werden.', error);
return {};
}
}
function getStoredUsers() {
try {
const stored = localStorage.getItem(USERS_STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error('Benutzerdaten konnten nicht gelesen werden.', error);
return [];
}
}
function getUserDisplayName(user) {
if (!user) return '';
const firstName = String(user.vorname || '').trim();
const lastName = String(user.nachname || '').trim();
const fullName = `${firstName} ${lastName}`.trim();
return (fullName || firstName || String(user.email || '').trim()).trim();
}
function getResolvedParticipants(event, registrationMap) {
const baseParticipants = Array.isArray(event.participants)
? event.participants.map(name => String(name || '').trim()).filter(Boolean)
: [];
const usersByEmail = new Map(
getStoredUsers().map(user => [String(user.email || '').trim().toLowerCase(), user])
);
const participantLookup = new Set(baseParticipants.map(name => name.toLowerCase()));
Object.entries(registrationMap || {}).forEach(([email, ids]) => {
const isRegisteredForEvent = Array.isArray(ids)
&& ids.map(id => Number(id)).includes(Number(event.id));
if (!isRegisteredForEvent) return;
const user = usersByEmail.get(String(email || '').trim().toLowerCase());
const displayName = getUserDisplayName(user) || String(email || '').trim();
const normalizedName = displayName.toLowerCase();
if (displayName && !participantLookup.has(normalizedName)) {
baseParticipants.push(displayName);
participantLookup.add(normalizedName);
}
});
return baseParticipants;
}
function getParticipantNameForViewer(name, canSeeLastName) {
const rawName = String(name || '').trim();
if (!rawName) return '';
if (canSeeLastName) return rawName;
if (rawName.includes('@')) return rawName.split('@')[0].trim() || rawName;
return rawName.split(/\s+/)[0];
}
function setRegistrationMap(registrationMap) {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap));
}
function getRegistrationIdsForUser(registrationMap, user) {
const userEmail = String(user?.email || '').trim().toLowerCase();
if (!userEmail) return [];
const matchingIds = Object.entries(registrationMap || {})
.filter(([email]) => String(email || '').trim().toLowerCase() === userEmail)
.flatMap(([, ids]) => (Array.isArray(ids) ? ids : []))
.map(id => Number(id))
.filter(id => Number.isFinite(id));
return Array.from(new Set(matchingIds));
}
function parseEventDateTime(event) {
if (!event?.date) return null;
const dateValue = String(event.date).trim();
const isoDateMatch = dateValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
let year, month, day;
if (isoDateMatch) {
year = Number(isoDateMatch[1]);
month = Number(isoDateMatch[2]);
day = Number(isoDateMatch[3]);
} else {
const monthMap = {
jan: 1,
januar: 1,
feb: 2,
februar: 2,
'mär': 3,
mrz: 3,
mar: 3,
maerz: 3,
märz: 3,
apr: 4,
april: 4,
mai: 5,
jun: 6,
juni: 6,
jul: 7,
juli: 7,
aug: 8,
august: 8,
sep: 9,
sept: 9,
september: 9,
okt: 10,
oktober: 10,
nov: 11,
november: 11,
dez: 12,
dezember: 12
};
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-Za-zÄÖÜäöü]{3,9})\.?\s*(\d{4})$/);
if (!localizedMatch) return null;
day = Number(localizedMatch[1]);
month = monthMap[String(localizedMatch[2]).toLowerCase()];
year = Number(localizedMatch[3]);
if (!month) return null;
}
const timeMatch = String(event.time || '').match(/(\d{1,2}):(\d{2})/);
const hours = timeMatch ? Number(timeMatch[1]) : 0;
const minutes = timeMatch ? Number(timeMatch[2]) : 0;
return new Date(year, month - 1, day, hours, minutes, 0, 0);
}
function isRegistrationClosedForEvent(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
const msUntilStart = eventDateTime.getTime() - Date.now();
return msUntilStart <= 24 * 60 * 60 * 1000;
}
function getDeregistrationInfo(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return { daysLeft: null, isClosed: false };
const oneDayMs = 24 * 60 * 60 * 1000;
const msUntilDeadline = (eventDateTime.getTime() - oneDayMs) - Date.now();
if (msUntilDeadline <= 0) return { daysLeft: 0, isClosed: true };
return { daysLeft: Math.ceil(msUntilDeadline / oneDayMs), isClosed: false };
}
function isAddressVisibleWindow(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
const now = Date.now();
const start = eventDateTime.getTime();
const revealStart = start - (24 * 60 * 60 * 1000);
const revealEnd = start + (1 * 60 * 60 * 1000);
return now >= revealStart && now <= revealEnd;
}
function isEventPastAddressWindow(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
const revealEnd = eventDateTime.getTime() + (1 * 60 * 60 * 1000);
return Date.now() > revealEnd;
}
function isEventOwnedByCurrentUser(event, user) {
if (!event || !user) return false;
const userEmail = String(user.email || '').trim().toLowerCase();
const hostEmail = String(event.hostEmail || '').trim().toLowerCase();
if (userEmail && hostEmail) return userEmail === hostEmail;
const userFirstName = String(user.vorname || '').trim().toLowerCase();
const hostName = String(event.host?.name || '').trim().toLowerCase();
return Boolean(userFirstName && hostName && userFirstName === hostName);
}
function isUserListedInEventParticipants(event, user) {
if (!event || !user || !Array.isArray(event.participants)) return false;
const participantSet = new Set(
event.participants.map(name => String(name || '').trim().toLowerCase()).filter(Boolean)
);
const userFirstName = String(user.vorname || '').trim().toLowerCase();
const userFullName = `${String(user.vorname || '').trim()} ${String(user.nachname || '').trim()}`.trim().toLowerCase();
return Boolean(
(userFirstName && participantSet.has(userFirstName))
|| (userFullName && participantSet.has(userFullName))
);
}
function formatEventDate(dateString) {
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
const [year, month, day] = dateString.split('-');
return `${Number(day)}. ${['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'][Number(month)-1]} ${year}`;
}
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 monthLabel = labels[match[2]];
return monthLabel ? `${Number(match[1])}. ${monthLabel} ${match[3]}` : dateString;
}
function formatEventTime(timeString) {
return timeString.replace('UHR', 'Uhr').trim();
}
function getDietLabel(diet) {
const labels = { FLEISCH:'Fleisch', FISCH:'Fisch', VEGGIE:'Vegetarisch', VEGAN:'Vegan' };
return labels[diet] || diet;
}
function getPlaceholderImageByEventType(event) {
const rawType = String(event?.eventType || event?.category || '')
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[+&/_-]/g, ' ')
.replace(/\s+/g, ' ');
if (rawType.includes('brunch')) {
return 'assets/platzhalter_brunch.jpeg';
}
if (rawType.includes('lunch')) {
return 'assets/platzhalter_lunch.jpeg';
}
if (
rawType.includes('kaffee')
|| rawType.includes('coffee')
|| rawType.includes('cafe')
|| rawType.includes('kuchen')
) {
return 'assets/platzhalter_kaffee.jpeg';
}
if (rawType.includes('dinner')) {
return 'assets/platzhalter_dinner.jpeg';
}
return 'assets/platzhalter_dinner.jpeg';
}
// Fetch data source and resolve the matching event record.
try {
const response = await fetch('data/events.json');
const apiEvents = await response.json();
const allEvents = [...getStoredEvents(), ...apiEvents];
const event = allEvents.find(e => e.id === eventId);
if (event) {
renderDetailPage(event);
} else {
detailcontainer.innerHTML = "<h1>Event wurde nicht gefunden.</h1><a href='event_overview.html'>Zurück zur Übersicht</a>";
}
} catch (error) {
console.error("Fehler beim Laden der Details:", error);
}
function renderDetailPage(event) {
const displayDate = formatEventDate(event.date);
const displayTime = formatEventTime(event.time);
const eventCategory = event.category || 'EVENT';
const hostName = event.host?.name || 'Host';
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 Kürze bekannt gegeben.'];
const specifications = Array.isArray(event.specifications) && event.specifications.length > 0
? event.specifications : [];
const registrationMap = getRegistrationMap();
const participants = getResolvedParticipants(event, registrationMap);
const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser);
const participantNamesForView = participants
.map(name => getParticipantNameForViewer(name, isOwnEvent))
.filter(Boolean);
const galleryImages = Array.isArray(event.gallery) ? event.gallery.filter(Boolean) : [];
const resolvedGalleryImages = galleryImages.length > 0
? galleryImages
: [getPlaceholderImageByEventType(event)];
const galleryLayoutClass = resolvedGalleryImages.length === 1
? 'detail-gallery detail-gallery-large detail-gallery-large--single'
: 'detail-gallery detail-gallery-large';
const galleryMarkup = resolvedGalleryImages.length > 0
? `<div class="${galleryLayoutClass}">
${resolvedGalleryImages.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>` : '';
const visibleParticipants = participantNamesForView.slice(0, 6);
const remainingParticipants = Math.max(0, participantNamesForView.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 isRegistrationClosed = isRegistrationClosedForEvent(event);
const deregInfo = getDeregistrationInfo(event);
const userRegistrations = getRegistrationIdsForUser(registrationMap, currentUser);
const isRegistered = userRegistrations.includes(Number(event.id));
const isListedParticipant = isUserListedInEventParticipants(event, currentUser);
const hasAddressAccess = isRegistered || isListedParticipant || isOwnEvent;
const isCanceled = event.status === 'canceled';
const actionButtonLabel = isCanceled ? 'Abgesagt'
: isOwnEvent ? 'Dein Event!'
: !currentUser ? 'Einloggen'
: isRegistered ? (deregInfo.isClosed ? 'Abmeldung geschlossen' : 'Abmelden')
: isRegistrationClosed ? 'Anmeldung geschlossen'
: 'Anmelden';
const actionButtonDisabled = isCanceled
|| isOwnEvent
|| (!isRegistered && (isFull || isRegistrationClosed))
|| (isRegistered && deregInfo.isClosed);
const actionButtonVariantClass = isOwnEvent ? ' button-primary-eigener-event'
: (isRegistered || isRegistrationClosed) ? ' button-primary-abmelden '
: ' button-primary ';
const shouldRevealAddress = Boolean(event.address) && isAddressVisibleWindow(event) && hasAddressAccess;
let addressMessage = 'Wenn du dich anmeldest, wird die Adresse für diesen Event wird 24 Stunden vorher genau hier sichtbar sein.';
if (isCanceled) {
addressMessage = 'Dieses Event wurde leider vom Gastgeber abgesagt.';
} else if (isOwnEvent) {
addressMessage = 'Deine Adresse für diesen Event wird 24 Stunden vorher genau hier für alle Teilnehmer sichtbar sein';
} else if (hasAddressAccess) {
addressMessage = 'Vielen Dank für die Anmeldung! Die Adresse für diesen Event wird 24 Stunden vorher genau hier sichtbar sein.';
}
if (!isCanceled && isEventPastAddressWindow(event)) {
addressMessage = 'Vielen Dank, dass du an diesem Event teilgenommen hast.';
}
const addressPanelMarkup = shouldRevealAddress
? `<article class="detail-panel"><h2 class="detail-section-title">Adresse</h2><p>${event.address}</p></article>`
: `<article class="detail-panel"><h2 class="detail-section-title">Adresse</h2><p>${addressMessage}</p></article>`;
const detailChips = [
`<span class="event-tag">${eventCategory}</span>`,
...event.diet.split(', ').filter(d => d.trim() && d !== 'Keine Angabe').map(d => `<span class="event-tag">${getDietLabel(d.trim())}</span>`),
...specifications.map(item => `<span class="event-tag">${item}</span>`)
].join('');
detailcontainer.innerHTML = `
<section class="detail-hero">
<div class="detail-top-row">
<span class="event-location"><img src="${locationIconPath}" class="icon" alt="">${event.location}</span>
<span class="event-date-time"><img src="${calendarIconPath}" class="icon" alt=""> ${displayDate} | ${displayTime}</span>
<span class="event-date-time"><img src="${gastIconPath}" class="icon" alt="">${confirmedGuests}/${totalGuests}</span>
</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="detail-panel">
<header class="host-header">
<span class="host-role">Host</span>
<span class="host-name">${hostName}</span>
</header>
${hostMessage.map(paragraph => `<p>${paragraph}</p>`).join('')}
</article>
<article class="detail-panel">
<h2 class="detail-section-title">Menu</h2>
<ul class="detail-menu-list">
${menuItems.map(item => `<li>${item}</li>`).join('')}
</ul>
</article>
<article class="detail-panel">
<div class="detail-participants-head">
<h2 class="detail-section-title">Teilnehmer</h2>
<button type="button" class="detail-participants-link" data-show-all-participants>Alle ansehen</button>
</div>
<div class="detail-avatar-row" data-participants-row>
${visibleParticipants.map(name => `<span class="participant-avatar">${name.charAt(0).toUpperCase()}</span>`).join('')}
${remainingParticipants > 0 ? `<span class="participant-more">+${remainingParticipants}</span>` : ''}
</div>
<div class="detail-participants-full hidden" data-participants-full>
${participantNamesForView.map(name => `
<div class="detail-participant-item">
<span class="participant-name">${name}</span>
</div>
`).join('')}
</div>
</article>
${addressPanelMarkup}
</div>
${galleryMarkup}
</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="event-date-time detail-action-location"><img src="${calendarIconPath}" alt=""> ${displayDate} | ${displayTime}</span>
<span class="event-gast detail-action-location"><img src="${gastIconPath}" alt="">${confirmedGuests}/${totalGuests}</span>
</small>
<strong>${event.title}</strong>
</div>
<div class="detail-action-btn-wrap">
<div class="detail-action-row" style="margin-left:auto; display:flex; gap:12px; align-items:center;">
${isFull ? `
<div class="detail-dereg-column">
<button class="button-plaetze event-spots-full" type="button" disabled>Ausgebucht</button>
<small class="detail-dereg-hint detail-dereg-hint--placeholder">&nbsp;</small>
</div>
` : ''}
${(!isFull || isRegistered) ? `
<div class="detail-dereg-column">
<button class="${actionButtonVariantClass.trim()}" type="button" data-register-button ${actionButtonDisabled ? 'disabled' : ''}>
${actionButtonLabel}
</button>
</div>
` : ''}
</div>
</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>
`;
// DOM references after render
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');
const registerButton = detailcontainer.querySelector('[data-register-button]');
// Eigene Events immer deaktiviert
if (registerButton && isOwnEvent) {
registerButton.disabled = true;
registerButton.textContent = 'Dein Event!';
registerButton.setAttribute('aria-disabled', 'true');
}
// Anmeldung / Abmeldung mit Bestätigungs-Modal
if (registerButton) {
registerButton.addEventListener('click', () => {
if (isOwnEvent) return;
if (!currentUser || !currentUser.email) {
window.location.href = 'login.html';
return;
}
const alreadyRegistered = (() => {
const map = getRegistrationMap();
const ids = Array.isArray(map[currentUser.email])
? map[currentUser.email].map(id => Number(id)) : [];
return ids.includes(Number(event.id));
})();
if (alreadyRegistered) {
const modal = document.getElementById('unregister-confirm-modal');
if (modal) modal.classList.add('show');
document.getElementById('confirm-unregister-btn').onclick = () => {
modal.classList.remove('show');
const map = getRegistrationMap();
const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
ids.delete(Number(event.id));
map[currentUser.email] = Array.from(ids);
setRegistrationMap(map);
const snackbar = document.getElementById('snackbar');
if (snackbar) {
snackbar.textContent = 'Du wurdest erfolgreich abgemeldet.';
snackbar.classList.add('snackbar--danger', 'snackbar--visible');
setTimeout(() => {
snackbar.classList.remove('snackbar--visible');
setTimeout(() => snackbar.classList.remove('snackbar--danger'), 400);
}, 4000);
}
renderDetailPage(event);
};
const closeUnregister = () => modal.classList.remove('show');
document.getElementById('unregister-modal-close')?.addEventListener('click', closeUnregister);
document.getElementById('unregister-modal-cancel')?.addEventListener('click', closeUnregister);
modal.addEventListener('click', e => { if (e.target === modal) closeUnregister(); });
} else if (!isFull && !isRegistrationClosed) {
const modal = document.getElementById('register-confirm-modal');
if (modal) modal.classList.add('show');
document.getElementById('confirm-register-btn').onclick = () => {
modal.classList.remove('show');
const map = getRegistrationMap();
const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
ids.add(Number(event.id));
map[currentUser.email] = Array.from(ids);
setRegistrationMap(map);
const snackbar = document.getElementById('snackbar');
if (snackbar) {
snackbar.textContent = 'Du wurdest erfolgreich angemeldet.';
snackbar.classList.add('snackbar--visible');
setTimeout(() => snackbar.classList.remove('snackbar--visible'), 4000);
}
renderDetailPage(event);
};
const closeRegister = () => modal.classList.remove('show');
document.getElementById('register-modal-close')?.addEventListener('click', closeRegister);
document.getElementById('register-modal-cancel')?.addEventListener('click', closeRegister);
modal.addEventListener('click', e => { if (e.target === modal) closeRegister(); });
}
});
}
// "Alle ansehen": Teilnehmerliste aufklappen / zuklappen
const showAllBtn = detailcontainer.querySelector('[data-show-all-participants]');
const avatarRow = detailcontainer.querySelector('[data-participants-row]');
const fullList = detailcontainer.querySelector('[data-participants-full]');
if (showAllBtn && avatarRow && fullList) {
showAllBtn.addEventListener('click', () => {
const isExpanded = !fullList.classList.contains('hidden');
fullList.classList.toggle('hidden');
avatarRow.classList.toggle('hidden');
showAllBtn.textContent = isExpanded ? 'Alle ansehen' : 'Weniger anzeigen';
});
}
// Lightbox
function closeLightbox() {
if (!lightbox) return;
lightbox.classList.remove('is-open');
lightbox.setAttribute('aria-hidden', 'true');
}
if (lightbox && lightboxImage) {
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');
});
});
lightbox.addEventListener('click', event => {
const target = event.target;
if (target instanceof HTMLElement && target.hasAttribute('data-close-lightbox')) {
closeLightbox();
}
});
lightboxClose?.addEventListener('click', closeLightbox);
document.addEventListener('keydown', event => {
if (event.key === 'Escape') closeLightbox();
});
}
}
});

View File

@ -1,838 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const USERS_STORAGE_KEY = 'socialCookingUsers';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
const INFO_MODAL_SHOWN_KEY = 'infoModalShownOnFirstLogin';
// -------------------------------------------------------------
// DOM references used throughout the page lifecycle.
// -------------------------------------------------------------
const eventGrid = document.getElementById('event-grid');
const filterButtons = document.querySelectorAll('.category-item');
const locationFilter = document.getElementById('location-filter');
const dateFilter = document.getElementById('date-filter');
const locationIconPath = 'assets/icon_location.svg';
const calendarIconPath = 'assets/icon_calendar.svg';
const gastIconPath = 'assets/icon_gast.svg';
// -------------------------------------------------------------
// In-memory state for fetched events and currently active filters.
// Separate state for category, diet, and allergie selections.
// -------------------------------------------------------------
let allEvents = [];
let activeCategory = 'ALLE';
let activeDiets = new Set();
let activeAllergies = new Set();
const currentUser = getCurrentUser();
function getCurrentUser() {
try {
const stored = localStorage.getItem(CURRENT_USER_KEY);
return stored ? JSON.parse(stored) : null;
} catch (error) {
console.error('Aktueller Benutzer konnte nicht gelesen werden.', error);
return null;
}
}
function getInfoModalShownKeyForUser(user) {
const email = String(user?.email || '').trim().toLowerCase();
return email ? `${INFO_MODAL_SHOWN_KEY}:${email}` : INFO_MODAL_SHOWN_KEY;
}
// Prüft, ob ein Event dem aktuellen Benutzer gehört.
function isEventOwnedByCurrentUser(event, user) {
if (!event || !user) {
return false;
}
const userEmail = String(user.email || '').trim().toLowerCase();
const hostEmail = String(event.hostEmail || '').trim().toLowerCase();
if (userEmail && hostEmail) {
return userEmail === hostEmail;
}
const userFirstName = String(user.vorname || '').trim().toLowerCase();
const hostName = String(event.host?.name || '').trim().toLowerCase();
return Boolean(userFirstName && hostName && userFirstName === hostName);
}
function getStoredEvents() {
try {
const stored = localStorage.getItem(EVENTS_STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error('Lokale Events konnten nicht gelesen werden.', error);
return [];
}
}
function getRegistrationMap() {
try {
const stored = localStorage.getItem(REGISTRATION_STORAGE_KEY);
return stored ? JSON.parse(stored) : {};
} catch (error) {
console.error('Anmeldedaten konnten nicht gelesen werden.', error);
return {};
}
}
function getStoredUsers() {
try {
const stored = localStorage.getItem(USERS_STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error('Benutzerdaten konnten nicht gelesen werden.', error);
return [];
}
}
function getUserDisplayName(user) {
if (!user) {
return '';
}
const firstName = String(user.vorname || '').trim();
const lastName = String(user.nachname || '').trim();
const fullName = `${firstName} ${lastName}`.trim();
return (fullName || firstName || String(user.email || '').trim()).trim();
}
function getResolvedParticipants(event, registrationMap) {
const baseParticipants = Array.isArray(event.participants)
? event.participants.map(name => String(name || '').trim()).filter(Boolean)
: [];
const usersByEmail = new Map(
getStoredUsers().map(user => [String(user.email || '').trim().toLowerCase(), user])
);
const participantLookup = new Set(baseParticipants.map(name => name.toLowerCase()));
Object.entries(registrationMap || {}).forEach(([email, ids]) => {
const isRegisteredForEvent = Array.isArray(ids)
&& ids.map(id => Number(id)).includes(Number(event.id));
if (!isRegisteredForEvent) {
return;
}
const user = usersByEmail.get(String(email || '').trim().toLowerCase());
const displayName = getUserDisplayName(user) || String(email || '').trim();
const normalizedName = displayName.toLowerCase();
if (displayName && !participantLookup.has(normalizedName)) {
baseParticipants.push(displayName);
participantLookup.add(normalizedName);
}
});
return baseParticipants;
}
function setRegistrationMap(registrationMap) {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap));
}
// -------------------------------------------------------------
// Initial data bootstrap:
// 1) fetch JSON,
// 2) populate select options,
// 3) restore filter state from sessionStorage,
// 4) render filtered list.
// -------------------------------------------------------------
async function fetchEvents() {
try {
const response = await fetch('data/events.json');
const apiEvents = await response.json();
const localEvents = getStoredEvents();
allEvents = [...localEvents, ...apiEvents];
populateMetaFilters();
const savedCategory = sessionStorage.getItem('activeFilter') || 'ALLE';
const savedLocation = sessionStorage.getItem('activeLocation') || 'ALLE_ORTE';
const savedDate = sessionStorage.getItem('activeDate') || '';
const savedDiets = sessionStorage.getItem('activeDiets') || '';
const savedAllergies = sessionStorage.getItem('activeAllergies') || '';
activeCategory = savedCategory;
activeDiets = new Set(savedDiets ? savedDiets.split(',') : []);
activeAllergies = new Set(savedAllergies ? savedAllergies.split(',') : []);
if (locationFilter) {
locationFilter.value = hasOption(locationFilter, savedLocation) ? savedLocation : 'ALLE_ORTE';
}
if (dateFilter) {
dateFilter.value = savedDate;
}
updateDietAvailability();
applyFilters();
} catch (error) {
console.error('Fehler:', error);
eventGrid.innerHTML = '<p>Events konnten nicht geladen werden.</p>';
}
}
// 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) {
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
return 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) {
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
const [year, month, day] = dateString.split('-');
return `${Number(day)}. ${['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'][Number(month) - 1]} ${year}`;
}
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) {
if (!timeString) {
return '';
}
return timeString.includes('UHR')
? timeString.replace('UHR', 'Uhr').trim()
: `${timeString} Uhr`;
}
// Baut aus Eventdatum/-zeit ein Date-Objekt für Fristlogik und Vergleiche.
function parseEventDateTime(event) {
if (!event?.date) {
return null;
}
const dateValue = String(event.date).trim();
const isoDateMatch = dateValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
let year;
let month;
let day;
if (isoDateMatch) {
year = Number(isoDateMatch[1]);
month = Number(isoDateMatch[2]);
day = Number(isoDateMatch[3]);
} else {
const monthMap = {
jan: 1,
januar: 1,
feb: 2,
februar: 2,
'mär': 3,
mrz: 3,
mar: 3,
maerz: 3,
märz: 3,
apr: 4,
april: 4,
mai: 5,
jun: 6,
juni: 6,
jul: 7,
juli: 7,
aug: 8,
august: 8,
sep: 9,
sept: 9,
september: 9,
okt: 10,
oktober: 10,
nov: 11,
november: 11,
dez: 12,
dezember: 12
};
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-Za-zÄÖÜäöü]{3,9})\.?\s*(\d{4})$/);
if (!localizedMatch) {
return null;
}
day = Number(localizedMatch[1]);
month = monthMap[String(localizedMatch[2]).toLowerCase()];
year = Number(localizedMatch[3]);
if (!month) {
return null;
}
}
const timeMatch = String(event.time || '').match(/(\d{1,2}):(\d{2})/);
const hours = timeMatch ? Number(timeMatch[1]) : 0;
const minutes = timeMatch ? Number(timeMatch[2]) : 0;
return new Date(year, month - 1, day, hours, minutes, 0, 0);
}
// Zählt eindeutige Registrierungen eines Events über alle Benutzer.
function countRegistrationsForEvent(registrationMap, eventId) {
return Object.values(registrationMap).reduce((count, ids) => {
const hasEvent = Array.isArray(ids)
&& ids.map(id => Number(id)).includes(Number(eventId));
return hasEvent ? count + 1 : count;
}, 0);
}
// Schliesst neue Anmeldungen ab 24h vor Start (inkl. bereits gestarteter Events).
function isRegistrationClosedForEvent(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) {
return false;
}
const msUntilStart = eventDateTime.getTime() - Date.now();
const twentyfourHoursInMs = 24 * 60 * 60 * 1000;
return msUntilStart <= twentyfourHoursInMs;
}
// 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, diet, allergie, location, date), update button state, render and persist.
function applyFilters() {
const selectedLocation = locationFilter ? locationFilter.value : 'ALLE_ORTE';
const selectedDate = dateFilter ? dateFilter.value : '';
// Update active states for all filter types
filterButtons.forEach(btn => {
const isCategoryButton = btn.getAttribute('data-cat') !== null;
const isDietButton = btn.getAttribute('data-diet') !== null;
const isAllergieButton = btn.getAttribute('data-allergie') !== null;
if (isCategoryButton) {
if (btn.getAttribute('data-cat') === activeCategory) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
} else if (isDietButton) {
if (activeDiets.has(btn.getAttribute('data-diet'))) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
} else if (isAllergieButton) {
if (activeAllergies.has(btn.getAttribute('data-allergie'))) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
}
});
const filtered = allEvents.filter(event => {
if (event.status === 'canceled') return false;
const categoryMatch = activeCategory === 'ALLE' || event.category === activeCategory;
// Diet filter: if no diets selected, show all. Otherwise, event MUST have at least one selected diet.
const dietMatch = activeDiets.size === 0 ||
(event.diet && event.diet.split(', ').some(d => activeDiets.has(d.trim())));
// Allergie filter: if no allergies selected, show all. Otherwise, event MUST have at least one selected allergie.
const allergieMatch = activeAllergies.size === 0 ||
(event.specifications && event.specifications.some(spec => activeAllergies.has(spec)));
const locationMatch = selectedLocation === 'ALLE_ORTE' || event.location === selectedLocation;
const eventDateIso = parseEventDateToIso(event.date);
const dateMatch = !selectedDate || eventDateIso === selectedDate;
return categoryMatch && dietMatch && allergieMatch && locationMatch && dateMatch;
});
renderEvents(filtered);
sessionStorage.setItem('activeFilter', activeCategory);
sessionStorage.setItem('activeLocation', selectedLocation);
sessionStorage.setItem('activeDate', selectedDate);
sessionStorage.setItem('activeDiets', Array.from(activeDiets).join(','));
sessionStorage.setItem('activeAllergies', Array.from(activeAllergies).join(','));
}
// Render either:
// - empty state call-to-action when no results match,
// - or event cards with status and metadata.
function renderEvents(events) {
eventGrid.innerHTML = '';
const registrationMap = getRegistrationMap();
const userRegistrationSet = currentUser?.email && Array.isArray(registrationMap[currentUser.email])
? new Set(registrationMap[currentUser.email].map(id => Number(id)))
: new Set();
if (events.length === 0) {
eventGrid.innerHTML = `
<div class="empty-state">
<p class="empty-state-kicker">Keine Treffer</p>
<h3>Schade, aktuell gibt es hier keine Events.</h3>
<p>Starte dein eigenes Event und bringe die Community an deinen Tisch.</p>
<a class="empty-state-link" href="event_create.html">
<button class="button-primary" type="button">Event erstellen</button>
</a>
</div>
`;
return;
}
events.forEach(event => {
// Card shell and click-through navigation to detail page.
const card = document.createElement('article');
card.className = 'event-card';
card.style.cursor = 'pointer';
card.addEventListener('click', clickedEvent => {
if (clickedEvent.target instanceof HTMLElement && clickedEvent.target.closest('button')) {
return;
}
window.location.href = `event_detail.html?id=${event.id}`;
});
const displayDate = formatEventDate(event.date);
const displayTime = formatEventTime(event.time);
// Capacity logic:
// spots = total capacity, resolved participants = booked seats.
const resolvedParticipants = getResolvedParticipants(event, registrationMap);
const bookedSeats = resolvedParticipants.length;
const totalCapacity = event.spots;
const freePlaces = Math.max(0, totalCapacity - bookedSeats);
const isFull = freePlaces === 0;
const isOwnEvent = isEventOwnedByCurrentUser(event, currentUser);
const isRegistered = userRegistrationSet.has(Number(event.id));
const isRegistrationClosed = isRegistrationClosedForEvent(event);
const isCanceled = event.status === 'canceled';
if (isCanceled) {
card.style.opacity = '0.6';
}
// 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('')
: '';
// Build diet tags: split by comma and create individual tags
const dietTags = event.diet && event.diet !== 'Keine Angabe' && event.diet !== ''
? event.diet.split(', ').map(d => `<span class="event-tag">${d.trim()}</span>`).join('')
: '';
let actionMarkup = '';
if (isCanceled) {
actionMarkup = '<button class="button-primary" type="button" disabled>Abgesagt</button>';
} else if (isOwnEvent) {
actionMarkup = '<button class="button-primary-eigener-event" type="button" data-registration-action="own" disabled>Dein Event!</button>';
} else if (isRegistered) {
actionMarkup = isRegistrationClosed
? '<button class="button-primary-abmelden" type="button" disabled>Abmeldung geschlossen</button>'
: '<button class="button-primary-abmelden" type="button" data-registration-action="unregister">Abmelden</button>';
} else if (isRegistrationClosed) {
actionMarkup = '<button class="button-primary-abmelden" type="button" data-registration-action="closed" disabled>Anmeldung geschlossen</button>';
} else if (!isFull) {
if (!currentUser) {
actionMarkup = '<button class="button-primary btn-primary-register" type="button" data-registration-action="login">Anmelden</button>';
} else {
actionMarkup = '<button class="button-primary btn-primary-register" type="button" data-registration-action="register">Anmelden</button>';
}
}
let sideInfoMarkup = '';
if (isCanceled) {
sideInfoMarkup = '<span class="button-plaetze">Event abgesagt</span>';
} else if (!isRegistrationClosed) {
const spotLabel = freePlaces === 1 ? 'Platz frei' : 'Plätze frei';
sideInfoMarkup = `<span class="button-plaetze${isFull ? ' event-spots-full' : ''}">${isFull ? 'Ausgebucht' : `${freePlaces} ${spotLabel}`}</span>`;
}
card.innerHTML = `
<div class="event-main">
<div class="event-top-row">
<span class="event-location">
<img src="${locationIconPath}" class="icon" alt="">
${event.location}
</span>
<span class="event-date-time"> <img src="${calendarIconPath}" class="icon" alt=""> ${displayDate} | ${displayTime}
</span>
<span class="event-gast"> <img src="${gastIconPath}" class="icon" alt="Gaeste Icon">${bookedSeats}/${totalCapacity} </span>
</div>
<h2>${event.title}</h2>
<div class="event-meta-row">
<span class="event-tag">${event.category}</span>
${dietTags}
${specsChips}
</div>
</div>
<div class="event-side${isFull ? ' event-side-full' : ''}">
${actionMarkup}
${sideInfoMarkup}
</div>
`;
const actionButton = card.querySelector('[data-registration-action]');
if (actionButton) {
actionButton.addEventListener('click', clickEvent => {
clickEvent.stopPropagation();
const action = actionButton.getAttribute('data-registration-action');
if (action === 'own') {
return;
}
if (action === 'closed') {
return;
}
if (action === 'login') {
window.location.href = 'login.html';
return;
}
if (!currentUser?.email) {
window.location.href = 'login.html';
return;
}
const nextRegistrationMap = getRegistrationMap();
const currentIds = Array.isArray(nextRegistrationMap[currentUser.email])
? nextRegistrationMap[currentUser.email].map(id => Number(id))
: [];
const idSet = new Set(currentIds);
// Anmelde-Modal öffnen
if (action === 'register' && !isFull && !isRegistrationClosed) {
const modal = document.getElementById('register-confirm-modal');
if (modal) {
modal.classList.add('show');
document.getElementById('confirm-register-btn').onclick = () => {
modal.classList.remove('show');
const map = getRegistrationMap();
const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
ids.add(Number(event.id));
map[currentUser.email] = Array.from(ids);
setRegistrationMap(map);
const snackbar = document.getElementById('snackbar');
if (snackbar) {
snackbar.textContent = 'Du wurdest erfolgreich angemeldet.';
snackbar.classList.add('snackbar--visible');
setTimeout(() => snackbar.classList.remove('snackbar--visible'), 3000);
}
applyFilters();
};
}
return;
}
// Abmelde-Modal öffnen
if (action === 'unregister') {
const modal = document.getElementById('unregister-confirm-modal');
if (modal) {
modal.classList.add('show');
document.getElementById('confirm-unregister-btn').onclick = () => {
modal.classList.remove('show');
const map = getRegistrationMap();
const ids = new Set((map[currentUser.email] || []).map(id => Number(id)));
ids.delete(Number(event.id));
map[currentUser.email] = Array.from(ids);
setRegistrationMap(map);
const snackbar = document.getElementById('snackbar');
if (snackbar) {
snackbar.textContent = 'Du wurdest erfolgreich abgemeldet.';
snackbar.classList.add('snackbar--danger', 'snackbar--visible');
setTimeout(() => {
snackbar.classList.remove('snackbar--visible');
setTimeout(() => snackbar.classList.remove('snackbar--danger'), 400);
}, 3000);
}
applyFilters();
};
}
return;
}
nextRegistrationMap[currentUser.email] = Array.from(idSet);
setRegistrationMap(nextRegistrationMap);
applyFilters();
});
}
eventGrid.appendChild(card);
});
}
// Verhindert widersprüchliche Ernährungsformen:
// Fleisch/Fisch schliessen Vegetarisch/Vegan aus.
// Vegan schliesst alles andere (Fleisch, Fisch, Vegetarisch) aus.
// Vegetarisch schliesst Fleisch/Fisch aus, aber nicht Vegan.
function updateDietAvailability() {
const dietButtons = Array.from(filterButtons).filter(btn => btn.getAttribute('data-diet') !== null);
const meatFishButtons = dietButtons.filter(btn => ['Fleisch', 'Fisch'].includes(btn.getAttribute('data-diet')));
const plantButtons = dietButtons.filter(btn => ['Vegetarisch', 'Vegan'].includes(btn.getAttribute('data-diet')));
const vegetarischBtn = dietButtons.find(btn => btn.getAttribute('data-diet') === 'Vegetarisch');
const veganBtn = dietButtons.find(btn => btn.getAttribute('data-diet') === 'Vegan');
const hasVegan = activeDiets.has('Vegan');
const hasVegetarisch = activeDiets.has('Vegetarisch');
const hasMeatOrFish = meatFishButtons.some(btn => activeDiets.has(btn.getAttribute('data-diet')));
// If Vegan is selected, disable everything else
if (hasVegan) {
meatFishButtons.forEach(btn => {
btn.classList.add('disabled');
});
if (vegetarischBtn) {
vegetarischBtn.classList.add('disabled');
}
}
// If Vegetarisch is selected, disable only Fleisch/Fisch
else if (hasVegetarisch) {
meatFishButtons.forEach(btn => {
btn.classList.add('disabled');
});
if (veganBtn) {
veganBtn.classList.remove('disabled');
}
}
// If Fleisch/Fisch is selected, disable both Vegetarisch and Vegan
else if (hasMeatOrFish) {
if (vegetarischBtn) {
vegetarischBtn.classList.add('disabled');
}
if (veganBtn) {
veganBtn.classList.add('disabled');
}
}
// No conflicts, enable everything
else {
plantButtons.forEach(btn => {
btn.classList.remove('disabled');
});
meatFishButtons.forEach(btn => {
btn.classList.remove('disabled');
});
}
}
// Category filter interactions: mutually exclusive (radio button behavior).
filterButtons.forEach(button => {
button.addEventListener('click', () => {
const categoryValue = button.getAttribute('data-cat');
const dietValue = button.getAttribute('data-diet');
const allergieValue = button.getAttribute('data-allergie');
if (categoryValue !== null) {
// Category filter: exclusive selection
activeCategory = categoryValue;
} else if (dietValue !== null) {
// Diet filter: toggle selection with conflict handling
const isCurrentlySelected = activeDiets.has(dietValue);
if (!isCurrentlySelected) {
// Adding a diet - handle conflicts
if (dietValue === 'Vegetarisch') {
// Vegetarisch removes Fleisch/Fisch but not Vegan
activeDiets.delete('Fleisch');
activeDiets.delete('Fisch');
activeDiets.add('Vegetarisch');
} else if (dietValue === 'Vegan') {
// Vegan removes all other options
activeDiets.delete('Vegetarisch');
activeDiets.delete('Fleisch');
activeDiets.delete('Fisch');
activeDiets.add('Vegan');
} else if (dietValue === 'Fleisch' || dietValue === 'Fisch') {
// Fleisch/Fisch remove Vegetarisch/Vegan
activeDiets.delete('Vegetarisch');
activeDiets.delete('Vegan');
activeDiets.add(dietValue);
}
} else {
// Removing a diet
activeDiets.delete(dietValue);
}
updateDietAvailability();
} else if (allergieValue !== null) {
// Allergie filter: toggle selection
if (activeAllergies.has(allergieValue)) {
activeAllergies.delete(allergieValue);
} else {
activeAllergies.add(allergieValue);
}
}
applyFilters();
});
});
// Secondary filter interactions.
if (locationFilter) {
locationFilter.addEventListener('change', applyFilters);
}
if (dateFilter) {
dateFilter.addEventListener('change', applyFilters);
// Make calendar icon clickable to focus the date input
const calendarIcon = document.querySelector('.calendar-icon');
if (calendarIcon) {
calendarIcon.addEventListener('click', () => {
dateFilter.focus();
dateFilter.click();
});
}
}
// Clear all filters button
const clearAllFiltersBtn = document.getElementById('clear-all-filters');
if (clearAllFiltersBtn) {
clearAllFiltersBtn.addEventListener('click', () => {
activeCategory = 'ALLE';
activeDiets.clear();
activeAllergies.clear();
if (locationFilter) {
locationFilter.value = 'ALLE_ORTE';
}
if (dateFilter) {
dateFilter.value = '';
}
updateDietAvailability();
applyFilters();
});
}
// Info button modal behavior
const infoButton = document.getElementById('info-button');
const infoModal = document.getElementById('info-modal');
const modalClose = infoModal?.querySelector('.modal-close');
if (infoButton && infoModal) {
infoButton.addEventListener('click', () => {
infoModal.classList.add('show');
});
}
if (modalClose && infoModal) {
modalClose.addEventListener('click', () => {
infoModal.classList.remove('show');
});
}
if (infoModal) {
infoModal.addEventListener('click', (event) => {
if (event.target === infoModal) {
infoModal.classList.remove('show');
}
});
}
// Auto-open info modal on first login
if (currentUser && infoModal) {
const userInfoModalKey = getInfoModalShownKeyForUser(currentUser);
const hasShownInfoModal = localStorage.getItem(userInfoModalKey);
if (!hasShownInfoModal) {
infoModal.classList.add('show');
localStorage.setItem(userInfoModalKey, 'true');
}
}
// Kick off initial load/render cycle.
fetchEvents();
});
// Modal closing helper functions
function closeRegisterModal() {
const modal = document.getElementById('register-confirm-modal');
if (modal) {
modal.classList.remove('show');
}
}
function closeUnregisterModal() {
const modal = document.getElementById('unregister-confirm-modal');
if (modal) {
modal.classList.remove('show');
}
}
function closeInfoModal() {
const modal = document.getElementById('info-modal');
if (modal) {
modal.classList.remove('show');
}
}

View File

@ -1,143 +0,0 @@
// =============================================
// Galerie-Karussell (Startseite)
// Diese Datei steuert die Foto-Galerie mit Pfeilen.
// =============================================
// Wichtige Elemente aus dem HTML holen.
const carouselTrack = document.querySelector('.gallery__track');
const prevArrow = document.querySelector('.gallery__arrow--prev');
const nextArrow = document.querySelector('.gallery__arrow--next');
const dotscontainer = document.querySelector('.gallery_dots');
// Nur ausführen, wenn die Galerie auf der Seite vorhanden ist.
if (carouselTrack) {
// Alle einzelnen Karten/Bilder im Track sammeln.
const items = Array.from(carouselTrack.querySelectorAll('.gallery__item'));
// Auf Mobile zeigen wir 1 Bild, auf Desktop 3 Bilder pro "Seite".
const getItemsPerPage = () => (window.matchMedia('(max-width: 900px)').matches ? 1 : 3);
let itemsPerPage = getItemsPerPage();
let pageCount = Math.ceil(items.length / itemsPerPage);
let activePage = 0;
var dots = [];
function buildDots() {
if (!dotscontainer) return;
dotscontainer.innerHTML = '';
dots = [];
for (var i = 0; i < pageCount; i++) {
var dot = document.createElement('button');
dot.type = 'button';
dot.className = 'gallery_dot' + (i === activePage ? ' gallery_dot--active' : '');
dot.setAttribute('role', 'tab');
dot.setAttribute('aria-selected', i === activePage ? 'true' : 'false');
dot.setAttribute('aria-label', 'Seite ' + (i + 1) + ' von ' + pageCount);
dot.dataset.page = i;
dot.addEventListener('click', function() {
goToPage(parseInt(this.dataset.page));
});
dotscontainer.appendChild(dot);
dots.push(dot);
}
}
function updateDots() {
dots.forEach(function(dot, i) {
dot.classList.toggle('gallery_dot--active', i === activePage);
dot.setAttribute('aria-selected', i === activePage ? 'true' : 'false');
});
}
function updateTrack() {
var gap = parseFloat(getComputedStyle(carouselTrack).gap) || 20;
var itemWidth = items[0].getBoundingClientRect().width;
var offset = activePage * (itemWidth + gap) * itemsPerPage;
carouselTrack.style.transform = 'translateX(-' + offset + 'px)';
carouselTrack.style.transition = 'transform 0.4s ease';
updateDots();
}
function goToPage(page) {
activePage = page;
updateTrack();
}
// Geht zur nächsten Seite (mit Wrap-around am Ende).
function showNext() {
activePage = (activePage + 1) % pageCount;
updateTrack();
}
// Geht zur vorherigen Seite (mit Wrap-around zum Ende).
function showPrev() {
activePage = (activePage - 1 + pageCount) % pageCount;
updateTrack();
}
buildDots();
// Klick-Steuerung der Pfeile.
if (nextArrow) nextArrow.addEventListener('click', showNext);
if (prevArrow) prevArrow.addEventListener('click', showPrev);
// Tastatur-Support für Barrierefreiheit.
document.addEventListener('keydown', function(event) {
if (event.key === 'ArrowRight') showNext();
if (event.key === 'ArrowLeft') showPrev();
});
// Reagiert auf Bildschirmgrössen-Änderungen.
window.addEventListener('resize', function() {
var newPerPage = getItemsPerPage();
if (newPerPage !== itemsPerPage) {
itemsPerPage = newPerPage;
pageCount = Math.ceil(items.length / itemsPerPage);
activePage = 0;
}
buildDots();
updateTrack();
});
// =============================================
// Lightbox: Bild vergrössern bei Klick
// =============================================
const lightbox = document.getElementById('gallery-lightbox');
const lightboxImage = lightbox ? lightbox.querySelector('.lightbox__image') : null;
function openLightbox(src, alt) {
if (!lightbox || !lightboxImage) return;
lightboxImage.src = src;
lightboxImage.alt = alt || 'Grossansicht';
lightbox.classList.add('is-open');
lightbox.setAttribute('aria-hidden', 'false');
}
function closeLightbox() {
if (!lightbox) return;
lightbox.classList.remove('is-open');
lightbox.setAttribute('aria-hidden', 'true');
lightboxImage.src = '';
}
// Klick auf Galerie-Bild öffnet die Lightbox.
items.forEach(function(item) {
var img = item.querySelector('img');
if (img) {
item.addEventListener('click', function() {
openLightbox(img.src, img.alt);
});
}
});
// Lightbox schliessen: Backdrop, Close-Button oder ESC-Taste.
if (lightbox) {
lightbox.querySelector('.lightbox__close').addEventListener('click', closeLightbox);
lightbox.querySelector('[data-close-lightbox]').addEventListener('click', closeLightbox);
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && lightbox.classList.contains('is-open')) {
closeLightbox();
}
});
}
}

View File

@ -1,52 +0,0 @@
// =============================================
// Mini-Galerie auf der Landingpage
// Diese Datei hebt immer ein Bild hervor und
// erlaubt Navigation mit Pfeilen/Tastatur.
// =============================================
// Elemente aus dem DOM lesen.
const prevBtn = document.querySelector('.arrow--prev');
const nextBtn = document.querySelector('.arrow--next');
const items = Array.from(document.querySelectorAll('.gallery__item'));
let activeIndex = 0;
// Aktualisiert die Darstellung aller Bilder:
// - aktives Bild ist klar sichtbar
// - inaktive Bilder sind abgeblendet
function updateGallery() {
items.forEach((item, i) => {
item.style.opacity = i === activeIndex ? '1' : '0.35';
item.style.transform = i === activeIndex ? 'scale(1)' : 'scale(0.95)';
});
}
// Ein Schritt nach rechts.
function showNext() {
if (!items.length) return;
activeIndex = (activeIndex + 1) % items.length;
updateGallery();
}
// Ein Schritt nach links.
function showPrev() {
if (!items.length) return;
activeIndex = (activeIndex - 1 + items.length) % items.length;
updateGallery();
}
// Event-Handler nur registrieren, wenn die Buttons existieren.
if (nextBtn) nextBtn.addEventListener('click', showNext);
if (prevBtn) prevBtn.addEventListener('click', showPrev);
// Tastatursteuerung für bessere Bedienbarkeit.
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowRight') {
showNext();
}
if (event.key === 'ArrowLeft') {
showPrev();
}
});
// Initialen Zustand einmal setzen.
updateGallery();

View File

@ -1,140 +0,0 @@
// =============================================
// Login-Logik
// Diese Datei validiert die Eingaben, sucht den
// Benutzer im localStorage und legt die Session an.
// =============================================
// Formular und Felder aus dem HTML holen.
const loginForm = document.getElementById('loginForm');
const emailInput = document.getElementById('email');
const passwortInput = document.getElementById('passwort');
const emailError = document.getElementById('emailError');
const passwortError = document.getElementById('passwortError');
const USERS_STORAGE_KEY = 'socialCookingUsers';
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
// Liest alle registrierten Benutzer robust aus localStorage.
function getStoredUsers() {
try {
const raw = localStorage.getItem(USERS_STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
} catch (error) {
console.error('Benutzerdaten konnten nicht gelesen werden.', error);
return [];
}
}
// Speichert den aktiven Benutzer für nachfolgende Seiten.
function setCurrentUser(user) {
localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(user));
}
// Erstellt einen Demo-Benutzer, falls für die E-Mail noch kein Account existiert.
function createFallbackUser(email, passwort) {
const localPart = email.split('@')[0] || 'Gast';
const normalized = localPart.replace(/[._-]/g, ' ').trim();
const guessedVorname = normalized ? normalized.split(' ')[0] : 'Gast';
return {
id: Date.now(),
vorname: guessedVorname.charAt(0).toUpperCase() + guessedVorname.slice(1),
nachname: '',
email,
passwort,
createdAt: new Date().toISOString(),
source: 'login-fallback'
};
}
// Validierungsfunktion
function validateForm(event) {
event.preventDefault();
// Wir zeigen bewusst immer nur den ersten Fehler im Formular an.
// So bleibt der Ablauf ruhig und führt den Nutzer Feld für Feld.
const emailValue = emailInput.value.trim();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const emailGroup = emailInput.parentElement;
const passwortGroup = passwortInput.parentElement;
emailGroup.classList.remove('has-error');
passwortGroup.classList.remove('has-error');
if (!emailValue) {
emailGroup.classList.add('has-error');
emailError.textContent = 'Bitte gib deine E-Mail Adresse ein.';
emailInput.focus();
return;
}
if (!emailRegex.test(emailValue)) {
emailGroup.classList.add('has-error');
emailError.textContent = 'Bitte gib eine gültige E-Mail Adresse ein.';
emailInput.focus();
return;
}
// Passwort-Validierung
const passwortValue = passwortInput.value;
if (!passwortValue) {
passwortGroup.classList.add('has-error');
passwortError.textContent = 'Bitte gib dein Passwort ein.';
passwortInput.focus();
return;
}
if (passwortValue.length < 6) {
passwortGroup.classList.add('has-error');
passwortError.textContent = 'Dein Passwort ist zu kurz. Bitte überprüfe dein Passwort.';
passwortInput.focus();
return;
}
// Wenn alle Validierungen bestanden, prüfen wir:
// 1) gibt es den Benutzer schon?
// 2) ist das Passwort korrekt?
// Danach speichern wir die aktive Session.
const users = getStoredUsers();
const matchedUser = users.find(user => user.email?.toLowerCase() === emailValue.toLowerCase());
if (matchedUser && matchedUser.passwort !== passwortValue) {
passwortGroup.classList.add('has-error');
passwortError.textContent = 'Das Passwort ist nicht korrekt.';
passwortInput.focus();
return;
}
const userToLogin = matchedUser || createFallbackUser(emailValue, passwortValue);
setCurrentUser(userToLogin);
// Snackbar anzeigen und dann zur Event-Übersicht weiterleiten.
var snackbar = document.getElementById('snackbar');
if (snackbar) {
snackbar.classList.add('snackbar--visible');
setTimeout(function() {
window.location.href = 'event_overview.html';
}, 2000);
} else {
window.location.href = 'event_overview.html';
}
}
// Fehlerbehandlung bei Input-Änderung (entfernt Fehler wenn Benutzer korrigiert)
emailInput.addEventListener('input', function() {
const emailGroup = this.parentElement;
if (this.value.trim()) {
emailGroup.classList.remove('has-error');
}
});
passwortInput.addEventListener('input', function() {
const passwortGroup = this.parentElement;
if (this.value) {
passwortGroup.classList.remove('has-error');
}
});
// Form Submit Event
loginForm.addEventListener('submit', validateForm);

View File

@ -1,847 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const USERS_STORAGE_KEY = 'socialCookingUsers';
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
// Zentrale DOM-Referenzen für klare, testbare Funktionen.
const loggedOutState = document.getElementById('logged-out-state');
const loggedInContent = document.getElementById('logged-in-content');
const profileHeadline = document.getElementById('headline');
const profileSubline = document.getElementById('profile-subline');
const logoutButton = document.getElementById('logout-button');
const profileTabButtons = Array.from(document.querySelectorAll('[data-category-item]'));
const profileTabPanels = Array.from(document.querySelectorAll('[data-profile-panel]'));
const myEventsCount = document.getElementById('my-events-count');
const myEventsBtnCount = document.getElementById('btn-my-events-count');
const myRegistrationsCount = document.getElementById('my-registrations-count');
const myRegistrationsBtnCount = document.getElementById('btn-my-registrations-count');
const myEventsList = document.getElementById('my-events-list');
const myRegistrationsList = document.getElementById('my-registrations-list');
const profileForm = document.getElementById('profile-form');
const profileFeedback = document.getElementById('profile-feedback');
const vornameInput = document.getElementById('vorname');
const nachnameInput = document.getElementById('nachname');
const emailInput = document.getElementById('email');
const passwortInput = document.getElementById('passwort');
let currentUser = getCurrentUser();
let allEvents = [];
init();
async function init() {
if (!currentUser) {
renderLoggedOutState();
return;
}
renderLoggedInState(currentUser);
bindFormHandlers();
activateProfileTab('hosting');
allEvents = await loadAllEvents();
renderMyEvents(allEvents, currentUser);
renderMyRegistrations(allEvents, currentUser);
}
// Liest den aktuell eingeloggten Benutzer robust aus dem Storage.
function getCurrentUser() {
try {
const raw = localStorage.getItem(CURRENT_USER_KEY);
return raw ? JSON.parse(raw) : null;
} catch (error) {
console.error('Der aktuelle Benutzer konnte nicht geladen werden.', error);
return null;
}
}
// Liest lokal erstellte Events aus dem Storage.
function getStoredEvents() {
try {
const raw = localStorage.getItem(EVENTS_STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
} catch (error) {
console.error('Lokale Events konnten nicht gelesen werden.', error);
return [];
}
}
// Liest den Anmeldestatus pro Benutzer-E-Mail.
function getRegistrationMap() {
try {
const raw = localStorage.getItem(REGISTRATION_STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
} catch (error) {
console.error('Anmeldedaten konnten nicht gelesen werden.', error);
return {};
}
}
// Schreibt den gesamten Registrierungszustand in localStorage.
function setRegistrationMap(registrationMap) {
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(registrationMap));
}
// Schreibt die lokal erstellten Events in den Storage.
function setStoredEvents(events) {
localStorage.setItem(EVENTS_STORAGE_KEY, JSON.stringify(events));
}
// Fuehrt JSON-Daten und lokal erstellte Events in einer Liste zusammen.
async function loadAllEvents() {
try {
const response = await fetch('data/events.json');
const apiEvents = await response.json();
return [...getStoredEvents(), ...apiEvents];
} catch (error) {
console.error('Events konnten nicht geladen werden.', error);
return getStoredEvents();
}
}
// Schaltet in den ausgeloggten Zustand und blendet geschuetzte Inhalte aus.
function renderLoggedOutState() {
loggedOutState.classList.remove('hidden');
loggedInContent.classList.add('hidden');
logoutButton.classList.add('hidden');
profileHeadline.textContent = 'Mein Profil';
profileSubline.textContent = 'Bitte logge dich ein, um deinen Bereich zu sehen.';
}
// Füllt Überschriften und Formular mit den aktuellen Benutzerdaten.
function renderLoggedInState(user) {
loggedOutState.classList.add('hidden');
loggedInContent.classList.remove('hidden');
logoutButton.classList.remove('hidden');
profileHeadline.textContent = `Hallo ${user.vorname || 'Gast'}`;
profileSubline.textContent = 'Hier kannst du deine Events und Anmeldungen verwalten.';
vornameInput.value = user.vorname || '';
nachnameInput.value = user.nachname || '';
emailInput.value = user.email || '';
}
// Bindet Submit-, Input- und Logout-Verhalten an die Profilseite.
function bindFormHandlers() {
profileForm.addEventListener('submit', handleProfileSubmit);
myRegistrationsList.addEventListener('click', handleRegistrationListClick);
myEventsList.addEventListener('click', handleHostedListClick);
profileTabButtons.forEach(button => {
button.addEventListener('click', () => {
const tabName = button.getAttribute('data-category-item');
if (!tabName) {
return;
}
activateProfileTab(tabName);
});
});
[vornameInput, nachnameInput, emailInput, passwortInput].forEach(input => {
input.addEventListener('input', () => {
input.parentElement.classList.remove('has-error');
profileFeedback.textContent = '';
});
});
logoutButton.addEventListener('click', () => {
const logoutModal = document.getElementById('logoutModal');
logoutModal.classList.add('show');
document.body.style.overflow = 'hidden';
});
}
// Globale Funktionen für das Logout-Modal.
window.closeLogoutModal = function() {
const logoutModal = document.getElementById('logoutModal');
logoutModal.classList.remove('show');
document.body.style.overflow = 'auto';
};
window.confirmLogout = function() {
localStorage.removeItem(CURRENT_USER_KEY);
window.location.href = 'index.html';
};
// Reagiert auf Aktionen in der Liste "Meine Events" per Event Delegation.
function handleHostedListClick(event) {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
const cancelButton = target.closest('[data-cancel-event-id]');
if (cancelButton && currentUser?.email) {
const eventId = Number(cancelButton.getAttribute('data-cancel-event-id'));
if (Number.isFinite(eventId)) {
openCancelEventModal(eventId);
}
return;
}
if (target.closest('a, button')) {
return;
}
const card = target.closest('[data-event-id]');
if (!card) {
return;
}
const eventId = Number(card.getAttribute('data-event-id'));
if (!Number.isFinite(eventId)) {
return;
}
window.location.href = `event_detail.html?id=${eventId}`;
}
// Schaltet den sichtbaren Profilbereich per Tabname um.
function activateProfileTab(tabName) {
profileTabButtons.forEach(button => {
const isActive = button.getAttribute('data-category-item') === tabName;
button.classList.toggle('is-active', isActive);
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
profileTabPanels.forEach(panel => {
const isActive = panel.getAttribute('data-profile-panel') === tabName;
panel.classList.toggle('hidden', !isActive);
});
if (tabName === 'teilnehmen') {
const registeredEvents = getMyRegisteredEvents(allEvents, currentUser);
markRegistrationsAsRead(registeredEvents);
}
}
// Reagiert auf Aktionen in der Liste "Meine Anmeldungen" per Event Delegation.
function handleRegistrationListClick(event) {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
const unregisterButton = target.closest('[data-unregister-id]');
if (unregisterButton) {
if (!currentUser?.email) return;
const eventId = Number(unregisterButton.getAttribute('data-unregister-id'));
if (!Number.isFinite(eventId)) return;
const modal = document.getElementById('unregister-confirm-modal');
if (modal) modal.classList.add('show');
document.getElementById('confirm-unregister-btn').onclick = () => {
modal.classList.remove('show');
unregisterFromEvent(eventId, currentUser.email);
const snackbar = document.getElementById('snackbar');
if (snackbar) {
snackbar.textContent = 'Du wurdest erfolgreich abgemeldet.';
snackbar.classList.add('snackbar--danger', 'snackbar--visible');
setTimeout(() => {
snackbar.classList.remove('snackbar--visible');
setTimeout(() => snackbar.classList.remove('snackbar--danger'), 400);
}, 3000);
}
};
document.getElementById('unregister-modal-close').onclick = () => modal.classList.remove('show');
document.getElementById('unregister-modal-cancel').onclick = () => modal.classList.remove('show');
modal.addEventListener('click', e => { if (e.target === modal) modal.classList.remove('show'); });
return;
}
if (target.closest('a, button')) {
return;
}
const card = target.closest('[data-event-id]');
if (!card) {
return;
}
const eventId = Number(card.getAttribute('data-event-id'));
if (!Number.isFinite(eventId)) {
return;
}
window.location.href = `event_detail.html?id=${eventId}`;
}
// Sagt ein gehostetes Event ab (aus eigener Profilansicht entfernen).
let pendingCancelEventId = null;
function openCancelEventModal(eventId) {
pendingCancelEventId = eventId;
const modal = document.getElementById('cancelEventModal');
modal.classList.add('show');
}
window.closeCancelEventModal = function() {
pendingCancelEventId = null;
const modal = document.getElementById('cancelEventModal');
modal.classList.remove('show');
};
document.getElementById('confirmCancelEventBtn').addEventListener('click', function() {
if (pendingCancelEventId !== null && currentUser?.email) {
cancelHostedEvent(pendingCancelEventId, currentUser.email);
}
closeCancelEventModal();
const snackbar = document.getElementById('snackbar');
if (snackbar) {
snackbar.textContent = 'Dein Event wurde erfolgreich abgesagt.';
snackbar.classList.add('snackbar--danger', 'snackbar--visible');
setTimeout(() => {
snackbar.classList.remove('snackbar--visible');
setTimeout(() => snackbar.classList.remove('snackbar--danger'), 400);
}, 3000);
}
});
// Schliesst das Modal bei Klick ausserhalb des Inhalts.
document.getElementById('cancelEventModal').addEventListener('click', function(e) {
if (e.target === this) {
closeCancelEventModal();
}
});
function cancelHostedEvent(eventId, userEmail) {
// Lokal erstellte, eigene Events werden direkt aus dem Storage geloescht.
const storedEvents = getStoredEvents();
const nextStoredEvents = storedEvents.filter(event => {
const isTargetEvent = Number(event.id) === eventId;
const isOwnedByUser = normalizeText(event.hostEmail || '') === normalizeText(userEmail)
|| normalizeText(event.host?.name || '') === normalizeText(currentUser?.vorname || '');
return !(isTargetEvent && isOwnedByUser);
});
setStoredEvents(nextStoredEvents);
// Event-ID für alle Benutzer aus den Anmeldungen entfernen.
const registrationMap = getRegistrationMap();
Object.keys(registrationMap).forEach(email => {
const ids = Array.isArray(registrationMap[email])
? registrationMap[email].map(id => Number(id)).filter(Number.isFinite)
: [];
registrationMap[email] = ids.filter(id => id !== eventId);
});
setRegistrationMap(registrationMap);
allEvents = allEvents.filter(event => Number(event.id) !== eventId);
renderMyEvents(allEvents, currentUser);
renderMyRegistrations(allEvents, currentUser);
}
// Entfernt eine Event-ID aus der Benutzerliste und aktualisiert die UI sofort.
function unregisterFromEvent(eventId, userEmail) {
const registrationMap = getRegistrationMap();
const currentIds = Array.isArray(registrationMap[userEmail]) ? registrationMap[userEmail] : [];
const nextIds = currentIds
.map(id => Number(id))
.filter(id => Number.isFinite(id) && id !== eventId);
registrationMap[userEmail] = nextIds;
setRegistrationMap(registrationMap);
renderMyRegistrations(allEvents, currentUser);
}
// Validiert Profildaten konsistent und liefert true/false zur Submit-Steuerung.
function validateProfileForm() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!vornameInput.value.trim()) {
vornameInput.parentElement.classList.add('has-error');
isValid = false;
}
if (!nachnameInput.value.trim()) {
nachnameInput.parentElement.classList.add('has-error');
isValid = false;
}
if (!emailRegex.test(emailInput.value.trim())) {
emailInput.parentElement.classList.add('has-error');
isValid = false;
}
if (passwortInput.value && passwortInput.value.length < 6) {
passwortInput.parentElement.classList.add('has-error');
isValid = false;
}
return isValid;
}
// Speichert Profiländerungen lokal und synchronisiert auch den Benutzerkatalog.
function handleProfileSubmit(event) {
event.preventDefault();
if (!validateProfileForm()) {
profileFeedback.textContent = 'Bitte prüfe die markierten Felder.';
return;
}
const previousEmail = currentUser.email;
const nextUser = {
...currentUser,
vorname: vornameInput.value.trim(),
nachname: nachnameInput.value.trim(),
email: emailInput.value.trim(),
passwort: passwortInput.value ? passwortInput.value : currentUser.passwort,
updatedAt: new Date().toISOString()
};
currentUser = nextUser;
localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(nextUser));
syncUserInUserStore(previousEmail, nextUser);
// Falls sich die E-Mail geändert hat, verschieben wir bestehende Anmeldungen auf die neue E-Mail.
migrateRegistrationEmail(previousEmail, nextUser.email);
passwortInput.value = '';
profileHeadline.textContent = `Hallo ${nextUser.vorname}`;
profileFeedback.textContent = 'Profil erfolgreich gespeichert.';
}
// Synchronisiert einen Benutzer im zentralen User-Array.
function syncUserInUserStore(previousEmail, nextUser) {
let users = [];
try {
const raw = localStorage.getItem(USERS_STORAGE_KEY);
users = raw ? JSON.parse(raw) : [];
} catch (error) {
console.error('Benutzerdaten konnten nicht gelesen werden.', error);
}
const nextUsers = users.filter(user => user.email !== previousEmail && user.email !== nextUser.email);
nextUsers.unshift(nextUser);
localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(nextUsers));
}
// Migriert bestehende Registrierungen, falls die E-Mail aktualisiert wurde.
function migrateRegistrationEmail(previousEmail, nextEmail) {
if (!previousEmail || !nextEmail || previousEmail === nextEmail) {
return;
}
const map = getRegistrationMap();
const existingRegistrations = Array.isArray(map[previousEmail]) ? map[previousEmail] : [];
const alreadyPresent = Array.isArray(map[nextEmail]) ? map[nextEmail] : [];
map[nextEmail] = Array.from(new Set([...alreadyPresent, ...existingRegistrations]));
delete map[previousEmail];
localStorage.setItem(REGISTRATION_STORAGE_KEY, JSON.stringify(map));
}
// Ermittelt gehostete Events aus lokal erstellten Daten des aktuellen Benutzers.
function getMyHostedEvents(events, user) {
const userFirstName = normalizeText(user.vorname || '');
const userEmail = normalizeText(user.email || '');
return events.filter(event => {
if (event.source !== 'local') {
return false;
}
const hostEmail = normalizeText(event.hostEmail || '');
const hostName = normalizeText(event.host?.name || '');
if (hostEmail && hostEmail === userEmail) {
return true;
}
return userFirstName && hostName === userFirstName;
});
}
// Ermittelt angemeldete Events über die Registration-Map und participants-Liste.
function getMyRegisteredEvents(events, user) {
const registrationMap = getRegistrationMap();
const registeredIds = Array.isArray(registrationMap[user.email]) ? registrationMap[user.email] : [];
const idSet = new Set(registeredIds.map(id => Number(id)));
const userFirstName = String(user.vorname || '').trim().toLowerCase();
const userFullName = `${String(user.vorname || '').trim()} ${String(user.nachname || '').trim()}`.trim().toLowerCase();
return events.filter(event => {
if (idSet.has(Number(event.id))) {
return true;
}
if (Array.isArray(event.participants)) {
const participantSet = new Set(event.participants.map(name => String(name || '').trim().toLowerCase()).filter(Boolean));
if ((userFirstName && participantSet.has(userFirstName)) || (userFullName && participantSet.has(userFullName))) {
return true;
}
}
return false;
});
}
// Rendert angemeldete Events inkl. Zähler.
function renderMyEvents(events, user) {
const hostedEvents = getMyHostedEvents(events, user);
const count = hostedEvents.length;
myEventsCount.textContent = String(count);
if (myEventsBtnCount) myEventsBtnCount.textContent = String(count);
renderEventCards(myEventsList, hostedEvents, {
title: 'Noch kein eigenes Event',
text: 'Starte dein erstes Dinner und lade die Community an deinen Tisch ein.',
buttonLabel: 'Event erstellen',
href: 'event_create.html'
}, 'hosting');
}
function getSeenAddresses() {
try {
const raw = localStorage.getItem('socialCookingSeenAddresses');
return raw ? JSON.parse(raw) : [];
} catch (err) {
return [];
}
}
function markRegistrationsAsRead(events) {
const seen = getSeenAddresses();
let changed = false;
events.forEach(event => {
if (isAddressVisibleWindow(event) && !seen.includes(Number(event.id))) {
seen.push(Number(event.id));
changed = true;
}
});
if (changed) {
localStorage.setItem('socialCookingSeenAddresses', JSON.stringify(seen));
// Remove dots from UI
const tabDot = document.querySelector('[data-category-item="teilnehmen"] .notification-dot');
if (tabDot) tabDot.remove();
const navDot = document.querySelector('.profile-pill .notification-dot');
if (navDot) navDot.remove();
}
}
// Rendert angemeldete Events inkl. Zähler.
function renderMyRegistrations(events, user) {
const registeredEvents = getMyRegisteredEvents(events, user);
const count = registeredEvents.length;
myRegistrationsCount.textContent = String(count);
if (myRegistrationsBtnCount) myRegistrationsBtnCount.textContent = String(count);
const seenAddresses = getSeenAddresses();
const unreadEvents = registeredEvents.filter(e => isAddressVisibleWindow(e) && !seenAddresses.includes(Number(e.id)));
const hasNotifications = unreadEvents.length > 0;
const tabButton = document.querySelector('[data-category-item="teilnehmen"]');
if (tabButton) {
let dot = tabButton.querySelector('.notification-dot');
if (hasNotifications) {
if (!dot) {
dot = document.createElement('span');
dot.className = 'notification-dot';
tabButton.appendChild(dot);
}
} else if (dot) {
dot.remove();
}
}
renderEventCards(myRegistrationsList, registeredEvents, {
title: 'Noch keine Anmeldungen',
text: 'Entdecke spannende Dinner in deiner Naehe und melde dich direkt an.',
buttonLabel: 'Events entdecken',
href: 'event_overview.html'
}, 'registrations', seenAddresses);
// Falls wir bereits auf dem Tab sind, direkt als gelesen markieren
const activeTab = document.querySelector('[data-category-item="teilnehmen"].is-active');
if (activeTab && hasNotifications) {
// Kurze Verzögerung, damit UI sich erst aufbaut
setTimeout(() => markRegistrationsAsRead(registeredEvents), 500);
}
}
// Gibt true zurück, wenn die Abmeldung gesperrt ist (innerhalb von 24h oder in der Vergangenheit).
function isDeregistrationClosed(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) {
return false;
}
const msUntilStart = eventDateTime.getTime() - Date.now();
return msUntilStart <= 24 * 60 * 60 * 1000;
}
// Baut die Eventkarten für beide Listen in einheitlichem Markup.
function renderEventCards(container, events, emptyStateConfig, mode, seenAddresses = []) {
container.innerHTML = '';
if (events.length === 0) {
const emptyElement = document.createElement('div');
emptyElement.className = 'empty-state';
emptyElement.innerHTML = `
<div class="empty-state-kicker">Keine Treffer</div>
<h3>${emptyStateConfig.title}</h3>
<p>${emptyStateConfig.text}</p>
<a class="empty-state-link button-primary" href="${emptyStateConfig.href}">${emptyStateConfig.buttonLabel}</a>
`;
container.appendChild(emptyElement);
return;
}
events.forEach(event => {
const card = document.createElement('article');
card.className = 'profile-event-card profile-event-card-clickable';
card.setAttribute('data-event-id', String(event.id));
const isCanceled = event.status === 'canceled';
if (isCanceled) {
card.style.opacity = '0.6';
}
let addressMessage = 'Vielen Dank für die Anmeldung! Die Adresse für diesen Event wird 24 Stunden vorher genau hier sichtbar sein.';
if (isEventPastAddressWindow(event)) {
addressMessage = 'Vielen Dank, dass du an diesem Event teilgenommen hast.';
}
let addressMarkup = '';
if (mode === 'registrations' && event.address) {
if (isCanceled) {
addressMarkup = `
<div class="profile-event-address-block" aria-label="Hinweis zur Adresse">
<p class="profile-event-address-label">Adresse</p>
<p class="profile-event-address">Dieses Event wurde leider vom Gastgeber abgesagt.</p>
</div>
`;
} else if (isAddressVisibleWindow(event)) {
addressMarkup = `
<div class="profile-event-address-block" aria-label="Event Adresse">
<p class="profile-event-address-label">Adresse</p>
<p class="profile-event-address">${event.address}</p>
</div>
`;
} else {
addressMarkup = `
<div class="profile-event-address-block" aria-label="Hinweis zur Adresse">
<p class="profile-event-address-label">Adresse</p>
<p class="profile-event-address">${addressMessage}</p>
</div>
`;
}
}
const isDeregClosed = isDeregistrationClosed(event);
let actionMarkup = '';
if (mode === 'registrations') {
if (isCanceled) {
actionMarkup = `
<div class="event-side">
<button class="button-primary-abmelden" type="button" disabled>Abgesagt</button>
</div>
`;
} else if (isDeregClosed) {
actionMarkup = `
<div class="event-side">
<button class="button-primary-abmelden" type="button" disabled>Abmeldung geschlossen</button>
</div>
`;
} else {
actionMarkup = `
<div class="event-side">
<button class="button-primary-abmelden" type="button" data-unregister-id="${event.id}">Abmelden</button>
</div>
`;
}
} else {
if (isCanceled) {
actionMarkup = `
<div class="event-side">
<button class="button-primary-eigener-event" type="button" disabled>Abgesagt</button>
</div>
`;
} else {
actionMarkup = `
<div class="event-side">
<button class="button-primary-eigener-event" type="button" data-cancel-event-id="${event.id}">Event absagen</button>
</div>
`;
}
}
card.innerHTML = `
<div>
<h3 class="profile-event-title">${event.title}</h3>
<p class="profile-event-meta">${event.location} | ${formatEventDate(event.date)} | ${formatEventTime(event.time)}</p>
${addressMarkup}
</div>
${actionMarkup}
`;
container.appendChild(card);
});
}
// Gibt true zurück, wenn die Adresse sichtbar sein soll (24h vor bis 1h nach Start).
function isAddressVisibleWindow(event) {
if (event.status === 'canceled') return false;
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
const now = Date.now();
const start = eventDateTime.getTime();
const revealStart = start - (24 * 60 * 60 * 1000);
const revealEnd = start + (1 * 60 * 60 * 1000);
return now >= revealStart && now <= revealEnd;
}
// Gibt true zurück, wenn ein Event bereits vorbei ist (1h nach Start).
function isEventPastAddressWindow(event) {
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
const revealEnd = eventDateTime.getTime() + (1 * 60 * 60 * 1000);
return Date.now() > revealEnd;
}
// Parse für ISO- und lokalisierte Datumsformate aus den Eventdaten.
function parseEventDateTime(event) {
if (!event?.date) {
return null;
}
const dateValue = String(event.date).trim();
const isoDateMatch = dateValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
let year;
let month;
let day;
if (isoDateMatch) {
year = Number(isoDateMatch[1]);
month = Number(isoDateMatch[2]);
day = Number(isoDateMatch[3]);
} else {
const monthMap = {
jan: 1,
januar: 1,
feb: 2,
februar: 2,
'mär': 3,
mrz: 3,
mar: 3,
maerz: 3,
märz: 3,
apr: 4,
april: 4,
mai: 5,
jun: 6,
juni: 6,
jul: 7,
juli: 7,
aug: 8,
august: 8,
sep: 9,
sept: 9,
september: 9,
okt: 10,
oktober: 10,
nov: 11,
november: 11,
dez: 12,
dezember: 12
};
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-Za-zÄÖÜäöü]{3,9})\.?\s*(\d{4})$/);
if (!localizedMatch) {
return null;
}
day = Number(localizedMatch[1]);
month = monthMap[String(localizedMatch[2]).toLowerCase()];
year = Number(localizedMatch[3]);
if (!month) {
return null;
}
}
const timeMatch = String(event.time || '').match(/(\d{1,2}):(\d{2})/);
const hours = timeMatch ? Number(timeMatch[1]) : 0;
const minutes = timeMatch ? Number(timeMatch[2]) : 0;
return new Date(year, month - 1, day, hours, minutes, 0, 0);
}
// Formatiert ein Eventdatum konsistent für die Profilkarten.
function formatEventDate(dateString) {
if (!dateString) {
return 'Kein Datum';
}
// ISO Format: 2026-02-12
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
const [year, month, day] = dateString.split('-');
const monthLabel = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'][Number(month) - 1];
return `${Number(day)}. ${monthLabel} ${year}`;
}
// Format: 12. FEB. 2026
const match = dateString.match(/^(\d{1,2})\.\s*([A-ZÄÖÜ]{3})\.\s*(\d{4})$/);
if (match) {
const day = match[1];
const month = match[2];
const shortMonthMap = {
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'
};
return `${day}. ${shortMonthMap[month] || month} ${match[3]}`;
}
return dateString;
}
// Vereinheitlicht die Zeitanzeige für die Profilseite.
function formatEventTime(timeString) {
if (!timeString) {
return 'Keine Uhrzeit';
}
return timeString.includes('UHR') ? timeString.replace('UHR', 'Uhr').trim() : timeString;
}
// Normalisiert Vergleichswerte für robuste String-Matches.
function normalizeText(value) {
return String(value || '').trim().toLowerCase();
}
});

View File

@ -1,222 +0,0 @@
// =============================================
// Dynamische Navigation
// Je nach Login-Status wird die Kopfzeile für
// alle Seiten mit passendem Markup aufgebaut.
// =============================================
document.addEventListener('DOMContentLoaded', () => {
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
const REGISTRATION_STORAGE_KEY = 'socialCookingRegistrations';
const EVENTS_STORAGE_KEY = 'socialCookingEvents';
const navcontainers = document.querySelectorAll('.nav-tab-links');
const currentPage = (window.location.pathname.split('/').pop() || 'index.html').toLowerCase();
// Beendet früh, falls auf einer Seite keine Hauptnavigation vorhanden ist.
if (!navcontainers.length) {
return;
}
// Liest den aktiven Benutzer robust aus localStorage.
function getCurrentUser() {
try {
const stored = localStorage.getItem(CURRENT_USER_KEY);
return stored ? JSON.parse(stored) : null;
} catch (error) {
console.error('Aktueller Benutzer konnte nicht gelesen werden.', error);
return null;
}
}
// Logout-Funktion
window.logout = function() {
localStorage.removeItem(CURRENT_USER_KEY);
window.location.href = 'index.html';
};
// Hilfsfunktionen für Datumsberechnungen
function parseEventDateTime(event) {
if (!event?.date) return null;
const dateValue = String(event.date).trim();
const isoDateMatch = dateValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
let year, month, day;
if (isoDateMatch) {
year = Number(isoDateMatch[1]);
month = Number(isoDateMatch[2]);
day = Number(isoDateMatch[3]);
} else {
const monthMap = {
jan: 1, januar: 1,
feb: 2, februar: 2,
'mär': 3, mrz: 3, mar: 3, maerz: 3, märz: 3,
apr: 4, april: 4,
mai: 5,
jun: 6, juni: 6,
jul: 7, juli: 7,
aug: 8, august: 8,
sep: 9, sept: 9, september: 9,
okt: 10, oktober: 10,
nov: 11, november: 11,
dez: 12, dezember: 12
};
const localizedMatch = dateValue.match(/^(\d{1,2})\.\s*([A-Za-zÄÖÜäöü]{3,9})\.?\s*(\d{4})$/);
if (!localizedMatch) return null;
day = Number(localizedMatch[1]);
month = monthMap[String(localizedMatch[2]).toLowerCase()];
year = Number(localizedMatch[3]);
if (!month) return null;
}
const timeMatch = String(event.time || '').match(/(\d{1,2}):(\d{2})/);
const hours = timeMatch ? Number(timeMatch[1]) : 0;
const minutes = timeMatch ? Number(timeMatch[2]) : 0;
return new Date(year, month - 1, day, hours, minutes, 0, 0);
}
function isAddressVisibleWindow(event) {
if (event.status === 'canceled') return false;
const eventDateTime = parseEventDateTime(event);
if (!eventDateTime || Number.isNaN(eventDateTime.getTime())) return false;
const now = Date.now();
const start = eventDateTime.getTime();
const revealStart = start - (24 * 60 * 60 * 1000);
const revealEnd = start + (1 * 60 * 60 * 1000);
return now >= revealStart && now <= revealEnd;
}
async function hasUnreadNotifications(user) {
if (!user || !user.email) return false;
let events = [];
try {
const rawStored = localStorage.getItem(EVENTS_STORAGE_KEY);
const storedEvents = rawStored ? JSON.parse(rawStored) : [];
const response = await fetch('data/events.json');
const apiEvents = await response.json();
events = [...storedEvents, ...apiEvents];
} catch (err) {
console.error('Fehler beim Laden der Events für Benachrichtigungen', err);
return false;
}
let map = {};
try {
const rawReg = localStorage.getItem(REGISTRATION_STORAGE_KEY);
map = rawReg ? JSON.parse(rawReg) : {};
} catch (err) {}
let seenAddresses = [];
try {
const rawSeen = localStorage.getItem('socialCookingSeenAddresses');
seenAddresses = rawSeen ? JSON.parse(rawSeen) : [];
} catch (err) {}
const registeredIds = Array.isArray(map[user.email]) ? map[user.email] : [];
const idSet = new Set(registeredIds.map(id => Number(id)));
const myRegisteredEvents = events.filter(e => idSet.has(Number(e.id)));
// Unread = address visible AND NOT marked as seen
return myRegisteredEvents.some(e => isAddressVisibleWindow(e) && !seenAddresses.includes(Number(e.id)));
}
// Baut die Navigation für ausgeloggte Besucher.
function buildLoggedOutNavigation() {
const loginIsActive = currentPage === 'login.html';
const signupIsActive = currentPage === 'signup.html';
const isIndex = currentPage === 'index.html' || currentPage === '';
// Auf der Startseite, Login und Signup nur Login anzeigen.
if (isIndex || loginIsActive || signupIsActive) {
return `
<a
class="button-small"
href="login.html"
aria-label="Login"
>
Login
</a>
`;
}
return `
<a
class="button-small auth-nav-button ${loginIsActive ? 'auth-nav-button--active' : 'auth-nav-button--default'}"
href="login.html"
aria-label="Login"
${loginIsActive ? 'aria-current="page"' : ''}
>
Login
</a>
<a
class="button-small auth-nav-button ${signupIsActive ? 'auth-nav-button--active' : 'auth-nav-button--default'}"
href="signup.html"
aria-label="Signup"
${signupIsActive ? 'aria-current="page"' : ''}
>
Signup
</a>
`;
}
// Baut die Navigation für eingeloggte Benutzer.
function buildLoggedInNavigation(user, hasNotifications) {
const initial = (user.vorname || 'U').charAt(0).toUpperCase();
const isEventOverview = currentPage === 'event_overview.html';
const isEventCreate = currentPage === 'event_create.html';
const notificationMarkup = hasNotifications ? '<span class="notification-dot"></span>' : '';
return `
<a
class="nav-tab${isEventCreate ? ' nav-tab--active' : ''}"
href="event_create.html"
${isEventCreate ? 'aria-current="page"' : ''}
>
Event erstellen
</a>
<a
class="nav-tab${isEventOverview ? ' nav-tab--active' : ''}"
href="event_overview.html"
${isEventOverview ? 'aria-current="page"' : ''}
>
Event finden
</a>
<button
class="button-small logout-button"
onclick="logout()"
aria-label="Logout"
>
Logout
</button>
<a
class="profile-pill"
href="my_profil.html"
aria-label="Mein Profil"
title="${user.vorname || 'Profil'}"
>
${initial}
${notificationMarkup}
</a>
`;
}
async function initNavigation() {
const currentUser = getCurrentUser();
let nextMarkup;
if (currentUser) {
const hasNotifications = await hasUnreadNotifications(currentUser);
nextMarkup = buildLoggedInNavigation(currentUser, hasNotifications);
} else {
nextMarkup = buildLoggedOutNavigation();
}
// Wendet das passende Markup auf alle vorhandenen Kopf-Navigationen an.
navcontainers.forEach(container => {
container.innerHTML = nextMarkup;
});
}
initNavigation();
});

View File

@ -1,188 +0,0 @@
// =============================================
// Signup-Logik
// Diese Datei validiert das Formular, speichert
// neue Benutzer lokal und startet direkt die Session.
// =============================================
// Formular und Felder aus dem HTML holen.
const signupForm = document.getElementById('signupForm');
const vornameInput = document.getElementById('vorname');
const nachnameInput = document.getElementById('nachname');
const emailInput = document.getElementById('email');
const passwortInput = document.getElementById('passwort');
const welcomeModal = document.getElementById('welcomeModal');
const USERS_STORAGE_KEY = 'socialCookingUsers';
const CURRENT_USER_KEY = 'socialCookingCurrentUser';
// Liest bestehende Benutzerliste robust aus localStorage.
function getStoredUsers() {
try {
const raw = localStorage.getItem(USERS_STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
} catch (error) {
console.error('Benutzerdaten konnten nicht gelesen werden.', error);
return [];
}
}
// Schreibt die komplette Benutzerliste in localStorage.
function setStoredUsers(users) {
localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(users));
}
// Speichert den aktiven Benutzer für nachfolgende Seiten.
function setCurrentUser(user) {
localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(user));
}
// Funktion zum Öffnen des Welcome Modals
function openWelcomeModal() {
welcomeModal.classList.add('show');
document.body.style.overflow = 'hidden';
}
// Funktion zum Schliessen des Welcome Modals
function closeWelcomeModal() {
welcomeModal.classList.remove('show');
document.body.style.overflow = 'auto';
window.location.href = 'event_overview.html';
}
// Hauptfunktion für Formularvalidierung und Speicherung.
function validateForm(event) {
event.preventDefault();
// Wir zeigen pro Submit nur den ersten Fehler an.
// So bleibt der Formularfluss klar und ruhig.
const vornameValue = vornameInput.value.trim();
const vornameGroup = vornameInput.parentElement;
const nachnameGroup = nachnameInput.parentElement;
const emailGroup = emailInput.parentElement;
const passwortGroup = passwortInput.parentElement;
vornameGroup.classList.remove('has-error');
nachnameGroup.classList.remove('has-error');
emailGroup.classList.remove('has-error');
passwortGroup.classList.remove('has-error');
if (!vornameValue) {
vornameGroup.classList.add('has-error');
vornameInput.focus();
return;
}
// Nachname-Validierung
const nachnameValue = nachnameInput.value.trim();
if (!nachnameValue) {
nachnameGroup.classList.add('has-error');
nachnameInput.focus();
return;
}
// Email-Validierung
const emailValue = emailInput.value.trim();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailValue) {
emailGroup.classList.add('has-error');
document.getElementById('emailError').textContent = 'Bitte gib deine E-Mail Adresse ein.';
emailInput.focus();
return;
}
if (!emailRegex.test(emailValue)) {
emailGroup.classList.add('has-error');
document.getElementById('emailError').textContent = 'Bitte gib eine gültige E-Mail Adresse ein.';
emailInput.focus();
return;
}
// Passwort-Validierung
const passwortValue = passwortInput.value;
if (!passwortValue) {
passwortGroup.classList.add('has-error');
document.getElementById('passwortError').textContent = 'Bitte gib ein Passwort ein.';
passwortInput.focus();
return;
}
if (passwortValue.length < 8) {
passwortGroup.classList.add('has-error');
document.getElementById('passwortError').textContent = 'Dein Passwort muss mindestens 8 Zeichen lang sein.';
passwortInput.focus();
return;
}
// Wenn alles gültig ist:
// 1) auf doppelte E-Mail prüfen
// 2) neuen Benutzer speichern
// 3) als aktuellen Benutzer einloggen
const existingUsers = getStoredUsers();
const emailLower = emailValue.toLowerCase();
const emailAlreadyUsed = existingUsers.some(user => user.email?.toLowerCase() === emailLower);
if (emailAlreadyUsed) {
emailGroup.classList.add('has-error');
document.getElementById('emailError').textContent = 'Diese E-Mail ist bereits registriert. Bitte nutze den Login.';
emailInput.focus();
return;
}
const newUser = {
id: Date.now(),
vorname: vornameValue,
nachname: nachnameValue,
email: emailValue,
passwort: passwortValue,
createdAt: new Date().toISOString(),
source: 'signup'
};
setStoredUsers([newUser, ...existingUsers]);
setCurrentUser(newUser);
openWelcomeModal();
// Weiterleitung erfolgt beim Klick auf "Weiter zu den Events".
}
// Fehlerbehandlung bei Input-Änderung (entfernt Fehler wenn Benutzer korrigiert)
vornameInput.addEventListener('input', function() {
const vornameGroup = this.parentElement;
if (this.value.trim()) {
vornameGroup.classList.remove('has-error');
}
});
nachnameInput.addEventListener('input', function() {
const nachnameGroup = this.parentElement;
if (this.value.trim()) {
nachnameGroup.classList.remove('has-error');
}
});
emailInput.addEventListener('input', function() {
const emailGroup = this.parentElement;
if (this.value.trim()) {
emailGroup.classList.remove('has-error');
}
});
passwortInput.addEventListener('input', function() {
const passwortGroup = this.parentElement;
if (this.value) {
passwortGroup.classList.remove('has-error');
}
});
// Modal schliessen wenn ausserhalb geklickt wird
welcomeModal.addEventListener('click', function(event) {
if (event.target === welcomeModal) {
closeWelcomeModal();
}
});
// Form Submit Event
signupForm.addEventListener('submit', validateForm);

View File

@ -1,78 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invité | Login</title>
<!-- Stylesheet für diese Seite -->
<link rel="stylesheet" href="css/login_signup.css">
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<script src="js/navigation.js" defer></script>
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invité">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<!-- Main Content -->
<div class="container-login layout-narrow">
<div>
<h1>Login</h1>
<form id="loginForm" novalidate >
<div class="form-group margin-bottom-16">
<p class= "label-input-field" for="email">E-Mail</p>
<input type="email" id="email" name="email" required placeholder="Deine E-mail-Adresse">
<div class="error-message error-message--field-callout" id="emailError">Bitte gib eine gültige E-Mail-Adresse ein.</div>
</div>
<div class="form-group margin-bottom-40">
<p class= "label-input-field" for="passwort">Passwort</p>
<input type="password" id="passwort" name="passwort" required placeholder="Dein Passwort">
<div class="error-message error-message--field-callout" id="passwortError">Bitte gib dein Passwort ein.</div>
</div>
<button class="button-primary margin-bottom-24">Login</button>
<div class="link-text">
Du hast noch keinen Account? <a href="signup.html">Hier geht es zur Registration.</a>
</div>
</form>
</div>
</div>
</div> <!-- Schliesst container -->
<div class="snackbar" id="snackbar">Willkommen zurück! Du wirst weitergeleitet...</div>
<script src="js/login.js"></script>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>

View File

@ -1,189 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mein Profil | Invité</title>
<!-- Stylesheet für diese Seite -->
<link rel="stylesheet" href="css/my_profil.css">
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<script src="js/navigation.js" defer></script>
</head>
<body>
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<button id="logout-button" class="button-small profile-logout" type="button">
Logout
</button>
<a class="button-small" href="login.html" aria-label="Login">
Login
</a>
</nav>
</div>
</header>
<main class="layout-wide">
<section class="profile-hero" aria-label="Profilübersicht">
<div>
<p class="badge margin-bottom-40">Mein Bereich</p>
<h1 id="headline">Mein Profil</h1>
<p id="profile-subline" class="profile-subline">Hier findest du deine Events, deine Anmeldungen und kannst deine Profildaten verwalten.</p>
</div>
</section>
<section id="logged-out-state" class="profile-panel hidden" aria-live="polite">
<h2 class="panel-title">Du bist noch nicht eingeloggt</h2>
<p>Melde dich an, damit wir deine Events und Anmeldungen anzeigen können.</p>
<div class="profile-cta-row">
<a class="button-primary" href="login.html">Zum Login</a>
<a class="button-primary profile-button-secondary" href="signup.html">Konto erstellen</a>
</div>
</section>
<section id="logged-in-content" class="profile-grid">
<nav class="category-items" aria-label="Profilbereiche">
<button type="button" class="category-item is-active category-item-profile" data-category-item="hosting">
Meine Events <span class="btn-count" id="btn-my-events-count">0</span>
</button>
<button type="button"
class="category-item category-item-profile" data-category-item="teilnehmen">
Meine Anmeldungen <span class="btn-count" id="btn-my-registrations-count">0</span>
</button>
<button type="button" class="category-item category-item-profile" data-category-item="einstellungen">Profil-Einstellungen</button>
</nav>
<article data-profile-panel="hosting">
<div class="panel-head">
<span id="my-events-count" class="panel-count">0</span>
</div>
<div id="my-events-list" class="profile-card-list"></div>
</article>
<article data-profile-panel="teilnehmen">
<div class="panel-head">
<span id="my-registrations-count" class="panel-count">0</span>
</div>
<p class="info-abmeldung">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--olive)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink: 0; margin-top: 1px;">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="8"/>
<line x1="12" y1="12" x2="12" y2="16"/>
</svg>
Eine Abmeldung ist bis 24 Stunden vor Eventbeginn möglich. Bitte respektiere den Host und melde dich rechtzeitig ab, wenn du nicht kommen kannst.
</p>
<div id="my-registrations-list" class="profile-card-list"></div>
</article>
<article class="profile-panel profile-panel-form hidden" data-profile-panel="einstellungen">
<h2 class="panel-title">Profil verwalten</h2>
<form id="profile-form" novalidate>
<div class="form-grid">
<div class="margin-bottom-16">
<label class="label-input-field" for="vorname">Vorname</label>
<input type="text" id="vorname" name="vorname" required>
<p class="input-error" id="vorname-error">Bitte gib deinen Vornamen ein.</p>
</div>
<div class="margin-bottom-16">
<label class="label-input-field" for="nachname">Nachname</label>
<input type="text" id="nachname" name="nachname" required>
<p class="input-error" id="nachname-error">Bitte gib deinen Nachnamen ein.</p>
</div>
</div>
<div class="margin-bottom-16">
<label class="label-input-field" for="email">E-Mail</label>
<input type="email" id="email" name="email" required>
<p class="input-error" id="email-error">Bitte gib eine gültige E-Mail-Adresse ein.</p>
</div>
<div class="margin-bottom-40">
<label class="label-input-field" for="passwort">Passwort</label>
<input type="password" id="passwort" name="passwort" minlength="6" placeholder="Mindestens 6 Zeichen">
<p class="input-hint">Nur ausfüllen, wenn du dein Passwort ändern möchtest.</p>
<p class="input-error" id="passwort-error">Das Passwort muss mindestens 6 Zeichen lang sein.</p>
</div>
<button class="button-primary" type="submit">Profil speichern</button>
<p id="profile-feedback" class="profile-feedback" aria-live="polite"></p>
</form>
</article>
</section>
</main>
<!-- Logout Confirmation Modal -->
<div id="logoutModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Abmelden?</h2>
</div>
<p class="modal-body">
Bist du sicher, dass du dich abmelden möchtest?
</p>
<div class="modal-footer">
<button class="button-primary button--outline" type="button" onclick="closeLogoutModal()">Abbrechen</button>
<button class="button-primary" type="button" onclick="confirmLogout()">Abmelden</button>
</div>
</div>
</div>
<div id="unregister-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Pläne haben sich geändert?</h2>
</div>
<div class="modal-body">
<p>Schade, dass du nicht dabei sein kannst! Aber manchmal kommt einfach etwas dazwischen. Wenn du dich jetzt abmeldest, gibst du deinen Stuhl am Tisch für jemand anderen aus der Community frei. So hilfst du bei der Planung und ein anderer Feinschmecker freut sich über den freien Platz.</p>
</div>
<div class="modal-footer">
<button class="button-primary button--outline" type="button" onclick="closeLogoutModal()">Abbrechen</button>
<button class="button-primary button-primary-abmelden" type="button" id="confirm-unregister-btn">Abmelden</button>
</div>
</div>
</div>
<!-- Snackbar: Feedback bei Abmeldung von Events -->
<div class="snackbar" id="snackbar"></div>
<!-- Event-Absage Confirmation Modal -->
<div id="cancelEventModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Event absagen?</h2>
</div>
<p class="modal-body">
Bist du sicher, dass du dieses Event absagen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.
</p>
<div class="modal-footer">
<button class="button-secondary" type="button" onclick="closeCancelEventModal()">Abbrechen</button>
<button class="button-primary-abmelden" type="button" id="confirmCancelEventBtn";>Event absagen</button>
</div>
</div>
</div>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
<script src="js/my_profil.js"></script>
</body>
</html>

View File

@ -1,113 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invité | Registration</title>
<!-- Globales Stylesheet -->
<link rel="stylesheet" href="css/stylesheet_global.css">
<!-- Stylesheet für diese Seite-->
<link rel="stylesheet" href="css/login_signup.css">
<script src="js/navigation.js" defer></script>
</head>
<body>
<!-- Top Navigation mit Seitenlinks -->
<header class="top-nav-wrap">
<div class="top-nav">
<a class="brand" href="index.html" aria-label="Zur Startseite">
<img src="assets/logo_invite.svg" alt="Invite Logo">
</a>
<nav class="nav-tab-links" aria-label="Hauptnavigation">
<a class="button-small" href="login.html" aria-label="Login">Login</a>
</nav>
</div>
</header>
<!-- Main Content -->
<div class="container-registration layout-wide">
<div class="text-section">
<div class="form-section">
<h1>Erstelle deinen Account</h1>
<div class="info-box">
<strong>Hinweis:</strong> Sichtbar auf der Plattform ist nur dein Vorname. Erst einer Anmeldung zum Event ist der Nachname für die Teilnehmenden sichtbar.
</div>
<form id="signupForm" novalidate>
<div class="form-group margin-bottom-16">
<p class= "label-input-field" for="vorname">Vorname*</p>
<input type="text" id="vorname" name="vorname" required placeholder="Dein Vorname">
<div class="error-message error-message--field-callout" id="vornameError">Bitte gib deinen Vornamen ein.</div>
</div>
<div class="form-group margin-bottom-16">
<p class= "label-input-field" for="nachname">Nachname*</p>
<input type="text" id="nachname" name="nachname" required placeholder="Dein Nachname">
<div class="error-message error-message--field-callout" id="nachnameError">Bitte gib deinen Nachnamen ein.</div>
</div>
<div class="form-group margin-bottom-16">
<p class= "label-input-field" for="email">E-Mail*</p>
<input type="email" id="email" name="email" required placeholder="Deine E-mail-Adresse">
<div class="error-message error-message--field-callout" id="emailError">Bitte gib eine gültige E-Mail-Adresse ein.</div>
</div>
<div class="form-group margin-bottom-40">
<p class= "label-input-field" for="passwort">Passwort*</p>
<input type="password" id="passwort" name="passwort" required placeholder="Mindestens 8 Zeichen">
<div class="error-message error-message--field-callout" id="passwortError">Dein Passwort muss mindestens 8 Zeichen lang sein.</div>
</div>
<button type="submit" class="button-primary margin-bottom-24">Konto erstellen</button>
<div class="link-text">
Du hast bereits einen Account? <a href="login.html">Hier geht es zum Login.</a>
</div>
</form>
</div>
</div>
<div class="image-section">
<img src="assets/index_cooking.jpg" alt="Social Cooking">
</div>
</div>
</div> <!-- Schliesst container -->
<!-- Welcome Modal -->
<div id="welcomeModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Konto erfolgreich erstellt!</h2>
</div>
<div class="modal-body">
Willkommen bei Invité! Dein Account wurde erfolgreich erstellt. Entdecke jetzt die neuesten Events in deiner Nähe.
</div>
<div class="modal-footer">
<button class="button-primary" onclick="closeWelcomeModal()">Weiter zu den Events</button>
</div>
</div>
</div>
<script src="js/signup.js"></script>
<div class="footer">
<div class="footer-left">
<p class="p-small inline">© <img src="assets/logo_invite.svg" alt="Invité Logo" class="footer-invite_logo" /></p>
</div>
<div class="footer-center">
<a href="https://www.instagram.com" target="_blank" rel="noopener noreferrer" class="instagram-invite__link">
<img src="assets/Icon_instagram.png" alt="Instagram" class="instagram-invite_icon" />
</a>
</div>
<div class="footer-right footer-links">
<a href="impressum.html" class="link-text-footer">Impressum</a>
<a href="datenschutz.html" class="link-text-footer">Datenschutz</a>
</div>
</div>
</body>
</html>