Add: Feedback-System, GitHub Actions Deploy, Assets-Demo User-Suche

This commit is contained in:
2026-06-01 21:04:20 +02:00
parent 8023765e6c
commit a4685c09b8
8 changed files with 2158 additions and 0 deletions

View File

@@ -2,6 +2,126 @@ import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
/* ── FeedbackModal ───────────────────────────────────────────────── */
const CATEGORY_OPTIONS = [
{ value: 'bug', label: 'Bug 🐛' },
{ value: 'feature', label: 'Feature-Wunsch ✨' },
{ value: 'idea', label: 'Idee 💡' },
];
function FeedbackModal({ onClose }) {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [category, setCategory] = useState('bug');
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
if (!title.trim() || !body.trim()) { setError('Bitte Titel und Beschreibung ausfüllen.'); return; }
setLoading(true);
setError('');
try {
const token = localStorage.getItem('token');
const API = process.env.REACT_APP_API_URL || '';
const res = await fetch(`${API}/api/feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ title, body, category })
});
const json = await res.json();
if (json.status === 'success') { setSuccess(true); }
else { setError('Fehler beim Senden.'); }
} catch {
setError('Server nicht erreichbar.');
} finally {
setLoading(false);
}
};
return (
<div
onClick={onClose}
style={{
position: 'fixed', inset: 0, zIndex: 9999,
background: 'rgba(0,0,0,0.55)',
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}
>
<div
onClick={e => e.stopPropagation()}
style={{
background: 'var(--card-bg, #1e1e2e)',
border: '1px solid var(--border-color, rgba(255,255,255,0.1))',
borderRadius: 12,
padding: 28,
width: '100%',
maxWidth: 440,
boxShadow: '0 20px 60px rgba(0,0,0,0.5)'
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
<h2 style={{ margin: 0, fontSize: 17, fontWeight: 700, color: 'var(--text-primary, #f1f5f9)' }}>
Feedback senden
</h2>
<button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 20, color: 'var(--text-muted, #64748b)', lineHeight: 1 }}>×</button>
</div>
{success ? (
<div style={{ textAlign: 'center', padding: '24px 0' }}>
<div style={{ fontSize: 36, marginBottom: 12 }}></div>
<p style={{ color: 'var(--text-primary, #f1f5f9)', fontWeight: 600, marginBottom: 6 }}>Feedback gesendet!</p>
<p style={{ color: 'var(--text-secondary, #94a3b8)', fontSize: 13 }}>Das Issue wurde auf GitHub erstellt.</p>
<button onClick={onClose} style={{ marginTop: 16, padding: '8px 20px', borderRadius: 8, border: 'none', background: 'var(--accent, #6366f1)', color: '#fff', cursor: 'pointer', fontWeight: 600 }}>Schließen</button>
</div>
) : (
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary, #94a3b8)', display: 'block', marginBottom: 6 }}>Kategorie</label>
<select
value={category}
onChange={e => setCategory(e.target.value)}
style={{ width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--border-color, rgba(255,255,255,0.1))', background: 'var(--input-bg, #252535)', color: 'var(--text-primary, #f1f5f9)', fontSize: 13 }}
>
{CATEGORY_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary, #94a3b8)', display: 'block', marginBottom: 6 }}>Titel</label>
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="Kurze Zusammenfassung…"
style={{ width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--border-color, rgba(255,255,255,0.1))', background: 'var(--input-bg, #252535)', color: 'var(--text-primary, #f1f5f9)', fontSize: 13, boxSizing: 'border-box' }}
/>
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary, #94a3b8)', display: 'block', marginBottom: 6 }}>Beschreibung</label>
<textarea
value={body}
onChange={e => setBody(e.target.value)}
placeholder="Was ist passiert? Was hast du erwartet?"
rows={5}
style={{ width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--border-color, rgba(255,255,255,0.1))', background: 'var(--input-bg, #252535)', color: 'var(--text-primary, #f1f5f9)', fontSize: 13, resize: 'vertical', boxSizing: 'border-box', fontFamily: 'inherit' }}
/>
</div>
{error && <p style={{ color: '#ef4444', fontSize: 12, margin: 0 }}>{error}</p>}
<button
type="submit"
disabled={loading}
style={{ padding: '9px 0', borderRadius: 8, border: 'none', background: 'var(--accent, #6366f1)', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading ? 'not-allowed' : 'pointer', opacity: loading ? 0.7 : 1 }}
>
{loading ? 'Senden…' : 'Feedback senden'}
</button>
</form>
)}
</div>
</div>
);
}
/* ── SVG Icon Components ────────────────────────────────────────── */
const I = ({ d, children, ...p }) => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" {...p}>
@@ -32,6 +152,7 @@ const icons = {
ki: <I><path d="M12 2a3 3 0 0 0-3 3v1H7a3 3 0 0 0-3 3v2H3a2 2 0 0 0 0 4h1v2a3 3 0 0 0 3 3h2v1a3 3 0 0 0 6 0v-1h2a3 3 0 0 0 3-3v-2h1a2 2 0 0 0 0-4h-1V9a3 3 0 0 0-3-3h-2V5a3 3 0 0 0-3-3z"/></I>,
onboarding: <I><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><path d="M20 8v6M23 11h-6"/></I>,
announcements: <I><path d="M11 5L6 9H2v6h4l5 4V5zM19 12c0-2.5-1.5-5-4-6"/></I>,
feedback: <I><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></I>,
system: <I><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></I>,
};
@@ -75,6 +196,7 @@ const SubMenu = ({ label, icon, items, isActive }) => {
const Sidebar = () => {
const location = useLocation();
const { user, isAdmin, isSupport, isTechniker, canViewLifecycle, canModifyFidoKeys } = useAuth();
const [feedbackOpen, setFeedbackOpen] = useState(false);
const isStaff = isSupport();
const role = user?.role_name;
@@ -162,9 +284,41 @@ const Sidebar = () => {
<NavItem to="/proxmox" icon="monitoring" label="Proxmox" active={isActive('/proxmox')} />
<NavItem to="/docker" icon="monitoring" label="Docker" active={isActive('/docker')} />
<NavItem to="/system" icon="system" label="Systemeinstellungen" active={isActive('/system')} />
<NavItem to="/feedback" icon="feedback" label="Feedback Issues" active={isActive('/feedback')} />
</div>
)}
</nav>
{/* Feedback Button */}
<div style={{ padding: '12px 12px 16px' }}>
<button
onClick={() => setFeedbackOpen(true)}
style={{
width: '100%',
padding: '9px 14px',
borderRadius: 8,
border: '1px solid var(--border-color, rgba(255,255,255,0.08))',
background: 'transparent',
color: 'var(--text-secondary, #94a3b8)',
fontSize: 13,
fontWeight: 500,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 8,
transition: 'background .15s'
}}
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.04)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" width="16" height="16">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Feedback geben
</button>
</div>
{feedbackOpen && <FeedbackModal onClose={() => setFeedbackOpen(false)} />}
</aside>
);
};