Dashboard: Globale Suche mit Live-Ergebnissen fuer Tools, Links, Code, Archiv, Bestellungen

This commit is contained in:
2026-06-05 08:50:00 +02:00
parent e9e639b654
commit 90fed8fb5f
4 changed files with 280 additions and 4 deletions

View File

@@ -68,6 +68,7 @@ fs.readdirSync(toolsDir, { withFileTypes: true })
app.use('/api/tools/snippets', require('./tools/snippets/routes'));
app.use('/api/tools/qrcodes', require('./tools/qrcodes/routes'));
app.use('/api/search', require('./routes/search'));
const PUBLIC = path.join(__dirname, '../public');

View File

@@ -0,0 +1,48 @@
const express = require('express');
const db = require('../db');
const { authenticate } = require('../middleware/auth');
const router = express.Router();
router.get('/', authenticate, (req, res) => {
const q = (req.query.q || '').trim();
if (q.length < 2) return res.json({ links:[], snippets:[], calculations:[], orders:[] });
const uid = req.user.id;
const like = `%${q.toLowerCase()}%`;
const links = db.prepare(`
SELECT id, title, url, icon, description, folder_id FROM link_list
WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(url) LIKE ? OR LOWER(description) LIKE ?)
ORDER BY title LIMIT 8
`).all(uid, like, like, like);
let snippets = [];
try {
snippets = db.prepare(`
SELECT id, title, description, language FROM snippets
WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(description) LIKE ?)
ORDER BY title LIMIT 5
`).all(uid, like, like);
} catch {}
let calculations = [];
try {
calculations = db.prepare(`
SELECT id, name FROM calculations
WHERE user_id=? AND LOWER(name) LIKE ?
ORDER BY name LIMIT 5
`).all(uid, like);
} catch {}
let orders = [];
try {
orders = db.prepare(`
SELECT id, name, status FROM orders
WHERE user_id=? AND LOWER(name) LIKE ?
ORDER BY name LIMIT 5
`).all(uid, like);
} catch {}
res.json({ links, snippets, calculations, orders });
});
module.exports = router;

View File

