DOGFATHER UNIVERSE: erste lauffähige Website + Bewerbungs-Postfach + Hosting-Vorbereitung

This commit is contained in:
2026-07-30 20:09:09 +02:00
commit f8e4cf82db
45 changed files with 3462 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
/* =====================================================================
data-aktionen.js — Aktionen & Projekte-Datenobjekt
aktionen.html rendert daraus die Übersichtskarten, aktion-detail.html
liest per URL-Parameter (?id=slug) den passenden Eintrag aus und
zeigt die Detailansicht — so gibt es EIN Template für alle Aktionen,
ohne für jede neue Aktion eine neue HTML-Datei zu brauchen.
Felder:
id eindeutiger Slug (für aktion-detail.html?id=...)
title, summary, description (Array von Absätzen)
status "geplant" | "läuft" | "abgeschlossen"
tag kurzer Kategorie-Text fürs Badge
photo Pfad zu assets/img/... oder "" für Platzhalter
===================================================================== */
const AKTIONEN = [
{
id: "weihnachtsaktion-portugal",
title: "Weihnachtsaktion & Portugal-Trip",
tag: "Tierwohl",
status: "geplant",
summary: "Weihnachtsaktion für Tierheime in Portugal — Team Dogi hilft Tieren in Not.",
description: [
"Jedes Jahr organisiert Team Dogi eine Weihnachtsaktion zugunsten von Tierheimen in Portugal.",
// TODO: genaue Details (Spendenziel, Ablauf, Partner-Tierheime, Termin) ergänzen.
],
photo: "",
},
{
id: "community-treffen",
title: "Community-Treffen (Geburtstag)",
tag: "Community",
status: "geplant",
summary: "Ein persönliches Treffen mit der Community rund um Dogfathers Geburtstag.",
description: [
"Team Dogi ist Familie — deshalb gibt es ein reales Treffen mit der Community.",
// TODO: Ort, Datum, Anmeldeprozess ergänzen.
],
photo: "",
},
{
id: "vanvan-teddys",
title: "Kooperation: VanVan Teddys",
tag: "Merchandise",
status: "läuft",
summary: "Merchandise-Kooperation rund um VanVan — Teddys für die Community.",
description: [
"Gemeinsam mit VanVan entstehen limitierte Teddys als Teil des Merchandise-Angebots.",
// TODO: Preis, Verfügbarkeit, Bestellweg ergänzen.
],
photo: "",
},
{
id: "online-shop",
title: "Online-Shop (in Planung)",
tag: "Merchandise",
status: "geplant",
summary: "Der eigene Online-Shop für Tassen, Decken, Popsockets, Schlüsselanhänger, Sticker & mehr.",
description: [
"Aktuell läuft der Merchandise-Vertrieb nur über private Einzelbestellungen.",
"Ziel: ein eigener Online-Shop, damit die Community jederzeit bestellen kann.",
// TODO: Launch-Termin, Plattform (z.B. Shopify) ergänzen.
],
photo: "",
},
// TODO: weitere Aktionen/Projekte hier nach gleichem Muster ergänzen.
];
if (typeof window !== "undefined") window.AKTIONEN = AKTIONEN;
+27
View File
@@ -0,0 +1,27 @@
/* =====================================================================
data-creator.js — Creator-Datenobjekt (Creator im Spicy-Media-Team)
TODO: Noch keine Creator-Profile final geklärt (siehe Masterplan-
Fragenkatalog, Block Creator/Team). Sobald Namen + Infos da sind,
einfach nach dem Muster unten Objekte ins Array einfügen —
creator.html liest automatisch aus und rendert Filter/Karten.
Felder:
name, category (z.B. "Gaming", "Lifestyle", "Comedy" ...),
role, photo, quote, social, status
===================================================================== */
const CREATORS = [
// Beispiel-Struktur (auskommentiert, zur Orientierung beim Befüllen):
// {
// name: "Name",
// category: "Comedy",
// role: "Creator bei Spicy Media",
// photo: "assets/img/creator-name.jpg",
// quote: "Kurzes Zitat",
// social: { tiktok: "https://tiktok.com/@handle" },
// status: "aktiv",
// },
];
if (typeof window !== "undefined") window.CREATORS = CREATORS;
+94
View File
@@ -0,0 +1,94 @@
/* =====================================================================
data-modis.js — Modi-/Core-Team-Datenobjekt
Hierarchie (wichtig, bitte bei Änderungen respektieren):
VanVan (rechte Hand, eigene Seite vanvan.html)
> Diene (rechte Hand von VanVan)
> Funny (Modi)
> Miss (Modi + Cutterin)
> Marina / Michi (Kommentare auf Clip-Accounts)
Feld "rank" steuert nur die Sortier-/Anzeigereihenfolge (1 = höchste
Stufe). VanVan wird auf team-modis.html separat hervorgehoben UND hat
eine eigene exklusive Seite (vanvan.html) — hier trotzdem gelistet,
damit die Hierarchie an einer Stelle vollständig bleibt.
Felder: name, role, rank, photo, quote, social, page (optionale eigene
Unterseite), status
===================================================================== */
const MODIS = [
{
name: "VanVan",
role: "Rechte Hand von Dogfather · Musik-Produzentin (Van-Van, Spotify)",
rank: 1,
photo: "",
quote: "", // TODO: Zitat von VanVan einholen
social: {},
page: "vanvan.html",
status: "aktiv",
},
{
name: "Diene",
role: "Organisiert & plant gemeinsam mit VanVan",
rank: 2,
photo: "",
quote: "",
social: {},
page: "",
status: "aktiv",
},
{
name: "Funny",
role: "Modi",
rank: 3,
photo: "",
quote: "",
social: {},
page: "",
status: "aktiv",
},
{
name: "Miss",
role: "Modi & Cutterin",
rank: 4,
photo: "",
quote: "",
social: {},
page: "",
status: "aktiv",
},
{
name: "Marina",
role: "Kommentare auf den Clip-Accounts",
rank: 5,
photo: "",
quote: "",
social: {},
page: "",
status: "aktiv",
},
{
name: "Michi",
role: "Kommentare auf den Clip-Accounts",
rank: 5,
photo: "",
quote: "",
social: {},
page: "",
status: "aktiv",
},
{
name: "Dan",
role: "Team Dogi — Rolle noch zu klären", // TODO: genaue Rolle von Dan im Team klären
rank: 6,
photo: "",
quote: "",
social: {},
page: "",
status: "aktiv",
},
// TODO: weitere Modis/Team-Mitglieder hier nach gleichem Muster ergänzen.
];
if (typeof window !== "undefined") window.MODIS = MODIS;
+42
View File
@@ -0,0 +1,42 @@
/* =====================================================================
data-scouts.js — Scout-Datenobjekt
Scouts arbeiten mit Dogfather als Manager zusammen (Spicy Media),
50/50 Gewinn-Sharing-Modell (Details siehe manager.html / TODO).
NEUEN SCOUT HINZUFÜGEN: einfach ein neues Objekt unten ins Array
kopieren und Felder ausfüllen. Kein Code-Umbau nötig — team-scouts.html
liest dieses Array automatisch aus.
Felder:
name Anzeigename (Groß-/Kleinschreibung korrekt!)
role Kurzrolle / Fokus
photo Pfad zu assets/img/... oder "" für Platzhalter-Avatar
quote kurzes Zitat/Motto (optional, "" wenn keins)
social { platform: "url", ... } optional
status "aktiv" | "neu" | "pausiert"
===================================================================== */
const SCOUTS = [
{
name: "Patrick",
role: "Scout — Team Dogi",
photo: "",
quote: "", // TODO: Zitat von Patrick einholen
social: {},
status: "aktiv",
},
{
name: "Bananenstift",
role: "Scout — Team Dogi",
photo: "",
quote: "", // TODO: Zitat von Bananenstift einholen
social: {},
status: "aktiv",
},
// TODO: Dogi liefert die vollständige, aktuelle Scout-Liste (Namen +
// Kurzrolle) per Text nach — hier weitere Objekte nach obigem Muster
// ergänzen. Struktur bleibt gleich, egal wie viele dazukommen.
];
// Für <script>-Einbindung ohne Modul-Bundler global verfügbar machen
if (typeof window !== "undefined") window.SCOUTS = SCOUTS;
+129
View File
@@ -0,0 +1,129 @@
/* =====================================================================
forms.js — Validierung + Versand für die drei Bewerbungsformulare
(Creator / Scout / Kooperation) auf bewerben.html
Versand-Logik:
1) Wenn API_BASE_URL gesetzt ist (Cloudflare-Worker-Postfach deployed,
siehe cloudflare-worker/README.md): Bewerbung wird per POST an
`${API_BASE_URL}/submit` gesendet und landet im code-geschützten
Postfach (postfach.html).
2) Solange API_BASE_URL leer ist ODER der Versand fehlschlägt (z.B.
offline, Worker noch nicht deployed): Fallback auf einen
vorausgefüllten "mailto:"-Link, wie bisher.
===================================================================== */
// TODO: nach dem Deploy des Cloudflare Workers hier die Worker-URL ohne
// abschließenden Slash eintragen, z.B.
// "https://dogfather-universe-postfach.DEIN-ACCOUNT.workers.dev"
const API_BASE_URL = "";
const CONTACT_EMAIL = "[email protected]"; // TODO: echte Kontakt-E-Mail eintragen
const FORM_TYPES = {
"form-creator": "creator",
"form-scout": "scout",
"form-kooperation": "kooperation",
};
function validateField(field) {
const wrapper = field.closest(".field");
if (!wrapper) return true;
let valid = field.checkValidity();
if (valid && field.type === "email" && field.value) {
valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(field.value);
}
wrapper.classList.toggle("has-error", !valid);
return valid;
}
function buildMailto(subjectLabel, data) {
const lines = [...data.entries()].map(([k, v]) => `${k}: ${v}`).join("\n");
return `mailto:${CONTACT_EMAIL}?subject=${encodeURIComponent(
"Bewerbung: " + subjectLabel
)}&body=${encodeURIComponent(lines)}`;
}
async function submitToWorker(type, data) {
if (!API_BASE_URL) return false;
const payload = { type, data: Object.fromEntries(data.entries()) };
try {
const res = await fetch(`${API_BASE_URL}/submit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) return false;
const json = await res.json();
return !!json.ok;
} catch {
return false; // z.B. offline oder Worker nicht erreichbar -> Fallback greift
}
}
function setupForm(formId, subjectLabel) {
const form = document.getElementById(formId);
if (!form) return;
const status = form.querySelector(".form-status");
const type = FORM_TYPES[formId];
form.addEventListener("submit", async (e) => {
e.preventDefault();
const fields = [...form.querySelectorAll("input[required], select[required], textarea[required], input[type=email]")];
const allValid = fields.map(validateField).every(Boolean);
if (!allValid) {
if (status) {
status.textContent = "Bitte prüfe die markierten Felder — es fehlt noch etwas.";
status.className = "form-status show error";
}
return;
}
const data = new FormData(form);
const submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) submitBtn.disabled = true;
const delivered = await submitToWorker(type, data);
if (status) {
if (delivered) {
status.innerHTML = `Danke, das sieht super aus! 🩵 Deine Bewerbung ist sicher im Postfach von Team Dogi angekommen.`;
status.className = "form-status show success";
} else {
const mailto = buildMailto(subjectLabel, data);
status.innerHTML = `Danke, das sieht super aus! 🩵 Solange noch kein automatischer Versand eingerichtet ist:
<a href="${mailto}" class="btn btn-outline" style="margin-top:.6rem;">E-Mail jetzt senden</a>`;
status.className = "form-status show success";
}
}
if (submitBtn) submitBtn.disabled = false;
form.reset();
});
form.querySelectorAll("input, select, textarea").forEach((field) => {
field.addEventListener("blur", () => validateField(field));
});
}
function setupTabs() {
const tabs = document.querySelectorAll(".tab-btn");
const panels = document.querySelectorAll(".tab-panel");
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
tabs.forEach((t) => t.classList.remove("active"));
panels.forEach((p) => p.classList.remove("active"));
tab.classList.add("active");
document.getElementById(tab.dataset.tab).classList.add("active");
});
});
}
document.addEventListener("DOMContentLoaded", () => {
setupTabs();
setupForm("form-creator", "Creator");
setupForm("form-scout", "Scout");
setupForm("form-kooperation", "Kooperation");
});
+93
View File
@@ -0,0 +1,93 @@
/* =====================================================================
gallery.js — filterbare Mediengalerie + Lightbox (für medien.html)
MEDIA-Array unten mit echten Bildern befüllen sobald verfügbar
(assets/img/...). Bis dahin rendert jeder Eintrag ohne "src" einen
Platzhalter-Block mit passendem Alt-Text, damit die Struktur schon
steht und nur noch Bilder eingehängt werden müssen.
===================================================================== */
const MEDIA = [
{ id: 1, category: "dogfather", alt: "Dogfather Profilbild", src: "" },
{ id: 2, category: "dogfather", alt: "Dogfather beim Stream", src: "" },
{ id: 3, category: "team", alt: "Team Dogi Gruppenbild", src: "" },
{ id: 4, category: "team", alt: "Patrick (Scout)", src: "" },
{ id: 5, category: "team", alt: "Bananenstift (Scout)", src: "" },
{ id: 6, category: "team", alt: "VanVan", src: "" },
{ id: 7, category: "hasidog", alt: "HasiDog Logo/Maskottchen", src: "" },
{ id: 8, category: "hasidog", alt: "Husky Casper", src: "" },
{ id: 9, category: "hasidog", alt: "Casper &amp; Kater Simba", src: "" },
{ id: 10, category: "manager", alt: "Spicy Media Logo", src: "" },
{ id: 11, category: "aktionen", alt: "Weihnachtsaktion Portugal", src: "" },
{ id: 12, category: "aktionen", alt: "VanVan Teddys Merch", src: "" },
// TODO: echte Bilder aus den vom User gelieferten Assets ergänzen
// (assets/img/... Pfad eintragen, dann wird automatisch das Bild
// statt des Platzhalters angezeigt).
];
function renderGalleryItem(item) {
const media = item.src
? `<img src="${item.src}" alt="${item.alt}" loading="lazy" />`
: `<div class="img-placeholder">${item.alt}<br><span class="small">Foto folgt</span></div>`;
return `
<figure class="gallery-item card" data-category="${item.category}" data-index="${item.id}">
${media}
<figcaption class="small muted" style="margin-top:.6rem;">${item.alt}</figcaption>
</figure>`;
}
function initGallery() {
const grid = document.getElementById("gallery-grid");
const filterBar = document.getElementById("gallery-filters");
if (!grid || !filterBar) return;
const categories = ["alle", ...new Set(MEDIA.map((m) => m.category))];
filterBar.innerHTML = categories
.map(
(c, i) =>
`<button class="filter-btn${i === 0 ? " active" : ""}" data-filter="${c}">${c[0].toUpperCase() + c.slice(1)}</button>`
)
.join("");
function draw(filter) {
const items = filter === "alle" ? MEDIA : MEDIA.filter((m) => m.category === filter);
grid.innerHTML = items.map(renderGalleryItem).join("") || `<p class="muted">Keine Bilder in dieser Kategorie.</p>`;
bindLightbox();
}
filterBar.addEventListener("click", (e) => {
const btn = e.target.closest(".filter-btn");
if (!btn) return;
filterBar.querySelectorAll(".filter-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
draw(btn.dataset.filter);
});
function bindLightbox() {
const lb = document.getElementById("lightbox");
const lbContent = document.getElementById("lightbox-content");
if (!lb || !lbContent) return;
grid.querySelectorAll(".gallery-item").forEach((el) => {
el.addEventListener("click", () => {
lbContent.innerHTML = el.innerHTML;
lb.classList.add("open");
});
});
}
draw("alle");
const lb = document.getElementById("lightbox");
if (lb) {
lb.addEventListener("click", (e) => {
if (e.target === lb || e.target.closest(".lightbox-close")) {
lb.classList.remove("open");
}
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") lb.classList.remove("open");
});
}
}
document.addEventListener("DOMContentLoaded", initGallery);
+193
View File
@@ -0,0 +1,193 @@
/* =====================================================================
DOGFATHER UNIVERSE — main.js
Gemeinsame Navigation, mobiles Menü, Footer, Nebel-Effekt.
Läuft rein clientseitig (kein Build-Tool, kein Fetch von Partials),
damit jede Seite auch direkt per Doppelklick (file://) funktioniert.
===================================================================== */
(function () {
"use strict";
/* ---------- Navigationsstruktur (an einer Stelle pflegen) ---------- */
const NAV = [
{ label: "Start", href: "index.html" },
{
label: "Dogfather",
children: [
{ label: "Die Person", href: "dogfather.html" },
{ label: "Streamer", href: "streamer.html" },
{ label: "Streamplan", href: "streamplan.html" },
{ label: "Zeitreise", href: "zeitreise.html" },
],
},
{
label: "HasiDog",
children: [
{ label: "HasiDog-Welt", href: "hasidog.html" },
{ label: "Kasper", href: "kasper.html" },
{ label: "Community", href: "community.html" },
{ label: "Stimmen", href: "stimmen.html" },
],
},
{
label: "Manager",
children: [
{ label: "Manager Dogfather", href: "manager.html" },
{ label: "Werte", href: "werte.html" },
{ label: "Scout-Team", href: "team-scouts.html" },
{ label: "Modi-Team", href: "team-modis.html" },
{ label: "VanVan", href: "vanvan.html" },
{ label: "Creator", href: "creator.html" },
{ label: "Bewerben", href: "bewerben.html" },
],
},
{
label: "Medien",
children: [
{ label: "Galerie", href: "medien.html" },
{ label: "Videos", href: "videos.html" },
{ label: "Aktionen", href: "aktionen.html" },
],
},
{ label: "Links", href: "links.html" },
];
const currentFile = (location.pathname.split("/").pop() || "index.html");
function isActive(href) {
return href === currentFile;
}
function renderNavItem(item) {
if (!item.children) {
return `<a class="nav-flat${isActive(item.href) ? " active" : ""}" href="${item.href}">${item.label}</a>`;
}
const activeChild = item.children.some((c) => isActive(c.href));
return `
<div class="nav-group${activeChild ? " open-hint" : ""}">
<button type="button" aria-expanded="false">${item.label} <span aria-hidden="true">▾</span></button>
<div class="nav-dropdown">
${item.children
.map(
(c) =>
`<a href="${c.href}"${isActive(c.href) ? ' class="active"' : ""}>${c.label}</a>`
)
.join("")}
</div>
</div>`;
}
function renderHeader() {
const el = document.getElementById("site-header");
if (!el) return;
el.innerHTML = `
<header class="site-header">
<div class="nav-bar">
<a class="brand" href="index.html">
<span class="heart">🩵</span>
<span>DOGFATHER UNIVERSE
<small>Streamer · Community · HasiDog · Manager</small>
</span>
</a>
<button class="nav-toggle" type="button" aria-label="Menü öffnen" aria-expanded="false">☰</button>
<nav class="nav-links">
${NAV.map(renderNavItem).join("")}
<a class="nav-cta" href="bewerben.html">Bewerben 🩵</a>
</nav>
</div>
</header>`;
const toggle = el.querySelector(".nav-toggle");
const links = el.querySelector(".nav-links");
toggle.addEventListener("click", () => {
const open = links.classList.toggle("open");
toggle.setAttribute("aria-expanded", String(open));
});
el.querySelectorAll(".nav-group > button").forEach((btn) => {
btn.addEventListener("click", () => {
const group = btn.parentElement;
const wasOpen = group.classList.contains("open");
el.querySelectorAll(".nav-group.open").forEach((g) => g.classList.remove("open"));
if (!wasOpen) group.classList.add("open");
btn.setAttribute("aria-expanded", String(!wasOpen));
});
});
document.addEventListener("click", (e) => {
if (!el.contains(e.target)) {
el.querySelectorAll(".nav-group.open").forEach((g) => g.classList.remove("open"));
}
});
}
function renderFooter() {
const el = document.getElementById("site-footer");
if (!el) return;
const year = new Date().getFullYear();
el.innerHTML = `
<footer class="site-footer">
<div class="container footer-grid">
<div>
<div class="brand"><span class="heart">🩵</span> DOGFATHER UNIVERSE</div>
<p class="small" style="margin-top:.8rem;max-width:32ch;">
Streamer. Community. HasiDog. Manager. Gemeinsam stark.
</p>
</div>
<div>
<h4>Dogfather</h4>
<ul>
<li><a href="dogfather.html">Die Person</a></li>
<li><a href="streamer.html">Streamer</a></li>
<li><a href="zeitreise.html">Zeitreise</a></li>
</ul>
</div>
<div>
<h4>HasiDog &amp; Manager</h4>
<ul>
<li><a href="hasidog.html">HasiDog-Welt</a></li>
<li><a href="manager.html">Manager Dogfather</a></li>
<li><a href="team-scouts.html">Scouts</a></li>
<li><a href="team-modis.html">Modis</a></li>
</ul>
</div>
<div>
<h4>Mehr</h4>
<ul>
<li><a href="links.html">Alle Links</a></li>
<li><a href="bewerben.html">Bewerben</a></li>
<li><a href="kontakt.html">Kontakt</a></li>
</ul>
</div>
</div>
<div class="container footer-bottom">
<span>© ${year} Dogfather Universe. Team Dogi ist Familie 🩵</span>
<span><a href="kontakt.html">Impressum &amp; Datenschutz</a> · <a href="postfach.html">Team-Postfach 🔒</a></span>
</div>
</footer>`;
}
function renderFog() {
if (document.querySelector(".fog-layer")) return;
const layer = document.createElement("div");
layer.className = "fog-layer";
layer.setAttribute("aria-hidden", "true");
const blobs = 6;
let html = "";
for (let i = 0; i < blobs; i++) {
const size = 220 + Math.round(Math.random() * 260);
const top = Math.round(Math.random() * 100);
const left = Math.round(Math.random() * 100);
const delay = (Math.random() * 10).toFixed(1);
html += `<span style="width:${size}px;height:${size}px;top:${top}%;left:${left}%;animation-delay:${delay}s;"></span>`;
}
layer.innerHTML = html;
document.body.prepend(layer);
}
document.addEventListener("DOMContentLoaded", () => {
renderHeader();
renderFooter();
renderFog();
});
})();