Dateien nach "frontend/src" hochladen
This commit is contained in:
@@ -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)}/>}
|
||||
|
||||
Reference in New Issue
Block a user