diff --git a/backend/src/index.js b/backend/src/index.js index 2ae6824..47d7d95 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -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'); diff --git a/backend/src/routes/search.js b/backend/src/routes/search.js new file mode 100644 index 0000000..f9e0473 --- /dev/null +++ b/backend/src/routes/search.js @@ -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; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index df7f325..2408d7c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1430,7 +1430,222 @@ function ChangelogModal({ user, toast, onClose, onRead }) { ); } -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 ( +
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)'}}> + {icon} +
+
{primary}
+
{secondary}
+
+ {item.type==='link' && } + {(item.type==='tool'||item.type==='devtool') && } +
+ ); + }; + + const hasResults = allItems.length > 0; + const showDropdown = !!query.trim() && results !== null; + + return ( +
+
+ + 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 && ( + + )} +
+ + {/* Dropdown */} + {showDropdown && ( +
+ {loading && ( +
Suche…
+ )} + {!loading && !hasResults && ( +
+ Keine Treffer für „{query}" +
+ )} + {!loading && hasResults && ( + <> + {results.static?.length>0 && ( +
+
NAVIGATION
+ {results.static.map((item,i)=>)} +
+ )} + {results.links?.length>0 && ( +
+
LINKS
+ {results.links.map((item,i)=>)} +
+ )} + {results.snippets?.length>0 && ( +
+
CODE-SCHNIPSEL
+ {results.snippets.map((item,i)=>{ + const off=results.static.length+(results.links||[]).length; + return ; + })} +
+ )} + {results.calculations?.length>0 && ( +
+
3D-ARCHIV
+ {results.calculations.map((item,i)=>{ + const off=results.static.length+(results.links||[]).length+(results.snippets||[]).length; + return ; + })} +
+ )} + {results.orders?.length>0 && ( +
+
BESTELLUNGEN
+ {results.orders.map((item,i)=>{ + const off=results.static.length+(results.links||[]).length+(results.snippets||[]).length+(results.calculations||[]).length; + return ; + })} +
+ )} + + )} +
+ )} + + {/* Backdrop zum Schließen */} + {showDropdown && ( +
+ )} +
+ ); +} + + +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
{showBoard && setShowBoard(false)} onRead={onBoardRead}/>} {showChangelog && setShowChangelog(false)} onRead={onChangelogRead}/>} + + {/* Globale Suche */} +
@@ -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}/> )}
- {active==='dashboard' && { setUnreadBoard(0); setUnreadPromoted(0); }} unreadChangelog={unreadChangelog} onChangelogRead={()=>setUnreadChangelog(0)} user={user}/>} + {active==='dashboard' && { setUnreadBoard(0); setUnreadPromoted(0); }} unreadChangelog={unreadChangelog} onChangelogRead={()=>setUnreadChangelog(0)} user={user}/>} {active==='admin' && } - {activeTool && } + {activeTool && }
{mobile && ( diff --git a/frontend/src/tools/devtools.jsx b/frontend/src/tools/devtools.jsx index 45064a3..c60def4 100644 --- a/frontend/src/tools/devtools.jsx +++ b/frontend/src/tools/devtools.jsx @@ -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 });