From 3b4b5cad3e3255c0fa78ae34f1d3b6c0caf2ab8f Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Thu, 25 Dec 2025 01:50:07 +0100 Subject: [PATCH] commit --- config/prod/db.php | 10 +- config/staging/db.php | 12 +- partials/landing/main/home.php | 205 +++++++++++++++--- partials/structure/layout_start.php | 24 ++- partials/structure/nav.php | 44 ++++ public/assets/css/app.css | 131 +++++++++++- public/assets/css/styles.css | 21 +- public/assets/js/app.js | 207 +++++++++++++++++- public/page/index.php | 313 +--------------------------- schema.sql | 142 +++++++++++++ src/App/I18n.php | 6 +- 11 files changed, 743 insertions(+), 372 deletions(-) create mode 100644 partials/structure/nav.php mode change 100755 => 100644 public/page/index.php create mode 100644 schema.sql diff --git a/config/prod/db.php b/config/prod/db.php index 64d78b4..ae19cb8 100644 --- a/config/prod/db.php +++ b/config/prod/db.php @@ -12,8 +12,8 @@ declare(strict_types=1); // ------------------------------------------------------------ // 1) Driver selection (choose one) // ------------------------------------------------------------ -$driver = 'pgsql'; -// $driver = 'mysql'; +//$driver = 'pgsql'; + $driver = 'mysql'; // $driver = 'sqlite'; // ------------------------------------------------------------ @@ -44,14 +44,14 @@ $mysql = [ 'driver' => 'mysql', 'host' => 'localhost', 'port' => 3306, - 'dbname' => 'mydb', + 'dbname' => 'd0444c21', 'charset' => 'utf8mb4', // Alternative to host/port: // 'unix_socket' => '/var/run/mysqld/mysqld.sock', - 'user' => 'myuser', - 'password' => 'secret', + 'user' => 'd0444c21', + 'password' => 'Rnßü7ROxFTxmOazLNhz/', 'options' => [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, diff --git a/config/staging/db.php b/config/staging/db.php index a7ba632..754c9c8 100644 --- a/config/staging/db.php +++ b/config/staging/db.php @@ -12,8 +12,8 @@ declare(strict_types=1); // ------------------------------------------------------------ // 1) Driver selection (choose one) // ------------------------------------------------------------ -$driver = 'pgsql'; -// $driver = 'mysql'; +//$driver = 'pgsql'; + $driver = 'mysql'; // $driver = 'sqlite'; // ------------------------------------------------------------ @@ -31,7 +31,7 @@ $pgsql = [ 'schema' => 'public', 'user' => 'd0444c25', - 'password' => '/7ü9+§ÄfkiQvGPr§2Op7', + 'password' =>secret '/7ü9+§ÄfkiQvGPr§2Op7', 'options' => [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, @@ -44,14 +44,14 @@ $mysql = [ 'driver' => 'mysql', 'host' => 'localhost', 'port' => 3306, - 'dbname' => 'mydb', + 'dbname' => 'd0444c25', 'charset' => 'utf8mb4', // Alternative to host/port: // 'unix_socket' => '/var/run/mysqld/mysqld.sock', - 'user' => 'myuser', - 'password' => 'secret', + 'user' => 'd0444c25', + 'password' => '/7ü9+§ÄfkiQvGPr§2Op7', 'options' => [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, diff --git a/partials/landing/main/home.php b/partials/landing/main/home.php index 9521e30..348962b 100644 --- a/partials/landing/main/home.php +++ b/partials/landing/main/home.php @@ -1,39 +1,190 @@ assets()->addStyle('/assets/app.css', 'early'); -$app->assets()->addScript('/assets/app.js', 'footer', true); - $flash = $app->flash()->get(); ?> -
-
env: config()->env, ENT_QUOTES) ?>
-

- -

- +
-
+
:
-
-
-

Runtime

-
Current URL: request()->currentUrl(), ENT_QUOTES) ?>
-
Client-ID:
+
+
+
+

Gemeinsam stark

