diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..735ad8e --- /dev/null +++ b/admin.php @@ -0,0 +1,344 @@ + $ev) { + if ($ev['id'] === $id) { $idx = $k; break; } + } + $record = [ + 'id' => $id, + 'title' => $title, + 'speaker' => $speaker, + 'date' => $date, + 'time' => $time, + 'venue_name' => $venue_name, + 'venue_address' => $venue_address, + 'price' => $price, + 'doors_open' => $doors_open, + 'tickets_info' => $tickets_info, + 'catering' => $catering, + 'tags' => $tags, + 'description' => $description, + 'image' => $imagePath, + 'slug' => $slug, + 'updated_at' => now_iso() + ]; + if ($idx !== null) { + $events[$idx] = $record; + } else { + $record['created_at'] = now_iso(); + $events[] = $record; + } + save_events($events); + header('Location: admin.php?ok=1'); + exit; + } catch (Throwable $e) { + $error = 'Fehler: ' . $e->getMessage(); + } + } + } + + // DELETE + if (isset($_GET['delete'])) { + $events = load_events(); + $id = $_GET['delete']; + $events = array_values(array_filter($events, fn($e) => $e['id'] !== $id)); + save_events($events); + header('Location: admin.php?deleted=1'); + exit; + } + + // EDIT load + $editEvent = null; + if (isset($_GET['edit'])) { + $id = $_GET['edit']; + foreach (load_events() as $e) { + if ($e['id'] === $id) { $editEvent = $e; break; } + } + } +} +?> + + + + + + Admin | <?= h(SITE_NAME) ?> + + + + + + + + + + +
+
+
+

Admin-Login

+
+
+ +
+
+
+
+
+
+ +
+ + +
+ +
+
+
+
+
+ + +
+
+
+

Vorträge verwalten

+

Anlegen, bearbeiten, Bilder hochladen.

+
+
+ +
+
+ Gespeichert + Gelöscht +
+ +
+
+
+
+

+
+ +
+ + +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ aktuelles Bild +
+ + + +
+ + +
+ + + + Abbrechen + +
+
+
+
+ +
+
+
+

Alle Vorträge

+ + +

Noch keine Einträge.

+ +
+ + + + + + + + + + + + + + + + + + + +
DatumTitelOrt
+
+
+
+ Bearbeiten + Löschen +
+
+ +
+
+ +
+
+
+

Tipp

