Key-Features now editable

Section headers editable

Added markdown to text fields
This commit is contained in:
Daniel Heße 2026-08-04 01:05:39 +02:00
parent 4f6a31dfa3
commit 302bdff052
8 changed files with 347 additions and 23 deletions

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { X, Lock, Save, RotateCcw, Download, Upload, Plus, Trash2, Check, Sparkles, Layout, BookOpen, Image as ImageIcon, Newspaper, BarChart3, KeyRound, AlertCircle } from "lucide-react";
import { X, Lock, Save, RotateCcw, Download, Upload, Plus, Trash2, Check, Sparkles, Layout, BookOpen, Image as ImageIcon, Newspaper, BarChart3, KeyRound, AlertCircle, FileText } from "lucide-react";
import { CMSData, StoryTab, ScreenshotItem, NewsPost, DevCategoryProgress, Milestone } from "../types";
// Helper function to process and compress uploaded images to sharp Base64 Data URLs
@ -170,7 +170,7 @@ export const AdminCMSModal: React.FC<AdminCMSModalProps> = ({
const [pinInput, setPinInput] = useState("");
const [pinError, setPinError] = useState(false);
const [activeTab, setActiveTab] = useState<"hero" | "story" | "gallery" | "news" | "progress" | "backup">("hero");
const [activeTab, setActiveTab] = useState<"hero" | "sections" | "story" | "gallery" | "news" | "progress" | "backup">("hero");
// Local draft state for editing - sync when cms updates
const [formData, setFormData] = useState<CMSData>(cms);
@ -460,6 +460,18 @@ export const AdminCMSModal: React.FC<AdminCMSModalProps> = ({
<span>Allgemein & Hero</span>
</button>
<button
onClick={() => setActiveTab("sections")}
className={`w-full flex items-center gap-2.5 px-3.5 py-2.5 rounded-lg font-serif text-xs font-semibold tracking-wider transition-colors text-left shrink-0 ${
activeTab === "sections"
? "bg-amber-900/60 text-amber-100 border border-amber-600/50"
: "text-zinc-400 hover:text-amber-200 hover:bg-zinc-900"
}`}
>
<FileText className="w-4 h-4 text-amber-400" />
<span>Sektionen & Überschriften</span>
</button>
<button
onClick={() => setActiveTab("story")}
className={`w-full flex items-center gap-2.5 px-3.5 py-2.5 rounded-lg font-serif text-xs font-semibold tracking-wider transition-colors text-left shrink-0 ${
@ -624,6 +636,146 @@ export const AdminCMSModal: React.FC<AdminCMSModalProps> = ({
</div>
)}
{/* TAB SECTIONS & OVERVIEW TITLES */}
{activeTab === "sections" && (
<div className="space-y-6">
<div className="border-b border-amber-900/40 pb-2">
<h3 className="text-lg font-serif font-bold text-amber-200">
Sektionen & Überschriften anpassen
</h3>
<p className="text-xs text-zinc-400 mt-1">
Hier kannst du alle Haupt-Überschriften und Kopfzeilen (Eyebrows) der einzelnen Bereiche der Webseite frei anpassen.
</p>
</div>
<div className="space-y-4">
{/* Story Section */}
<div className="bg-zinc-950 border border-zinc-800 rounded-lg p-4 space-y-3">
<h4 className="text-xs font-mono font-bold text-amber-400 uppercase">1. Story & Tabs Sektion</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-[11px] font-mono text-zinc-400 mb-1">Kopfzeile (Eyebrow)</label>
<input
type="text"
value={formData.sectionTitles?.storyEyebrow ?? "Projekt-Informationen"}
onChange={(e) => setFormData({
...formData,
sectionTitles: { ...(formData.sectionTitles || {}), storyEyebrow: e.target.value }
})}
className="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-100 focus:border-amber-500"
/>
</div>
<div>
<label className="block text-[11px] font-mono text-zinc-400 mb-1">Hauptüberschrift</label>
<input
type="text"
value={formData.sectionTitles?.storyTitle ?? "Die Welt, Features & Fraktionen"}
onChange={(e) => setFormData({
...formData,
sectionTitles: { ...(formData.sectionTitles || {}), storyTitle: e.target.value }
})}
className="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-100 focus:border-amber-500"
/>
</div>
</div>
</div>
{/* Gallery Section */}
<div className="bg-zinc-950 border border-zinc-800 rounded-lg p-4 space-y-3">
<h4 className="text-xs font-mono font-bold text-amber-400 uppercase">2. Screenshots & Galerie Sektion</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-[11px] font-mono text-zinc-400 mb-1">Kopfzeile (Eyebrow)</label>
<input
type="text"
value={formData.sectionTitles?.galleryEyebrow ?? "Impressionen & Ingame-Showcase"}
onChange={(e) => setFormData({
...formData,
sectionTitles: { ...(formData.sectionTitles || {}), galleryEyebrow: e.target.value }
})}
className="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-100 focus:border-amber-500"
/>
</div>
<div>
<label className="block text-[11px] font-mono text-zinc-400 mb-1">Hauptüberschrift (z.B. Screenshots aus ...)</label>
<input
type="text"
value={formData.sectionTitles?.galleryTitle ?? "Screenshots aus Khorinis"}
onChange={(e) => setFormData({
...formData,
sectionTitles: { ...(formData.sectionTitles || {}), galleryTitle: e.target.value }
})}
className="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-100 focus:border-amber-500"
/>
</div>
</div>
</div>
{/* News Section */}
<div className="bg-zinc-950 border border-zinc-800 rounded-lg p-4 space-y-3">
<h4 className="text-xs font-mono font-bold text-amber-400 uppercase">3. News & Blog Sektion</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-[11px] font-mono text-zinc-400 mb-1">Kopfzeile (Eyebrow)</label>
<input
type="text"
value={formData.sectionTitles?.newsEyebrow ?? "Neuigkeiten & Entwickler-Tagebuch"}
onChange={(e) => setFormData({
...formData,
sectionTitles: { ...(formData.sectionTitles || {}), newsEyebrow: e.target.value }
})}
className="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-100 focus:border-amber-500"
/>
</div>
<div>
<label className="block text-[11px] font-mono text-zinc-400 mb-1">Hauptüberschrift</label>
<input
type="text"
value={formData.sectionTitles?.newsTitle ?? "Aktuelle News & Blog"}
onChange={(e) => setFormData({
...formData,
sectionTitles: { ...(formData.sectionTitles || {}), newsTitle: e.target.value }
})}
className="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-100 focus:border-amber-500"
/>
</div>
</div>
</div>
{/* Progress Section */}
<div className="bg-zinc-950 border border-zinc-800 rounded-lg p-4 space-y-3">
<h4 className="text-xs font-mono font-bold text-amber-400 uppercase">4. Entwicklungsfortschritt Sektion</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-[11px] font-mono text-zinc-400 mb-1">Kopfzeile (Eyebrow)</label>
<input
type="text"
value={formData.sectionTitles?.progressEyebrow ?? "Transparente Entwicklung"}
onChange={(e) => setFormData({
...formData,
sectionTitles: { ...(formData.sectionTitles || {}), progressEyebrow: e.target.value }
})}
className="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-100 focus:border-amber-500"
/>
</div>
<div>
<label className="block text-[11px] font-mono text-zinc-400 mb-1">Hauptüberschrift</label>
<input
type="text"
value={formData.sectionTitles?.progressTitle ?? "Entwicklungsfortschritt"}
onChange={(e) => setFormData({
...formData,
sectionTitles: { ...(formData.sectionTitles || {}), progressTitle: e.target.value }
})}
className="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-100 focus:border-amber-500"
/>
</div>
</div>
</div>
</div>
</div>
)}
{/* TAB 2: STORY & TABS */}
{activeTab === "story" && (
<div className="space-y-6">
@ -639,6 +791,14 @@ export const AdminCMSModal: React.FC<AdminCMSModalProps> = ({
</button>
</div>
{/* Markdown Info Banner */}
<div className="bg-amber-950/40 border border-amber-800/50 rounded-lg p-3 text-xs text-amber-200/90 flex items-start gap-2">
<Sparkles className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
<div>
<strong>Vollständiges Markdown unterstützt:</strong> Du kannst in allen Inhalts- und News-Feldern Markdown nutzen (z.B. <code>**Fett**</code>, <code>*Kursiv*</code>, <code>- Aufzählungspunkte</code>, <code>1. Nummerierte Listen</code>, <code>### Überschriften</code> etc.).
</div>
</div>
{formData.tabs.map((tab, idx) => (
<div key={tab.id} className="bg-zinc-950 border border-zinc-800 rounded-lg p-4 space-y-3 relative">
<div className="flex items-center justify-between gap-2">
@ -679,7 +839,9 @@ export const AdminCMSModal: React.FC<AdminCMSModalProps> = ({
</div>
<div>
<label className="block text-[11px] font-mono text-zinc-400 mb-1">Haupttext</label>
<label className="block text-[11px] font-mono text-zinc-400 mb-1">
Haupttext (Markdown unterstützt)
</label>
<textarea
rows={4}
value={tab.content}
@ -687,6 +849,39 @@ export const AdminCMSModal: React.FC<AdminCMSModalProps> = ({
className="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-2 text-sm text-zinc-200"
/>
</div>
{/* Key Features / Kernaspekte Box Editor */}
<div className="pt-3 border-t border-zinc-800 space-y-3">
<div>
<label className="block text-[11px] font-mono text-amber-400 mb-1">
Kernaspekte-Kasten Überschrift
</label>
<input
type="text"
placeholder="Wichtigste Merkmale & Kernaspekte"
value={tab.keyFeaturesTitle ?? "Wichtigste Merkmale & Kernaspekte"}
onChange={(e) => handleTabChange(idx, "keyFeaturesTitle", e.target.value)}
className="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-xs text-amber-200 font-mono"
/>
</div>
<div>
<label className="block text-[11px] font-mono text-amber-400 mb-1 flex items-center justify-between">
<span>Kernaspekte / Highlights (Ein Punkt pro Zeile)</span>
<span className="text-[10px] text-zinc-500 font-sans">Unterstützt Markdown</span>
</label>
<textarea
rows={3}
placeholder="Merkmal 1&#10;Merkmal 2&#10;Merkmal 3"
value={(tab.highlights || []).join("\n")}
onChange={(e) => {
const lines = e.target.value.split("\n");
handleTabChange(idx, "highlights", lines);
}}
className="w-full bg-zinc-900 border border-zinc-700 rounded px-3 py-2 text-xs text-zinc-200 font-mono"
/>
</div>
</div>
</div>
))}
</div>

View file

@ -49,14 +49,17 @@ export const GallerySection: React.FC<GallerySectionProps> = ({
);
};
const galleryEyebrow = cms.sectionTitles?.galleryEyebrow || "Impressionen & Ingame-Showcase";
const galleryTitle = cms.sectionTitles?.galleryTitle || "Screenshots aus Khorinis";
return (
<section id="screenshots" className="py-20 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative">
<div className="text-center mb-12">
<span className="text-xs font-mono font-bold uppercase tracking-widest text-amber-500/80 block mb-2">
Impressionen & Ingame-Showcase
{galleryEyebrow}
</span>
<h2 className="text-3xl sm:text-4xl font-serif font-extrabold text-amber-100 tracking-wide mb-4">
Screenshots aus Oakhaven
{galleryTitle}
</h2>
<div className="w-24 h-1 bg-gradient-to-r from-transparent via-amber-600 to-transparent mx-auto rounded-full" />
</div>

View file

@ -0,0 +1,87 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
interface MarkdownRendererProps {
content?: string;
className?: string;
}
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content = "", className = "" }) => {
if (!content) return null;
return (
<div className={`markdown-content ${className}`}>
<ReactMarkdown
components={{
h1: ({ children }) => (
<h1 className="text-xl font-serif font-bold text-amber-200 mt-4 mb-2 border-b border-amber-900/40 pb-1">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-lg font-serif font-bold text-amber-300 mt-3 mb-2">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-base font-serif font-semibold text-amber-400 mt-2 mb-1">
{children}
</h3>
),
p: ({ children }) => (
<p className="mb-3 leading-relaxed font-sans text-zinc-300">
{children}
</p>
),
ul: ({ children }) => (
<ul className="list-disc list-inside my-3 space-y-1.5 pl-2 text-zinc-300">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="list-decimal list-inside my-3 space-y-1.5 pl-2 text-zinc-300">
{children}
</ol>
),
li: ({ children }) => (
<li className="text-amber-100/90 leading-relaxed font-sans inline-block w-full">
{children}
</li>
),
strong: ({ children }) => (
<strong className="font-bold text-amber-200">
{children}
</strong>
),
em: ({ children }) => (
<em className="italic text-amber-300/90 font-serif">
{children}
</em>
),
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-amber-600 bg-amber-950/30 italic my-3 py-2 px-4 rounded-r text-amber-200/90 font-serif">
{children}
</blockquote>
),
code: ({ children }) => (
<code className="bg-zinc-950 text-amber-400 font-mono text-xs px-1.5 py-0.5 rounded border border-amber-900/50">
{children}
</code>
),
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-amber-400 hover:text-amber-200 underline decoration-amber-600 underline-offset-2 transition-colors"
>
{children}
</a>
),
}}
>
{content}
</ReactMarkdown>
</div>
);
};

View file

@ -1,6 +1,7 @@
import React, { useState } from "react";
import { Newspaper, Calendar, User, Tag, ChevronRight, X, Plus, Edit3 } from "lucide-react";
import { CMSData, NewsPost } from "../types";
import { MarkdownRenderer } from "./MarkdownRenderer";
interface NewsSectionProps {
cms: CMSData;
@ -15,14 +16,17 @@ export const NewsSection: React.FC<NewsSectionProps> = ({
}) => {
const [activeNewsModal, setActiveNewsModal] = useState<NewsPost | null>(null);
const newsEyebrow = cms.sectionTitles?.newsEyebrow || "Neuigkeiten & Entwickler-Tagebuch";
const newsTitle = cms.sectionTitles?.newsTitle || "Aktuelle News & Blog";
return (
<section id="news" className="py-20 px-4 sm:px-6 lg:px-8 max-w-6xl mx-auto relative">
<div className="text-center mb-12">
<span className="text-xs font-mono font-bold uppercase tracking-widest text-amber-500/80 block mb-2">
Neuigkeiten & Entwickler-Tagebuch
{newsEyebrow}
</span>
<h2 className="text-3xl sm:text-4xl font-serif font-extrabold text-amber-100 tracking-wide mb-4">
Aktuelle News & Blog
{newsTitle}
</h2>
<div className="w-24 h-1 bg-gradient-to-r from-transparent via-amber-600 to-transparent mx-auto rounded-full" />
</div>
@ -58,9 +62,9 @@ export const NewsSection: React.FC<NewsSectionProps> = ({
{post.title}
</h3>
<p className="text-zinc-300 text-sm leading-relaxed mb-6 flex-1 line-clamp-3">
{post.summary}
</p>
<div className="text-zinc-300 text-sm leading-relaxed mb-6 flex-1 line-clamp-3">
<MarkdownRenderer content={post.summary} />
</div>
<div className="pt-4 border-t border-zinc-800/80 flex items-center justify-between mt-auto">
<span className="text-xs text-zinc-400 font-mono flex items-center gap-1">
@ -125,8 +129,8 @@ export const NewsSection: React.FC<NewsSectionProps> = ({
</div>
)}
<div className="overflow-y-auto pr-2 space-y-4 text-zinc-300 leading-relaxed font-sans text-base whitespace-pre-line flex-1">
{activeNewsModal.content}
<div className="overflow-y-auto pr-2 text-zinc-300 leading-relaxed font-sans text-base flex-1">
<MarkdownRenderer content={activeNewsModal.content} />
</div>
<div className="mt-6 pt-4 border-t border-zinc-800 flex justify-end">

View file

@ -1,6 +1,7 @@
import React from "react";
import { Code, Compass, Mic, Box, Swords, CheckCircle2, Clock, Calendar, Edit3, Sparkles } from "lucide-react";
import { CMSData, DevCategoryProgress, Milestone } from "../types";
import { MarkdownRenderer } from "./MarkdownRenderer";
interface ProgressSectionProps {
cms: CMSData;
@ -28,6 +29,9 @@ export const ProgressSection: React.FC<ProgressSectionProps> = ({
}
};
const progressEyebrow = cms.sectionTitles?.progressEyebrow || "Transparente Entwicklung";
const progressTitle = cms.sectionTitles?.progressTitle || "Entwicklungsfortschritt";
const getStatusBadge = (status: Milestone["status"]) => {
switch (status) {
case "completed":
@ -58,10 +62,10 @@ export const ProgressSection: React.FC<ProgressSectionProps> = ({
<section id="progress" className="py-20 px-4 sm:px-6 lg:px-8 max-w-6xl mx-auto relative">
<div className="text-center mb-12">
<span className="text-xs font-mono font-bold uppercase tracking-widest text-amber-500/80 block mb-2">
Transparente Entwicklung
{progressEyebrow}
</span>
<h2 className="text-3xl sm:text-4xl font-serif font-extrabold text-amber-100 tracking-wide mb-4">
Entwicklungsfortschritt
{progressTitle}
</h2>
<div className="w-24 h-1 bg-gradient-to-r from-transparent via-amber-600 to-transparent mx-auto rounded-full" />
</div>
@ -116,7 +120,9 @@ export const ProgressSection: React.FC<ProgressSectionProps> = ({
<h4 className="font-serif font-semibold text-amber-100 text-base">
{cat.name}
</h4>
<p className="text-xs text-zinc-400">{cat.details}</p>
<div className="text-xs text-zinc-400">
<MarkdownRenderer content={cat.details} />
</div>
</div>
</div>
<span className="font-mono font-bold text-amber-400 text-base">
@ -181,7 +187,9 @@ export const ProgressSection: React.FC<ProgressSectionProps> = ({
</h4>
<span className="text-xs font-mono text-amber-500/80">({ms.date})</span>
</div>
<p className="text-zinc-400 text-xs">{ms.description}</p>
<div className="text-zinc-400 text-xs">
<MarkdownRenderer content={ms.description} />
</div>
</div>
<div className="shrink-0">{getStatusBadge(ms.status)}</div>

View file

@ -1,6 +1,7 @@
import React, { useState } from "react";
import { BookOpen, Sparkles, Shield, HelpCircle, CheckCircle2, FileText, Plus, Edit3 } from "lucide-react";
import { CMSData, StoryTab } from "../types";
import { MarkdownRenderer } from "./MarkdownRenderer";
interface StoryTabsSectionProps {
cms: CMSData;
@ -32,15 +33,18 @@ export const StoryTabsSection: React.FC<StoryTabsSectionProps> = ({
}
};
const storyEyebrow = cms.sectionTitles?.storyEyebrow || "Projekt-Informationen";
const storyTitle = cms.sectionTitles?.storyTitle || "Die Welt, Features & Fraktionen";
return (
<section id="story" className="py-20 px-4 sm:px-6 lg:px-8 max-w-6xl mx-auto relative">
{/* Medieval Header */}
<div className="text-center mb-12">
<span className="text-xs font-mono font-bold uppercase tracking-widest text-amber-500/80 block mb-2">
Projekt-Informationen
{storyEyebrow}
</span>
<h2 className="text-3xl sm:text-4xl font-serif font-extrabold text-amber-100 tracking-wide mb-4">
Die Welt, Features & Fraktionen
{storyTitle}
</h2>
<div className="w-24 h-1 bg-gradient-to-r from-transparent via-amber-600 to-transparent mx-auto rounded-full" />
</div>
@ -99,22 +103,24 @@ export const StoryTabsSection: React.FC<StoryTabsSectionProps> = ({
)}
</div>
{/* Main Content Paragraphs */}
<div className="text-zinc-300 leading-relaxed space-y-4 font-sans text-base whitespace-pre-line">
{activeTab.content}
{/* Main Content Paragraphs (Markdown supported) */}
<div className="text-zinc-300 leading-relaxed font-sans text-base">
<MarkdownRenderer content={activeTab.content} />
</div>
{/* Key Highlights Bullet Box */}
{activeTab.highlights && activeTab.highlights.length > 0 && (
<div className="mt-8 pt-6 border-t border-zinc-800/80 bg-zinc-950/60 rounded-lg p-5 border-l-4 border-l-amber-600">
<h4 className="text-xs font-mono font-bold uppercase tracking-widest text-amber-400 mb-3">
Wichtigste Merkmale & Kernaspekte
{activeTab.keyFeaturesTitle || "Wichtigste Merkmale & Kernaspekte"}
</h4>
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
{activeTab.highlights.map((item, idx) => (
<li key={idx} className="flex items-start gap-2.5 text-sm text-amber-100/90">
<CheckCircle2 className="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
<span>{item}</span>
<div className="flex-1">
<MarkdownRenderer content={item} />
</div>
</li>
))}
</ul>

View file

@ -223,5 +223,15 @@ Wenn der Held tiefer in die Stollen vordringt, verfinstert sich die Atmosphäre
intensity: 40,
enabled: true
},
sectionTitles: {
storyEyebrow: "Projekt-Informationen",
storyTitle: "Die Welt, Features & Fraktionen",
galleryEyebrow: "Impressionen & Ingame-Showcase",
galleryTitle: "Screenshots aus Khorinis",
newsEyebrow: "Neuigkeiten & Entwickler-Tagebuch",
newsTitle: "Aktuelle News & Blog",
progressEyebrow: "Transparente Entwicklung",
progressTitle: "Entwicklungsfortschritt",
},
adminPin: "gothic2026"
};

View file

@ -4,6 +4,7 @@ export interface StoryTab {
iconName: string; // lucide icon identifier
content: string;
highlights?: string[];
keyFeaturesTitle?: string;
}
export interface ScreenshotItem {
@ -63,5 +64,15 @@ export interface CMSData {
intensity: number; // 0 to 100
enabled: boolean;
};
sectionTitles?: {
storyEyebrow?: string;
storyTitle?: string;
galleryEyebrow?: string;
galleryTitle?: string;
newsEyebrow?: string;
newsTitle?: string;
progressEyebrow?: string;
progressTitle?: string;
};
adminPin: string;
}