From b9e9a8666ef1c6f99a5867e5615e731c6c703b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20He=C3=9Fe?= Date: Sun, 16 Aug 2026 18:58:36 +0200 Subject: [PATCH] feat: add Discord contact links --- README.md | 6 +++--- server.ts | 2 +- src/components/AdminPanel.tsx | 6 +++++- src/components/PortfolioPage.tsx | 9 +++++++-- src/types.ts | 1 + tests/server.test.mjs | 2 ++ 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d9a0679..bd6309a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Technisch besteht die Anwendung aus einem React-/Tailwind-Frontend und einem Exp - Optionale serverseitige Gemini-Unterstützung für Klappentexte - Profilabhängige SEO-Metadaten, Open Graph, strukturierte Daten, Sitemap und `robots.txt` - Health-Endpunkte, persistente JSON-Datenhaltung und automatische Migrationsbackups -- optionale Kontaktsektion je Profil für E-Mail, Instagram und Threads +- optionale Kontaktsektion je Profil für E-Mail, Instagram, Threads und Discord - adressierbare und teilbare Buch-/Projektansichten - neutrale, nicht indexierbare 404-Seite ohne Hinweise auf andere Profile @@ -169,7 +169,7 @@ Die Buttons öffnen das Ziel in einem neuen Tab und werden automatisch mit den A ## Kontakt und Social Media -Jedes Profil kann unabhängig eine Kontakt-E-Mail-Adresse sowie Links zu Instagram und Threads erhalten. Der Abschnitt erscheint unterhalb des zusätzlichen Textmoduls und zeigt nur tatsächlich gepflegte Angaben. Es gibt bewusst kein serverseitiges Kontaktformular; dadurch entstehen weder Spam-Endpunkt noch zusätzliche gespeicherte Kontaktdaten. +Jedes Profil kann unabhängig eine Kontakt-E-Mail-Adresse sowie Links zu Instagram, Threads und Discord erhalten. Der Abschnitt erscheint unterhalb des zusätzlichen Textmoduls und zeigt nur tatsächlich gepflegte Angaben. Es gibt bewusst kein serverseitiges Kontaktformular; dadurch entstehen weder Spam-Endpunkt noch zusätzliche gespeicherte Kontaktdaten. ## Buchreihen, KDP-Links und Leseproben @@ -191,7 +191,7 @@ Weitere mögliche externe Verbindungen: - `GEMINI_API_KEY`: Der optionale Admin-Assistent sendet die eingegebenen Buchinformationen serverseitig an die Google-Gemini-API. - Kauf- und sonstige Markdown-Links: Erst ein Klick führt zur jeweiligen externen Website. -- Instagram-, Threads- und bewusst gepflegte Profilverlinkungen: Eine Verbindung entsteht erst beim Klick. +- Instagram-, Threads-, Discord- und bewusst gepflegte Profilverlinkungen: Eine Verbindung entsteht erst beim Klick. - Externe Bild-URLs: Das System unterstützt sie weiterhin, empfohlen werden jedoch lokal hochgeladene Bilder. Die Anwendung enthält kein Analytics- oder Tracking-System und lädt keine externen Webfonts. Sie verwendet für Besucher weder `localStorage` noch `sessionStorage`. Der Browser speichert lediglich das notwendige Admin-Sitzungscookie nach einer Anmeldung. Diese Punkte sowie das übliche Logging des Reverse Proxys/Hosters sollten passend zur tatsächlichen Installation in der Datenschutzerklärung beschrieben werden. diff --git a/server.ts b/server.ts index 3a45f7c..218e751 100644 --- a/server.ts +++ b/server.ts @@ -545,7 +545,7 @@ function validateProfile(profile: unknown): string | null { return "Mindestens eine Bild-URL ist ungültig."; } if (value.contactEmail && (value.contactEmail.length > 320 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.contactEmail))) return "Die Kontakt-E-Mail-Adresse ist ungültig."; - if (!isSafeContentUrl(value.instagramUrl) || !isSafeContentUrl(value.threadsUrl)) return "Mindestens ein Social-Media-Link ist ungültig."; + if (!isSafeContentUrl(value.instagramUrl) || !isSafeContentUrl(value.threadsUrl) || !isSafeContentUrl(value.discordUrl)) return "Mindestens ein Social-Media-Link ist ungültig."; if (value.fontFamily && !["sans", "serif", "mono"].includes(value.fontFamily)) return "Die Schriftart ist ungültig."; for (const project of value.projects) { if (!project || typeof project.id !== "string" || typeof project.title !== "string" || project.title.length > 500 || diff --git a/src/components/AdminPanel.tsx b/src/components/AdminPanel.tsx index c80cdf3..cbd85ac 100644 --- a/src/components/AdminPanel.tsx +++ b/src/components/AdminPanel.tsx @@ -666,7 +666,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {

Diese drei Kacheln erscheinen unter der Biografie. Sie können die Kategoriename (z.B. GENRE, TONALITÄT, FOKUS) und den zugehörigen Inhalt frei anpassen.

-
+
{/* Schlagwort 1 */}
@@ -874,6 +874,10 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { updateProfileField("threadsUrl", event.target.value)} placeholder="https://www.threads.net/@…" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm" />
+
+ + updateProfileField("discordUrl", event.target.value)} placeholder="https://discord.gg/…" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm" /> +
diff --git a/src/components/PortfolioPage.tsx b/src/components/PortfolioPage.tsx index 68d484a..d2fa7f7 100644 --- a/src/components/PortfolioPage.tsx +++ b/src/components/PortfolioPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import Markdown from "react-markdown"; import { Book, Project, AuthorProfile, LegalDocument, PortfolioTheme } from "../types"; -import { BookOpen, Star, HelpCircle, Award, Compass, ArrowRight, ExternalLink, Moon, Mail, Instagram, AtSign, Share2, FileText } from "lucide-react"; +import { BookOpen, Star, HelpCircle, Award, Compass, ArrowRight, ExternalLink, Moon, Mail, Instagram, AtSign, Share2, FileText, MessageCircle } from "lucide-react"; import SpotlightSection from "./SpotlightSection"; import LegalModal from "./LegalModal"; import ProjectDetailMedia from "./ProjectDetailMedia"; @@ -406,7 +406,7 @@ export default function PortfolioPage({ data, legalDocuments = [], theme }: Port )} - {(data.contactEmail || data.instagramUrl || data.threadsUrl) && ( + {(data.contactEmail || data.instagramUrl || data.threadsUrl || data.discordUrl) && (

Kontakt & Social Media

Weitere Neuigkeiten, Einblicke und Kontaktmöglichkeiten.

@@ -426,6 +426,11 @@ export default function PortfolioPage({ data, legalDocuments = [], theme }: Port Threads )} + {safeExternalUrl(data.discordUrl || "") && ( + + Discord + + )}
)} diff --git a/src/types.ts b/src/types.ts index b57f4c5..8155322 100644 --- a/src/types.ts +++ b/src/types.ts @@ -46,6 +46,7 @@ export interface AuthorProfile { contactEmail?: string; instagramUrl?: string; threadsUrl?: string; + discordUrl?: string; // Domain & Path Routing customDomain?: string; // e.g., "annieslone.de, annie-slone.de" diff --git a/tests/server.test.mjs b/tests/server.test.mjs index 3e60f1e..d0473ed 100644 --- a/tests/server.test.mjs +++ b/tests/server.test.mjs @@ -121,6 +121,7 @@ test("production hardening and public routing", async (t) => { const adminData = await response.json(); adminData.erotica.contactEmail = "kontakt@example.test"; adminData.erotica.instagramUrl = "https://instagram.com/example-author"; + adminData.erotica.discordUrl = "https://discord.gg/example-author"; adminData.erotica.books[0].seriesName = "Beispiel-Reihe"; adminData.erotica.books[0].seriesNumber = 1; adminData.erotica.books[0].ebookLink = "https://amazon.example/ebook"; @@ -166,6 +167,7 @@ test("production hardening and public routing", async (t) => { { label: "Zum befreundeten Pseudonym", url: "https://example-pseudonym.test/" }, ]); assert.equal(publicData.profile.contactEmail, "kontakt@example.test"); + assert.equal(publicData.profile.discordUrl, "https://discord.gg/example-author"); assert.equal(publicData.profile.books[0].seriesName, "Beispiel-Reihe"); assert.equal(publicData.profile.books[0].seriesNumber, 1); assert.equal(publicResponse.body.includes("Clara Finch"), false);