Schnellzugriff Mobile: Pfeile nur wenn wischbar, overflow:hidden, Seitenindikatoren
This commit is contained in:
@@ -435,6 +435,112 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
|
||||
|
||||
|
||||
// ── 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 }) {
|
||||
const { confirm, ConfirmDialog } = useConfirm();
|
||||
const [links, setLinks] = useState([]);
|
||||
@@ -485,54 +591,8 @@ function QuickLinks({ toast, mobile }) {
|
||||
{/* Link-Grid */}
|
||||
{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>
|
||||
: mobile ? (() => {
|
||||
// Mobile: max 2 Zeilen, dann horizontal scrollbar
|
||||
// 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>
|
||||
);
|
||||
})()
|
||||
: mobile
|
||||
? <QuickLinksCarousel links={links}/>
|
||||
: <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(58px,1fr))', gap:6 }}>
|
||||
{links.map(l => (
|
||||
<div key={l.id} style={{ position:'relative' }}>
|
||||
|
||||
Reference in New Issue
Block a user