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 ─────────────────────────────────────────────────────────
|
// ── 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' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user