@@ -1430,7 +1430,222 @@ function ChangelogModal({ user, toast, onClose, onRead }) {
</div>
);
}
function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, unreadChangelog=0, onChangelogRead, user }) {
// ── Globale Suche ─────────────────────────────────────────────────────────────
// Statischer Such-Index für Tools & Dev-Tools Sub-Tools
const SEARCH_TOOL_INDEX = [
// Haupt-Tools
{type:'tool',label:'Dashboard',icon:'🏠',sub:'',id:'dashboard'},
{type:'tool',label:'Bestellungen',icon:'📦',sub:'3D-Druck',id:'bestellungen'},
{type:'tool',label:'Kalkulator3D',icon:'🖨',sub:'3D-Druck',id:'kalkulator3d'},
{type:'tool',label:'Dateien',icon:'📁',sub:'Werkzeuge',id:'dateien'},
{type:'tool',label:'Nachrichten',icon:'💬',sub:'Werkzeuge',id:'nachrichten'},
{type:'tool',label:'Linkliste',icon:'🔗',sub:'Werkzeuge',id:'linkliste'},
{type:'tool',label:'Code-Schnipsel',icon:'📄',sub:'Werkzeuge',id:'codeschnipsel'},
{type:'tool',label:'CAD-Skizzen',icon:'📐',sub:'Werkzeuge',id:'skizze'},
{type:'tool',label:'Dev-Tools',icon:'🔧',sub:'Werkzeuge',id:'devtools'},
{type:'tool',label:'Einstellungen',icon:'⚙️',sub:'',id:'admin'},
// Dev-Tools Sub-Tools
{type:'devtool',label:'Crontab',icon:'⏱',sub:'Dev-Tools',tool:'cron'},
{type:'devtool',label:'Elektro-Rechner',icon:'⚡',sub:'Dev-Tools',tool:'elektro'},
{type:'devtool',label:'JSON Viewer',icon:'{}',sub:'Dev-Tools',tool:'json'},
{type:'devtool',label:'Regex Builder',icon:'🔍',sub:'Dev-Tools',tool:'regex'},
{type:'devtool',label:'Text Diff',icon:'±',sub:'Dev-Tools',tool:'diff'},
{type:'devtool',label:'Netzwerk',icon:'🌐',sub:'Dev-Tools',tool:'netzwerk'},
{type:'devtool',label:'Datetime',icon:'📅',sub:'Dev-Tools',tool:'datetime'},
{type:'devtool',label:'Farben',icon:'🎨',sub:'Dev-Tools',tool:'farben'},
{type:'devtool',label:'Key-Generator',icon:'🔐',sub:'Dev-Tools',tool:'keygen'},
{type:'devtool',label:'QR-Code Generator',icon:'◻',sub:'Dev-Tools',tool:'qrcode'},
];
function GlobalSearch({ setActive, setDevToolsNav, mobile }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState(null);
const [active, setActiveR] = useState(-1); // keyboard nav
const [loading, setLoading] = useState(false);
const inputRef = useRef(null);
const listRef = useRef(null);
const debounce = useRef(null);
const close = () => { setQuery(''); setResults(null); setActiveR(-1); };
// Static matches
function matchStatic(q) {
const lq = q.toLowerCase();
return SEARCH_TOOL_INDEX.filter(t =>
t.label.toLowerCase().includes(lq) ||
t.sub.toLowerCase().includes(lq) ||
(t.tool||t.id||'').toLowerCase().includes(lq)
).slice(0, 6);
}
useEffect(() => {
if (!query.trim()) { setResults(null); setActiveR(-1); return; }
clearTimeout(debounce.current);
debounce.current = setTimeout(async () => {
const staticMatches = matchStatic(query);
if (query.trim().length < 2) { setResults({static:staticMatches,links:[],snippets:[],calculations:[],orders:[]}); return; }
setLoading(true);
try {
const d = await api(`/search?q=${encodeURIComponent(query.trim())}`);
setResults({ static:staticMatches, ...d });
} catch { setResults({static:staticMatches,links:[],snippets:[],calculations:[],orders:[]}); }
setLoading(false);
}, 250);
}, [query]);
// Alle Einträge flach für Keyboard-Nav
const allItems = results ? [
...results.static,
...(results.links||[]).map(l=>({...l,type:'link'})),
...(results.snippets||[]).map(s=>({...s,type:'snippet'})),
...(results.calculations||[]).map(c=>({...c,type:'calc'})),
...(results.orders||[]).map(o=>({...o,type:'order'})),
] : [];
const navigate = (item) => {
close();
if (item.type === 'tool') { setActive(item.id); }
else if (item.type === 'devtool') { setActive('devtools'); setDevToolsNav({tool: item.tool, ts: Date.now()}); }
else if (item.type === 'link') { window.open(item.url, '_blank', 'noopener'); }
else if (item.type === 'snippet') { setActive('codeschnipsel'); }
else if (item.type === 'calc') { setActive('kalkulator3d'); }
else if (item.type === 'order') { setActive('bestellungen'); }
};
const onKey = e => {
if (!results) return;
if (e.key === 'ArrowDown') { e.preventDefault(); setActiveR(p => Math.min(p+1, allItems.length-1)); }
else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveR(p => Math.max(p-1, -1)); }
else if (e.key === 'Enter' && active >= 0 && allItems[active]) { navigate(allItems[active]); }
else if (e.key === 'Escape') { close(); inputRef.current?.blur(); }
};
const ResultItem = ({item, idx}) => {
const isActive = idx === active;
let icon = '🔍', primary = '', secondary = '';
if (item.type==='tool') { icon=item.icon; primary=item.label; secondary=item.sub||'Navigation'; }
if (item.type==='devtool') { icon=item.icon; primary=item.label; secondary='Dev-Tools'; }
if (item.type==='link') {
const hasCustomIcon = item.icon && item.icon!=='🔗';
icon = hasCustomIcon ? item.icon : '🔗';
primary = item.title; secondary = item.url;
}
if (item.type==='snippet') { icon='📄'; primary=item.title; secondary=`Code · ${item.language||''}`; }
if (item.type==='calc') { icon='🖨'; primary=item.name; secondary='3D-Archiv'; }
if (item.type==='order') { icon='📦'; primary=item.name; secondary=`Bestellung · ${item.status||''}`; }
return (
<div onClick={()=>navigate(item)}
onMouseEnter={()=>setActiveR(idx)}
style={{display:'flex',alignItems:'center',gap:10,padding:'9px 14px',cursor:'pointer',
background:isActive?'rgba(78,205,196,0.1)':'transparent',
borderBottom:'1px solid rgba(255,255,255,0.04)'}}>
<span style={{fontSize:16,flexShrink:0,width:24,textAlign:'center'}}>{icon}</span>
<div style={{flex:1,minWidth:0}}>
<div style={{color:isActive?'#4ecdc4':'#fff',fontFamily:'monospace',fontSize:13,fontWeight:500,
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{primary}</div>
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10,
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{secondary}</div>
</div>
{item.type==='link' && <span style={{color:'rgba(255,255,255,0.2)',fontSize:10,flexShrink:0}}></span>}
{(item.type==='tool'||item.type==='devtool') && <span style={{color:'rgba(255,255,255,0.2)',fontSize:10,flexShrink:0}}></span>}
</div>
);
};
const hasResults = allItems.length > 0;
const showDropdown = !!query.trim() && results !== null;
return (
<div style={{position:'relative',marginBottom:16}}>
<div style={{position:'relative'}}>
<span style={{position:'absolute',left:12,top:'50%',transform:'translateY(-50%)',
color:'rgba(255,255,255,0.4)',fontSize:16,pointerEvents:'none'}}></span>
<input
ref={inputRef}
value={query}
onChange={e=>setQuery(e.target.value)}
onKeyDown={onKey}
placeholder="Suchen in der App… Tools, Links, Code, Bestellungen"
style={{...S.inp, paddingLeft:38, paddingRight: query?36:14, width:'100%', boxSizing:'border-box', fontSize:14}}
/>
{!!query && (
<button onClick={close} style={{position:'absolute',right:10,top:'50%',transform:'translateY(-50%)',
background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18,padding:'0 2px'}}></button>
)}
</div>
{/* Dropdown */}
{showDropdown && (
<div ref={listRef} style={{
position:'absolute',top:'calc(100% + 6px)',left:0,right:0,zIndex:9000,
background:'#1a1a1e',borderRadius:12,border:'1px solid rgba(255,255,255,0.12)',
boxShadow:'0 8px 32px rgba(0,0,0,0.6)',maxHeight:'60vh',overflowY:'auto',
}}>
{loading && (
<div style={{padding:'12px 14px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11}}>Suche</div>
)}
{!loading && !hasResults && (
<div style={{padding:'16px 14px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:12,textAlign:'center'}}>
Keine Treffer für {query}"
</div>
)}
{!loading && hasResults && (
<>
{results.static?.length>0 && (
<div>
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>NAVIGATION</div>
{results.static.map((item,i)=><ResultItem key={`s${i}`} item={item} idx={i}/>)}
</div>
)}
{results.links?.length>0 && (
<div>
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>LINKS</div>
{results.links.map((item,i)=><ResultItem key={`l${i}`} item={item} idx={results.static.length+i}/>)}
</div>
)}
{results.snippets?.length>0 && (
<div>
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>CODE-SCHNIPSEL</div>
{results.snippets.map((item,i)=>{
const off=results.static.length+(results.links||[]).length;
return <ResultItem key={`sn${i}`} item={item} idx={off+i}/>;
})}
</div>
)}
{results.calculations?.length>0 && (
<div>
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>3D-ARCHIV</div>
{results.calculations.map((item,i)=>{
const off=results.static.length+(results.links||[]).length+(results.snippets||[]).length;
return <ResultItem key={`c${i}`} item={item} idx={off+i}/>;
})}
</div>
)}
{results.orders?.length>0 && (
<div>
<div style={{padding:'7px 14px 4px',color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1}}>BESTELLUNGEN</div>
{results.orders.map((item,i)=>{
const off=results.static.length+(results.links||[]).length+(results.snippets||[]).length+(results.calculations||[]).length;
return <ResultItem key={`o${i}`} item={item} idx={off+i}/>;
})}
</div>
)}
</>
)}
</div>
)}
{/* Backdrop zum Schließen */}
{showDropdown && (
<div style={{position:'fixed',inset:0,zIndex:8999}} onClick={close}/>
)}
</div>
);
}
function Dashboard({ toast, mobile, setActive, setDevToolsNav, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, unreadChangelog=0, onChangelogRead, user }) {
const [now, setNow] = useState(new Date());
const [showBoard, setShowBoard] = useState(false);
const [showChangelog, setShowChangelog] = useState(false);
@@ -1439,6 +1654,9 @@ function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unre
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
{showBoard && <BoardModal user={user} toast={toast} onClose={()=>setShowBoard(false)} onRead={onBoardRead}/>}
{showChangelog && <ChangelogModal user={user} toast={toast} onClose={()=>setShowChangelog(false)} onRead={onChangelogRead}/>}
{/* Globale Suche */}
<GlobalSearch setActive={setActive} setDevToolsNav={setDevToolsNav} mobile={mobile}/>
<div style={{ marginBottom:16, display:'flex', alignItems:'flex-start', justifyContent:'space-between' }}>
<div>
<div style={{ display:'flex', alignItems:'center', gap:10 }}>
@@ -2360,6 +2578,7 @@ export default function App() {
});
const [user,setUser]=useState(null);
const [active,setActiveRaw]=useState('dashboard');
const [devToolsNav, setDevToolsNav] = useState(null);
const setActive = (page) => {
// Reload nur beim Klick auf Dashboard und nur wenn ein neuer Build vorliegt
// und keine Eingabe aktiv ist (schützt ungespeicherte Eingaben)
@@ -2462,9 +2681,9 @@ export default function App() {
unreadMsgs={unreadMsgs}/>
)}
<main style={mainStyle}>
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile} setActive={setActive} unreadMsgs={unreadMsgs} unreadBoard={unreadBoard} unreadPromoted={unreadPromoted} onBoardRead={()=>{ setUnreadBoard(0); setUnreadPromoted(0); }} unreadChangelog={unreadChangelog} onChangelogRead={()=>setUnreadChangelog(0)} user={user}/>}
{active==='dashboard' && <Dashboard toast={toast} mobile={mobile} setActive={setActive} setDevToolsNav={setDevToolsNav} unreadMsgs={unreadMsgs} unreadBoard={unreadBoard} unreadPromoted={unreadPromoted} onBoardRead={()=>{ setUnreadBoard(0); setUnreadPromoted(0); }} unreadChangelog={unreadChangelog} onChangelogRead={()=>setUnreadChangelog(0)} user={user}/>}
{active==='admin' && <AdminPanel toast={toast} mobile={mobile} user={user}/>}
{activeTool && <activeTool.component toast={toast} mobile={mobile} setActive={setActive}/>}
{activeTool && <activeTool.component toast={toast} mobile={mobile} setActive={setActive} {...(activeTool.id==='devtools'?{nav:devToolsNav}:{})}/>}
</main>
</div>
{mobile && (

View File

@@ -3089,9 +3089,17 @@ const TOOL_DEFS = [
{id:'qrcode', label:'◻ QR-Code', desc:'QR-Codes erstellen & verwalten', ready:true, component:QrGenerator},
];
export default function DevTools({ toast, mobile }) {
export default function DevTools({ toast, mobile, nav }) {
const [activeTool, setActiveTool] = useState('cron');
const [activeTab, setActiveTab] = useState('explain');
// Von außen navigieren (z.B. über Suche)
useEffect(() => {
if (nav?.tool) {
setActiveTool(nav.tool);
setActiveTab('');
}
}, [nav?.ts]);
const chipScrollRef = useRef(null);
const [chipScroll, setChipScroll] = useState({ left: false, right: true });