Dateien nach "frontend/src" hochladen

This commit is contained in:
2026-05-27 19:01:26 +02:00
parent 2da50e2c71
commit 14a67787a9
3 changed files with 152 additions and 9 deletions

View File

@@ -4,7 +4,7 @@ import { TOOLS, getGroupedTools } from './toolRegistry.js';
import CalendarWidget, { CalendarSettings } from './calendar.jsx';
import { DateiSettings } from './tools/dateien.jsx';
import { useConfirm } from './confirm.jsx';
import { HomeIcon, AppsIcon, AdminIcon, MoreIcon, UserIcon, LogOutIcon, ChevronIcon, UpdateIcon, TrashIcon } from './icons.jsx';
import { HomeIcon, AppsIcon, AdminIcon, MoreIcon, UserIcon, LogOutIcon, ChevronIcon, UpdateIcon, TrashIcon, MessageIcon } from './icons.jsx';
const ICONS = ['🔗','🌐','📊','📁','🛠','📧','🗓','💾','🖥','📡','🔒','📖','🎯','⚡','🏠','🔧','📱','🎮','🚀','💡'];
@@ -167,7 +167,7 @@ function UpdateModal({ data, onClose }) {
}
// ── Desktop Sidebar ───────────────────────────────────────────────────────────
function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick }) {
function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick, unreadMsgs=0 }) {
const [appsOpen, setAppsOpen] = useState(true);
const [collapsed, setCollapsed] = useState(false);
@@ -183,12 +183,21 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
cursor:'pointer', fontSize:12,
fontFamily:"'Space Mono',monospace", textAlign:'left', marginBottom:1,
borderLeft: active===item.id ? '2px solid #4ecdc4' : '2px solid transparent',
position:'relative',
}}>
{!collapsed && sub && <span style={{ width:1, alignSelf:'stretch', background:'rgba(255,255,255,0.08)', marginRight:3, flexShrink:0 }}/>}
<span style={{ display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }}>
<span style={{ display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, position:'relative' }}>
{item.Icon ? (() => { const I = item.Icon; return <I size={collapsed?18:15} color={ic}/>; })() : <span style={{color:ic,fontSize:collapsed?16:13}}>{item.icon}</span>}
{collapsed && item.id==='nachrichten' && unreadMsgs>0 && (
<span style={{ position:'absolute', top:-3, right:-3, width:7, height:7,
borderRadius:'50%', background:'#4ecdc4', boxShadow:'0 0 5px #4ecdc4' }}/>
)}
</span>
{!collapsed && <span style={{ flex:1, color:ic }}>{item.label}</span>}
{!collapsed && item.id==='nachrichten' && unreadMsgs>0 && (
<span style={{ background:'#4ecdc4', color:'#000', borderRadius:10, fontSize:9,
fontFamily:'monospace', padding:'1px 6px', fontWeight:700, flexShrink:0 }}>{unreadMsgs}</span>
)}
</button>
);
};
@@ -279,7 +288,7 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
}
// ── Mobile Bottom Navigation ──────────────────────────────────────────────────
function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClick }) {
function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClick, unreadMsgs=0 }) {
const [menuOpen, setMenuOpen] = useState(false);
const [appsOpen, setAppsOpen] = useState(false);
const NAV_H = 56;
@@ -384,6 +393,11 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
width:8, height:8, borderRadius:'50%', background:'#ffe66d',
boxShadow:'0 0 6px #ffe66d', display:'inline-block' }} />
)}
{isApps && unreadMsgs>0 && (
<span style={{ position:'absolute', top:6, right:'calc(50% - 16px)',
width:8, height:8, borderRadius:'50%', background:'#4ecdc4',
boxShadow:'0 0 6px #4ecdc4', display:'inline-block' }} />
)}
{(() => { const I = item.Icon; return I ? <I size={20} color={isActive ? '#4ecdc4' : 'rgba(255,255,255,0.45)'}/> : null; })()}
<span style={{ fontSize:9, fontFamily:'monospace', letterSpacing:0.3,
color: isActive ? '#4ecdc4' : 'rgba(255,255,255,0.4)' }}>{item.label}</span>
@@ -687,7 +701,109 @@ function BestellStats() {
);
}
function Dashboard({ toast, mobile }) {
// ── Nachrichten Dashboard Widget ──────────────────────────────────────────────
function NachrichtenWidget({ unreadMsgs, setActive }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div style={{ ...S.card, marginBottom:10 }}>
<button onClick={() => setIsOpen(v=>!v)} style={{
width:'100%', background:'transparent', border:'none', cursor:'pointer',
display:'flex', alignItems:'center', padding:0, gap:8 }}>
<MessageIcon size={13} color="rgba(255,255,255,0.5)"/>
<span style={{ ...S.head, marginBottom:0, flex:1, textAlign:'left' }}>NACHRICHTEN</span>
{unreadMsgs > 0 && !isOpen && (
<span style={{ background:'#4ecdc4', color:'#000', borderRadius:10, fontSize:9,
fontFamily:'monospace', padding:'2px 7px', fontWeight:700 }}>
{unreadMsgs} neu
</span>
)}
<span style={{ color:'rgba(255,255,255,0.4)', fontSize:11,
transform:isOpen?'rotate(90deg)':'rotate(0)', transition:'transform 0.2s' }}></span>
</button>
{isOpen && (
<div style={{ marginTop:12, display:'flex', alignItems:'center', justifyContent:'space-between', gap:10 }}>
<span style={{ color: unreadMsgs > 0 ? '#4ecdc4' : 'rgba(255,255,255,0.35)',
fontSize:12, fontFamily:'monospace' }}>
{unreadMsgs > 0
? `${unreadMsgs} ungelesene Nachricht${unreadMsgs !== 1 ? 'en' : ''}`
: 'Keine neuen Nachrichten'}
</span>
<button onClick={() => setActive('nachrichten')} style={{ ...S.btn('#4ecdc4', true), flexShrink:0 }}>
Öffnen
</button>
</div>
)}
</div>
);
}
// ── Pushover Settings ─────────────────────────────────────────────────────────
function PushoverSettings({ toast }) {
const [form, setForm] = useState({ user_key:'', app_token:'' });
const [busy, setBusy] = useState(false);
const [hasSaved, setHasSaved] = useState(false);
useEffect(() => {
api('/tools/nachrichten/pushover')
.then(d => {
const v = { user_key: d.user_key||'', app_token: d.app_token||'' };
setForm(v);
setHasSaved(!!(v.user_key && v.app_token));
}).catch(()=>{});
}, []);
const save = async () => {
if (!form.user_key.trim() || !form.app_token.trim()) { toast('Beide Felder ausfüllen', 'error'); return; }
setBusy(true);
try { await api('/tools/nachrichten/pushover', { body: form }); toast('Gespeichert'); setHasSaved(true); }
catch(e) { toast(e.message, 'error'); }
setBusy(false);
};
const test = async () => {
setBusy(true);
try { await api('/tools/nachrichten/pushover/test', { method:'POST' }); toast('Testbenachrichtigung gesendet ✓'); }
catch(e) { toast(e.message, 'error'); }
setBusy(false);
};
const clear = async () => {
try {
await api('/tools/nachrichten/pushover', { method:'DELETE' });
setForm({ user_key:'', app_token:'' });
setHasSaved(false);
toast('Pushover entfernt');
} catch(e) { toast(e.message, 'error'); }
};
return (
<div>
<p style={{ color:'rgba(255,255,255,0.42)', fontSize:11, fontFamily:'monospace', marginBottom:12, lineHeight:1.7 }}>
Push-Benachrichtigungen bei neuen Nachrichten via{' '}
<a href="https://pushover.net" target="_blank" rel="noopener noreferrer" style={{ color:'#4ecdc4' }}>pushover.net</a>.
Du brauchst einen Account und eine eigene App (kostenlos).
</p>
<Field label="USER KEY" value={form.user_key} onChange={e=>setForm(p=>({...p,user_key:e.target.value}))} />
<Field label="APP TOKEN" value={form.app_token} onChange={e=>setForm(p=>({...p,app_token:e.target.value}))} />
<div style={{ display:'flex', gap:8 }}>
<button onClick={save} disabled={busy}
style={{ ...S.btn('#4ecdc4'), flex:1, textAlign:'center', padding:'10px 0', opacity:busy?0.5:1 }}>
{busy ? '…' : '✓ Speichern'}
</button>
{hasSaved && (
<button onClick={test} disabled={busy}
style={{ ...S.btn('#ffe66d', true), padding:'0 14px', opacity:busy?0.5:1 }}>Test</button>
)}
{hasSaved && (
<button onClick={clear}
style={{ ...S.btn('#ff6b9d', true), padding:'0 14px' }}></button>
)}
</div>
</div>
);
}
function Dashboard({ toast, mobile, setActive, unreadMsgs=0 }) {
return (
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
<div style={{ marginBottom:16 }}>
@@ -699,6 +815,7 @@ function Dashboard({ toast, mobile }) {
<QuickLinks toast={toast} mobile={mobile} />
<CalendarWidget/>
<BestellStats/>
<NachrichtenWidget unreadMsgs={unreadMsgs} setActive={setActive}/>
<TodoList toast={toast} />
<Notepad toast={toast} />
</div>
@@ -1111,6 +1228,9 @@ function AdminPanel({ toast, mobile, user }) {
</p>
<DeleteAccountSection toast={toast} user={user}/>
</Sec>
<Sec title="PUSHOVER BENACHRICHTIGUNGEN">
<PushoverSettings toast={toast}/>
</Sec>
</div>
)}
@@ -1171,6 +1291,7 @@ export default function App() {
const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
const [updateInfo,setUpdateInfo]=useState(null);
const [showUpd,setShowUpd]=useState(false);
const [unreadMsgs,setUnreadMsgs]=useState(0);
const [authChecked, setAuthChecked] = useState(false);
useEffect(()=>{
@@ -1192,6 +1313,14 @@ export default function App() {
return()=>clearInterval(t);
},[user]);
useEffect(()=>{
if(!user)return;
const poll=()=>api('/tools/nachrichten/unread').then(d=>setUnreadMsgs(d.count||0)).catch(()=>{});
poll();
const t=setInterval(poll,30*1000);
return()=>clearInterval(t);
},[user]);
const toast=useCallback((msg,type='success')=>{
setToastMsg({msg,type});
setTimeout(()=>setToastMsg({msg:'',type:'success'}),3500);
@@ -1222,10 +1351,11 @@ export default function App() {
{!mobile && (
<Sidebar active={active} setActive={setActive} user={user}
onLogout={()=>{localStorage.removeItem('sk_token');setUser(null);}}
updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}/>
updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}
unreadMsgs={unreadMsgs}/>
)}
<main style={mainStyle}>
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile}/>}
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile} setActive={setActive} unreadMsgs={unreadMsgs}/>}
{active==='admin' && <AdminPanel toast={toast} mobile={mobile} user={user}/>}
{activeTool && <activeTool.component toast={toast} mobile={mobile}/>}
</main>
@@ -1233,7 +1363,8 @@ export default function App() {
{mobile && (
<BottomNav active={active} setActive={setActive} user={user}
onLogout={()=>{localStorage.removeItem('sk_token');setUser(null);}}
updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}/>
updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}
unreadMsgs={unreadMsgs}/>
)}
<Toast msg={toastMsg.msg} type={toastMsg.type}/>
{showUpd&&updateInfo&&<UpdateModal data={updateInfo} onClose={()=>setShowUpd(false)}/>}