Files
dogfather-universe/profil.html
T
DogFatherGit c940538b1d PWA-Installierbarkeit: Seite kann jetzt als App installiert werden (Desktop-Icon)
Recherche (developer.chrome.com, Lighthouse-Doku, Stand 2026): Chrome/Edge
zeigen "App installieren" bei gültigem Web App Manifest (name, icons inkl.
512x512, start_url, display:standalone) UND einem registrierten Service
Worker mit fetch()-Handler — reines Manifest reicht für den zuverlässigen
Install-Prompt nicht mehr aus.

- Neues manifest.json (Name, Theme-/Hintergrundfarbe passend zum dunklen
  Design, Icons 192x192 + 512x512 aus dem bestehenden Husky-Favicon erzeugt)
- Neuer sw.js: minimaler Service Worker, primär für die Installierbarkeit,
  cached nebenbei die wichtigsten Shell-Dateien (Network-first mit
  Cache-Fallback, kein Offline-Vollausbau)
- main.js: registriert den Service Worker nur bei http(s) (nie bei
  file://, damit die Seite laut Projektregel weiterhin per Doppelklick
  ohne Server funktioniert — stiller Fallback statt Konsolenfehler)
- Alle 28 HTML-Seiten: <link rel="manifest"> + <meta name="theme-color">
  im <head> ergänzt (identischer Ankerpunkt nach dem Favicon-Link geprüft
  und automatisiert eingefügt)

Installation für Dogi: Seite in Chrome/Edge öffnen → Symbol rechts in der
Adressleiste ("App installieren") oder Menü ⋮ → "DogFather Universe
installieren" → landet als eigenes Fenster + Icon auf Desktop/Startmenü.
2026-08-03 01:50:56 +02:00