+

Treffen für Väter – mit und ohne Kinder. Lokal, sicher, verschlüsselt.

+

Finde andere Väter in deiner Nähe, plane Events oder tritt bestehenden Treffen bei. Daten bleiben geschützt, du entscheidest die Sichtbarkeit.

+
+ + +
+
+
Verschlüsselte Profile
+
Kinderinfos separat freigeben
+
Events nur für Members erstellen
+
+
+
+
Schnellsuche
+

Events in deiner Region

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

Geodaten werden nur zur Anzeige verwendet.

+
-
-

Actions

-
- -
-

Flash uses SessionManager, no direct globals.

+
+ +
+
+
+

Termine entdecken

+

Upcoming Events in deiner Nähe

+

Gäste sehen nur Basisinfos. Als Mitglied siehst du vollständige Details, kannst zusagen und neue Treffen anlegen.

+
+
+ + + + + +
-
-
+
+
+
+
+ Nur für eingeloggte Mitglieder +

Volle Beschreibung, Kontakt, Kinder-Infos und Anmeldeoptionen.

+
+ +
+
+ + +
+
+
+

Profil & Datenschutz

+

Du entscheidest, was sichtbar ist.

+

Papa-Daten und Kinder-Daten sind getrennt. Sichtbarkeiten steuerst du per Schalter, sensible Felder werden verschlüsselt abgelegt.

+
+
+

Papa-Sichtbarkeit

+
    +
  • basic: Name, Region, Kinderanzahl
  • +
  • papa: + Beruf, Sprachen, About
  • +
  • papa_contact: + Telefon/Email
  • +
+
+
+

Kinder separat

+
    +
  • hidden: keine Details
  • +
  • age_only: nur Alter/Altersgruppe
  • +
  • details: Vorname, Geschlecht, Alter
  • +
+
+
+
+ Verschlüsselung mit libsodium + Nonce pro Feld + Key aus ENV +
+
+
+
Sicherheit
+

Wie wird gespeichert?

+
    +
  • Symmetrische Verschlüsselung (XChaCha20-Poly1305)
  • +
  • Key nur im Backend (ENV), nicht in der DB
  • +
  • Felder: Kontakt, Beruf, Sprachen, About, Kinder
  • +
  • Events unterscheiden Public vs. Members-Details
  • +
+

Bei Datenabzug bleiben Felder unlesbar. Zugriff nur mit Key.

+
+
+
+ +
+
+
+

Ablauf

+

So funktioniert’s

+
+
+
+
+
1
+

Profil anlegen

+

Papa-Daten ausfüllen, Kinder optional. Sichtbarkeit pro Bereich einstellen.

+
+
+
2
+

Events finden

+

Suche nach Thema, Alter oder Region. Gäste sehen nur Basisinfos.

+
+
+
3
+

Treffen planen

+

Als Mitglied neue Events anlegen, andere einladen, Teilnahme verwalten.

+
+
+
+ +
+
+
+

Noch Fragen?

+

FAQ

+
+
+ Wer sieht meine Daten? +

Du stellst die Sichtbarkeit: basic/papa/papa_contact für Papa-Daten, Kinder separat (hidden/age_only/details). Gäste sehen nur rudimentäre Event-Infos.

+
+
+ Wie funktionieren Events? +

Eingeloggt kannst du Events erstellen. Andere Mitglieder melden sich an. Gäste sehen nur Teaser, Ort grob und Datum.

+
+
+ Welche Daten sind verschlüsselt? +

Kontakt, Beruf, Sprachen, About, Kinder-Vornamen/Geschlecht/Notizen werden app-seitig verschlüsselt gespeichert.

+
+
+
+
+
Bereit?
+

Jetzt loslegen

+

Registriere dich kostenlos, lege dein Profil an und vernetze dich mit anderen Vätern.

