Added image picker and upload function
This commit is contained in:
parent
8ffffcdf8d
commit
4bc91ad9b5
3 changed files with 58 additions and 12 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -6,5 +6,5 @@ coverage/
|
||||||
*.log
|
*.log
|
||||||
.env*
|
.env*
|
||||||
!.env.example
|
!.env.example
|
||||||
data/
|
data/database.json
|
||||||
|
|
||||||
|
|
|
||||||
46
server.ts
46
server.ts
|
|
@ -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
|
// Configure Vite middleware or static serve
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
// Serve the dynamic uploads directory statically
|
// Serve the dynamic uploads directory statically
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
Lock, Save, Plus, Trash2, Edit2, Check, RefreshCw, Sparkles,
|
Lock, Save, Plus, Trash2, Edit2, Check, RefreshCw, Sparkles,
|
||||||
BookOpen, Compass, Clipboard, Eye, Music, LogOut, ArrowLeftRight
|
BookOpen, Compass, Clipboard, Eye, Music, LogOut, ArrowLeftRight
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import ImagePicker from "./ImagePicker";
|
||||||
|
|
||||||
interface AdminPanelProps {
|
interface AdminPanelProps {
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
|
|
@ -536,12 +537,12 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs font-mono text-slate-400 uppercase">Avatar/Autorenfoto URL</label>
|
<label className="text-xs font-mono text-slate-400 uppercase font-bold text-white">Avatar / Autorenfoto</label>
|
||||||
<input
|
<ImagePicker
|
||||||
type="text"
|
|
||||||
value={profile.avatarUrl}
|
value={profile.avatarUrl}
|
||||||
onChange={(e) => updateProfileField("avatarUrl", e.target.value)}
|
onChange={(url) => updateProfileField("avatarUrl", url)}
|
||||||
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"
|
token={token}
|
||||||
|
label="Autorenfoto / Avatar"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -858,13 +859,12 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs font-mono text-slate-400 uppercase">Cover-Bild URL</label>
|
<label className="text-xs font-mono text-slate-400 uppercase font-bold text-white">Cover-Bild</label>
|
||||||
<input
|
<ImagePicker
|
||||||
type="text"
|
|
||||||
value={bookForm.coverUrl || ""}
|
value={bookForm.coverUrl || ""}
|
||||||
onChange={(e) => setBookForm({ ...bookForm, coverUrl: e.target.value })}
|
onChange={(url) => setBookForm({ ...bookForm, coverUrl: url })}
|
||||||
placeholder="Link oder hochgeladener Pfad..."
|
token={token}
|
||||||
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"
|
label="Buch-Cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue