Schnellzugriff Mobile: Pfeile nur wenn wischbar, overflow:hidden, Seitenindikatoren

This commit is contained in:
2026-05-29 10:52:26 +02:00
parent 4f2c06f817
commit f01ee37280

View File

@@ -435,6 +435,112 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
// ── Dashboard Widgets ───────────────────────────────────────────────────────── // ── Dashboard Widgets ─────────────────────────────────────────────────────────
// ── QuickLinks Carousel (Mobile) ─────────────────────────────────────────────
function QuickLinksCarousel({ links }) {
const iconW = 58; const gap = 6;
const containerW = window.innerWidth - 28;
const perRow = Math.max(3, Math.floor((containerW + gap) / (iconW + gap)));
const pageSize = perRow * 2;
const pageCount = Math.ceil(links.length / pageSize);
const [page, setPage] = useState(0);
const scrollRef = useRef(null);
// Sync scroll position → page indicator
const onScroll = () => {
if (!scrollRef.current) return;
const p = Math.round(scrollRef.current.scrollLeft / containerW);
setPage(p);
};
const canLeft = page > 0;
const canRight = page < pageCount - 1;
const scrollTo = (p) => {
scrollRef.current?.scrollTo({ left: p * containerW, behavior:'smooth' });
};
if (pageCount <= 1) return (
<div style={{ display:'grid', gridTemplateColumns:`repeat(${perRow}, 1fr)`, gap }}>
{links.map(l => <QuickLinkIcon key={l.id} l={l}/>)}
</div>
);
return (
<div style={{ position:'relative' }}>
{/* Scroll container overflow hidden so nothing peeks */}
<div ref={scrollRef} onScroll={onScroll} style={{
display:'flex', overflowX:'auto', scrollSnapType:'x mandatory',
scrollbarWidth:'none', WebkitOverflowScrolling:'touch',
marginInline:-14, paddingInline:14,
}}>
{Array.from({length: pageCount}, (_,pi) => (
<div key={pi} style={{
display:'grid',
gridTemplateColumns:`repeat(${perRow}, 1fr)`,
gridTemplateRows:'repeat(2, auto)',
gap, flexShrink:0, scrollSnapAlign:'start',
width: containerW,
}}>
{links.slice(pi*pageSize, (pi+1)*pageSize).map(l => <QuickLinkIcon key={l.id} l={l}/>)}
</div>
))}
</div>
{/* Left arrow */}
{canLeft && (
<button onClick={() => scrollTo(page-1)} style={{
position:'absolute', left:-10, top:'50%', transform:'translateY(-50%)',
background:'rgba(0,0,0,0.55)', border:'1px solid rgba(255,255,255,0.12)',
borderRadius:'50%', width:24, height:24, cursor:'pointer',
color:'rgba(255,255,255,0.6)', fontSize:13, display:'flex',
alignItems:'center', justifyContent:'center', zIndex:2,
}}></button>
)}
{/* Right arrow */}
{canRight && (
<button onClick={() => scrollTo(page+1)} style={{
position:'absolute', right:-10, top:'50%', transform:'translateY(-50%)',
background:'rgba(0,0,0,0.55)', border:'1px solid rgba(255,255,255,0.12)',
borderRadius:'50%', width:24, height:24, cursor:'pointer',
color:'rgba(255,255,255,0.6)', fontSize:13, display:'flex',
alignItems:'center', justifyContent:'center', zIndex:2,
}}></button>
)}
{/* Page dots */}
{pageCount > 1 && (
<div style={{ display:'flex', justifyContent:'center', gap:4, marginTop:6 }}>
{Array.from({length:pageCount}, (_,i) => (
<div key={i} onClick={() => scrollTo(i)} style={{
width: i===page ? 14 : 5, height:5, borderRadius:3, cursor:'pointer',
background: i===page ? '#4ecdc4' : 'rgba(255,255,255,0.2)',
transition:'all 0.2s',
}}/>
))}
</div>
)}
</div>
);
}
function QuickLinkIcon({ l }) {
return (
<a href={l.url} target="_blank" rel="noopener noreferrer"
style={{ display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:3,
padding:'4px 2px', background:'rgba(255,255,255,0.04)',
border:'1px solid rgba(255,255,255,0.08)', borderRadius:10,
textDecoration:'none', height:48, cursor:'pointer' }}>
{l.icon && l.icon.startsWith('http')
? <img src={l.icon} alt="" style={{width:16,height:16,objectFit:'contain'}} onError={e=>e.target.style.display='none'}/>
: <span style={{ fontSize:16 }}>{l.icon}</span>}
<span style={{ color:'rgba(255,255,255,0.65)', fontSize:8, fontFamily:'monospace',
maxWidth:52, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{l.title}</span>
</a>
);
}
function QuickLinks({ toast, mobile }) { function QuickLinks({ toast, mobile }) {
const { confirm, ConfirmDialog } = useConfirm(); const { confirm, ConfirmDialog } = useConfirm();
const [links, setLinks] = useState([]); const [links, setLinks] = useState([]);
@@ -485,54 +591,8 @@ function QuickLinks({ toast, mobile }) {
{/* Link-Grid */} {/* Link-Grid */}
{links.length === 0 && !editMode {links.length === 0 && !editMode
? <div style={{ color:'rgba(255,255,255,0.5)', fontSize:11, fontFamily:'monospace', padding:'6px 0' }}>Links können unter Einstellungen Dashboard hinzugefügt werden.</div> ? <div style={{ color:'rgba(255,255,255,0.5)', fontSize:11, fontFamily:'monospace', padding:'6px 0' }}>Links können unter Einstellungen Dashboard hinzugefügt werden.</div>
: mobile ? (() => { : mobile
// Mobile: max 2 Zeilen, dann horizontal scrollbar ? <QuickLinksCarousel links={links}/>
// Berechne Icons pro Zeile anhand der Viewport-Breite
const iconW = 58 + 6; // minWidth + gap
const containerW = window.innerWidth - 28 - 28; // padding links+rechts (S.card=14px*2)
const perRow = Math.max(3, Math.floor(containerW / iconW));
const maxVisible = perRow * 2;
const needsScroll = links.length > maxVisible;
const pages = needsScroll
? Math.ceil(links.length / maxVisible)
: 1;
// Gruppen zu je maxVisible Icons
const groups = [];
for (let i = 0; i < links.length; i += maxVisible) groups.push(links.slice(i, i + maxVisible));
return (
<div style={{ overflowX:'auto', scrollSnapType:'x mandatory',
display:'flex', gap:0, marginInline:-14, paddingInline:14,
scrollbarWidth:'none', WebkitOverflowScrolling:'touch' }}>
{groups.map((group, gi) => (
<div key={gi} style={{
display:'grid',
gridTemplateColumns:`repeat(${perRow}, ${iconW-6}px)`,
gridTemplateRows:'repeat(2, auto)',
gap:6,
flexShrink:0,
scrollSnapAlign:'start',
paddingRight: gi < groups.length-1 ? 14 : 0,
}}>
{group.map(l => (
<div key={l.id}>
<a href={l.url} target="_blank" rel="noopener noreferrer"
style={{ display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:3,
padding:'4px 2px', background:'rgba(255,255,255,0.04)',
border:'1px solid rgba(255,255,255,0.08)', borderRadius:10,
textDecoration:'none', height:48, cursor:'pointer', width:'100%' }}>
{l.icon && l.icon.startsWith('http')
? <img src={l.icon} alt="" style={{width:16,height:16,objectFit:'contain'}} onError={e=>e.target.style.display='none'}/>
: <span style={{ fontSize:16 }}>{l.icon}</span>}
<span style={{ color:'rgba(255,255,255,0.65)', fontSize:8, fontFamily:'monospace',
maxWidth:52, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{l.title}</span>
</a>
</div>
))}
</div>
))}
</div>
);
})()
: <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(58px,1fr))', gap:6 }}> : <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(58px,1fr))', gap:6 }}>
{links.map(l => ( {links.map(l => (
<div key={l.id} style={{ position:'relative' }}> <div key={l.id} style={{ position:'relative' }}>