Enhance ContributionCalendar component for mobile responsiveness and user experience

- Added mobile detection and state management to handle screen size changes.
- Implemented a scroll indicator for mobile users to enhance navigation.
- Introduced a toggle button for expanding the calendar view to show the full year.
- Adjusted the rendering logic to display a limited number of weeks based on the mobile state.
This commit is contained in:
2025-06-07 12:26:56 +02:00
parent 7b2975238b
commit 7ce6c1390f
2 changed files with 211 additions and 52 deletions

View File

@@ -65,6 +65,8 @@ export default function ContributionCalendar({ data }) {
const days = getCalendarDays();
const monthLabels = getMonthLabels(days);
const [isDark, setIsDark] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [expanded, setExpanded] = useState(false);
useEffect(() => {
// Detect dark mode by checking for 'dark' class on <html>
@@ -77,6 +79,54 @@ export default function ContributionCalendar({ data }) {
return () => observer.disconnect();
}, []);
useEffect(() => {
// Detect mobile screen size
const checkMobile = () => {
setIsMobile(window.innerWidth <= 640); // Tailwind's sm breakpoint
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
// Determine how many weeks to show
const weeksToShow = isMobile && !expanded ? 20 : 52;
const startWeek = 52 - weeksToShow;
const visibleDays = days.slice(startWeek * 7, 52 * 7);
const visibleMonthLabels = monthLabels.filter(l => l.week >= startWeek);
// Scroll indicator for mobile
const [showScrollHint, setShowScrollHint] = useState(false);
const calendarRef = React.useRef(null);
useEffect(() => {
if (!isMobile) return;
const el = calendarRef.current;
if (!el) return;
const checkScroll = () => {
setShowScrollHint(el.scrollWidth > el.clientWidth && el.scrollLeft < 16);
};
checkScroll();
el.addEventListener('scroll', checkScroll);
window.addEventListener('resize', checkScroll);
return () => {
el.removeEventListener('scroll', checkScroll);
window.removeEventListener('resize', checkScroll);
};
}, [isMobile, expanded]);
// When expanding to full year on mobile, scroll to the right (most recent weeks)
useEffect(() => {
if (!isMobile) return;
const el = calendarRef.current;
if (!el) return;
if (expanded) {
// Wait for the DOM to update, then scroll
setTimeout(() => {
el.scrollLeft = el.scrollWidth;
}, 100);
}
}, [expanded, isMobile]);
// Get max count for scaling (optional, for more dynamic color)
// const max = Math.max(...Object.values(data));
@@ -99,14 +149,33 @@ export default function ContributionCalendar({ data }) {
alignItems: 'center',
justifyContent: 'center',
minHeight: 160,
position: 'relative',
}}
className="contribution-calendar"
ref={calendarRef}
>
{/* Scroll indicator for mobile */}
{isMobile && showScrollHint && (
<div style={{
position: 'absolute',
top: 8,
right: 16,
zIndex: 10,
background: isDark ? 'rgba(30,41,59,0.85)' : 'rgba(255,255,255,0.7)',
borderRadius: 8,
padding: '2px 8px',
fontSize: 12,
color: isDark ? '#b6c2d1' : '#555',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
}}>
<span role="img" aria-label="scroll"></span> Scroll
</div>
)}
{/* Month labels */}
<div style={{ display: 'flex', marginBottom: 4, justifyContent: 'center' }}>
<div style={{ width: 44 }} />
{Array.from({ length: 52 }).map((_, weekIdx) => {
const label = monthLabels.find((l) => l.week === weekIdx);
{Array.from({ length: weeksToShow }).map((_, weekIdx) => {
const label = visibleMonthLabels.find((l) => l.week === weekIdx + startWeek);
return (
<div
key={weekIdx}
@@ -146,13 +215,14 @@ export default function ContributionCalendar({ data }) {
</div>
{/* Weeks (columns) */}
<div style={{ display: 'flex' }}>
{Array.from({ length: 52 }).map((_, weekIdx) => (
{Array.from({ length: weeksToShow }).map((_, weekIdx) => (
<div key={weekIdx} style={{ display: 'flex', flexDirection: 'column', flex: '0 0 14px' }}>
{/* Days (rows) */}
{Array.from({ length: 7 }).map((_, dayIdx) => {
// Shift the dayIdx so that Monday is the first row and Sunday is the last
const shiftedDayIdx = (dayIdx + 1) % 7;
const day = days[weekIdx * 7 + shiftedDayIdx];
const day = visibleDays[weekIdx * 7 + shiftedDayIdx];
if (!day) return <div key={dayIdx} style={{ width: 12, height: 12, margin: 1 }} />;
const dateStr = day.toISOString().slice(0, 10);
const count = data[dateStr] || 0;
return (
@@ -180,6 +250,28 @@ export default function ContributionCalendar({ data }) {
))}
<span>More</span>
</div>
{/* Toggle button for mobile */}
{isMobile && (
<button
onClick={() => setExpanded(e => !e)}
style={{
marginTop: 8,
background: isDark ? '#23272e' : '#f3f4f6',
color: isDark ? '#b6c2d1' : '#333',
border: 'none',
borderRadius: 6,
padding: '6px 16px',
fontSize: 14,
fontWeight: 500,
cursor: 'pointer',
boxShadow: isDark ? '0 1px 4px rgba(0,0,0,0.16)' : '0 1px 4px rgba(0,0,0,0.06)',
transition: 'background 0.2s',
}}
aria-expanded={expanded}
>
{expanded ? 'Show less' : 'Show full year'}
</button>
)}
</div>
);
}