Dateien nach "frontend/src" hochladen

This commit is contained in:
2026-05-25 23:29:25 +02:00
parent cccf4f4bb3
commit 91bd239587
3 changed files with 177 additions and 39 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { api, S } from './lib.js';
import { TOOLS, getGroupedTools } from './toolRegistry.js';
import { HomeIcon, AppsIcon, AdminIcon, MoreIcon, UserIcon, LogOutIcon, ChevronIcon, UpdateIcon } from './icons.jsx';
const ICONS = ['🔗','🌐','📊','📁','🛠','📧','🗓','💾','🖥','📡','🔒','📖','🎯','⚡','🏠','🔧','📱','🎮','🚀','💡'];
@@ -136,22 +137,25 @@ function UpdateModal({ data, onClose }) {
function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick }) {
const [appsOpen, setAppsOpen] = useState(true);
const NavBtn = ({ item, sub=false }) => (
<button onClick={()=>setActive(item.id)} style={{
width:'100%', display:'flex', alignItems:'center', gap:9,
padding: '8px 12px 8px 12px',
borderRadius:7, border:'none',
background: active===item.id ? 'linear-gradient(90deg,rgba(78,205,196,0.14),rgba(78,205,196,0.02))' : 'transparent',
color: active===item.id ? '#4ecdc4' : 'rgba(255,255,255,0.6)',
cursor:'pointer', fontSize:12,
fontFamily:"'Space Mono',monospace", textAlign:'left', marginBottom:1,
borderLeft: active===item.id ? '2px solid #4ecdc4' : '2px solid transparent',
}}>
{sub && <span style={{ width:1, alignSelf:'stretch', background:'rgba(255,255,255,0.08)', marginRight:4, flexShrink:0 }}/>}
<span style={{ fontSize:13 }}>{item.icon}</span>
<span style={{ flex:1 }}>{item.label}</span>
</button>
);
const NavBtn = ({ item, sub=false }) => {
const ic = active===item.id ? '#4ecdc4' : 'rgba(255,255,255,0.55)';
return (
<button onClick={()=>setActive(item.id)} style={{
width:'100%', display:'flex', alignItems:'center', gap:9,
padding:'8px 12px', borderRadius:7, border:'none',
background: active===item.id ? 'linear-gradient(90deg,rgba(78,205,196,0.14),rgba(78,205,196,0.02))' : 'transparent',
cursor:'pointer', fontSize:12,
fontFamily:"'Space Mono',monospace", textAlign:'left', marginBottom:1,
borderLeft: active===item.id ? '2px solid #4ecdc4' : '2px solid transparent',
}}>
{sub && <span style={{ width:1, alignSelf:'stretch', background:'rgba(255,255,255,0.08)', marginRight:3, flexShrink:0 }}/>}
<span style={{ display:'flex', alignItems:'center', width:18, justifyContent:'center', flexShrink:0 }}>
{item.Icon ? <item.Icon size={15} color={ic}/> : <span style={{color:ic,fontSize:13}}>{item.icon}</span>}
</span>
<span style={{ flex:1, color:ic }}>{item.label}</span>
</button>
);
};
return (
<aside style={{ width:210, minHeight:'100vh', background:'#0d0d0f',
@@ -169,7 +173,7 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
</div>
</div>
<nav style={{ flex:1, padding:'0 8px' }}>
<NavBtn item={{ id:'dashboard', icon:'⬡', label:'Dashboard' }} />
<NavBtn item={{ id:'dashboard', Icon:HomeIcon, label:'Dashboard' }} />
{/* Apps Sektion mit Gruppenüberschriften */}
<button onClick={()=>setAppsOpen(v=>!v)} style={{
@@ -191,7 +195,7 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
</div>
))}
{user?.role==='admin' && <NavBtn item={{ id:'admin', icon:'⚙', label:'Admin' }} />}
{user?.role==='admin' && <NavBtn item={{ id:'admin', icon:'⚙', label:'Admin' }} />}
</nav>
{updateInfo?.hasUpdate && (
<button onClick={onUpdateClick} style={{
@@ -207,7 +211,7 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
<div style={{ padding:'10px 12px 16px', borderTop:'1px solid rgba(255,255,255,0.06)' }}>
<div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:8 }}>
<div style={{ width:24, height:24, borderRadius:'50%', background:'linear-gradient(135deg,#ff6b9d,#4ecdc4)',
display:'flex', alignItems:'center', justifyContent:'center', fontSize:10 }}>👤</div>
display:'flex', alignItems:'center', justifyContent:'center' }}><UserIcon size={14} color="#0d0d0f"/></div>
<div style={{ color:'#fff', fontSize:11, fontFamily:'monospace' }}>{user?.username}</div>
</div>
<button onClick={onLogout} style={{ ...S.btn('#ff6b9d',true), width:'100%', textAlign:'center' }}>Ausloggen</button>
@@ -225,7 +229,7 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
const MI = ({icon, label, onClick, color='#fff'}) => (
<button onClick={onClick} style={{ width:'100%', padding:'11px 20px', background:'transparent',
border:'none', display:'flex', alignItems:'center', gap:10, cursor:'pointer' }}>
<span style={{fontSize:17, flexShrink:0, width:22, textAlign:'center', display:'inline-block', color:'rgba(255,255,255,0.85)'}}>{icon}</span>
<span style={{fontSize:17, flexShrink:0, width:22, textAlign:'center', display:'inline-block', }}>{icon}</span>
<span style={{color, fontFamily:'monospace', fontSize:13}}>{label}</span>
</button>
);
@@ -246,10 +250,10 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
);
const mainNav = [
{ id:'dashboard', icon:'⬡', label:'Home' },
{ id:'_apps', icon:'◫', label:'Apps' },
...(user?.role==='admin' ? [{ id:'admin', icon:'⚙', label:'Admin' }] : []),
{ id:'_mehr', icon:'⋯', label:'Mehr' },
{ id:'dashboard', Icon:HomeIcon, label:'Home' },
{ id:'_apps', Icon:AppsIcon, label:'Apps' },
...(user?.role==='admin' ? [{ id:'admin', Icon:AdminIcon, label:'Admin' }] : []),
{ id:'_mehr', Icon:MoreIcon, label:'Mehr' },
];
return (
@@ -262,7 +266,7 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
<div style={{ color:'rgba(255,255,255,0.28)', fontSize:9, fontFamily:'monospace',
letterSpacing:2, padding:'12px 20px 5px' }}>{group.toUpperCase()}</div>
{tools.map(t => (
<MI key={t.id} icon={t.icon} label={t.label}
<MI key={t.id} Icon={t.Icon} label={t.label}
onClick={() => { setActive(t.id); setAppsOpen(false); }} />
))}
</div>
@@ -272,10 +276,10 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
{/* Mehr Sheet */}
<Sheet open={menuOpen} onClose={()=>setMenuOpen(false)}>
{updateInfo?.hasUpdate && <MI icon="📦" label={`Update: ${updateInfo.latestVersion}`} onClick={()=>{setMenuOpen(false);onUpdateClick();}} color="#ffe66d"/>}
{updateInfo?.hasUpdate && <MI Icon={UpdateIcon} label={`Update: ${updateInfo.latestVersion}`} onClick={()=>{setMenuOpen(false);onUpdateClick();}} color="#ffe66d"/>}
<div style={{height:1, background:'rgba(255,255,255,0.08)', margin:'4px 0'}}/>
<MI icon="👤" label={user?.username||''} onClick={()=>{}} color="rgba(255,255,255,0.35)"/>
<MI icon="🚪" label="Ausloggen" onClick={()=>{onLogout();setMenuOpen(false);}} color="#ff6b9d"/>
<MI Icon={UserIcon} label={user?.username||''} onClick={()=>{}} color="rgba(255,255,255,0.35)"/>
<MI Icon={LogOutIcon} label="Ausloggen" onClick={()=>{onLogout();setMenuOpen(false);}} color="#ff6b9d"/>
</Sheet>
{/* Bottom Bar */}
@@ -301,7 +305,7 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
width:8, height:8, borderRadius:'50%', background:'#ffe66d',
boxShadow:'0 0 6px #ffe66d', display:'inline-block' }} />
)}
<span style={{ fontSize:20, lineHeight:1 }}>{item.icon}</span>
<item.Icon size={20} color={isActive ? '#4ecdc4' : 'rgba(255,255,255,0.45)'}/>
<span style={{ fontSize:9, fontFamily:'monospace', letterSpacing:0.3,
color: isActive ? '#4ecdc4' : 'rgba(255,255,255,0.4)' }}>{item.label}</span>
{isActive && !isApps && !isMehr && (

139
frontend/src/icons.jsx Normal file
View File

@@ -0,0 +1,139 @@
// ── Icon Library stroke-basierte SVG Icons ──────────────────────────────────
// Verwendung: <Icon name="home" size={20} color="#4ecdc4" />
// Oder direkt: <HomeIcon size={20} color="#fff" />
const base = (path, size, color, sw = 1.75) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none"
stroke={color} strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
{path}
</svg>
);
export const HomeIcon = ({size=20,color='currentColor',sw}) => base(<>
<path d="M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9.5z"/>
<path d="M9 21V12h6v9"/>
</>, size, color, sw);
export const AppsIcon = ({size=20,color='currentColor',sw}) => base(<>
<rect x="3" y="3" width="7" height="7" rx="1"/>
<rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/>
<rect x="14" y="14" width="7" height="7" rx="1"/>
</>, size, color, sw);
export const CalculatorIcon = ({size=20,color='currentColor',sw}) => base(<>
<rect x="4" y="2" width="16" height="20" rx="2"/>
<line x1="8" y1="7" x2="16" y2="7"/>
<line x1="8" y1="12" x2="10" y2="12"/>
<line x1="14" y1="12" x2="16" y2="12"/>
<line x1="8" y1="17" x2="10" y2="17"/>
<line x1="14" y1="17" x2="16" y2="17"/>
</>, size, color, sw);
export const ArchiveIcon = ({size=20,color='currentColor',sw}) => base(<>
<rect x="2" y="3" width="20" height="4" rx="1"/>
<path d="M4 7v13a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V7"/>
<line x1="9" y1="12" x2="15" y2="12"/>
</>, size, color, sw);
export const AdminIcon = ({size=20,color='currentColor',sw}) => base(<>
<circle cx="12" cy="12" r="3"/>
<path d="M12 2v2M12 20v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M2 12h2M20 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</>, size, color, sw);
export const MoreIcon = ({size=20,color='currentColor',sw}) => base(<>
<circle cx="5" cy="12" r="1.2" fill={color}/>
<circle cx="12" cy="12" r="1.2" fill={color}/>
<circle cx="19" cy="12" r="1.2" fill={color}/>
</>, size, color, sw);
export const UserIcon = ({size=20,color='currentColor',sw}) => base(<>
<circle cx="12" cy="8" r="4"/>
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
</>, size, color, sw);
export const LogOutIcon = ({size=20,color='currentColor',sw}) => base(<>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</>, size, color, sw);
export const SearchIcon = ({size=20,color='currentColor',sw}) => base(<>
<circle cx="11" cy="11" r="7"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</>, size, color, sw);
export const PlusIcon = ({size=20,color='currentColor',sw}) => base(<>
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</>, size, color, sw);
export const XIcon = ({size=20,color='currentColor',sw}) => base(<>
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</>, size, color, sw);
export const CheckIcon = ({size=20,color='currentColor',sw}) => base(<>
<polyline points="20 6 9 17 4 12"/>
</>, size, color, sw);
export const EditIcon = ({size=20,color='currentColor',sw}) => base(<>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</>, size, color, sw);
export const TrashIcon = ({size=20,color='currentColor',sw}) => base(<>
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
<path d="M10 11v6M14 11v6"/>
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
</>, size, color, sw);
export const DownloadIcon = ({size=20,color='currentColor',sw}) => base(<>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</>, size, color, sw);
export const UploadIcon = ({size=20,color='currentColor',sw}) => base(<>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</>, size, color, sw);
export const UpdateIcon = ({size=20,color='currentColor',sw}) => base(<>
<circle cx="12" cy="12" r="9"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</>, size, color, sw);
export const ChevronIcon = ({size=20,color='currentColor',sw,dir='right'}) => base(<>
{dir==='right' && <polyline points="9 18 15 12 9 6"/>}
{dir==='down' && <polyline points="6 9 12 15 18 9"/>}
</>, size, color, sw);
export const CameraIcon = ({size=20,color='currentColor',sw}) => base(<>
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
<circle cx="12" cy="13" r="4"/>
</>, size, color, sw);
export const KeyIcon = ({size=20,color='currentColor',sw}) => base(<>
<circle cx="8" cy="12" r="5"/>
<path d="M13 12h8M17 10v4"/>
</>, size, color, sw);
export const DatabaseIcon = ({size=20,color='currentColor',sw}) => base(<>
<ellipse cx="12" cy="5" rx="9" ry="3"/>
<path d="M3 5v4c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/>
<path d="M3 9v4c0 1.66 4.03 3 9 3s9-1.34 9-3V9"/>
<path d="M3 13v4c0 1.66 4.03 3 9 3s9-1.34 9-3v-4"/>
</>, size, color, sw);
export const LinkIcon = ({size=20,color='currentColor',sw}) => base(<>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</>, size, color, sw);
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"/>
</>, size, color, sw);

View File

@@ -1,17 +1,14 @@
import Kalkulator3D, { SavedList } from './tools/kalkulator3d.jsx';
import { CalculatorIcon, ArchiveIcon } from './icons.jsx';
// ══════════════════════════════════════════════════════════════════════════════
// Tool-Registry
//
// Neues Tool:
// 1. backend/src/tools/<id>/routes.js → automatisch geladen
// 2. frontend/src/tools/<id>.jsx
// 3. Hier eintragen, group = Bereichsüberschrift
// Neues Tool: id, icon (SVG-Komponente), label, navLabel, group, component
// ══════════════════════════════════════════════════════════════════════════════
export const TOOLS = [
{
id: 'kalkulator3d',
icon: '◈',
Icon: CalculatorIcon,
label: 'Kostenrechner',
navLabel: 'Kosten',
group: '3D-Druck',
@@ -19,16 +16,14 @@ export const TOOLS = [
},
{
id: 'kalkulator3d-saved',
icon: '◉',
Icon: ArchiveIcon,
label: 'Archiv',
navLabel: 'Archiv',
group: '3D-Druck',
component: SavedList,
},
// { id:'newtool', icon:'◇', label:'Mein Tool', navLabel:'Tool', group:'Mein Bereich', component:MeinTool },
];
// Werkzeug zum Gruppieren für Navigation
export function getGroupedTools() {
const groups = {};
for (const t of TOOLS) {
@@ -36,5 +31,5 @@ export function getGroupedTools() {
if (!groups[g]) groups[g] = [];
groups[g].push(t);
}
return Object.entries(groups); // [['3D-Druck', [...tools]], ...]
return Object.entries(groups);
}