|
|
| Zeile 1: |
Zeile 1: |
| <!-- ============================================================
| | from pathlib import Path |
| 20 JAHRE BÜNDNIS FÜR FAMILIE TÜBINGEN
| | import re, html, json |
| BEGLEITPROGRAMM 2026
| |
|
| |
| KOMPLETTER RESPONSIVER VERANSTALTUNGSKALENDER
| |
|
| |
| Für MediaWiki:
| |
| Diesen gesamten Block in die Jubiläumsseite übernehmen.
| |
| Veranstaltungen werden ausschließlich im events-Array gepflegt.
| |
| ============================================================ -->
| |
|
| |
|
| | src = Path("/mnt/data/Eingefügter Text(8).txt") |
| | text = src.read_text(encoding="utf-8") |
|
| |
|
| <style>
| | events = re.findall( |
| /* ============================================================
| | r'<div class="event"\s+title="(.*?)">\s*<div class="et">(.*?)</div>\s*</div>', |
| GRUNDLAGEN
| | text, flags=re.S |
| ============================================================ */
| | ) |
|
| |
|
| #buefa-jubilaeum {
| | def parse_info(title): |
| --buefa-orange: #e86f2d; | | title = html.unescape(title) |
| --buefa-orange-light: #fff1e8; | | parts = {} |
| --buefa-yellow: #f4c542; | | for key in ["Uhrzeit", "Zielgruppe", "Treffpunkt", "Beschreibung"]: |
| --buefa-green: #6a9f58;
| | m = re.search( |
| --buefa-blue: #5b8fb9;
| | rf'{key}:\s*(.*?)(?=\n\s*(?:Uhrzeit|Zielgruppe|Treffpunkt|Beschreibung):|$)', |
| --buefa-purple: #8b6aa8;
| | title, re.S |
| --buefa-text: #333333;
| | ) |
| --buefa-text-light: #666666;
| | if m: |
| --buefa-border: #e5e5e5;
| | parts[key] = " ".join(m.group(1).split()) |
| --buefa-bg: #f8f7f4;
| | return parts |
| --buefa-white: #ffffff; | |
|
| |
|
| max-width: 1200px; | | date_map = [ |
| margin: 0 auto; | | ("2026-10-06","Oktober 2026",6), ("2026-10-08","Oktober 2026",8), |
| color: var(--buefa-text); | | ("2026-10-09","Oktober 2026",9), ("2026-10-09","Oktober 2026",9), |
| font-family: Arial, Helvetica, sans-serif; | | ("2026-10-09","Oktober 2026",9), ("2026-10-21","Oktober 2026",21), |
| line-height: 1.5; | | ("2026-10-22","Oktober 2026",22), ("2026-10-31","Oktober 2026",31), |
| box-sizing: border-box; | | ("2026-11-05","November 2026",5), ("2026-11-05","November 2026",5), |
| }
| | ("2026-11-09","November 2026",9), ("2026-11-10","November 2026",10), |
| | ("2026-11-10","November 2026",10), ("2026-11-11","November 2026",11), |
| | ("2026-11-15","November 2026",15), ("2026-11-17","November 2026",17), |
| | ("2026-11-19","November 2026",19), ("2026-11-21","November 2026",21), |
| | ("2026-11-24","November 2026",24), ("2026-11-24","November 2026",24), |
| | ("2026-11-25","November 2026",25), ("2026-11-26","November 2026",26) |
| | ] |
| | assert len(events) == len(date_map) |
|
| |
|
| #buefa-jubilaeum *,
| | records=[] |
| #buefa-jubilaeum *::before,
| | for i, ((date, month, day), (title, visible)) in enumerate(zip(date_map, events), 1): |
| #buefa-jubilaeum *::after {
| | info=parse_info(title) |
| box-sizing: border-box;
| | records.append({ |
| } | | "id":i, "date":date, "month":month, "day":day, |
| | "title":" ".join(html.unescape(visible).split()), |
| | "time":info.get("Uhrzeit",""), |
| | "audience":info.get("Zielgruppe",""), |
| | "place":info.get("Treffpunkt",""), |
| | "description":info.get("Beschreibung","") |
| | }) |
|
| |
|
| | data_json=json.dumps(records,ensure_ascii=False,indent=2) |
|
| |
|
| /* ============================================================
| | html_out = """<!doctype html> |
| HERO
| | <html lang="de"> |
| ============================================================ */
| | <head> |
| | | <meta charset="utf-8"> |
| .buefa-hero {
| | <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> |
| background:
| | <title>Familienangebote – Oktober & November 2026</title> |
| linear-gradient(
| | <style> |
| 135deg,
| | :root{--bg:#f6f7fb;--surface:#fff;--surface2:#fbfcfe;--text:#1f2937;--muted:#667085;--line:#dbe1ea;--accent:#0f766e;--accent-soft:#ecfdf5;--shadow:0 8px 24px rgba(0,0,0,.06);--radius:16px} |
| rgba(232,111,45,0.96),
| | *{box-sizing:border-box} |
| rgba(244,197,66,0.94)
| | html{scroll-behavior:smooth} |
| );
| | body{margin:0;font-family:Inter,Arial,sans-serif;background:var(--bg);color:var(--text);line-height:1.45} |
| border-radius: 22px;
| | .wrap{width:min(1200px,100%);margin:auto;padding:24px} |
| padding: 48px 45px;
| | h1{font-size:clamp(1.65rem,4vw,2.6rem);line-height:1.15;margin:0 0 8px} |
| margin: 0 0 35px 0;
| | h2{font-size:clamp(1.2rem,2.5vw,1.45rem);margin:0 0 16px} |
| color: #ffffff;
| | .intro,.details-intro{color:var(--muted);margin:0 0 22px} |
| position: relative;
| | .month{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:18px;margin-bottom:24px;box-shadow:var(--shadow)} |
| overflow: hidden;
| | .weekdays,.grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:8px} |
| } | | .weekdays div{font-size:.86rem;font-weight:700;color:var(--muted);padding:0 5px 5px} |
| | | .day{min-height:112px;background:var(--surface2);border:1px solid var(--line);border-radius:12px;padding:8px;display:flex;flex-direction:column;gap:6px;min-width:0} |
| .buefa-hero::after { | | .day.muted{opacity:.45} |
| content: "20";
| | .dn{font-size:.86rem;font-weight:800;color:#374151} |
| position: absolute;
| | .event{background:var(--accent-soft);border-left:4px solid var(--accent);padding:7px 8px;border-radius:8px;cursor:pointer;min-width:0} |
| right: 25px;
| | .et{font-size:.82rem;font-weight:650;line-height:1.3;overflow-wrap:anywhere} |
| bottom: -45px;
| | .details-section{margin-top:34px} |
| font-size: 190px;
| | .offer-list{display:grid;gap:14px} |
| font-weight: 900;
| | .offer{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px 18px;box-shadow:0 4px 14px rgba(0,0,0,.04)} |
| line-height: 1;
| | .offer-header{display:flex;gap:12px;align-items:flex-start;justify-content:space-between;margin-bottom:10px} |
| opacity: 0.12;
| | .offer-date{flex:0 0 auto;background:var(--accent);color:#fff;border-radius:9px;padding:5px 8px;font-size:.82rem;font-weight:800;white-space:nowrap} |
| }
| | .offer-title{flex:1;font-size:1rem;font-weight:750} |
| | | .offer-info{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 18px} |
| .buefa-hero-content {
| | .info-row{min-width:0} |
| position: relative;
| | .info-label{display:block;font-size:.76rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.03em;margin-bottom:2px} |
| z-index: 2;
| | .info-value{overflow-wrap:anywhere} |
| max-width: 760px;
| | .info-description{grid-column:1/-1;margin-top:4px} |
| } | | .tooltip{position:fixed;display:none;pointer-events:none;max-width:min(420px,calc(100vw - 24px));background:#fff;border:1px solid #cfd8e3;border-radius:10px;padding:12px 14px;box-shadow:0 8px 24px rgba(0,0,0,.2);font-size:.85rem;line-height:1.4;white-space:pre-line;z-index:9999} |
| | | @media(max-width:900px){.wrap{padding:18px}.day{min-height:100px}.weekdays,.grid{gap:6px}.offer-info{grid-template-columns:1fr}} |
| .buefa-kicker { | | @media(max-width:640px){ |
| font-size: 15px;
| | .wrap{padding:12px}.month{padding:12px;border-radius:12px} |
| font-weight: 700;
| | .weekdays{display:none} |
| letter-spacing: 1.5px;
| | .grid{display:flex;flex-direction:column;gap:8px} |
| text-transform: uppercase;
| | .day{min-height:0;display:block;padding:9px 10px;border-radius:10px} |
| margin-bottom: 10px;
| | .day:has(.event){border-color:#b7ddd4;background:#fff} |
| }
| | .day:not(:has(.event)){display:none} |
| | | .dn{font-size:.9rem;margin-bottom:5px} |
| .buefa-hero h1 { | | .event{margin-top:6px;padding:9px 10px} |
| color: #ffffff !important;
| | .et{font-size:.88rem} |
| font-size: clamp(30px, 5vw, 52px);
| | .offer{padding:14px}.offer-header{display:block}.offer-date{display:inline-block;margin-bottom:7px}.offer-title{display:block} |
| line-height: 1.08;
| | .info-description{grid-column:auto}.tooltip{display:none!important} |
| margin: 0 0 18px 0;
| |
| font-weight: 800;
| |
| } | |
| | |
| .buefa-hero p { | |
| font-size: 19px;
| |
| line-height: 1.55;
| |
| margin: 0;
| |
| max-width: 700px;
| |
| } | |
| | |
| | |
| /* ============================================================
| |
| SCHNELLE EINSTIEGE
| |
| ============================================================ */
| |
| | |
| .buefa-intro {
| |
| text-align: center;
| |
| margin: 0 0 28px 0;
| |
| } | |
| | |
| .buefa-intro h2 { | |
| font-size: 30px;
| |
| margin: 0 0 8px 0;
| |
| color: var(--buefa-text);
| |
| }
| |
| | |
| .buefa-intro p {
| |
| color: var(--buefa-text-light);
| |
| margin: 0;
| |
| } | |
| | |
| .buefa-month-buttons { | |
| display: flex;
| |
| gap: 10px;
| |
| justify-content: center;
| |
| flex-wrap: wrap;
| |
| margin: 25px 0 38px 0;
| |
| } | |
| | |
| .buefa-month-button { | |
| border: 2px solid var(--buefa-orange);
| |
| background: #ffffff;
| |
| color: var(--buefa-orange);
| |
| padding: 11px 20px;
| |
| border-radius: 30px;
| |
| font-weight: 700;
| |
| cursor: pointer;
| |
| transition: all .2s ease;
| |
| } | |
| | |
| .buefa-month-button:hover, | |
| .buefa-month-button.active { | |
| background: var(--buefa-orange);
| |
| color: #ffffff;
| |
| }
| |
| | |
| | |
| /* ============================================================
| |
| ALS NÄCHSTES
| |
| ============================================================ */
| |
| | |
| .buefa-next { | |
| margin: 0 0 42px 0;
| |
| } | |
| | |
| .buefa-section-title { | |
| font-size: 25px;
| |
| font-weight: 800;
| |
| margin: 0 0 16px 0;
| |
| } | |
| | |
| .buefa-next-card { | |
| background: var(--buefa-orange-light);
| |
| border-left: 7px solid var(--buefa-orange);
| |
| border-radius: 16px;
| |
| padding: 25px 28px;
| |
| } | |
| | |
| .buefa-next-label { | |
| color: var(--buefa-orange);
| |
| font-size: 13px;
| |
| font-weight: 800;
| |
| text-transform: uppercase;
| |
| letter-spacing: 1px;
| |
| margin-bottom: 8px;
| |
| } | |
| | |
| .buefa-next-card h3 { | |
| margin: 0 0 12px 0;
| |
| font-size: 25px;
| |
| } | |
| | |
| .buefa-next-meta { | |
| display: flex;
| |
| flex-wrap: wrap;
| |
| gap: 12px 25px;
| |
| color: #555555;
| |
| margin-bottom: 15px;
| |
| } | |
| | |
| .buefa-next-description { | |
| color: #555555;
| |
| }
| |
| | |
| | |
| /* ============================================================
| |
| FILTER
| |
| ============================================================ */
| |
| | |
| .buefa-controls {
| |
| display: flex;
| |
| justify-content: space-between;
| |
| align-items: center;
| |
| gap: 20px;
| |
| margin-bottom: 25px;
| |
| flex-wrap: wrap;
| |
| }
| |
| | |
| .buefa-filter {
| |
| display: flex;
| |
| gap: 8px;
| |
| flex-wrap: wrap;
| |
| }
| |
| | |
| .buefa-filter button {
| |
| border: 1px solid var(--buefa-border);
| |
| background: #ffffff;
| |
| color: #555555;
| |
| border-radius: 22px;
| |
| padding: 8px 15px;
| |
| cursor: pointer;
| |
| font-size: 14px;
| |
| }
| |
| | |
| .buefa-filter button.active {
| |
| background: #333333;
| |
| color: #ffffff;
| |
| border-color: #333333;
| |
| }
| |
| | |
| | |
| /* ============================================================
| |
| MONAT
| |
| ============================================================ */
| |
| | |
| .buefa-month-section {
| |
| margin: 0 0 45px 0;
| |
| }
| |
| | |
| .buefa-month-heading {
| |
| display: flex;
| |
| align-items: baseline;
| |
| gap: 15px;
| |
| border-bottom: 3px solid var(--buefa-orange);
| |
| padding-bottom: 10px;
| |
| margin-bottom: 20px;
| |
| }
| |
| | |
| .buefa-month-heading h2 {
| |
| margin: 0;
| |
| font-size: 31px;
| |
| }
| |
| | |
| .buefa-month-heading span {
| |
| color: var(--buefa-text-light);
| |
| font-size: 15px;
| |
| }
| |
| | |
| | |
| /* ============================================================
| |
| VERANSTALTUNGS-KARTE
| |
| ============================================================ */
| |
| | |
| .buefa-event {
| |
| display: grid;
| |
| grid-template-columns: 105px 1fr;
| |
| gap: 25px;
| |
| background: #ffffff;
| |
| border: 1px solid var(--buefa-border);
| |
| border-radius: 17px;
| |
| margin-bottom: 14px;
| |
| overflow: hidden;
| |
| transition:
| |
| transform .2s ease,
| |
| box-shadow .2s ease,
| |
| border-color .2s ease;
| |
| }
| |
| | |
| .buefa-event:hover {
| |
| transform: translateY(-2px);
| |
| box-shadow: 0 7px 22px rgba(0,0,0,.08);
| |
| border-color: #d5d5d5;
| |
| }
| |
| | |
| | |
| /* Datum */
| |
| | |
| .buefa-date {
| |
| background: var(--buefa-bg);
| |
| min-height: 135px;
| |
| padding: 20px 10px;
| |
| display: flex;
| |
| flex-direction: column;
| |
| align-items: center;
| |
| justify-content: center;
| |
| text-align: center;
| |
| }
| |
| | |
| .buefa-weekday {
| |
| font-size: 13px;
| |
| font-weight: 800;
| |
| text-transform: uppercase;
| |
| color: var(--buefa-orange);
| |
| }
| |
| | |
| .buefa-day {
| |
| font-size: 42px;
| |
| line-height: 1;
| |
| font-weight: 900;
| |
| margin: 3px 0;
| |
| }
| |
| | |
| .buefa-month-small {
| |
| font-size: 13px;
| |
| font-weight: 700;
| |
| text-transform: uppercase;
| |
| color: #666666;
| |
| }
| |
| | |
| | |
| /* Inhalt */
| |
| | |
| .buefa-event-content {
| |
| padding: 20px 22px 20px 0;
| |
| }
| |
| | |
| .buefa-event-title {
| |
| margin: 0 0 8px 0;
| |
| font-size: 21px;
| |
| line-height: 1.3;
| |
| font-weight: 800;
| |
| }
| |
| | |
| .buefa-event-meta {
| |
| display: flex;
| |
| flex-wrap: wrap;
| |
| gap: 7px 20px;
| |
| color: #555555;
| |
| font-size: 15px;
| |
| margin-bottom: 9px;
| |
| }
| |
| | |
| .buefa-event-description {
| |
| color: #666666;
| |
| font-size: 15px;
| |
| margin: 0 0 12px 0;
| |
| }
| |
| | |
| .buefa-event-actions {
| |
| display: flex;
| |
| gap: 10px;
| |
| flex-wrap: wrap;
| |
| }
| |
| | |
| .buefa-button {
| |
| border: none;
| |
| border-radius: 20px;
| |
| padding: 8px 15px;
| |
| cursor: pointer;
| |
| font-size: 14px;
| |
| font-weight: 700;
| |
| }
| |
| | |
| .buefa-button-primary {
| |
| background: var(--buefa-orange);
| |
| color: #ffffff;
| |
| }
| |
| | |
| .buefa-button-secondary {
| |
| background: #f1f1f1;
| |
| color: #444444;
| |
| }
| |
| | |
| | |
| /* Details */
| |
| | |
| .buefa-details {
| |
| display: none;
| |
| margin-top: 15px;
| |
| padding-top: 15px;
| |
| border-top: 1px solid var(--buefa-border);
| |
| color: #555555;
| |
| }
| |
| | |
| .buefa-details.open {
| |
| display: block;
| |
| }
| |
| | |
| .buefa-details-row {
| |
| margin-bottom: 8px;
| |
| }
| |
| | |
| .buefa-details-label {
| |
| font-weight: 800;
| |
| margin-right: 6px;
| |
| }
| |
| | |
| | |
| /* Kategorien */
| |
| | |
| .buefa-category {
| |
| display: inline-block;
| |
| border-radius: 15px;
| |
| padding: 4px 10px;
| |
| font-size: 12px;
| |
| font-weight: 700;
| |
| margin-bottom: 8px;
| |
| }
| |
| | |
| .category-familie {
| |
| background: #e8f3e3;
| |
| color: #507a43;
| |
| }
| |
| | |
| .category-kultur {
| |
| background: #eee6f5;
| |
| color: #73558c;
| |
| }
| |
| | |
| .category-wissen {
| |
| background: #e5f0f8;
| |
| color: #4d7798;
| |
| }
| |
| | |
| .category-workshop {
| |
| background: #fff1d6;
| |
| color: #9a6b1d;
| |
| }
| |
| | |
| .category-jubilaeum {
| |
| background: #fff0e7;
| |
| color: #bd5b25;
| |
| }
| |
| | |
| | |
| /* Vorläufig */
| |
| | |
| .buefa-event.provisional {
| |
| border-left: 6px solid var(--buefa-yellow);
| |
| }
| |
| | |
| .buefa-provisional-label {
| |
| display: inline-block;
| |
| background: #fff5d8;
| |
| color: #85651b;
| |
| padding: 4px 9px;
| |
| border-radius: 12px;
| |
| font-size: 12px;
| |
| font-weight: 800;
| |
| margin-bottom: 8px;
| |
| }
| |
| | |
| | |
| /* Vergangene Termine */
| |
| | |
| .buefa-event.past {
| |
| opacity: .55;
| |
| }
| |
| | |
| .buefa-past-label {
| |
| font-size: 12px;
| |
| color: #777777;
| |
| font-weight: 700;
| |
| }
| |
| | |
| | |
| /* Keine Termine */
| |
| | |
| .buefa-empty {
| |
| background: var(--buefa-bg);
| |
| border-radius: 15px;
| |
| padding: 25px;
| |
| text-align: center;
| |
| color: #777777;
| |
| }
| |
| | |
| | |
| /* ============================================================
| |
| IN KLÄRUNG
| |
| ============================================================ */
| |
| | |
| .buefa-open {
| |
| background: #f7f5ef;
| |
| border-radius: 18px;
| |
| padding: 25px;
| |
| margin-top: 35px;
| |
| }
| |
| | |
| .buefa-open h2 {
| |
| margin-top: 0;
| |
| }
| |
| | |
| | |
| /* ============================================================
| |
| FOOTER
| |
| ============================================================ */
| |
| | |
| .buefa-footer {
| |
| margin-top: 45px;
| |
| background: var(--buefa-bg);
| |
| border-radius: 18px;
| |
| padding: 30px;
| |
| text-align: center;
| |
| }
| |
| | |
| .buefa-footer h2 {
| |
| margin-top: 0;
| |
| }
| |
| | |
| .buefa-footer p {
| |
| max-width: 700px;
| |
| margin: 0 auto;
| |
| color: #666666;
| |
| }
| |
| | |
| | |
| /* ============================================================
| |
| MOBILE
| |
| ============================================================ */
| |
| | |
| @media (max-width: 700px) {
| |
| | |
| #buefa-jubilaeum {
| |
| width: 100%;
| |
| }
| |
| | |
| .buefa-hero {
| |
| border-radius: 0;
| |
| margin-left: -12px;
| |
| margin-right: -12px;
| |
| padding: 35px 22px;
| |
| }
| |
| | |
| .buefa-hero h1 {
| |
| font-size: 34px;
| |
| }
| |
| | |
| .buefa-hero p {
| |
| font-size: 17px;
| |
| }
| |
| | |
| .buefa-hero::after {
| |
| font-size: 120px;
| |
| right: 0;
| |
| }
| |
| | |
| .buefa-intro h2 {
| |
| font-size: 25px;
| |
| }
| |
| | |
| .buefa-month-buttons {
| |
| justify-content: flex-start;
| |
| flex-wrap: nowrap;
| |
| overflow-x: auto;
| |
| padding-bottom: 5px;
| |
| scrollbar-width: none;
| |
| }
| |
| | |
| .buefa-month-buttons::-webkit-scrollbar {
| |
| display: none;
| |
| }
| |
| | |
| .buefa-month-button {
| |
| flex: 0 0 auto;
| |
| }
| |
| | |
| .buefa-next-card {
| |
| padding: 20px;
| |
| }
| |
| | |
| .buefa-next-card h3 {
| |
| font-size: 21px;
| |
| }
| |
| | |
| .buefa-event {
| |
| display: block;
| |
| border-radius: 15px;
| |
| }
| |
| | |
| .buefa-date {
| |
| min-height: auto;
| |
| padding: 10px 15px;
| |
| flex-direction: row;
| |
| justify-content: flex-start;
| |
| gap: 8px;
| |
| }
| |
| | |
| .buefa-weekday,
| |
| .buefa-day,
| |
| .buefa-month-small {
| |
| font-size: 14px;
| |
| line-height: 1.2;
| |
| }
| |
| | |
| .buefa-day {
| |
| font-size: 22px;
| |
| }
| |
| | |
| .buefa-event-content {
| |
| padding: 16px;
| |
| }
| |
| | |
| .buefa-event-title {
| |
| font-size: 19px;
| |
| }
| |
| | |
| .buefa-event-meta {
| |
| display: block;
| |
| }
| |
| | |
| .buefa-event-meta span {
| |
| display: block;
| |
| margin-bottom: 5px;
| |
| }
| |
| | |
| .buefa-event-actions {
| |
| display: grid;
| |
| grid-template-columns: 1fr 1fr;
| |
| }
| |
| | |
| .buefa-button {
| |
| width: 100%;
| |
| }
| |
| | |
| .buefa-month-heading {
| |
| display: block;
| |
| }
| |
| | |
| .buefa-month-heading h2 {
| |
| font-size: 26px;
| |
| }
| |
| | |
| .buefa-controls {
| |
| display: block;
| |
| }
| |
| | |
| .buefa-filter {
| |
| overflow-x: auto;
| |
| flex-wrap: nowrap;
| |
| padding-bottom: 5px;
| |
| scrollbar-width: none;
| |
| }
| |
| | |
| .buefa-filter::-webkit-scrollbar {
| |
| display: none;
| |
| }
| |
| | |
| .buefa-filter button {
| |
| flex: 0 0 auto;
| |
| }
| |
| } | | } |
| | | @media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}} |
| | |
| /* ============================================================
| |
| SEHR KLEINE BILDSCHIRME
| |
| ============================================================ */
| |
| | |
| @media (max-width: 420px) { | |
| | |
| .buefa-event-actions {
| |
| grid-template-columns: 1fr;
| |
| }
| |
| | |
| .buefa-section-title {
| |
| font-size: 22px;
| |
| }
| |
| } | |
| | |
| </style> | | </style> |
| | | </head> |
| | | <body> |
| <div id="buefa-jubilaeum"> | | <div class="wrap"> |
| | | <h1>Familienangebote – Oktober & November 2026</h1> |
| <!-- ========================================================
| | <p class="intro">Alle im Kalender vorhandenen Termine sind enthalten. Unter dem Kalender finden Sie sämtliche Informationen zu jedem einzelnen Angebot.</p> |
| HERO
| | <div id="calendar"></div> |
| ======================================================== -->
| | <section class="details-section" id="angebote"> |
| | | <h2>Alle Angebote im Überblick</h2> |
| <section class="buefa-hero">
| | <p class="details-intro">Hier finden Sie zu jedem Termin Datum, Uhrzeit, Zielgruppe, Treffpunkt und die vollständige Beschreibung.</p> |
| | | <div id="offerList" class="offer-list"></div> |
| <div class="buefa-hero-content">
| | </section> |
| | |
| <div class="buefa-kicker">
| |
| 20 Jahre Bündnis für Familie Tübingen
| |
| </div>
| |
| | |
| <h1>
| |
| Unser Jubiläumsprogramm
| |
| </h1>
| |
| | |
| <p>
| |
| Von Oktober bis Dezember 2026 feiern wir gemeinsam
| |
| 20 Jahre Bündnis für Familie Tübingen.
| |
| Entdecken Sie Veranstaltungen, Workshops,
| |
| Gespräche, Kultur und Angebote für Familien.
| |
| </p>
| |
| | |
| </div>
| |
| | |
| </section>
| |
| | |
| | |
| <!-- ========================================================
| |
| EINSTIEG
| |
| ======================================================== -->
| |
| | |
| <section class="buefa-intro">
| |
| | |
| <h2>
| |
| Was ist wann und wo?
| |
| </h2>
| |
| | |
| <p>
| |
| Hier finden Sie alle Veranstaltungen auf einen Blick.
| |
| </p>
| |
| | |
| </section>
| |
| | |
| | |
| <!-- MONATSBUTTONS -->
| |
| | |
| <div
| |
| class="buefa-month-buttons"
| |
| id="monthButtons">
| |
| </div>
| |
| | |
| | |
| <!-- ========================================================
| |
| ALS NÄCHSTES
| |
| ======================================================== -->
| |
| | |
| <section class="buefa-next">
| |
| | |
| <h2 class="buefa-section-title">
| |
| Als Nächstes
| |
| </h2>
| |
| | |
| <div id="nextEvent">
| |
| </div>
| |
| | |
| </section>
| |
| | |
| | |
| <!-- ========================================================
| |
| FILTER
| |
| ======================================================== -->
| |
| | |
| <div class="buefa-controls">
| |
| | |
| <div>
| |
| <strong>
| |
| Alle Veranstaltungen
| |
| </strong>
| |
| </div>
| |
| | |
| <div
| |
| class="buefa-filter"
| |
| id="categoryFilter">
| |
| | |
| <button
| |
| class="active"
| |
| data-category="alle">
| |
| Alle
| |
| </button>
| |
| | |
| <button data-category="familie">
| |
| Familie
| |
| </button>
| |
| | |
| <button data-category="wissen">
| |
| Wissen
| |
| </button>
| |
| | |
| <button data-category="workshop">
| |
| Workshop
| |
| </button>
| |
| | |
| <button data-category="kultur">
| |
| Kultur
| |
| </button>
| |
| | |
| <button data-category="jubilaeum">
| |
| Jubiläum
| |
| </button>
| |
| | |
| </div>
| |
| | |
| </div>
| |
| | |
| | |
| <!-- ========================================================
| |
| VERANSTALTUNGEN
| |
| ======================================================== -->
| |
| | |
| <div id="eventList">
| |
| </div>
| |
| | |
| | |
| <!-- ========================================================
| |
| IN KLÄRUNG
| |
| ======================================================== -->
| |
| | |
| <section
| |
| class="buefa-open"
| |
| id="provisionalEvents"
| |
| style="display:none;">
| |
| | |
| <h2>
| |
| Noch in Klärung
| |
| </h2>
| |
| | |
| <div id="provisionalList">
| |
| </div>
| |
| | |
| </section>
| |
| | |
| | |
| <!-- ========================================================
| |
| FOOTER
| |
| ======================================================== -->
| |
| | |
| <section class="buefa-footer">
| |
| | |
| <h2>
| |
| 20 Jahre Bündnis für Familie Tübingen
| |
| </h2>
| |
| | |
| <p>
| |
| Viele Menschen, viele Ideen und viele Angebote –
| |
| gemeinsam für Familien in Tübingen.
| |
| </p>
| |
| | |
| </section>
| |
| | |
| </div> | | </div> |
| | | <div id="tooltip" class="tooltip" aria-hidden="true"></div> |
| | |
| | |
| <script> | | <script> |
| (function () {
| | const EVENTS = __DATA__; |
| | |
| "use strict";
| |
| | |
| | |
| /* ============================================================
| |
| VERANSTALTUNGEN
| |
| ============================================================
| |
| | |
| HIER WERDEN DIE VERANSTALTUNGEN GEPFLEGT.
| |
| | |
| Datum:
| |
| YYYY-MM-DD
| |
| | |
| Uhrzeit:
| |
| HH:MM
| |
| | |
| Kategorie:
| |
| familie
| |
| wissen
| |
| workshop
| |
| kultur
| |
| jubilaeum
| |
| | |
| provisional:
| |
| true = vorläufig / noch nicht endgültig
| |
| | |
| url:
| |
| Wenn vorhanden, wird ein "Mehr Informationen"-Button
| |
| erzeugt.
| |
| | |
| description:
| |
| Kurze Beschreibung.
| |
| | |
| location:
| |
| Veranstaltungsort.
| |
| ============================================================ */
| |
| | |
| | |
| const events = [ | |
| | |
| {
| |
| date: "2026-10-06",
| |
| time: "20:00",
| |
| title: "Hochsensible Kinder gut unterstützen und begleiten",
| |
| location: "Tübingen",
| |
| category: "wissen",
| |
| description: "Informationen und Impulse rund um den Umgang mit hochsensiblen Kindern."
| |
| },
| |
| | |
| {
| |
| date: "2026-10-08",
| |
| time: "18:00",
| |
| title: "Filmvorführung mit Gespräch über den Film „All Inclusive“",
| |
| location: "Tübingen",
| |
| category: "kultur",
| |
| description: "Filmvorführung mit anschließendem Gespräch."
| |
| },
| |
| | |
| {
| |
| date: "2026-10-09",
| |
| time: "15:00",
| |
| title: "Vorlesezeit in französischer Sprache mit dem LESE-HAUS",
| |
| location: "Stadtbücherei Tübingen",
| |
| category: "familie",
| |
| description: "Vorlesen für Kinder in französischer Sprache."
| |
| },
| |
| | |
| {
| |
| date: "2026-10-09",
| |
| time: "16:00",
| |
| title: "Vorlesezeit mit dem LESE-HAUS",
| |
| location: "Stadtbücherei Tübingen",
| |
| category: "familie",
| |
| description: "Gemeinsame Vorlesezeit für Kinder und Familien."
| |
| },
| |
| | |
| {
| |
| date: "2026-10-09",
| |
| time: "17:00",
| |
| title: "Vorlesezeit in ukrainischer Sprache mit dem LESE-HAUS",
| |
| location: "Stadtbücherei Tübingen",
| |
| category: "familie",
| |
| description: "Vorlesen für Kinder in ukrainischer Sprache."
| |
| },
| |
| | |
| {
| |
| date: "2026-10-09",
| |
| time: "18:00",
| |
| title: "Jubiläumsfeier & Eröffnung der Ausstellung „Familien in Tübingen – viele Formen von Zuhause“",
| |
| location: "Stadtbücherei Tübingen",
| |
| category: "jubilaeum",
| |
| description: "Eröffnung der Jubiläumsausstellung und gemeinsamer Auftakt des Jubiläumsprogramms."
| |
| },
| |
| | |
| {
| |
| date: "2026-10-21",
| |
| time: "",
| |
| title: "Familiensprachen und Sprachencafés im elkiko Familienzentrum",
| |
| location: "elkiko Familienzentrum",
| |
| category: "familie",
| |
| description: "Veranstaltung ist noch vorläufig.",
| |
| provisional: true
| |
| },
| |
| | |
| {
| |
| date: "2026-10-22",
| |
| time: "18:00",
| |
| title: "Preisverleihung des 23. Schreibwettbewerbs für junge Menschen der Buchhandlung Wekenmann",
| |
| location: "Tübingen",
| |
| category: "kultur",
| |
| description: "Preisverleihung des Schreibwettbewerbs für junge Menschen."
| |
| },
| |
| | |
| {
| |
| date: "2026-10-31",
| |
| time: "15:00",
| |
| title: "BamBam-Band & Halloween Fair for Kids",
| |
| location: "Tübingen",
| |
| category: "familie",
| |
| description: "Familienveranstaltung rund um Halloween."
| |
| },
| |
| | |
| | |
| /* ========================================================
| |
| NOVEMBER
| |
| ======================================================== */
| |
| | |
| {
| |
| date: "2026-11-05",
| |
| time: "09:00",
| |
| timeEnd: "11:00",
| |
| title: "„Der Widerspenstigen Zähmung??“ – Workshop",
| |
| location: "Tübingen",
| |
| category: "workshop",
| |
| description: "Workshop zum Thema Erziehung und Familie."
| |
| },
| |
| | |
| {
| |
| date: "2026-11-05",
| |
| time: "19:00",
| |
| title: "Nach der Elternzeit – welche Betreuung passt für mein Kind und unsere Familie?",
| |
| location: "Tübingen",
| |
| category: "wissen",
| |
| description: "Informationen und Orientierung rund um Betreuung nach der Elternzeit."
| |
| },
| |
| | |
| {
| |
| date: "2026-11-09",
| |
| time: "18:00",
| |
| timeEnd: "20:00",
| |
| title: "Wassergewöhnung – wie unterstütze ich mein Kind?",
| |
| location: "Tübingen",
| |
| category: "familie",
| |
| description: "Informationen und praktische Hinweise zur Wassergewöhnung."
| |
| },
| |
| | |
| {
| |
| date: "2026-11-10",
| |
| time: "18:00",
| |
| title: "Live-Show mit Miriam Davoudvandi: Das können wir uns nicht leisten",
| |
| location: "Tübingen",
| |
| category: "kultur",
| |
| description: "Live-Show mit Miriam Davoudvandi."
| |
| },
| |
| | |
| {
| |
| date: "2026-11-10",
| |
| time: "17:00",
| |
| timeEnd: "19:00",
| |
| title: "Das Geheimnis des Bildungserfolgs – Teil 1: Entwicklungsaufgaben",
| |
| location: "Tübingen",
| |
| category: "wissen",
| |
| description: "Teil 1 der Veranstaltungsreihe zum Bildungserfolg."
| |
| },
| |
| | |
| {
| |
| date: "2026-11-11",
| |
| time: "19:00",
| |
| timeEnd: "21:00",
| |
| title: "„Zu Hause in einer Erziehungsstelle“ – Podiumsdiskussion",
| |
| location: "Tübingen",
| |
| category: "wissen",
| |
| description: "Podiumsdiskussion mit Beteiligten in verschiedenen Rollen."
| |
| },
| |
| | |
| {
| |
| date: "2026-11-15",
| |
| time: "14:00",
| |
| timeEnd: "17:00",
| |
| title: "Familienbasteln im Café frieDa",
| |
| location: "Café frieDa",
| |
| category: "familie",
| |
| description: "Gemeinsam kreativ werden und einen schönen Familiennachmittag verbringen."
| |
| },
| |
| | |
| {
| |
| date: "2026-11-16",
| |
| time: "17:00",
| |
| timeEnd: "19:00",
| |
| title: "Das Geheimnis des Bildungserfolgs – Teil 2: Erfolg in der Schule, Bildung und Teilhabe",
| |
| location: "Tübingen",
| |
| category: "wissen",
| |
| description: "Teil 2 der Veranstaltungsreihe zum Bildungserfolg."
| |
| },
| |
| | |
| {
| |
| date: "2026-11-19",
| |
| time: "09:30",
| |
| timeEnd: "11:30",
| |
| title: "Zutaten für ein starkes Familienleben",
| |
| location: "Tübingen",
| |
| category: "familie",
| |
| description: "Impulse und Austausch rund um ein gutes Familienleben."
| |
| },
| |
| | |
| {
| |
| date: "2026-11-21",
| |
| time: "10:00",
| |
| timeEnd: "15:00",
| |
| title: "Bastelspaß für die ganze Familie – Thanksgiving Edition",
| |
| location: "Tübingen",
| |
| category: "familie",
| |
| description: "Kreativer Familientag mit Thanksgiving-Schwerpunkt."
| |
| },
| |
| | |
| {
| |
| date: "2026-11-24",
| |
| time: "17:00",
| |
| timeEnd: "19:00",
| |
| title: "Das Geheimnis des Bildungserfolgs – Teil 3: Medienerziehung",
| |
| location: "Tübingen",
| |
| category: "wissen",
| |
| description: "Teil 3 der Veranstaltungsreihe zum Bildungserfolg."
| |
| },
| |
| | |
| {
| |
| date: "2026-11-24",
| |
| time: "",
| |
| title: "Gemeinsam gegen Abfall – Papierrecycling in der Stadtbücherei",
| |
| location: "Stadtbücherei Tübingen",
| |
| category: "familie",
| |
| description: "Ganztägiges Angebot rund um Papierrecycling.",
| |
| allDay: true
| |
| },
| |
| | |
| {
| |
| date: "2026-11-25",
| |
| time: "",
| |
| title: "Gemeinsam gegen Abfall – Papierrecycling in der Stadtbücherei",
| |
| location: "Stadtbücherei Tübingen",
| |
| category: "familie",
| |
| description: "Ganztägiges Angebot rund um Papierrecycling.",
| |
| allDay: true
| |
| },
| |
| | |
| {
| |
| date: "2026-11-26",
| |
| time: "",
| |
| title: "Gemeinsam gegen Abfall – Papierrecycling in der Stadtbücherei",
| |
| location: "Stadtbücherei Tübingen",
| |
| category: "familie",
| |
| description: "Ganztägiges Angebot rund um Papierrecycling.",
| |
| allDay: true
| |
| }
| |
| | |
| ];
| |
| | |
|
| |
|
| /* ============================================================
| | const MONTHS=[ |
| MONATE
| | {year:2026,month:9,name:"Oktober 2026"}, |
| ============================================================ */
| | {year:2026,month:10,name:"November 2026"} |
| | |
| const monthNames = [ | |
| "Januar",
| |
| "Februar",
| |
| "März",
| |
| "April",
| |
| "Mai",
| |
| "Juni",
| |
| "Juli",
| |
| "August",
| |
| "September",
| |
| "Oktober",
| |
| "November",
| |
| "Dezember"
| |
| ]; | | ]; |
| | const WEEKDAYS=["Mo","Di","Mi","Do","Fr","Sa","So"]; |
|
| |
|
| const weekdayNames = [
| | function pad(n){return String(n).padStart(2,"0")} |
| "So",
| | function isoDate(year,month,day){return `${year}-${pad(month+1)}-${pad(day)}`} |
| "Mo",
| | function formatDate(date){ |
| "Di",
| | const d=new Date(date+"T12:00:00"); |
| "Mi",
| | return d.toLocaleDateString("de-DE",{weekday:"long",day:"2-digit",month:"long",year:"numeric"}) |
| "Do",
| |
| "Fr",
| |
| "Sa"
| |
| ];
| |
| | |
| | |
| /* ============================================================
| |
| STATUS
| |
| ============================================================ */
| |
| | |
| let selectedMonth = "all";
| |
| let selectedCategory = "alle";
| |
| | |
| | |
| /* ============================================================
| |
| HILFSFUNKTIONEN
| |
| ============================================================ */
| |
| | |
| function dateObject(dateString) { | |
| return new Date(dateString + "T00:00:00");
| |
| } | | } |
| | | function escapeHtml(value){ |
| | | return String(value??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'") |
| function formatDate(dateString) { | |
| | |
| const date = dateObject(dateString);
| |
| | |
| return {
| |
| day: date.getDate(),
| |
| month: monthNames[date.getMonth()],
| |
| monthShort: monthNames[date.getMonth()].substring(0,3),
| |
| weekday: weekdayNames[date.getDay()],
| |
| monthNumber: date.getMonth() + 1,
| |
| year: date.getFullYear()
| |
| };
| |
| } | | } |
| | | function eventTooltip(e){ |
| | | return [`Uhrzeit: ${e.time}`,`Zielgruppe: ${e.audience}`,`Treffpunkt: ${e.place}`,`Beschreibung: ${e.description}`].join("\\n") |
| function dateTimeValue(event) { | |
| | |
| const time = event.time || "00:00";
| |
| | |
| return new Date(
| |
| event.date + "T" + time + ":00"
| |
| ).getTime();
| |
| } | | } |
|
| |
|
| | | function renderCalendar(){ |
| function categoryName(category) { | | const root=document.getElementById("calendar"); |
| | | MONTHS.forEach(m=>{ |
| const names = {
| | const first=new Date(m.year,m.month,1); |
| familie: "Familie",
| | const days=new Date(m.year,m.month+1,0).getDate(); |
| wissen: "Wissen",
| | const offset=(first.getDay()+6)%7; |
| workshop: "Workshop",
| | let out=`<section class="month"><h2>${m.name}</h2><div class="weekdays">${WEEKDAYS.map(d=>`<div>${d}</div>`).join("")}</div><div class="grid">`; |
| kultur: "Kultur",
| | for(let i=0;i<offset;i++)out+=`<div class="day muted"><div class="dn"></div></div>`; |
| jubilaeum: "Jubiläum"
| | for(let day=1;day<=days;day++){ |
| };
| | const date=isoDate(m.year,m.month,day); |
| | | const dayEvents=EVENTS.filter(e=>e.date===date); |
| return names[category] || "";
| | out+=`<div class="day" data-date="${date}"><div class="dn">${day}</div>${dayEvents.map(e=>`<div class="event" data-event-id="${e.id}"><div class="et">${escapeHtml(e.title)}</div></div>`).join("")}</div>`; |
| | } |
| | const trailing=(7-(offset+days)%7)%7; |
| | for(let i=1;i<=trailing;i++)out+=`<div class="day muted"><div class="dn">${i}</div></div>`; |
| | root.insertAdjacentHTML("beforeend",out+"</div></section>"); |
| | }); |
| } | | } |
|
| |
|
| | | function renderOffers(){ |
| function categoryClass(category) {
| | document.getElementById("offerList").innerHTML=EVENTS.map(e=>` |
| | | <article class="offer" id="angebot-${e.id}"> |
| return "category-" + category;
| | <div class="offer-header"> |
| | <div class="offer-date">${escapeHtml(formatDate(e.date))}</div> |
| | <div class="offer-title">${escapeHtml(e.title)}</div> |
| | </div> |
| | <div class="offer-info"> |
| | <div class="info-row"><span class="info-label">Uhrzeit</span><div class="info-value">${escapeHtml(e.time)}</div></div> |
| | <div class="info-row"><span class="info-label">Zielgruppe</span><div class="info-value">${escapeHtml(e.audience)}</div></div> |
| | <div class="info-row"><span class="info-label">Treffpunkt</span><div class="info-value">${escapeHtml(e.place)}</div></div> |
| | <div class="info-row info-description"><span class="info-label">Beschreibung</span><div class="info-value">${escapeHtml(e.description)}</div></div> |
| | </div> |
| | </article>`).join(""); |
| } | | } |
|
| |
|
| | | function setupInteractions(){ |
| function isPast(event) {
| | const tooltip=document.getElementById("tooltip"); |
| | | document.querySelectorAll(".event").forEach(el=>{ |
| if (event.provisional) {
| | const event=EVENTS.find(e=>String(e.id)===el.dataset.eventId); |
| return false;
| | if(!event)return; |
| }
| | el.addEventListener("mouseenter",()=>{ |
| | | if(matchMedia("(max-width:640px)").matches)return; |
| const end = event.timeEnd || event.time || "23:59";
| | tooltip.textContent=eventTooltip(event); |
| | | tooltip.style.display="block"; |
| const eventDate = new Date(
| | }); |
| event.date + "T" + end + ":00"
| | el.addEventListener("mousemove",ev=>{ |
| );
| | if(matchMedia("(max-width:640px)").matches)return; |
| | | tooltip.style.left=(ev.clientX+14)+"px"; |
| return eventDate < new Date();
| | tooltip.style.top=(ev.clientY+14)+"px"; |
| | const r=tooltip.getBoundingClientRect(),pad=12; |
| | if(r.right>innerWidth-pad)tooltip.style.left=Math.max(pad,innerWidth-r.width-pad)+"px"; |
| | if(r.bottom>innerHeight-pad)tooltip.style.top=Math.max(pad,innerHeight-r.height-pad)+"px"; |
| | }); |
| | el.addEventListener("mouseleave",()=>tooltip.style.display="none"); |
| | el.addEventListener("click",()=>document.getElementById("angebot-"+event.id)?.scrollIntoView({behavior:"smooth",block:"start"})); |
| | }); |
| } | | } |
| | renderCalendar(); |
| | renderOffers(); |
| | setupInteractions(); |
| | </script> |
| | </body> |
| | </html>""" |
|
| |
|
| | | html_out = html_out.replace("__DATA__", data_json) |
| /* ============================================================
| | out_path = Path("/mnt/data/Familienangebote_Oktober_November_2026_mobile.html") |
| SORTIERUNG
| | out_path.write_text(html_out, encoding="utf-8") |
| ============================================================ */
| | print(f"Fertig: {out_path}") |
| | | print(f"Erhaltene Termine: {len(records)}") |
| events.sort(function(a,b) {
| |
| | |
| return dateTimeValue(a) - dateTimeValue(b);
| |
| | |
| });
| |
| | |
| | |
| /* ============================================================
| |
| MONATSBUTTONS ERZEUGEN
| |
| ============================================================ */
| |
| | |
| function createMonthButtons() {
| |
| | |
| const container =
| |
| document.getElementById("monthButtons");
| |
| | |
| const months = [...new Set(
| |
| events
| |
| .filter(event => !event.provisional)
| |
| .map(event => event.date.substring(0,7))
| |
| )];
| |
| | |
| let html = `
| |
| <button
| |
| class="buefa-month-button active"
| |
| data-month="all">
| |
| Alle Termine
| |
| </button>
| |
| `;
| |
| | |
| months.forEach(function(month) {
| |
| | |
| const monthNumber =
| |
| parseInt(month.substring(5,7),10);
| |
| | |
| html += `
| |
| <button
| |
| class="buefa-month-button"
| |
| data-month="${month}">
| |
| ${monthNames[monthNumber - 1]}
| |
| </button>
| |
| `;
| |
| | |
| });
| |
| | |
| container.innerHTML = html;
| |
| | |
| container
| |
| .querySelectorAll(".buefa-month-button")
| |
| .forEach(function(button) {
| |
| | |
| button.addEventListener("click", function() {
| |
| | |
| selectedMonth =
| |
| this.getAttribute("data-month");
| |
| | |
| container
| |
| .querySelectorAll(".buefa-month-button")
| |
| .forEach(btn =>
| |
| btn.classList.remove("active")
| |
| );
| |
| | |
| this.classList.add("active");
| |
| | |
| renderEvents();
| |
| | |
| });
| |
| | |
| });
| |
| | |
| }
| |
| | |
| | |
| /* ============================================================
| |
| NÄCHSTE VERANSTALTUNG
| |
| ============================================================ */
| |
| | |
| function renderNextEvent() {
| |
| | |
| const container =
| |
| document.getElementById("nextEvent");
| |
| | |
| const upcoming = events
| |
| .filter(event => !event.provisional && !isPast(event))
| |
| .sort((a,b) =>
| |
| dateTimeValue(a) - dateTimeValue(b)
| |
| );
| |
| | |
| if (!upcoming.length) {
| |
| | |
| container.innerHTML = `
| |
| <div class="buefa-empty">
| |
| Derzeit sind keine kommenden Veranstaltungen
| |
| hinterlegt.
| |
| </div>
| |
| `;
| |
| | |
| return;
| |
| }
| |
| | |
| const event = upcoming[0];
| |
| const date = formatDate(event.date);
| |
| | |
| let time = "";
| |
| | |
| if (event.allDay) {
| |
| | |
| time = "Ganztägig";
| |
| | |
| } else if (event.timeEnd) {
| |
| | |
| time =
| |
| event.time +
| |
| "–" +
| |
| event.timeEnd +
| |
| " Uhr";
| |
| | |
| } else if (event.time) {
| |
| | |
| time =
| |
| event.time +
| |
| " Uhr";
| |
| | |
| }
| |
| | |
| container.innerHTML = `
| |
| | |
| <div class="buefa-next-card">
| |
| | |
| <div class="buefa-next-label">
| |
| Als Nächstes
| |
| </div>
| |
| | |
| <h3>
| |
| ${event.title}
| |
| </h3>
| |
| | |
| <div class="buefa-next-meta">
| |
| | |
| <span>
| |
| 📅 ${date.weekday}, ${date.day}. ${date.month} ${date.year}
| |
| </span>
| |
| | |
| <span>
| |
| 🕐 ${time}
| |
| </span>
| |
| | |
| <span>
| |
| 📍 ${event.location}
| |
| </span>
| |
| | |
| </div>
| |
| | |
| <div class="buefa-next-description">
| |
| ${event.description || ""}
| |
| </div>
| |
| | |
| </div>
| |
| `;
| |
| | |
| }
| |
| | |
| | |
| /* ============================================================ | |
| VERANSTALTUNGS-KARTE
| |
| ============================================================ */
| |
| | |
| function createEventCard(event) {
| |
| | |
| const date = formatDate(event.date);
| |
| | |
| const past = isPast(event);
| |
| | |
| let time = "";
| |
| | |
| if (event.allDay) {
| |
| | |
| time = "Ganztägig";
| |
| | |
| } else if (event.timeEnd) {
| |
| | |
| time =
| |
| event.time +
| |
| "–" +
| |
| event.timeEnd +
| |
| " Uhr";
| |
| | |
| } else if (event.time) {
| |
| | |
| time =
| |
| event.time +
| |
| " Uhr";
| |
| | |
| }
| |
| | |
| | |
| let cardClass =
| |
| "buefa-event";
| |
| | |
| if (event.provisional) {
| |
| cardClass += " provisional";
| |
| }
| |
| | |
| if (past) {
| |
| cardClass += " past";
| |
| }
| |
| | |
| | |
| let provisionalLabel = "";
| |
| | |
| if (event.provisional) {
| |
| | |
| provisionalLabel = `
| |
| <div class="buefa-provisional-label">
| |
| Noch vorläufig
| |
| </div>
| |
| `;
| |
| | |
| }
| |
| | |
| | |
| let pastLabel = "";
| |
| | |
| if (past) {
| |
| | |
| pastLabel = `
| |
| <div class="buefa-past-label">
| |
| Veranstaltung bereits vorbei
| |
| </div>
| |
| `;
| |
| | |
| }
| |
| | |
| | |
| let urlButton = "";
| |
| | |
| if (event.url) {
| |
| | |
| urlButton = `
| |
| <a
| |
| href="${event.url}"
| |
| class="buefa-button buefa-button-primary"
| |
| target="_blank"
| |
| rel="noopener">
| |
| Mehr Informationen →
| |
| </a>
| |
| `;
| |
| | |
| }
| |
| | |
| | |
| const detailsId =
| |
| "details-" +
| |
| event.date.replace(/-/g,"") +
| |
| "-" +
| |
| Math.random().toString(36).substring(2,7);
| |
| | |
| | |
| return `
| |
| | |
| <article
| |
| class="${cardClass}"
| |
| data-category="${event.category}">
| |
| | |
| <div class="buefa-date">
| |
| | |
| <span class="buefa-weekday">
| |
| ${date.weekday}
| |
| </span>
| |
| | |
| <span class="buefa-day">
| |
| ${date.day}
| |
| </span>
| |
| | |
| <span class="buefa-month-small">
| |
| ${date.month}
| |
| </span>
| |
| | |
| </div>
| |
| | |
| | |
| <div class="buefa-event-content">
| |
| | |
| ${provisionalLabel}
| |
| | |
| <div class="
| |
| buefa-category
| |
| ${categoryClass(event.category)}
| |
| ">
| |
| ${categoryName(event.category)}
| |
| </div>
| |
| | |
| <h3 class="buefa-event-title">
| |
| ${event.title}
| |
| </h3>
| |
| | |
| | |
| <div class="buefa-event-meta">
| |
| | |
| <span>
| |
| 🕐 ${time}
| |
| </span>
| |
| | |
| <span>
| |
| 📍 ${event.location}
| |
| </span>
| |
| | |
| </div>
| |
| | |
| | |
| <p class="buefa-event-description">
| |
| ${event.description || ""}
| |
| </p>
| |
| | |
| | |
| ${pastLabel}
| |
| | |
| | |
| <div class="buefa-event-actions">
| |
| | |
| <button
| |
| class="buefa-button buefa-button-secondary"
| |
| onclick="buefaToggleDetails('${detailsId}')">
| |
| Details
| |
| </button>
| |
| | |
| ${urlButton}
| |
| | |
| <button
| |
| class="buefa-button buefa-button-secondary"
| |
| onclick='buefaAddToCalendar(${JSON.stringify(event)})'>
| |
| + Termin merken
| |
| </button>
| |
| | |
| <button
| |
| class="buefa-button buefa-button-secondary"
| |
| onclick='buefaShare(${JSON.stringify(event)})'>
| |
| ↗ Teilen
| |
| </button>
| |
| | |
| </div>
| |
| | |
| | |
| <div
| |
| class="buefa-details"
| |
| id="${detailsId}">
| |
| | |
| <div class="buefa-details-row">
| |
| | |
| <span class="buefa-details-label">
| |
| Datum:
| |
| </span>
| |
| | |
| ${date.weekday},
| |
| ${date.day}.
| |
| ${date.month}
| |
| ${date.year}
| |
| | |
| </div>
| |
| | |
| | |
| <div class="buefa-details-row">
| |
| | |
| <span class="buefa-details-label">
| |
| Uhrzeit:
| |
| </span>
| |
| | |
| ${time}
| |
| | |
| </div>
| |
| | |
| | |
| <div class="buefa-details-row">
| |
| | |
| <span class="buefa-details-label">
| |
| Ort:
| |
| </span>
| |
| | |
| ${event.location}
| |
| | |
| </div>
| |
| | |
| | |
| <div class="buefa-details-row">
| |
| | |
| ${event.description || ""}
| |
| | |
| </div>
| |
| | |
| </div>
| |
| | |
| </div>
| |
| | |
| </article>
| |
| | |
| `;
| |
| | |
| }
| |
| | |
| | |
| /* ============================================================
| |
| VERANSTALTUNGEN DARSTELLEN
| |
| ============================================================ */
| |
| | |
| function renderEvents() {
| |
| | |
| const container =
| |
| document.getElementById("eventList");
| |
| | |
| const filtered = events.filter(function(event) {
| |
| | |
| if (event.provisional) {
| |
| return false;
| |
| }
| |
| | |
| if (
| |
| selectedMonth !== "all" &&
| |
| event.date.substring(0,7) !== selectedMonth
| |
| ) {
| |
| return false;
| |
| }
| |
| | |
| if (
| |
| selectedCategory !== "alle" &&
| |
| event.category !== selectedCategory
| |
| ) {
| |
| return false;
| |
| }
| |
| | |
| return true;
| |
| | |
| });
| |
| | |
| | |
| if (!filtered.length) {
| |
| | |
| container.innerHTML = `
| |
| <div class="buefa-empty">
| |
| Für diese Auswahl sind derzeit keine
| |
| Veranstaltungen eingetragen.
| |
| </div>
| |
| `;
| |
| | |
| return;
| |
| }
| |
| | |
| | |
| const grouped = {};
| |
| | |
| filtered.forEach(function(event) {
| |
| | |
| const month =
| |
| event.date.substring(0,7);
| |
| | |
| if (!grouped[month]) {
| |
| grouped[month] = [];
| |
| }
| |
| | |
| grouped[month].push(event);
| |
| | |
| });
| |
| | |
| | |
| let html = "";
| |
| | |
| | |
| Object.keys(grouped)
| |
| .sort()
| |
| .forEach(function(monthKey) {
| |
| | |
| const monthNumber =
| |
| parseInt(monthKey.substring(5,7),10);
| |
| | |
| const year =
| |
| monthKey.substring(0,4);
| |
| | |
| html += `
| |
| | |
| <section
| |
| class="buefa-month-section">
| |
| | |
| <div class="buefa-month-heading">
| |
| | |
| <h2>
| |
| ${monthNames[monthNumber - 1]}
| |
| </h2>
| |
| | |
| <span>
| |
| ${year}
| |
| </span>
| |
| | |
| </div>
| |
| | |
| `;
| |
| | |
| | |
| grouped[monthKey]
| |
| .forEach(function(event) {
| |
| | |
| html += createEventCard(event);
| |
| | |
| });
| |
| | |
| | |
| html += `
| |
| </section>
| |
| `;
| |
| | |
| });
| |
| | |
| | |
| container.innerHTML = html;
| |
| | |
| }
| |
| | |
| | |
| /* ============================================================
| |
| VORLÄUFIGE TERMINE
| |
| ============================================================ */
| |
| | |
| function renderProvisional() {
| |
| | |
| const wrapper =
| |
| document.getElementById("provisionalEvents");
| |
| | |
| const container =
| |
| document.getElementById("provisionalList");
| |
| | |
| const provisional =
| |
| events.filter(event => event.provisional);
| |
| | |
| | |
| if (!provisional.length) {
| |
| | |
| wrapper.style.display = "none";
| |
| | |
| return;
| |
| | |
| }
| |
| | |
| | |
| wrapper.style.display = "block";
| |
| | |
| let html = "";
| |
| | |
| | |
| provisional.forEach(function(event) {
| |
| | |
| const date =
| |
| formatDate(event.date);
| |
| | |
| html += `
| |
| | |
| <div class="buefa-event provisional">
| |
| | |
| <div class="buefa-date">
| |
| | |
| <span class="buefa-weekday">
| |
| ${date.weekday}
| |
| </span>
| |
| | |
| <span class="buefa-day">
| |
| ${date.day}
| |
| </span>
| |
| | |
| <span class="buefa-month-small">
| |
| ${date.month}
| |
| </span>
| |
| | |
| </div>
| |
| | |
| <div class="buefa-event-content">
| |
| | |
| <div class="buefa-provisional-label">
| |
| Noch in Klärung
| |
| </div>
| |
| | |
| <h3 class="buefa-event-title">
| |
| ${event.title}
| |
| </h3>
| |
| | |
| <div class="buefa-event-meta">
| |
| | |
| <span>
| |
| 📍 ${event.location}
| |
| </span>
| |
| | |
| </div>
| |
| | |
| <p class="buefa-event-description">
| |
| ${event.description || ""}
| |
| </p>
| |
| | |
| </div>
| |
| | |
| </div>
| |
| | |
| `;
| |
| | |
| });
| |
| | |
| | |
| container.innerHTML = html;
| |
| | |
| }
| |
| | |
| | |
| /* ============================================================
| |
| FILTER
| |
| ============================================================ */
| |
| | |
| document
| |
| .querySelectorAll("#categoryFilter button")
| |
| .forEach(function(button) {
| |
| | |
| button.addEventListener("click", function() {
| |
| | |
| selectedCategory =
| |
| this.getAttribute("data-category");
| |
| | |
| document
| |
| .querySelectorAll("#categoryFilter button")
| |
| .forEach(btn =>
| |
| btn.classList.remove("active")
| |
| );
| |
| | |
| this.classList.add("active");
| |
| | |
| renderEvents();
| |
| | |
| });
| |
| | |
| });
| |
| | |
| | |
| /* ============================================================
| |
| DETAILS AUFKLAPPEN
| |
| ============================================================ */
| |
| | |
| window.buefaToggleDetails = function(id) {
| |
| | |
| const element =
| |
| document.getElementById(id);
| |
| | |
| if (element) {
| |
| | |
| element.classList.toggle("open");
| |
| | |
| }
| |
| | |
| };
| |
| | |
| | |
| /* ============================================================
| |
| KALENDERDATEI
| |
| ============================================================ */
| |
| | |
| window.buefaAddToCalendar = function(event) {
| |
| | |
| const start =
| |
| event.date.replace(/-/g,"") +
| |
| "T" +
| |
| (event.time || "0000").replace(":","") +
| |
| "00";
| |
| | |
| | |
| let endTime =
| |
| event.timeEnd ||
| |
| event.time ||
| |
| "23:59";
| |
| | |
| | |
| const end =
| |
| event.date.replace(/-/g,"") +
| |
| "T" +
| |
| endTime.replace(":","") +
| |
| "00";
| |
| | |
| | |
| const ics = [
| |
| "BEGIN:VCALENDAR",
| |
| "VERSION:2.0",
| |
| "PRODID:-//Bündnis für Familie Tübingen//Jubiläum//DE",
| |
| "BEGIN:VEVENT",
| |
| "UID:" +
| |
| Date.now() +
| |
| "@buendnis-fuer-familie-tuebingen.de",
| |
| "DTSTAMP:" +
| |
| new Date()
| |
| .toISOString()
| |
| .replace(/[-:]/g,"")
| |
| .replace(/\.\d{3}/,""),
| |
| "DTSTART:" + start,
| |
| "DTEND:" + end,
| |
| "SUMMARY:" + escapeICS(event.title),
| |
| "LOCATION:" + escapeICS(event.location),
| |
| "DESCRIPTION:" + escapeICS(event.description || ""),
| |
| "END:VEVENT",
| |
| "END:VCALENDAR"
| |
| ].join("\r\n");
| |
| | |
| | |
| const blob =
| |
| new Blob([ics], {
| |
| type: "text/calendar;charset=utf-8"
| |
| });
| |
| | |
| | |
| const url =
| |
| URL.createObjectURL(blob);
| |
| | |
| | |
| const link =
| |
| document.createElement("a");
| |
| | |
| link.href = url;
| |
| | |
| link.download =
| |
| event.title
| |
| .replace(/[^a-z0-9äöüß]+/gi,"-")
| |
| .replace(/^-|-$/g,"")
| |
| + ".ics";
| |
| | |
| | |
| document.body.appendChild(link);
| |
| | |
| link.click();
| |
| | |
| document.body.removeChild(link);
| |
| | |
| URL.revokeObjectURL(url);
| |
| | |
| }; | |
| | |
| | |
| function escapeICS(text) {
| |
| | |
| return String(text)
| |
| .replace(/\\/g,"\\\\")
| |
| .replace(/\n/g,"\\n")
| |
| .replace(/,/g,"\\,")
| |
| .replace(/;/g,"\\;");
| |
| | |
| }
| |
| | |
| | |
| /* ============================================================
| |
| TEILEN
| |
| ============================================================ */
| |
| | |
| window.buefaShare = function(event) {
| |
| | |
| const date =
| |
| formatDate(event.date);
| |
| | |
| | |
| const text =
| |
| event.title +
| |
| "\n" +
| |
| date.day +
| |
| ". " +
| |
| date.month +
| |
| " " +
| |
| date.year +
| |
| "\n" +
| |
| (event.time ?
| |
| event.time + " Uhr\n" :
| |
| "") +
| |
| event.location;
| |
| | |
| | |
| if (
| |
| navigator.share
| |
| ) {
| |
| | |
| navigator.share({
| |
| | |
| title: event.title,
| |
| | |
| text: text,
| |
| | |
| url: event.url ||
| |
| window.location.href
| |
| | |
| }).catch(function() {});
| |
| | |
| } else {
| |
| | |
| if (
| |
| navigator.clipboard
| |
| ) {
| |
| | |
| navigator.clipboard
| |
| .writeText(text)
| |
| .then(function() {
| |
| | |
| alert(
| |
| "Die Veranstaltungsinformationen wurden kopiert."
| |
| );
| |
| | |
| });
| |
| | |
| } else {
| |
| | |
| alert(text);
| |
| | |
| }
| |
| | |
| }
| |
| | |
| };
| |
| | |
| | |
| /* ============================================================
| |
| INITIALISIERUNG
| |
| ============================================================ */
| |
| | |
| createMonthButtons();
| |
| | |
| renderNextEvent();
| |
| | |
| renderEvents();
| |
| | |
| renderProvisional();
| |
| | |
| | |
| })();
| |
| </script>
| |
| ```
| |