+
Wenn „Ticket/VVK-Info“ eine URL enthält, zeigt die Programmseite automatisch den Button „Tickets sichern“.
+
+ Programm ansehen +
+
+ +
+
+
+ + + + + + + diff --git a/assets/hero.jpg b/assets/hero.jpg new file mode 100644 index 0000000..028ea23 Binary files /dev/null and b/assets/hero.jpg differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..56cf5fb Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/partner_blumenhof.png b/assets/partner_blumenhof.png new file mode 100644 index 0000000..682018c Binary files /dev/null and b/assets/partner_blumenhof.png differ diff --git a/assets/partner_empor.png b/assets/partner_empor.png new file mode 100644 index 0000000..7b538cf Binary files /dev/null and b/assets/partner_empor.png differ diff --git a/assets/partner_gemeinde.png b/assets/partner_gemeinde.png new file mode 100644 index 0000000..9821b5a Binary files /dev/null and b/assets/partner_gemeinde.png differ diff --git a/assets/partner_wbr.png b/assets/partner_wbr.png new file mode 100644 index 0000000..757c027 Binary files /dev/null and b/assets/partner_wbr.png differ diff --git a/assets/styles.css b/assets/styles.css new file mode 100644 index 0000000..d0cb621 --- /dev/null +++ b/assets/styles.css @@ -0,0 +1,205 @@ +/* assets/styles.css — KulTour Conversion Theme */ + +/* ===== Farbwelt (Logo-Blau + Akzent-Gelb) ===== */ +:root{ + --brand:#1789ca; + --brand-700:#116da3; + --accent:#f3c546; + --accent-700:#e0b233; + --ink:#0e2230; + --muted:#6b7b88; + --bg:#ffffff; + --soft:#f6f8fb; + --soft-2:#eef5fb; +} + +/* ===== Basis ===== */ +html{scroll-behavior:smooth} +body{color:var(--ink); background:var(--bg)} +a{color:var(--brand)} +a:hover{color:var(--brand-700)} +.navbar{backdrop-filter:saturate(120%) blur(6px)} +.navbar .nav-link{font-weight:500} +.navbar .nav-link.active{color:var(--brand)!important} +.brand-logo{display:flex;align-items:center;gap:.5rem;font-weight:800;} +.brand-logo img{height:34px;width:auto} + +/* zarte farbige Top-Border unter Navbar */ +.navbar.border-bottom{box-shadow:0 2px 0 0 rgba(23,137,202,.15)} + +/* ===== Hero (Foto + Farbe) ===== */ +.hero{ + position:relative; min-height:68vh; display:flex; align-items:center; + color:#fff; background-size:cover; background-position:center; background-repeat:no-repeat; +} +.hero::before{ + content:""; position:absolute; inset:0; + background: + linear-gradient( to bottom right, rgba(23,137,202,.65), rgba(243,197,70,.45) ), + linear-gradient( to bottom, rgba(0,0,0,.25), rgba(0,0,0,.55) ); +} +.hero .container{position:relative; z-index:2} +.hero h1{font-weight:850; line-height:1.04; letter-spacing:.2px; text-shadow:0 2px 16px rgba(0,0,0,.25)} +.hero .lead{font-size:1.18rem} + +/* ===== Buttons ===== */ +.btn-brand{ + --bs-btn-bg:var(--brand); --bs-btn-border-color:var(--brand); + --bs-btn-hover-bg:var(--brand-700); --bs-btn-hover-border-color:var(--brand-700); + --bs-btn-color:#fff; +} +.btn-accent{ + --bs-btn-bg:var(--accent); --bs-btn-border-color:var(--accent); + --bs-btn-hover-bg:var(--accent-700); --bs-btn-hover-border-color:var(--accent-700); + --bs-btn-color:#1a1a1a; font-weight:700; +} +.btn-outline-brand{ + --bs-btn-color:#fff; --bs-btn-border-color:rgba(255,255,255,.7); + --bs-btn-hover-color:#fff; --bs-btn-hover-bg:rgba(255,255,255,.12); + --bs-btn-hover-border-color:#fff; +} + +/* ===== Sektionen & Akzente ===== */ +.section-soft{background:var(--soft)} +.section-soft-2{background:var(--soft-2)} +.section-title{font-weight:800} +.section-kicker{color:var(--brand); font-weight:700; letter-spacing:.08em; text-transform:uppercase} + +/* schmale Akzentlinie */ +.akzent-line{height:4px; width:64px; background:linear-gradient(90deg,var(--accent),var(--brand)); border-radius:8px} + +/* ===== Karten (Events) ===== */ +.card{ + border:1px solid #e9edf3; border-radius:1rem; transition:transform .2s ease, box-shadow .2s ease; + background:linear-gradient(180deg,#fff, #fff), radial-gradient(1200px 300px at 0% -10%, rgba(23,137,202,.06), transparent 60%); + background-clip:padding-box, border-box; +} +.card:hover{transform:translateY(-2px); box-shadow:0 12px 34px rgba(0,0,0,.08)} +.card .top-ribbon{ + position:absolute; top:12px; left:12px; background:var(--brand); color:#fff; font-weight:700; + padding:.28rem .55rem; border-radius:.5rem; font-size:.8rem; box-shadow:0 6px 14px rgba(23,137,202,.25); +} +.card-img-top{object-fit:cover; max-height:230px; border-top-left-radius:1rem; border-top-right-radius:1rem} + +/* Badges */ +.badge.text-bg-primary{background-color:var(--brand)!important} +.badge-accent{background:var(--accent); color:#1a1a1a; font-weight:700} + +/* ===== CTA-Band ===== */ +.cta-band{ + background: + radial-gradient(1000px 300px at 0% 0%, rgba(23,137,202,.12), transparent 60%), + linear-gradient(90deg, rgba(23,137,202,.08), rgba(243,197,70,.14)); + border:1px solid rgba(23,137,202,.18); border-radius:1.25rem; +} + +/* ===== Feature-Icons ===== */ +.feature{ + display:flex; gap:.9rem; align-items:flex-start; + padding:1rem 1.1rem; border-radius:.9rem; background:#fff; border:1px solid #e9edf3; +} +.feature .icon{ + width:42px; height:42px; display:grid; place-items:center; border-radius:.7rem; + background:linear-gradient(135deg, var(--brand), var(--accent)); + color:#fff; font-size:1.2rem; box-shadow:0 10px 20px rgba(23,137,202,.25); +} + +/* ===== Partnerband ===== */ +.partner-band{ + display:flex; gap:1.5rem; align-items:center; flex-wrap:wrap; opacity:.9 +} +.partner-band img{height:34px; filter:grayscale(40%); transition:filter .2s} +.partner-band img:hover{filter:none} + +/* ===== Footer ===== */ +footer{background:#fff} +footer a{text-decoration:none} +footer .separator{opacity:.25} + +/* Container: keine fixe Höhe mehr, nur Polster */ +.coop-logo{ + display:inline-flex; align-items:center; justify-content:center; + padding:.35rem .55rem; border-radius:.75rem; + transition:transform .15s ease, filter .15s ease, box-shadow .15s ease; + filter:grayscale(40%) contrast(1.05) brightness(1.02) opacity(.9); + background:#fff; border:1px solid #e9edf3; + line-height:0; /* verhindert vertikale Zentrier-Zitterei */ +} +/* Bild: nur max-height begrenzen, Breite automatisch -> keine Verzerrung */ +.coop-logo img{ + display:block; + max-height:40px; /* einheitliche optische Höhe */ + height:auto; /* Ratio bleibt erhalten */ + width:auto; /* Ratio bleibt erhalten */ +} +.coop-logo:hover{ + filter:none; transform:translateY(-1px); + box-shadow:0 10px 24px rgba(23,137,202,.15); +} + +/* ===== Modals: Mobile korrekt einpassen ===== */ +.modal-content{ max-height: 100vh; } + +/* Standard: auf Desktop/Tablets */ +.modal-img { + width: auto; + max-width: 100%; + height: auto; + max-height: 90vh; /* nie höher als Viewport */ + object-fit: contain; + display: block; + margin: 0 auto; +} + +.modal-body{ + overflow: auto; /* falls Text länger ist */ +} + +/* Phones: Header (~56px) + Footer-Reserve (~56px) + Innenabstand (~16px*2) abziehen */ +@media (max-width: 576px) { + .modal-body { + max-height: 75vh; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } +} + +/* Beschreibung auf Karten & im Modal */ +.poster-desc{ + text-align: center; + margin: .75rem auto 0; + color:#344654; + max-width: 46ch; + line-height: 1.45; +} + +/* Abstände zwischen erlaubten HTML-Elementen harmonisieren */ +.poster-desc p{ margin: .5rem 0; } +.poster-desc ul, .poster-desc ol{ + display: inline-block; /* zentriert wirken lassen */ + text-align: left; /* Lesbarkeit bei Listen */ + margin: .5rem auto; padding-left: 1.2rem; +} +.poster-desc li{ margin: .15rem 0; } + +/* Karten-Beschreibung visuell kürzen, ohne HTML zu zerstören */ +.poster-desc{ + display: -webkit-box; + -webkit-line-clamp: 6; /* Anzahl Zeilen auf Karten */ + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* Im Modal die Klammerung aufheben – volle Beschreibung */ +.modal .poster-desc{ + display: block; + -webkit-line-clamp: initial; + -webkit-box-orient: initial; + overflow: visible; + max-width: 60ch; /* im Modal etwas breiter, aber zentriert */ + margin: 0 auto 0; +} + +.poster-desc h3, .poster-desc h4, .poster-desc h5{ + text-align:center; margin:.6rem 0 .35rem; line-height:1.25; +} diff --git a/config.php b/config.php new file mode 100644 index 0000000..82d9174 --- /dev/null +++ b/config.php @@ -0,0 +1,20 @@ + + + + + Datenschutz – Walschleber KulTour + + + + + + + + + + + + + + + + + + + +
+
+
+

Datenschutz

+

Informationen zur Verarbeitung personenbezogener Daten nach DSGVO.

+
+
+ +
+
+ +

1. Verantwortlicher

+

+ Verantwortlich im Sinne der Datenschutzgesetze, insbesondere der EU-Datenschutzgrundverordnung (DSGVO), ist:

+ Alexander Kerst
+ Erfurter Straße 29b
+ 99189 Walschleben
+ Deutschland
+ E-Mail: walschleber.kultour@gmail.com +

+ +

2. Allgemeine Hinweise zur Datenverarbeitung

+

+ Wir verarbeiten personenbezogene Daten der Nutzer grundsätzlich nur, soweit dies zur Bereitstellung einer funktionsfähigen Website sowie unserer Inhalte und Leistungen erforderlich ist. + Die Verarbeitung personenbezogener Daten erfolgt regelmäßig nur nach Einwilligung der Nutzer oder wenn die Verarbeitung durch gesetzliche Vorschriften gestattet ist. +

+ +

3. Erhebung und Speicherung personenbezogener Daten

+ + +

4. Rechtsgrundlagen der Verarbeitung

+

+ Die Verarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO (Vertrag/Anbahnung), + Art. 6 Abs. 1 lit. c DSGVO (rechtliche Verpflichtung) oder Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse, z. B. für den sicheren Betrieb der Website). + Bei Einwilligungen stützen wir uns auf Art. 6 Abs. 1 lit. a DSGVO. +

