Files
365devnet/src/components/ContributionCalendar.jsx
Richard Bergsma e127115f41 Update ContributionCalendar to adjust weeks displayed based on mobile view
- Modified the logic for determining the number of weeks to show in the ContributionCalendar component, reducing the count from 20 to 19 when in mobile view and not expanded.
- This change aims to optimize the calendar display for better usability on smaller screens.
2025-06-15 16:43:22 +02:00

250 lines
8.1 KiB
JavaScript

import React, { useEffect, useState } from 'react';
// Helper to get all days in the last 52 weeks, starting on Monday - UTC version
function getCalendarDays() {
const days = [];
// Work entirely in UTC to match Gitea
const today = new Date();
const todayUTC = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()));
// Find the Monday that starts our 52-week period
const startDate = new Date(todayUTC);
startDate.setUTCDate(todayUTC.getUTCDate() - (52 * 7 - 1));
// Adjust startDate to the Monday of that week (using UTC day)
const startDayOfWeek = startDate.getUTCDay(); // 0 = Sunday, 1 = Monday, etc.
const daysToMonday = startDayOfWeek === 0 ? -6 : 1 - startDayOfWeek;
startDate.setUTCDate(startDate.getUTCDate() + daysToMonday);
// Generate exactly 52 weeks (364 days) starting from that Monday
for (let i = 0; i < 7 * 52; i++) {
const d = new Date(startDate);
d.setUTCDate(startDate.getUTCDate() + i);
days.push(d);
}
return days;
}
// Get month labels for the top
function getMonthLabels(days) {
const labels = [];
let lastMonth = null;
for (let week = 0; week < 52; week++) {
const day = days[week * 7];
const month = day.toLocaleString('default', { month: 'short' });
if (month !== lastMonth) {
labels.push({ week, month });
lastMonth = month;
}
}
return labels;
}
// Day labels for rows (Monday to Sunday)
const DAY_LABELS = ['Mon', '', 'Wed', '', 'Fri', '', 'Sun'];
// Color scales
const COLORS_LIGHT = [
'#ebedf0', // 0
'#c6e48b', // 1
'#7bc96f', // 2
'#239a3b', // 3
'#196127', // 4+
];
const COLORS_DARK = [
'#23272e', // 0
'#3c4d36', // 1
'#4e7c4e', // 2
'#399150', // 3
'#6ee7b7', // 4+
];
function getColor(count, isDark) {
const palette = isDark ? COLORS_DARK : COLORS_LIGHT;
if (!count) return palette[0];
if (count >= 4) return palette[4];
return palette[count];
}
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>
const checkDark = () => {
setIsDark(document.documentElement.classList.contains('dark'));
};
checkDark();
const observer = new MutationObserver(checkDark);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
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 ? 19 : 52;
const startWeek = 52 - weeksToShow;
const visibleDays = days.slice(startWeek * 7, 52 * 7);
const visibleMonthLabels = monthLabels.filter(l => l.week >= startWeek);
const calendarRef = React.useRef(null);
// 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));
return (
<div
style={{
width: '100%',
maxWidth: '100%',
margin: '0 auto',
padding: 0,
borderRadius: '1rem',
background: isDark ? 'rgba(30,41,59,0.85)' : 'rgba(255,255,255,0.7)',
boxShadow: isDark ? '0 4px 24px 0 rgba(0,0,0,0.32)' : '0 4px 24px 0 rgba(0,0,0,0.08)',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
border: isDark ? '1px solid rgba(51,65,85,0.5)' : '1px solid rgba(255,255,255,0.3)',
overflowX: 'auto',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: 160,
position: 'relative',
}}
className="contribution-calendar"
ref={calendarRef}
>
{/* Month labels */}
<div style={{ display: 'flex', marginBottom: 4, justifyContent: 'center' }}>
<div style={{ width: 44 }} />
{Array.from({ length: weeksToShow }).map((_, weekIdx) => {
const label = visibleMonthLabels.find((l) => l.week === weekIdx + startWeek);
return (
<div
key={weekIdx}
style={{
flex: '0 0 14px',
textAlign: 'center',
fontSize: 12,
color: isDark ? '#b6c2d1' : '#888',
fontWeight: 500,
minWidth: 14,
}}
>
{label ? label.month : ''}
</div>
);
})}
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{/* Day labels */}
<div style={{ display: 'flex', flexDirection: 'column', marginRight: 4, justifyContent: 'center' }}>
{DAY_LABELS.map((label, i) => (
<div
key={i}
style={{
height: 14,
fontSize: 12,
color: isDark ? '#b6c2d1' : '#888',
textAlign: 'right',
lineHeight: '14px',
marginBottom: 1,
minWidth: 32,
}}
>
{label}
</div>
))}
</div>
{/* Weeks (columns) */}
<div style={{ display: 'flex' }}>
{Array.from({ length: weeksToShow }).map((_, weekIdx) => (
<div key={weekIdx} style={{ display: 'flex', flexDirection: 'column', flex: '0 0 14px' }}>
{/* Days (rows) - Using UTC for consistency with Gitea */}
{Array.from({ length: 7 }).map((_, dayIdx) => {
const day = visibleDays[weekIdx * 7 + dayIdx];
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 (
<div
key={dateStr}
title={`${dateStr}: ${count} contribution${count === 1 ? '' : 's'}`}
style={{
width: 12,
height: 12,
margin: 1,
background: getColor(count, isDark),
borderRadius: 2,
}}
/>
);
})}
</div>
))}
</div>
</div>
<div style={{ fontSize: 12, color: isDark ? '#b6c2d1' : '#888', marginTop: 4, textAlign: 'center' }}>
<span>Less</span>
{(isDark ? COLORS_DARK : COLORS_LIGHT).map((color, i) => (
<span key={i} style={{ display: 'inline-block', width: 12, height: 12, background: color, margin: '0 2px', borderRadius: 2 }} />
))}
<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>
);
}