diff --git a/src/components/ContributionCalendar.jsx b/src/components/ContributionCalendar.jsx index 14badd6..5f63b96 100644 --- a/src/components/ContributionCalendar.jsx +++ b/src/components/ContributionCalendar.jsx @@ -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 @@ -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 && ( +
+ ⬅️➡️ Scroll +
+ )} {/* Month labels */}
- {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 (
{/* Weeks (columns) */}
- {Array.from({ length: 52 }).map((_, weekIdx) => ( + {Array.from({ length: weeksToShow }).map((_, weekIdx) => (
{/* 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
; const dateStr = day.toISOString().slice(0, 10); const count = data[dateStr] || 0; return ( @@ -180,6 +250,28 @@ export default function ContributionCalendar({ data }) { ))} More
+ {/* Toggle button for mobile */} + {isMobile && ( + + )}
); } \ No newline at end of file diff --git a/src/pages/[lang]/development.astro b/src/pages/[lang]/development.astro index 4b1db2d..f786cc7 100644 --- a/src/pages/[lang]/development.astro +++ b/src/pages/[lang]/development.astro @@ -16,53 +16,80 @@ const metadata = { const lang = Astro.params.lang || 'en'; const t = getTranslation(lang); -// Cache Gitea commits across all language builds -let cachedCommits = null; -async function getCachedCommits() { - if (cachedCommits) return cachedCommits; - const GITEA_COMMITS_URL = 'https://git.365devnet.eu/api/v1/repos/365DevNet/365devnet/commits?sha=main&stat=true&verification=true&files=true'; - let commits = []; +// Fetch Gitea user heatmap at build time (cached) +async function getUserHeatmap(username) { + const url = `https://git.365devnet.eu/api/v1/users/${username}/heatmap`; + let heatmap = []; try { const headers = { accept: 'application/json' }; if (import.meta.env.GITEA_TOKEN) { headers['Authorization'] = `token ${import.meta.env.GITEA_TOKEN}`; } - const commitsResponse = await fetch(GITEA_COMMITS_URL, { headers }); - if (commitsResponse.ok) { - commits = await commitsResponse.json(); + const response = await fetch(url, { headers }); + if (response.ok) { + heatmap = await response.json(); } } catch (error) { - console.error('Error fetching commits:', error); + console.error('Error fetching user heatmap:', error); } - cachedCommits = commits; - return commits; -} - -// Fetch Git repository data at build time (cached) -const commits = await getCachedCommits(); - -// Format date to a readable format -const formatDate = (dateString: string) => { - const date = new Date(dateString); - const day = date.toLocaleString(lang, { day: '2-digit' }); - const month = date.toLocaleString(lang, { month: 'short' }).toLowerCase().replace('.', ''); - const year = date.getFullYear(); - return `${day}-${month}-${year}`; -}; - -// Helper: Group commits by date (YYYY-MM-DD) -function getContributionData(commits) { + // Aggregate by date const contributions = {}; - for (const commit of commits) { - const date = commit.commit?.author?.date?.slice(0, 10); - if (date) { - contributions[date] = (contributions[date] || 0) + 1; - } + for (const entry of heatmap) { + const date = new Date(entry.timestamp * 1000).toISOString().slice(0, 10); + contributions[date] = (contributions[date] || 0) + entry.contributions; } return contributions; } -const contributionData = getContributionData(commits); +const contributionData = await getUserHeatmap('Richard'); + +// Fetch latest user commits from activities/feeds +async function getUserCommits(username) { + const url = `https://git.365devnet.eu/api/v1/users/${username}/activities/feeds`; + let feeds = []; + try { + const headers = { accept: 'application/json' }; + if (import.meta.env.GITEA_TOKEN) { + headers['Authorization'] = `token ${import.meta.env.GITEA_TOKEN}`; + } + const response = await fetch(url, { headers }); + if (response.ok) { + feeds = await response.json(); + } + } catch (error) { + console.error('Error fetching user feeds:', error); + } + // Only keep commit_repo events + const commits = []; + for (const feed of feeds) { + if (feed.op_type === 'commit_repo' && feed.content) { + let content; + try { + content = JSON.parse(feed.content); + } catch (e) { + continue; + } + // Each feed may have multiple commits + if (content.Commits && Array.isArray(content.Commits)) { + for (const commit of content.Commits) { + commits.push({ + sha: commit.Sha1, + message: commit.Message, + author: commit.AuthorName, + date: commit.Timestamp, + repo: feed.repo ? feed.repo.full_name : '', + repo_url: feed.repo ? feed.repo.html_url : '', + compare_url: content.CompareURL ? `https://git.365devnet.eu/${content.CompareURL}` : '', + }); + } + } + } + if (commits.length >= 10) break; + } + return commits.slice(0, 10); +} + +const userCommits = await getUserCommits('Richard'); export async function getStaticPaths() { return [ @@ -72,6 +99,15 @@ export async function getStaticPaths() { { params: { lang: 'fr' } }, ]; } + +// Format date to a readable format +const formatDate = (dateString) => { + const date = new Date(dateString); + const day = date.toLocaleString(lang, { day: '2-digit' }); + const month = date.toLocaleString(lang, { month: 'short' }).toLowerCase().replace('.', ''); + const year = date.getFullYear(); + return `${day}-${month}-${year}`; +}; --- @@ -86,21 +122,22 @@ export async function getStaticPaths() {

{t.development.latestCommits || 'Latest Commits'}

- {Array.isArray(commits) && commits.length > 0 ? ( +

Showing contributions for user Richard (all repositories).

+ {Array.isArray(userCommits) && userCommits.length > 0 ? (
- {commits.slice(0, 10).map((commit) => ( -
+ {userCommits.map((commit) => ( +
-

{commit.commit?.message?.split('\n')[0] || 'No message'}

+

{commit.message.split('\n')[0] || 'No message'}

{/* Format commit description with bullet points on new lines */} - {commit.commit?.message ? ( + {commit.message ? ( (() => { - const lines = commit.commit.message.split('\n'); + const lines = commit.message.split('\n'); const bullets = lines.filter(line => line.trim().startsWith('- ')); if (bullets.length > 0) { // Render as a list if there are bullet points return ( -
    +
      {lines.map((line, idx) => line.trim().startsWith('- ') ?
    • {line.trim().slice(2)}
    • @@ -110,25 +147,55 @@ export async function getStaticPaths() { ); } else { // Render as a paragraph if no bullet points - return

      {commit.commit.message}

      ; + return

      {commit.message}

      ; } })() ) : null}
-
- {commit.commit?.author?.date ? formatDate(commit.commit.author.date) : ''} - {commit.commit?.author?.name && ( - {commit.commit.author.name} + {/* Desktop commit info card */} + + {/* Mobile commit info area, visually separated and two rows with left/right alignment */} +
+
+ {commit.date ? formatDate(commit.date) : ''} + {commit.author && ( + {commit.author === 'becarta' ? 'Richard Bergsma' : commit.author} + )} +
+
+ {commit.repo && ( + + {commit.repo} + + )} + {commit.sha && ( + + Commit: + + {commit.sha.slice(0, 7)} + + + )} +
+
))}