Merge pull request #6 from Dada1981/ai-changes

Added image picker and upload function
This commit is contained in:
Dada1981 2026-07-20 22:12:16 +02:00 committed by GitHub
commit e46e59a2a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 58 additions and 12 deletions

2
.gitignore vendored
View file

@ -6,5 +6,5 @@ coverage/
*.log
.env*
!.env.example
data/
data/database.json

View file

@ -204,6 +204,52 @@ Die Synopsis soll neugierig machen, stark die visuelle Stimmung rüberbringen un
}
});
// 5. Upload a file via base64
app.post("/api/admin/upload-file", verifyToken, async (req, res) => {
const { fileName, base64Data } = req.body;
if (!fileName || !base64Data) {
res.status(400).json({ error: "Dateiname und Base64-Daten sind erforderlich." });
return;
}
try {
// Sanitize filename to prevent directory traversal
const safeName = path.basename(fileName).replace(/[^a-zA-Z0-9.\-_]/g, "_");
const uploadPath = path.join(DATA_DIR, "uploads", safeName);
// Strip base64 metadata prefix if present (e.g., "data:image/jpeg;base64,")
const base64Clean = base64Data.replace(/^data:image\/\w+;base64,/, "");
const buffer = Buffer.from(base64Clean, "base64");
await fs.writeFile(uploadPath, buffer);
res.json({ success: true, url: `/uploads/${safeName}` });
} catch (err: any) {
console.error("File upload failed:", err);
res.status(500).json({ error: "Fehler beim Speichern der Datei auf dem Server." });
}
});
// 6. List uploaded files
app.get("/api/admin/list-uploads", verifyToken, async (req, res) => {
try {
const uploadsDir = path.join(DATA_DIR, "uploads");
await fs.mkdir(uploadsDir, { recursive: true });
const files = await fs.readdir(uploadsDir);
const fileList = files
.filter(file => !file.startsWith(".")) // skip hidden files
.map(file => ({
name: file,
url: `/uploads/${file}`
}));
res.json({ success: true, files: fileList });
} catch (err: any) {
console.error("Failed to list uploads:", err);
res.status(500).json({ error: "Fehler beim Auflisten der hochgeladenen Dateien." });
}
});
// Configure Vite middleware or static serve
async function startServer() {
// Serve the dynamic uploads directory statically

View file

@ -4,6 +4,7 @@ import {
Lock, Save, Plus, Trash2, Edit2, Check, RefreshCw, Sparkles,
BookOpen, Compass, Clipboard, Eye, Music, LogOut, ArrowLeftRight
} from "lucide-react";
import ImagePicker from "./ImagePicker";
interface AdminPanelProps {
onLogout: () => void;
@ -536,12 +537,12 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
/>
</div>
<div className="space-y-1">
<label className="text-xs font-mono text-slate-400 uppercase">Avatar/Autorenfoto URL</label>
<input
type="text"
<label className="text-xs font-mono text-slate-400 uppercase font-bold text-white">Avatar / Autorenfoto</label>
<ImagePicker
value={profile.avatarUrl}
onChange={(e) => updateProfileField("avatarUrl", e.target.value)}
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"
onChange={(url) => updateProfileField("avatarUrl", url)}
token={token}
label="Autorenfoto / Avatar"
/>
</div>
<div className="space-y-1">
@ -858,13 +859,12 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
/>
</div>
<div className="space-y-1">
<label className="text-xs font-mono text-slate-400 uppercase">Cover-Bild URL</label>
<input
type="text"
<label className="text-xs font-mono text-slate-400 uppercase font-bold text-white">Cover-Bild</label>
<ImagePicker
value={bookForm.coverUrl || ""}
onChange={(e) => setBookForm({ ...bookForm, coverUrl: e.target.value })}
placeholder="Link oder hochgeladener Pfad..."
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 font-mono"
onChange={(url) => setBookForm({ ...bookForm, coverUrl: url })}
token={token}
label="Buch-Cover"
/>
</div>
<div className="space-y-1">