+ +

5. Speicherdauer

+

+ Personenbezogene Daten werden gelöscht, sobald der Zweck der Speicherung entfällt oder gesetzliche Aufbewahrungsfristen ablaufen. +

+ +

6. Weitergabe von Daten

+

+ Eine Übermittlung Ihrer persönlichen Daten an Dritte erfolgt nur, wenn dies gesetzlich erlaubt ist, + zur Erfüllung eines Vertrags erforderlich ist oder Sie ausdrücklich eingewilligt haben. +

+ +

7. Cookies

+

+ Unsere Website verwendet Cookies. Cookies sind kleine Textdateien, die auf Ihrem Endgerät gespeichert werden. + Sie dienen dazu, unsere Website nutzerfreundlicher und sicherer zu machen. + Sie können die Speicherung von Cookies durch Ihre Browsereinstellungen verhindern. + Bereits gespeicherte Cookies können jederzeit gelöscht werden. +

+ +

8. Rechte der betroffenen Personen

+

Sie haben folgende Rechte gemäß DSGVO:

+ + +

9. Datensicherheit

+

+ Wir setzen technische und organisatorische Sicherheitsmaßnahmen ein, um Ihre Daten gegen Manipulation, Verlust oder unbefugten Zugriff zu schützen. + Unsere Sicherheitsmaßnahmen werden entsprechend der technologischen Entwicklung fortlaufend verbessert. +