307 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="de" data-theme="dogfather">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Profil — DOGFATHER UNIVERSE</title>
<meta name="description" content="Team Dogi — persönliche Vorstellung." />
<link rel="icon" type="image/png" href="assets/img/favicon.png" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#0b0d10" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="DOGFATHER UNIVERSE" />
<meta property="og:title" content="Profil — DOGFATHER UNIVERSE" />
<meta property="og:description" content="Team Dogi — persönliche Vorstellung." />
<meta property="og:image" content="https://REPLACE-WITH-YOUR-DOMAIN.tld/assets/img/og-cover.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="robots" content="noindex" /> <!-- generische URL, echte Team-Seiten verlinken hierher -->
<link rel="stylesheet" href="assets/css/main.css" />
<link rel="stylesheet" href="assets/css/theme-dogfather.css" />
<link rel="stylesheet" href="assets/css/theme-hasidog.css" />
<link rel="stylesheet" href="assets/css/theme-spicymedia.css" />
<style>
.profil-photo { aspect-ratio: 4/5; max-width: 420px; margin: 0 auto; }
.profil-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.profil-badge { display: none; margin-bottom: .6rem; }
.profil-role { color: var(--accent); font-weight: 600; margin-bottom: 1.1rem; }
.profil-bio { font-size: 1.05rem; line-height: 1.7; }
.profil-quote { display: none; font-style: italic; font-size: 1.05rem; margin-top: 1.2rem; color: var(--text-muted); }
.profil-social { margin-top: 1.4rem; }
@media (max-width: 760px) { .profil-photo { max-width: 320px; } }
/* Auf der Profilseite ist die Person das Wichtigste — kein scharfes
Weltmotiv-Banner mehr im Weg. Das Motiv bleibt nur noch als
weichgezeichnete Stimmung ganz im Hintergrund sichtbar (über
body.mit-hintergrund, sitzt fest hinter allem und nimmt keinen
eigenen Platz im Textfluss ein). Dadurch ist Fotos/Name/Bio der
Person direkt beim Öffnen der Seite sofort im Blick, ohne dass
erst ein Banner-Bild "weggescrollt" werden muss. */
.profil-hero.page-bg { padding: clamp(1.2rem, 2.5vw, 1.8rem) 0 clamp(.8rem, 2vw, 1.2rem); }
/* Ausführliche Vorstellung (bioHtml) — eigener, breiterer Lesebereich
unterhalb der kurzen Steckbrief-Zeile, statt in die schmale 2-Spalten-
Box gequetscht zu werden. Gedacht für Texte in beliebiger Länge. */
.profil-bio-rich {
max-width: 70ch;
margin: 0 auto;
font-size: 1.05rem;
line-height: 1.8;
}
.profil-bio-rich p { margin: 0 0 1.1rem; }
.profil-bio-rich p:last-child { margin-bottom: 0; }
.profil-bio-rich h3 {
margin: 2rem 0 .7rem;
font-family: var(--font-head);
font-size: 1.15rem;
color: var(--accent);
}
.profil-bio-rich h3:first-child { margin-top: 0; }
.profil-bio-rich strong { color: var(--text); }
/* Aufzählungen (z.B. Bananenstifts "Dabei helfe ich unter anderem
bei:"-Liste) wirkten ohne eigene Regeln optisch "fett", weil sie —
anders als <p> — keine gedimmte Textfarbe geerbt hatten und dadurch
heller als der umgebende Fließtext wirkten. Jetzt gleiche Farbe/
Zeilenhöhe wie normaler Text, plus echte Aufzählungspunkte und
sauberer Abstand zum nächsten Absatz. */
.profil-bio-rich ul { margin: 0 0 1.1rem; padding-left: 1.4rem; }
.profil-bio-rich li {
margin-bottom: .5rem;
color: var(--text-muted);
list-style: disc;
}
.profil-bio-rich li:last-child { margin-bottom: 0; }
.profil-bio-rich li::marker { color: var(--accent); }
</style>
</head>
<body class="mit-hintergrund">
<div id="site-header"></div>
<main>
<section class="hero page-bg profil-hero">
<div class="container">
<span class="eyebrow" id="profil-eyebrow">🩵 Team Dogi</span>
<h1 id="profil-hero-name" data-i18n="pr_loading">Profil</h1>
<p class="lead worlds" id="profil-hero-role"></p>
</div>
</section>
<div id="profil-root">
<section class="section-tight">
<div class="container grid grid-2" style="align-items:center;gap:2.6rem;">
<div class="collage-photo profil-photo" id="profil-photo-frame" style="border-radius:var(--radius);">
<img id="profil-img" src="" alt="" loading="lazy" />
</div>
<div>
<span class="badge rank-1 profil-badge" id="profil-badge"></span>
<h2 id="profil-name" style="margin-top:0;"></h2>
<div class="profil-role" id="profil-role"></div>
<p class="profil-bio" id="profil-bio"></p>
<p class="profil-quote" id="profil-quote"></p>
<div class="btn-row profil-social" id="profil-social"></div>
</div>
</div>
</section>
<!-- Ausführliche Vorstellung, nur befüllt/sichtbar wenn person.bioHtml gesetzt ist -->
<section class="section-tight" id="profil-bio-rich-section" style="display:none;">
<div class="container">
<div class="profil-bio-rich" id="profil-bio-rich"></div>
</div>
</section>
<section>
<div class="container text-center">
<a class="btn btn-outline" id="profil-back" href="index.html">← Zurück</a>
</div>
</section>
</div>
</main>
<div id="site-footer"></div>
<script src="assets/js/data-modis.js"></script>
<script src="assets/js/data-scouts.js"></script>
<script src="assets/js/data-creator.js"></script>
<script src="assets/js/i18n-profil.js"></script>
<script src="assets/js/main.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const slug = new URLSearchParams(location.search).get("p") || "";
const modis = window.MODIS || [];
const scouts = window.SCOUTS || [];
const creators = window.CREATORS || [];
const alle = [...modis, ...scouts, ...creators];
const person = alle.find((p) => p.slug === slug);
// Für alle Rahmentexte, die rein per JS gesetzt werden (Name/Rolle/Bio
// der Person selbst kommen unverändert aus data-modis.js/data-scouts.js
// und werden NICHT übersetzt) — Wörterbuch kommt aus assets/js/i18n-profil.js.
const t = (schluessel) => (window.dogiUebersetzen ? window.dogiUebersetzen(schluessel) : schluessel);
if (!person) {
// Text kommt (auch bei Sprachwechsel) aus dem seiteneigenen
// i18n-Wörterbuch statt aus data-i18n, weil er per JS gesetzt wird.
const renderNichtGefunden = () => {
document.getElementById("profil-root").innerHTML = `
<section class="section-tight">
<div class="container text-center">
<h2>${t("pr_notfound_title")}</h2>
<p class="lead">${t("pr_notfound_lead")}</p>
<div class="btn-row" style="justify-content:center;">
<a class="btn btn-primary" href="team-modis.html">${t("pr_notfound_btn_modis")}</a>
<a class="btn btn-outline" href="team-scouts.html">${t("pr_notfound_btn_scouts")}</a>
</div>
</div>
</section>`;
};
renderNichtGefunden();
document.addEventListener("dogi-sprache-geaendert", renderNichtGefunden);
return;
}
const istScout = scouts.includes(person);
const istCreator = creators.includes(person);
const welt = istScout || istCreator ? "spicymedia" : "dogfather";
document.documentElement.setAttribute("data-theme", welt);
// Passendes Weltmotiv nur noch als weichgezeichnete Stimmung ganz im
// Hintergrund (body.mit-hintergrund) — bewusst KEIN scharfes Banner-Bild
// mehr oben im Kopfbereich, damit das Profil der Person (Foto/Name/Bio)
// sofort beim Öffnen der Seite im Fokus steht, ohne Banner davor.
const bgBasis = istScout || istCreator ? "bg-scouts" : "bg-dogfather";
document.body.style.setProperty("--page-bg", `url('/assets/img/${bgBasis}.jpg')`);
document.body.style.setProperty("--page-bg-mobile", `url('/assets/img/${bgBasis}-mobile.jpg')`);
document.title = `${person.name} — DOGFATHER UNIVERSE`;
// Personendaten-Felder (role/bio/bioHtml/intro/quote/rankLabel) sind
// entweder weiterhin reine Strings (z.B. leere Zitate) oder mehrsprachige
// Objekte { de, "de-CH", en, fr, pt } — siehe window.dogiFeld() in main.js.
const feld = (x) => (window.dogiFeld ? window.dogiFeld(x) : x || "");
// Für die Meta-Beschreibung reicht immer ein kurzer Textauszug, auch
// wenn die eigentliche Vorstellung als bioHtml (langer, formatierter
// Text) vorliegt statt als einfacher bio-String.
const metaDesc = document.querySelector('meta[name="description"]');
// Rahmenfarbe abwechslungsreich, aber für dieselbe Person immer gleich
const farben = ["frame-lila", "frame-silver", "frame-gold", "frame-tuerkis", ""];
const idx = [...person.slug].reduce((s, c) => s + c.charCodeAt(0), 0) % farben.length;
document.getElementById("profil-photo-frame").classList.add(...[farben[idx]].filter(Boolean));
const bild = person.photo || person.avatar;
const img = document.getElementById("profil-img");
img.src = bild;
img.alt = person.name;
img.addEventListener(
"error",
() => {
document.getElementById("profil-photo-frame").innerHTML =
`<div class="img-placeholder" style="position:absolute;inset:0;">${person.name}<br><span class="small">${window.dogiUebersetzen ? window.dogiUebersetzen("pr_foto_folgt") : "Foto folgt"}</span></div>`;
},
{ once: true }
);
document.getElementById("profil-name").textContent = person.name;
// Struktur-Sichtbarkeit hängt nur davon ab, OB ein Feld existiert (nicht
// von der Sprache) — daher einmalig hier, unabhängig von der reaktiven
// Textbefüllung in renderStatischeTexte() weiter unten.
if (person.bioHtml) {
if (person.intro) document.getElementById("profil-bio").style.display = "";
else document.getElementById("profil-bio").style.display = "none";
document.getElementById("profil-bio-rich-section").style.display = "";
}
if (person.rank) {
document.getElementById("profil-badge").style.display = "inline-block";
}
if (person.quote) {
document.getElementById("profil-quote").style.display = "block";
}
// Rahmentexte, die je nach Sprache und Team-Zugehörigkeit (Modi/Scout/
// Creator) unterschiedlich ausfallen — per JS statt data-i18n gesetzt,
// damit sie bei jedem Sprachwechsel (dogi-sprache-geaendert) sauber
// neu übersetzt werden. Auch die eigentlichen Personendaten (role/bio/
// bioHtml/intro/quote/rankLabel) werden hier über feld() aufgelöst, damit
// sie bei einem Sprachwechsel ebenfalls sofort aktualisiert werden.
const renderStatischeTexte = () => {
document.getElementById("profil-eyebrow").textContent = istScout
? t("pr_eyebrow_scout")
: istCreator
? t("pr_eyebrow_creator")
: t("pr_eyebrow_default");
document.getElementById("profil-hero-name").textContent = person.name;
document.getElementById("profil-hero-role").textContent = feld(person.role);
document.getElementById("profil-role").textContent = feld(person.role);
const introText = feld(person.intro);
const bioHtmlText = feld(person.bioHtml);
if (bioHtmlText) {
if (introText) document.getElementById("profil-bio").innerHTML = introText;
document.getElementById("profil-bio-rich").innerHTML = bioHtmlText;
} else {
document.getElementById("profil-bio").textContent =
feld(person.bio) || t("pr_bio_fallback").replace("{name}", person.name);
}
const kurzbeschreibung = feld(person.bio)
|| (bioHtmlText ? bioHtmlText.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 160) : "")
|| feld(person.role)
|| person.name;
if (metaDesc) metaDesc.setAttribute("content", kurzbeschreibung);
if (person.rank) {
document.getElementById("profil-badge").textContent = `${t("pr_badge_prefix")} ${feld(person.rankLabel) || person.rank}`;
}
const quoteText = feld(person.quote);
if (quoteText) {
document.getElementById("profil-quote").textContent = `${t("pr_quote_open")}${quoteText}${t("pr_quote_close")}`;
}
const zurueck = document.getElementById("profil-back");
if (istScout) {
zurueck.href = "team-scouts.html";
zurueck.textContent = t("pr_back_scouts");
} else if (istCreator) {
zurueck.href = "creator.html";
zurueck.textContent = t("pr_back_creator");
} else {
zurueck.href = "team-modis.html";
zurueck.textContent = t("pr_back_modis");
}
};
const socialLabels = { tiktok: "TikTok", instagram: "Instagram", spotify: "Spotify", discord: "Discord" };
const socialWrap = document.getElementById("profil-social");
Object.entries(person.social || {}).forEach(([plattform, url]) => {
if (!url) return;
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.rel = "noopener";
// TikTok-Links bekommen den eigenen Polarlicht-Button im TikTok-
// Farbschema (siehe .btn-tiktok in main.css) — bewusst NUR TikTok,
// alle anderen Plattformen (Instagram/Spotify/Discord) behalten den
// normalen Outline-Button-Stil.
if (plattform === "tiktok") {
a.className = "btn-tiktok";
const span = document.createElement("span");
span.textContent = socialLabels[plattform];
a.appendChild(span);
socialWrap.appendChild(a);
return;
}
a.className = "btn btn-outline";
a.textContent = socialLabels[plattform] || plattform;
socialWrap.appendChild(a);
});
renderStatischeTexte();
document.addEventListener("dogi-sprache-geaendert", renderStatischeTexte);
});
</script>
</body>
</html>