diff --git a/server.ts b/server.ts index a62fca1..a509860 100644 --- a/server.ts +++ b/server.ts @@ -8,7 +8,7 @@ const app = express(); const PORT = 3000; const DATA_FILE = path.join(process.cwd(), "cms-data.json"); -app.use(express.json({ limit: "10mb" })); +app.use(express.json({ limit: "50mb" })); // Helper to read persistent CMS data function loadCMSData() { diff --git a/src/components/AdminCMSModal.tsx b/src/components/AdminCMSModal.tsx index ffc4a71..31e8756 100644 --- a/src/components/AdminCMSModal.tsx +++ b/src/components/AdminCMSModal.tsx @@ -1,7 +1,152 @@ -import React, { useState } from "react"; +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 { CMSData, StoryTab, ScreenshotItem, NewsPost, DevCategoryProgress, Milestone } from "../types"; +// Helper function to process and compress uploaded images to sharp Base64 Data URLs +const processImageFile = (file: File, maxDim = 1920, quality = 0.85): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + const img = new Image(); + img.onload = () => { + let width = img.width; + let height = img.height; + + if (width > maxDim || height > maxDim) { + if (width > height) { + height = Math.round((height * maxDim) / width); + width = maxDim; + } else { + width = Math.round((width * maxDim) / height); + height = maxDim; + } + } + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + if (ctx) { + ctx.drawImage(img, 0, 0, width, height); + const mime = file.type === "image/png" ? "image/png" : "image/jpeg"; + const dataUrl = canvas.toDataURL(mime, quality); + resolve(dataUrl); + } else { + resolve(e.target?.result as string); + } + }; + img.onerror = () => resolve(e.target?.result as string); + img.src = e.target?.result as string; + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); +}; + +interface ImagePickerInputProps { + label: string; + value: string; + onChange: (url: string) => void; + placeholder?: string; + className?: string; +} + +const ImagePickerInput: React.FC = ({ + label, + value, + onChange, + placeholder = "Bild-URL oder Datei hochladen...", + className = "" +}) => { + const [isUploading, setIsUploading] = useState(false); + const fileInputRef = useRef(null); + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setIsUploading(true); + try { + const dataUrl = await processImageFile(file); + onChange(dataUrl); + } catch (err) { + console.error("Fehler beim Verarbeiten des Bildes:", err); + } finally { + setIsUploading(false); + if (fileInputRef.current) fileInputRef.current.value = ""; + } + }; + + return ( +
+ + +
+ onChange(e.target.value)} + placeholder={placeholder} + className="flex-1 bg-zinc-950 border border-zinc-800 rounded px-3 py-2 text-xs text-zinc-100 font-mono focus:border-amber-500 focus:ring-1 focus:ring-amber-500" + /> + + + + +
+ + {value && ( +
+ Vorschau { + (e.target as HTMLElement).style.display = 'none'; + }} + /> +
+ {value.substring(0, 55)}... + + Bild aktiv + +
+ +
+ )} +
+ ); +}; + interface AdminCMSModalProps { isOpen: boolean; onClose: () => void; @@ -27,10 +172,14 @@ export const AdminCMSModal: React.FC = ({ const [pinError, setPinError] = useState(false); const [activeTab, setActiveTab] = useState<"hero" | "story" | "gallery" | "news" | "progress" | "backup">("hero"); - // Local draft state for editing + // Local draft state for editing - sync when cms updates const [formData, setFormData] = useState(cms); const [isSavedNotice, setIsSavedNotice] = useState(false); + useEffect(() => { + setFormData(cms); + }, [cms]); + const handlePinSubmit = (e: React.FormEvent) => { e.preventDefault(); if (onUnlock(pinInput)) { @@ -415,25 +564,19 @@ export const AdminCMSModal: React.FC = ({
-
- - setFormData({ ...formData, hero: { ...formData.hero, logoUrl: e.target.value } })} - className="w-full bg-zinc-950 border border-zinc-800 rounded px-3 py-2 text-sm text-zinc-100 font-mono text-xs focus:border-amber-500" - /> -
+ setFormData({ ...formData, hero: { ...formData.hero, logoUrl: url } })} + placeholder="https://... oder Datei hochladen" + /> -
- - setFormData({ ...formData, hero: { ...formData.hero, heroBgUrl: e.target.value } })} - className="w-full bg-zinc-950 border border-zinc-800 rounded px-3 py-2 text-sm text-zinc-100 font-mono text-xs focus:border-amber-500" - /> -
+ setFormData({ ...formData, hero: { ...formData.hero, heroBgUrl: url } })} + placeholder="https://... oder Datei hochladen" + />
@@ -593,12 +736,11 @@ export const AdminCMSModal: React.FC = ({ className="w-full bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs text-purple-300 font-mono" /> - handleScreenshotChange(idx, "imageUrl", e.target.value)} - className="w-full bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-[11px] text-zinc-300 font-mono" + onChange={(url) => handleScreenshotChange(idx, "imageUrl", url)} + placeholder="Bild-URL oder Datei hochladen..." /> = ({ />
+ handleNewsChange(idx, "imageUrl", url)} + placeholder="Optionales News-Bild hochladen..." + /> +