+
+ + +
+
+
+
+
diff --git a/partials/structure/layout_start.php b/partials/structure/layout_start.php index 7d419fc..abcece6 100644 --- a/partials/structure/layout_start.php +++ b/partials/structure/layout_start.php @@ -1,22 +1,26 @@ assets()->addStyle('/assets/css/styles.css', 'early'); +$app->assets()->addStyle('/assets/css/app.css', 'normal'); +$app->assets()->addScript('/assets/js/app.js', 'footer', true, false); + +$childGender = $_SESSION['child_gender_summary'] ?? ''; // 'male' | 'female' | 'mixed' | '' +if (!in_array($childGender, ['male', 'female', 'mixed'], true)) { + $childGender = ''; +} ?> - + <?= htmlspecialchars(t('common.title'), ENT_QUOTES) ?> + - - + + diff --git a/partials/structure/nav.php b/partials/structure/nav.php new file mode 100644 index 0000000..e857454 --- /dev/null +++ b/partials/structure/nav.php @@ -0,0 +1,44 @@ + + diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 1867f66..dbba863 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -1 +1,130 @@ -/* minimal css placeholder */ +body { + margin: 0; + font-family: "Manrope", "Segoe UI", system-ui, -apple-system, sans-serif; + background: var(--color-bg); + color: var(--color-text); +} + +.site-header { + position: sticky; + top: 0; + z-index: 50; + background: rgba(255, 255, 255, 0.95); + border-bottom: 1px solid var(--color-border); + backdrop-filter: blur(8px); +} + +.nav-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 14px 0; +} + +.brand { + display: flex; + align-items: center; + gap: 10px; +} + +.brand__logo { width: 46px; height: 46px; object-fit: contain; } +.brand__text { display: flex; flex-direction: column; line-height: 1.1; } +.brand__name { font-weight: 700; letter-spacing: -0.3px; } +.brand__tag { color: var(--color-muted); font-size: 13px; } + +.nav-links { + display: flex; + align-items: center; + gap: 18px; + font-weight: 600; +} +.nav-links a { color: var(--color-text); } +.nav-links a:hover { color: var(--color-primary); } + +.nav-actions { display: flex; align-items: center; gap: 10px; } +.menu-toggle { display: none; background: transparent; border: 1px solid var(--color-border); padding: 8px 10px; border-radius: var(--radius-sm); } +.mobile-menu { display: none; padding: 12px 16px; border-top: 1px solid var(--color-border); background: #fff; } +.mobile-menu a, .mobile-menu button { display: block; width: 100%; text-align: left; margin-bottom: 10px; } + +@media (max-width: 900px){ + .nav-links { display: none; } + .menu-toggle { display: inline-flex; } + .nav-actions .btn { display: none; } + .mobile-menu.open { display: block; } +} + +.hero { + padding: 64px 0; + background: linear-gradient(135deg, #fdf6e6, #f1e8de); + border-bottom: 1px solid var(--color-border); +} +.hero__grid { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 32px; align-items: center; } +.hero__text h1 { margin: 12px 0 10px 0; font-size: clamp(28px, 4vw, 42px); line-height: 1.1; } +.hero__text .lede { color: var(--color-muted); font-size: 17px; } +.hero__actions { display:flex; gap: 12px; flex-wrap: wrap; margin: 18px 0; } +.hero__meta { display:flex; gap:8px; flex-wrap: wrap; } +.hero__card { padding: 20px; background: #fff; border:1px solid var(--color-border); box-shadow: var(--shadow-card); } + +.eyebrow { text-transform: uppercase; letter-spacing: 1px; font-size: 12px; color: var(--color-muted); margin: 0; } +.lede { margin: 0; } +.muted.small { font-size: 13px; } + +.section { padding: 64px 0; } +.section.alt { background: #ffffff; border-block: 1px solid var(--color-border); } +.section__head { display:flex; justify-content:space-between; align-items:flex-start; gap: 16px; flex-wrap: wrap; } + +.split { display:grid; grid-template-columns: 1.1fr 0.9fr; gap: 24px; align-items: start; } +@media (max-width: 960px){ .split, .hero__grid { grid-template-columns: 1fr; } } + +.card { background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 18px; box-shadow: var(--shadow-card); } +.badge { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; background: var(--color-accent-soft); color: var(--color-highlight); border-radius: 999px; font-weight: 700; font-size: 12px; letter-spacing: .3px; } + +.hero__card .form-row { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; } + +.pill-row { display:flex; flex-wrap: wrap; gap: 8px; } +.pill { display:inline-flex; align-items:center; gap:6px; padding: 6px 10px; border-radius: 999px; border: 1px solid var(--color-border); background:#fff; font-size: 13px; } + +.list { margin: 0; padding-left: 18px; display: grid; gap: 8px; } +.list li { color: var(--color-text); } + +.privacy-card { background: linear-gradient(145deg, #ffffff, #f7f0e6); } + +.step { text-align: left; } +.step__icon { width: 34px; height:34px; border-radius: 10px; background: var(--color-accent); color: #fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-bottom: 8px; } + +.faq details { border:1px solid var(--color-border); border-radius: var(--radius-sm); padding: 10px 12px; background: #fff; } +.faq summary { cursor:pointer; font-weight: 600; } +.faq p { margin: 8px 0 0 0; color: var(--color-muted); } + +.toast-bar { max-width: var(--maxw); margin: 16px auto; background: #fff; border:1px solid var(--color-border); border-radius: var(--radius-md); padding: 12px 16px; box-shadow: var(--shadow-card); } + +.btn { background: var(--color-primary); color: var(--color-primary-contrast); } +.btn:hover { transform: translateY(-1px); transition: transform 120ms ease; } +.btn.ghost { border:1px solid var(--color-border); color: var(--color-text); background: #fff; } + +.chip.inline { padding: 6px 10px; } + +.results .card { padding: 0; } +.event__body { padding: 16px; display:grid; gap:8px; } +.event__meta { display:flex; gap: 12px; flex-wrap: wrap; font-size: 13px; color: var(--color-muted); } +.event__tags { display:flex; gap: 6px; flex-wrap: wrap; } +.event__access { font-size: 12px; color: var(--color-highlight); font-weight: 700; text-transform: uppercase; letter-spacing: .5px; } + +.label { font-size: 13px; color: var(--color-muted); } +.input, .select { border-radius: var(--radius-sm); border-color: var(--color-border); background: #fff; } +.input:focus, .select:focus { border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(52,72,90,0.14); } + +.cta-card { background: linear-gradient(135deg, #fdf4e0, #ffffff); } + +.flex { display:flex; } +.between { justify-content: space-between; } +.center-y { align-items: center; } +.gap-12 { gap: 12px; } +.stack { display:flex; flex-direction: column; } + +@media (max-width: 720px){ + .nav-row { padding: 12px 0; } + .hero { padding: 40px 0; } + .section { padding: 48px 0; } +} diff --git a/public/assets/css/styles.css b/public/assets/css/styles.css index 8fe6cb7..4b2dd98 100755 --- a/public/assets/css/styles.css +++ b/public/assets/css/styles.css @@ -21,20 +21,21 @@ input, textarea, select { font: inherit; color: inherit; } /* 2) Tokens */ :root { - --color-bg: #f8fafc; /* primary soft */ + --color-bg: #f7f2ea; --color-surface: #ffffff; - --color-border: #e5e7eb; /* slate-200 */ - --color-muted: #64748b; /* slate-500 */ - --color-text: #0f172a; /* slate-900 */ - --color-primary: #111827; /* neutral-900 */ + --color-border: #e5d8c1; + --color-muted: #5f6b7a; + --color-text: #26323f; + --color-primary: #34485a; /* Basisfarbe */ --color-primary-contrast: #ffffff; - --color-accent: #f59e0b; /* amber-500 */ - --color-accent-soft: #fff7ed; /* amber-50 */ + --color-accent: #e9b049; /* warmes Highlight */ + --color-accent-soft: #fdf4e0; + --color-highlight: #99433f; /* Akzent 2 */ --radius-sm: 10px; --radius-md: 14px; --radius-lg: 18px; - --shadow-card: 0 1px 2px rgba(0,0,0,0.04), 0 6px 24px rgba(0,0,0,0.06); - --maxw: 1100px; + --shadow-card: 0 1px 2px rgba(0,0,0,0.04), 0 10px 32px rgba(0,0,0,0.08); + --maxw: 1180px; } /******************** 3) Utilities ********************/ @@ -58,7 +59,7 @@ input, textarea, select { font: inherit; color: inherit; } .clamp-3 { display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; } /******************** 4) Layout ********************/ -.header { position: sticky; top:0; z-index: 40; backdrop-filter: blur(4px); background: rgba(255,255,255,0.85); border-bottom:1px solid var(--color-border); } +.header { position: sticky; top:0; z-index: 40; backdrop-filter: blur(4px); background: rgba(255,255,255,0.9); border-bottom:1px solid var(--color-border); } .header .nav { display:flex; gap:24px; align-items:center; } .header .brand { display:flex; align-items:center; gap:8px; font-weight:600; font-size:18px; letter-spacing: -0.2px; } .header .menu-btn { display:none; border-radius: 10px; padding:8px; } diff --git a/public/assets/js/app.js b/public/assets/js/app.js index bbe8027..56219b3 100644 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -1 +1,206 @@ -console.log('mini example loaded'); +document.addEventListener('DOMContentLoaded', () => { + const body = document.body; + const isLoggedIn = body.dataset.auth === '1'; + const childGender = body.dataset.childGender || ''; + + // Logo-Logik + const pickLogo = (gender) => { + if (gender === 'female') return 'logo_female.png'; + if (gender === 'male') return 'logo_male.png'; + return Math.random() < 0.5 ? 'logo_female.png' : 'logo_male.png'; + }; + const chosenLogo = pickLogo(childGender); + document.querySelectorAll('[data-logo-img]').forEach(img => { + img.src = `/assets/bilder/${chosenLogo}`; + }); + + // Mobile Menü + const mobileMenu = document.getElementById('mobileMenu'); + document.querySelectorAll('.menu-toggle').forEach(btn => { + btn.addEventListener('click', () => { + mobileMenu?.classList.toggle('open'); + }); + }); + + // Scroll zu Events + const scrollBtn = document.getElementById('scrollToEvents'); + if (scrollBtn) { + scrollBtn.addEventListener('click', () => { + document.getElementById('events')?.scrollIntoView({ behavior: 'smooth' }); + }); + } + + // Demo-Events + const events = [ + { + id: 1, + title: 'Spielplatzrunde im Park', + teaser: 'Lockeres Treffen mit Kaffee, Sandspielzeug und Picknick.', + description: 'Wir treffen uns am großen Spielplatz im Kiez. Bringt Snacks mit, wir teilen. Für alle Altersstufen offen.', + city: 'Berlin', + zip: '10437', + region: 'Prenzlauer Berg', + topic: 'outdoor', + startsAt: '2025-08-10T10:00:00', + allowKids: true, + ageGroup: 'kids', + visibility: 'public', + contact: 'papa-berlin@example.com', + locationLabel: 'Mauerpark', + }, + { + id: 2, + title: 'Väter-Kaffee & Austausch', + teaser: 'Elternzeit, Job, Schlaf – wir reden über alles.', + description: 'Reservierter Tisch im Café. Fokus auf Austausch unter Vätern, Kinder optional.', + city: 'Hamburg', + zip: '22767', + region: 'Altona', + topic: 'kaffee', + startsAt: '2025-08-12T19:00:00', + allowKids: false, + ageGroup: '', + visibility: 'members', + contact: 'altona-dads@example.com', + locationLabel: 'Café Elbseite', + }, + { + id: 3, + title: 'Sport & Spiel im Park', + teaser: 'Ballspiele und leichtes Workout, Kinder toben mit.', + description: 'Wir bringen Bälle und Slackline. Warm-up, danach freies Spiel. Bitte Wasser mitbringen.', + city: 'München', + zip: '80804', + region: 'Schwabing', + topic: 'sport', + startsAt: '2025-08-15T11:00:00', + allowKids: true, + ageGroup: 'school', + visibility: 'public', + contact: 'schwabing-sport@example.com', + locationLabel: 'Luitpoldpark', + }, + { + id: 4, + title: 'Workshop: Erste Hilfe für Kids', + teaser: 'Erste Hilfe Basics speziell für Kinder-Unfälle.', + description: 'Zertifizierter Trainer, kleiner Materialbeitrag. Kinder können mitgebracht werden.', + city: 'Köln', + zip: '50667', + region: 'Innenstadt', + topic: 'workshop', + startsAt: '2025-08-20T18:30:00', + allowKids: true, + ageGroup: 'kids', + visibility: 'members', + contact: 'koeln-workshop@example.com', + locationLabel: 'Familienzentrum Mitte', + }, + ]; + + const state = { topic: 'all', age: '', region: '', query: '' }; + const el = { + list: document.getElementById('eventList'), + chips: document.querySelectorAll('[data-filter]'), + locInput: document.getElementById('locInput'), + topicSelect: document.getElementById('topicSelect'), + ageSelect: document.getElementById('ageSelect'), + btnSearch: document.getElementById('btnSearch'), + btnGeo: document.getElementById('btnGeo'), + }; + + const fmtDate = (iso) => { + const d = new Date(iso); + return d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }); + }; + + const tag = (label) => `${label}`; + + const renderCard = (item) => { + const guest = !isLoggedIn; + const access = item.visibility === 'members' && guest ? '
Nur für Mitglieder
' : ''; + const desc = guest ? `