+ +

10. Änderungen dieser Datenschutzerklärung

+

+ Wir behalten uns vor, diese Datenschutzerklärung bei Bedarf anzupassen, + um sie stets den aktuellen rechtlichen Anforderungen sowie Änderungen unserer Leistungen anzupassen. +

+ +
+
+ + + + + + diff --git a/functions.php b/functions.php new file mode 100644 index 0000000..81365cf --- /dev/null +++ b/functions.php @@ -0,0 +1,217 @@ + []], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + } + if (!is_dir(UPLOAD_DIR)) { + @mkdir(UPLOAD_DIR, 0775, true); + } +} + +function load_events(): array { + ensure_storage(); + $raw = file_get_contents(DATA_FILE); + $data = json_decode($raw ?: '{"events":[]}', true); + return is_array($data) && isset($data['events']) ? $data['events'] : []; +} + +function save_events(array $events): void { + ensure_storage(); + $fp = fopen(DATA_FILE, 'c+'); + if ($fp === false) { + throw new RuntimeException('Kann Datei nicht öffnen: ' . DATA_FILE); + } + flock($fp, LOCK_EX); + ftruncate($fp, 0); + fwrite($fp, json_encode(['events' => array_values($events)], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + fflush($fp); + flock($fp, LOCK_UN); + fclose($fp); +} + +function by_datetime_asc(array $a, array $b): int { + return strtotime($a['date'].' '.$a['time']) <=> strtotime($b['date'].' '.$b['time']); +} +function by_datetime_desc(array $a, array $b): int { + return strtotime($b['date'].' '.$b['time']) <=> strtotime($a['date'].' '.$a['time']); +} + +function now_iso(): string { + return date('Y-m-d\TH:i:sP'); +} + +function make_slug(string $title, string $date): string { + $t = mb_strtolower($title, 'UTF-8'); + $t = preg_replace('~[^\pL\d]+~u', '-', $t); + $t = trim($t, '-'); + $t = preg_replace('~[^-\w]+~', '', $t); + $t = preg_replace('~-+~', '-', $t); + return $t . '-' . $date; +} + +function sanitize_text(string $s): string { + return trim($s); +} + +function is_upcoming(array $event): bool { + $ts = strtotime($event['date'].' '.$event['time']); + return $ts >= time(); +} + +function handle_image_upload(?array $file): ?string { + if (!$file || ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) { + return null; + } + if ($file['error'] !== UPLOAD_ERR_OK) { + throw new RuntimeException('Upload-Fehler (Code '.$file['error'].')'); + } + $allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp']; + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $file['tmp_name']); + finfo_close($finfo); + if (!isset($allowed[$mime])) { + throw new RuntimeException('Nur JPG/PNG/WEBP erlaubt.'); + } + $ext = $allowed[$mime]; + $basename = 'event_'.date('Ymd_His').'_' . bin2hex(random_bytes(4)) . '.' . $ext; + $dest = rtrim(UPLOAD_DIR, '/').'/'.$basename; + if (!move_uploaded_file($file['tmp_name'], $dest)) { + throw new RuntimeException('Konnte Datei nicht speichern.'); + } + return '/uploads/'.$basename; // Pfad relativ zur Webroot +} + +function render_event_jsonld_graph(array $events): string { + $graph = []; + foreach ($events as $e) { + $start = $e['date'] . 'T' . ($e['time'] ?? '19:30') . ':00+02:00'; // Berlin Zeit im Sommer; vereinfacht + $price = $e['price'] ?? ''; + $offers = $price !== '' ? [ + "@type" => "Offer", + "price" => preg_replace('/[^\d,\.]/', '', $price), + "priceCurrency" => "EUR", + "availability" => "https://schema.org/InStock" + ] : null; + + $graph[] = array_filter([ + "@type" => "Event", + "name" => $e['title'], + "startDate" => $start, + "eventStatus" => "https://schema.org/EventScheduled", + "eventAttendanceMode" => "https://schema.org/OfflineEventAttendanceMode", + "location" => [ + "@type" => "Place", + "name" => $e['venue_name'] ?? "Veranstaltungsort", + "address" => [ + "@type" => "PostalAddress", + "streetAddress" => $e['venue_address'] ?? "", + "addressLocality" => "Walschleben", + "postalCode" => "99189", + "addressCountry" => "DE" + ], + ], + "image" => isset($e['image']) && $e['image'] !== '' ? [ BASE_URL . $e['image'] ] : [ BASE_URL . DEFAULT_OG_IMAGE ], + "description" => $e['description'] ?? "", + "organizer" => [ + "@type" => "Organization", + "name" => SITE_NAME, + "url" => BASE_URL + ], + "offers" => $offers + ]); + } + return json_encode([ + "@context" => "https://schema.org", + "@graph" => $graph + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); +} + +function h(?string $s): string { + return htmlspecialchars($s ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); +} + +/** + * Erkennt, ob HTML-Tags enthalten sind. + */ +function has_html_tags(string $s): bool { + return (bool) preg_match('/<\s*\w+[^>]*>/', $s); +} + +/** + * Erlaubte, sichere HTML-Ausgabe (Whitelist + Attribut-Filter). + * Erlaubt: p, br, ul/ol/li, b/strong, i/em, u, a, blockquote, h3-h5 + * Entfernt: on* Handler, javascript: URLs, style/iframe/script usw. + * Fügt bei Links rel="noopener nofollow ugc" hinzu und erlaubt nur http(s). + */ +function render_rich_text(string $input): string { + $input = trim($input); + + // Fall A: Kein HTML -> wir schützen & konvertieren Zeilenumbrüche + if (!has_html_tags($input)) { + return nl2br(h($input)); + } + + // Fall B: HTML vorhanden -> nur erlaubte Tags behalten + $allowed = '