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:
@@ -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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user