Melde dich an, um die volle Beschreibung zu sehen.

` : `

${item.description}

`; + const contact = !guest ? `
Kontakt: ${item.contact}
` : ''; + const kids = item.allowKids ? 'Mit Kindern' : 'Ohne Kinder'; + const tags = [ + tag(item.topic === 'kaffee' ? 'Kaffee & Austausch' : item.topic.charAt(0).toUpperCase() + item.topic.slice(1)), + tag(kids), + item.ageGroup ? tag(`Alter: ${item.ageGroup}`) : '', + ].filter(Boolean).join(''); + + return ` +
+
+ ${access} +
+ ${fmtDate(item.startsAt)} + 📍 ${item.region || item.city} + ${item.visibility === 'public' ? 'Öffentlich' : 'Mitglieder'} +
+

${item.title}

+

${guest ? item.teaser : item.description.slice(0, 140) + '...'}

+ ${guest ? '' : desc} +
${tags}
+ ${contact} +
+ + ${!guest ? '' : ''} +
+
+
`; + }; + + const matchesFilter = (ev) => { + const topicOk = state.topic === 'all' || state.topic === ev.topic; + const ageOk = !state.age || ev.ageGroup === state.age || (state.age === 'baby' && ev.ageGroup === 'kids'); // simple fallback + const regionOk = !state.region || ev.region.toLowerCase().includes(state.region) || ev.city.toLowerCase().includes(state.region) || ev.zip.startsWith(state.region); + const queryOk = !state.query || (ev.title + ev.teaser + ev.description + ev.city + ev.region).toLowerCase().includes(state.query); + return topicOk && ageOk && regionOk && queryOk; + }; + + const renderEvents = () => { + if (!el.list) return; + const filtered = events.filter(matchesFilter); + el.list.innerHTML = filtered.map(renderCard).join('') || '

Keine Events gefunden.

'; + }; + + if (el.chips.length > 0) { + el.chips[0].classList.add('active'); + } + + el.chips.forEach(chip => { + chip.addEventListener('click', () => { + el.chips.forEach(c => c.classList.remove('active')); + chip.classList.add('active'); + state.topic = chip.dataset.filter; + renderEvents(); + }); + }); + + if (el.btnSearch) { + el.btnSearch.addEventListener('click', () => { + state.region = (el.locInput?.value || '').trim().toLowerCase(); + state.topic = el.topicSelect?.value || 'all'; + state.age = el.ageSelect?.value || ''; + renderEvents(); + }); + } + + if (el.btnGeo) { + el.btnGeo.addEventListener('click', () => { + if (!navigator.geolocation) { + alert('Geolocation wird nicht unterstützt.'); + return; + } + navigator.geolocation.getCurrentPosition( + () => { + state.region = 'nähe'; + renderEvents(); + }, + () => alert('Standort konnte nicht ermittelt werden.') + ); + }); + } + + renderEvents(); +}); diff --git a/public/page/index.php b/public/page/index.php old mode 100755 new mode 100644 index 3c934f3..d035499 --- a/public/page/index.php +++ b/public/page/index.php @@ -1,310 +1,5 @@ - - - - - - schwarzesbrett.online – Digitales Schwarzes Brett - - - - - - -
-
- - 📌 - schwarzesbrett.online - - - -
-
-
- Kategorien - Neueste - So funktioniert’s - -
-
-
+ -
-
-
-

Dein digitales Schwarzes Brett für deine Nachbarschaft

-

Inserate, Nachbarschaftshilfe & Veranstaltungen – lokal, übersichtlich und frei von Spam.

- -
- - -
-
-
-
-
- - - - - - -
-
-
-
-
- - -
-

Entdecke Kategorien

-
- - - - - - - -
-
- - -
-
-

Neueste Einträge

- -
-
-
- - -
-
-
-

Mach dein Anliegen sichtbar

-

Erstelle jetzt kostenlos einen Aushang. Optional mit Bild – läuft automatisch nach 30 Tagen aus.

-
- -
-
- - -
-

So funktioniert’s

-
-
-
1️⃣
-

Inserat erstellen

-

Titel, Beschreibung, Kategorie, Ort – optional ein Bild. Fertig.

-
-
-
2️⃣
-

Lokal sichtbar

-

Dein Aushang erscheint bei Leuten in der Nähe – ohne Algorithmus‑Chaos.

-
-
-
3️⃣
-

Kontakt aufnehmen

-

Direkt per E‑Mail oder Telefon – ohne Zwischenhändler.

-
-
-
- - - - - - - - -
Aushang veröffentlicht
- - - - +// Landing-Page über Partials laden +tpl('home', 'landing', 'main'); diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..57f01ae --- /dev/null +++ b/schema.sql @@ -0,0 +1,142 @@ +-- Papa-Kind-Treff – Basis-Schema (MySQL 8) +-- Hinweise: +-- - Passwörter mit Argon2id hashen. +-- - Sensible Felder werden app-seitig mit libsodium (XChaCha20-Poly1305) verschlüsselt +-- und als base64 in VARBINARY-Spalten abgelegt (nonce + cipher). +-- - share_level steuert Papa-Infos (basic, papa, papa_contact). +-- - children_visibility steuert Kinder-Infos separat (hidden, age_only, details). + +CREATE TABLE users ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + status ENUM('active','pending','blocked') DEFAULT 'active', + email_verified_at DATETIME NULL, + last_login_at DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE user_profiles ( + user_id BIGINT UNSIGNED PRIMARY KEY, + display_name VARCHAR(120) NOT NULL, + share_level ENUM('basic','papa','papa_contact') NOT NULL DEFAULT 'basic', + children_visibility ENUM('hidden','age_only','details') NOT NULL DEFAULT 'hidden', + zip CHAR(5) NULL, + city VARCHAR(120) NULL, + region VARCHAR(120) NULL, + lat DECIMAL(10,7) NULL, + lng DECIMAL(10,7) NULL, + contact_phone VARBINARY(512) NULL, + contact_email VARBINARY(512) NULL, + profession VARBINARY(512) NULL, + languages VARBINARY(1024) NULL, -- JSON verschlüsselt + about VARBINARY(2048) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_profile_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_profile_city (city), + INDEX idx_profile_region (region), + INDEX idx_profile_latlng (lat,lng) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE children ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + gender ENUM('male','female','diverse','unknown') NOT NULL DEFAULT 'unknown', + birthdate DATE NULL, + age_years TINYINT UNSIGNED NULL, + encrypted_first_name VARBINARY(512) NOT NULL, + note VARBINARY(1024) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_child_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_child_user (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE events ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + created_by BIGINT UNSIGNED NOT NULL, + title VARCHAR(200) NOT NULL, + teaser_public VARCHAR(280) NOT NULL, + description TEXT NOT NULL, + location_label VARCHAR(180) NULL, + zip CHAR(5) NULL, + city VARCHAR(120) NULL, + region VARCHAR(120) NULL, + lat DECIMAL(10,7) NULL, + lng DECIMAL(10,7) NULL, + starts_at DATETIME NOT NULL, + ends_at DATETIME NULL, + max_participants SMALLINT UNSIGNED NULL, + allow_kids TINYINT(1) NOT NULL DEFAULT 1, + min_child_age TINYINT UNSIGNED NULL, + max_child_age TINYINT UNSIGNED NULL, + visibility ENUM('public','members') NOT NULL DEFAULT 'public', + status ENUM('draft','published','cancelled') NOT NULL DEFAULT 'published', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_event_user FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_event_city (city), + INDEX idx_event_region (region), + INDEX idx_event_time (starts_at), + INDEX idx_event_latlng (lat,lng) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE event_participants ( + event_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + status ENUM('going','interested','waitlist','cancelled') NOT NULL DEFAULT 'going', + child_count TINYINT UNSIGNED NULL, + note VARBINARY(1024) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (event_id, user_id), + CONSTRAINT fk_ep_event FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE, + CONSTRAINT fk_ep_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Session-Handling (neutral, keine sensiblen Inhalte) +CREATE TABLE sessions ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + token_hash CHAR(64) NOT NULL UNIQUE, -- SHA-256 Hash des Session-Tokens + ip VARCHAR(45) NULL, + user_agent VARCHAR(255) NULL, + expires_at DATETIME NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_session_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_session_expires (expires_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Kurzlebige Tokens (Passwort-Reset, Magic-Login, E-Mail-Verify) +CREATE TABLE user_tokens ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + type ENUM('reset','verify','magic_login') NOT NULL, + token_hash CHAR(64) NOT NULL UNIQUE, + expires_at DATETIME NOT NULL, + used_at DATETIME NULL, + ip VARCHAR(45) NULL, + user_agent VARCHAR(255) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_ut_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_ut_expires (expires_at), + INDEX idx_ut_type (type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Audit-Log für wichtige Aktionen +CREATE TABLE audit_log ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NULL, -- NULL bei Gast-Ereignissen + action VARCHAR(100) NOT NULL, -- z.B. login, profile.update, event.create + target_type VARCHAR(50) NULL, -- z.B. user, event, child + target_id BIGINT UNSIGNED NULL, + metadata JSON NULL, -- nicht sensible Zusatzinfos + ip VARCHAR(45) NULL, + user_agent VARCHAR(255) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_audit_user (user_id), + INDEX idx_audit_action (action), + INDEX idx_audit_target (target_type, target_id), + INDEX idx_audit_created (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/App/I18n.php b/src/App/I18n.php index f03b1f9..44a9b60 100644 --- a/src/App/I18n.php +++ b/src/App/I18n.php @@ -13,11 +13,11 @@ final class I18n // Minimal example translations (normally load JSON/PHP arrays from disk) $this->fallback = [ 'common' => [ - 'title' => 'Mini Example Landingpage', - 'intro' => 'This is a tiny project showing a clean bootstrap.', + 'title' => 'Papa-Kind-Treff', + 'intro' => 'Väter vernetzen sich für Treffen mit und ohne Kinder.', ], 'cta' => [ - 'primary' => 'Continue', + 'primary' => 'Weiter', ], ];