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 CalendarWidget, { CalendarSettings } from './calendar.jsx';
|
||||||
import { DateiSettings } from './tools/dateien.jsx';
|
import { DateiSettings } from './tools/dateien.jsx';
|
||||||
import { useConfirm } from './confirm.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 = ['🔗','🌐','📊','📁','🛠','📧','🗓','💾','🖥','📡','🔒','📖','🎯','⚡','🏠','🔧','📱','🎮','🚀','💡'];
|
const ICONS = ['🔗','🌐','📊','📁','🛠','📧','🗓','💾','🖥','📡','🔒','📖','🎯','⚡','🏠','🔧','📱','🎮','🚀','💡'];
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ function UpdateModal({ data, onClose }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Desktop Sidebar ───────────────────────────────────────────────────────────
|
// ── 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 [appsOpen, setAppsOpen] = useState(true);
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
@@ -183,12 +183,21 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
|
|||||||
cursor:'pointer', fontSize:12,
|
cursor:'pointer', fontSize:12,
|
||||||
fontFamily:"'Space Mono',monospace", textAlign:'left', marginBottom:1,
|
fontFamily:"'Space Mono',monospace", textAlign:'left', marginBottom:1,
|
||||||
borderLeft: active===item.id ? '2px solid #4ecdc4' : '2px solid transparent',
|
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 }}/>}
|
{!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>}
|
{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>
|
</span>
|
||||||
{!collapsed && <span style={{ flex:1, color:ic }}>{item.label}</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>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -279,7 +288,7 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Mobile Bottom Navigation ──────────────────────────────────────────────────
|
// ── 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 [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [appsOpen, setAppsOpen] = useState(false);
|
const [appsOpen, setAppsOpen] = useState(false);
|
||||||
const NAV_H = 56;
|
const NAV_H = 56;
|
||||||
@@ -384,6 +393,11 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
|
|||||||
width:8, height:8, borderRadius:'50%', background:'#ffe66d',
|
width:8, height:8, borderRadius:'50%', background:'#ffe66d',
|
||||||
boxShadow:'0 0 6px #ffe66d', display:'inline-block' }} />
|
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; })()}
|
{(() => { 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,
|
<span style={{ fontSize:9, fontFamily:'monospace', letterSpacing:0.3,
|
||||||
color: isActive ? '#4ecdc4' : 'rgba(255,255,255,0.4)' }}>{item.label}</span>
|
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 (
|
return (
|
||||||
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
|
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
|
||||||
<div style={{ marginBottom:16 }}>
|
<div style={{ marginBottom:16 }}>
|
||||||
@@ -699,6 +815,7 @@ function Dashboard({ toast, mobile }) {
|
|||||||
<QuickLinks toast={toast} mobile={mobile} />
|
<QuickLinks toast={toast} mobile={mobile} />
|
||||||
<CalendarWidget/>
|
<CalendarWidget/>
|
||||||
<BestellStats/>
|
<BestellStats/>
|
||||||
|
<NachrichtenWidget unreadMsgs={unreadMsgs} setActive={setActive}/>
|
||||||
<TodoList toast={toast} />
|
<TodoList toast={toast} />
|
||||||
<Notepad toast={toast} />
|
<Notepad toast={toast} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1111,6 +1228,9 @@ function AdminPanel({ toast, mobile, user }) {
|
|||||||
</p>
|
</p>
|
||||||
<DeleteAccountSection toast={toast} user={user}/>
|
<DeleteAccountSection toast={toast} user={user}/>
|
||||||
</Sec>
|
</Sec>
|
||||||
|
<Sec title="PUSHOVER BENACHRICHTIGUNGEN">
|
||||||
|
<PushoverSettings toast={toast}/>
|
||||||
|
</Sec>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1171,6 +1291,7 @@ export default function App() {
|
|||||||
const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
|
const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
|
||||||
const [updateInfo,setUpdateInfo]=useState(null);
|
const [updateInfo,setUpdateInfo]=useState(null);
|
||||||
const [showUpd,setShowUpd]=useState(false);
|
const [showUpd,setShowUpd]=useState(false);
|
||||||
|
const [unreadMsgs,setUnreadMsgs]=useState(0);
|
||||||
|
|
||||||
const [authChecked, setAuthChecked] = useState(false);
|
const [authChecked, setAuthChecked] = useState(false);
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
@@ -1192,6 +1313,14 @@ export default function App() {
|
|||||||
return()=>clearInterval(t);
|
return()=>clearInterval(t);
|
||||||
},[user]);
|
},[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')=>{
|
const toast=useCallback((msg,type='success')=>{
|
||||||
setToastMsg({msg,type});
|
setToastMsg({msg,type});
|
||||||
setTimeout(()=>setToastMsg({msg:'',type:'success'}),3500);
|
setTimeout(()=>setToastMsg({msg:'',type:'success'}),3500);
|
||||||
@@ -1222,10 +1351,11 @@ export default function App() {
|
|||||||
{!mobile && (
|
{!mobile && (
|
||||||
<Sidebar active={active} setActive={setActive} user={user}
|
<Sidebar active={active} setActive={setActive} user={user}
|
||||||
onLogout={()=>{localStorage.removeItem('sk_token');setUser(null);}}
|
onLogout={()=>{localStorage.removeItem('sk_token');setUser(null);}}
|
||||||
updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}/>
|
updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}
|
||||||
|
unreadMsgs={unreadMsgs}/>
|
||||||
)}
|
)}
|
||||||
<main style={mainStyle}>
|
<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}/>}
|
{active==='admin' && <AdminPanel toast={toast} mobile={mobile} user={user}/>}
|
||||||
{activeTool && <activeTool.component toast={toast} mobile={mobile}/>}
|
{activeTool && <activeTool.component toast={toast} mobile={mobile}/>}
|
||||||
</main>
|
</main>
|
||||||
@@ -1233,7 +1363,8 @@ export default function App() {
|
|||||||
{mobile && (
|
{mobile && (
|
||||||
<BottomNav active={active} setActive={setActive} user={user}
|
<BottomNav active={active} setActive={setActive} user={user}
|
||||||
onLogout={()=>{localStorage.removeItem('sk_token');setUser(null);}}
|
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}/>
|
<Toast msg={toastMsg.msg} type={toastMsg.type}/>
|
||||||
{showUpd&&updateInfo&&<UpdateModal data={updateInfo} onClose={()=>setShowUpd(false)}/>}
|
{showUpd&&updateInfo&&<UpdateModal data={updateInfo} onClose={()=>setShowUpd(false)}/>}
|
||||||
|
|||||||
@@ -139,3 +139,6 @@ export const LinkIcon = ({size=20,color='currentColor',sw}) => base(<>
|
|||||||
export const ZapIcon = ({size=20,color='currentColor',sw}) => base(<>
|
export const ZapIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
||||||
</>, size, color, sw);
|
</>, size, color, sw);
|
||||||
|
export const MessageIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinejoin="round" strokeLinecap="round"/>
|
||||||
|
</>, size, color, sw);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import Kalkulator3D, { SavedList } from './tools/kalkulator3d.jsx';
|
import Kalkulator3D, { SavedList } from './tools/kalkulator3d.jsx';
|
||||||
import Bestellungen from './tools/bestellungen.jsx';
|
import Bestellungen from './tools/bestellungen.jsx';
|
||||||
import Dateien from './tools/dateien.jsx';
|
import Dateien from './tools/dateien.jsx';
|
||||||
import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon } from './icons.jsx';
|
import Nachrichten from './tools/nachrichten.jsx';
|
||||||
|
import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon } from './icons.jsx';
|
||||||
|
|
||||||
export const TOOLS = [
|
export const TOOLS = [
|
||||||
{
|
{
|
||||||
@@ -36,6 +37,14 @@ export const TOOLS = [
|
|||||||
group: 'Werkzeuge',
|
group: 'Werkzeuge',
|
||||||
component: Dateien,
|
component: Dateien,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'nachrichten',
|
||||||
|
Icon: MessageIcon,
|
||||||
|
label: 'Nachrichten',
|
||||||
|
navLabel: 'Nachrichten',
|
||||||
|
group: 'Werkzeuge',
|
||||||
|
component: Nachrichten,
|
||||||
|
},
|
||||||
// Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... },
|
// Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user