diff --git a/apps/manacore/apps/landing/src/pages/devlog/activity.astro b/apps/manacore/apps/landing/src/pages/devlog/activity.astro index 383c0edee..096325a61 100644 --- a/apps/manacore/apps/landing/src/pages/devlog/activity.astro +++ b/apps/manacore/apps/landing/src/pages/devlog/activity.astro @@ -13,34 +13,69 @@ import { Icon } from 'astro-icon/components'; const posts = await getCollection('devlog'); const sortedPosts = posts.sort((a, b) => a.data.date.getTime() - b.data.date.getTime()); -// Generate activity data for the grid (last 365 days) +// Generate activity data for the grid (last 90 days for better visibility) const today = new Date(); const startDate = new Date(today); -startDate.setDate(startDate.getDate() - 364); +startDate.setDate(startDate.getDate() - 89); // Create a map of date -> activity level -const activityMap = new Map(); +const activityMap = new Map< + string, + { commits: number; linesAdded: number; linesRemoved: number; posts: string[]; slug: string } +>(); for (const post of sortedPosts) { const dateKey = post.data.date.toISOString().split('T')[0]; - const existing = activityMap.get(dateKey) || { commits: 0, linesAdded: 0, linesRemoved: 0, posts: [] }; + const existing = activityMap.get(dateKey) || { + commits: 0, + linesAdded: 0, + linesRemoved: 0, + posts: [], + slug: '', + }; activityMap.set(dateKey, { commits: existing.commits + (post.data.commits || 0), linesAdded: existing.linesAdded + (post.data.stats?.linesAdded || 0), linesRemoved: existing.linesRemoved + (post.data.stats?.linesRemoved || 0), posts: [...existing.posts, post.data.title], + slug: post.slug, }); } // Generate weeks for the grid -const weeks: { date: Date; dateKey: string; activity: number; commits: number; posts: string[] }[][] = []; -let currentWeek: { date: Date; dateKey: string; activity: number; commits: number; posts: string[] }[] = []; +const weeks: { + date: Date; + dateKey: string; + activity: number; + commits: number; + posts: string[]; + slug: string; + month: number; +}[][] = []; +let currentWeek: { + date: Date; + dateKey: string; + activity: number; + commits: number; + posts: string[]; + slug: string; + month: number; +}[] = []; const currentDate = new Date(startDate); -// Pad the first week to start on Sunday -const firstDayOfWeek = currentDate.getDay(); +// Pad the first week to start on Monday (European style) +let firstDayOfWeek = currentDate.getDay(); +firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; // Convert Sunday=0 to Monday=0 for (let i = 0; i < firstDayOfWeek; i++) { - currentWeek.push({ date: new Date(0), dateKey: '', activity: -1, commits: 0, posts: [] }); + currentWeek.push({ + date: new Date(0), + dateKey: '', + activity: -1, + commits: 0, + posts: [], + slug: '', + month: -1, + }); } while (currentDate <= today) { @@ -61,6 +96,8 @@ while (currentDate <= today) { activity, commits, posts: data?.posts || [], + slug: data?.slug || '', + month: currentDate.getMonth(), }); if (currentWeek.length === 7) { @@ -76,18 +113,77 @@ if (currentWeek.length > 0) { weeks.push(currentWeek); } +// Calculate month positions for labels +const monthPositions: { month: number; weekIndex: number }[] = []; +let lastMonth = -1; +weeks.forEach((week, weekIndex) => { + const firstValidDay = week.find((day) => day.activity !== -1); + if (firstValidDay && firstValidDay.month !== lastMonth) { + monthPositions.push({ month: firstValidDay.month, weekIndex }); + lastMonth = firstValidDay.month; + } +}); + // Calculate totals const totalCommits = sortedPosts.reduce((sum, post) => sum + (post.data.commits || 0), 0); -const totalLinesAdded = sortedPosts.reduce((sum, post) => sum + (post.data.stats?.linesAdded || 0), 0); -const totalLinesRemoved = sortedPosts.reduce((sum, post) => sum + (post.data.stats?.linesRemoved || 0), 0); -const totalFilesChanged = sortedPosts.reduce((sum, post) => sum + (post.data.stats?.filesChanged || 0), 0); +const totalLinesAdded = sortedPosts.reduce( + (sum, post) => sum + (post.data.stats?.linesAdded || 0), + 0 +); +const totalLinesRemoved = sortedPosts.reduce( + (sum, post) => sum + (post.data.stats?.linesRemoved || 0), + 0 +); +const totalFilesChanged = sortedPosts.reduce( + (sum, post) => sum + (post.data.stats?.filesChanged || 0), + 0 +); const totalDays = sortedPosts.length; +// Calculate streak +let currentStreak = 0; +let maxStreak = 0; +let tempStreak = 0; +const checkDate = new Date(today); +while (true) { + const dateKey = checkDate.toISOString().split('T')[0]; + if (activityMap.has(dateKey)) { + currentStreak++; + checkDate.setDate(checkDate.getDate() - 1); + } else { + break; + } +} + +// Calculate max streak from all posts +const sortedDates = Array.from(activityMap.keys()).sort(); +for (let i = 0; i < sortedDates.length; i++) { + tempStreak = 1; + for (let j = i + 1; j < sortedDates.length; j++) { + const prevDate = new Date(sortedDates[j - 1]); + const currDate = new Date(sortedDates[j]); + const diffDays = Math.round((currDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24)); + if (diffDays === 1) { + tempStreak++; + } else { + break; + } + } + maxStreak = Math.max(maxStreak, tempStreak); +} + // Get all contributors -const contributorMap = new Map(); +const contributorMap = new Map< + string, + { name: string; handle?: string; commits: number; avatar?: string } +>(); for (const post of sortedPosts) { for (const contributor of post.data.contributors || []) { - const existing = contributorMap.get(contributor.name) || { name: contributor.name, handle: contributor.handle, commits: 0 }; + const existing = contributorMap.get(contributor.name) || { + name: contributor.name, + handle: contributor.handle, + commits: 0, + }; contributorMap.set(contributor.name, { ...existing, commits: existing.commits + (contributor.commits || 0), @@ -97,15 +193,28 @@ for (const post of sortedPosts) { const contributors = Array.from(contributorMap.values()).sort((a, b) => b.commits - a.commits); const activityColors = [ - 'bg-gray-100 dark:bg-gray-800', // 0 - none - 'bg-green-200 dark:bg-green-900', // 1 - low - 'bg-green-400 dark:bg-green-700', // 2 - medium - 'bg-green-500 dark:bg-green-600', // 3 - high - 'bg-green-600 dark:bg-green-500', // 4 - very high + 'bg-gray-200 dark:bg-gray-700/50', // 0 - none + 'bg-emerald-300 dark:bg-emerald-800', // 1 - low + 'bg-emerald-400 dark:bg-emerald-600', // 2 - medium + 'bg-emerald-500 dark:bg-emerald-500', // 3 - high + 'bg-emerald-600 dark:bg-emerald-400', // 4 - very high ]; -const monthLabels = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']; -const dayLabels = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; +const monthLabels = [ + 'Jan', + 'Feb', + 'Mär', + 'Apr', + 'Mai', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Okt', + 'Nov', + 'Dez', +]; +const dayLabels = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; const formatNumber = (num: number) => { if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; @@ -120,199 +229,425 @@ const formatDate = (date: Date) => { year: 'numeric', }).format(date); }; + +const formatShortDate = (date: Date) => { + return new Intl.DateTimeFormat('de-DE', { + day: '2-digit', + month: 'short', + }).format(date); +}; + +// Average commits per active day +const avgCommitsPerDay = totalDays > 0 ? Math.round(totalCommits / totalDays) : 0; --- -
+
- -
-
-
-
-
+ +
+
+
+
+
+
+
+
- + +
+
+ + Entwicklungsstatistik +
+ + Entwicklungsaktivität + + + Transparente Einblicke in die Entwicklung des ManaCore Ökosystems. Jeder Commit zählt. + +
+
-
+
-
-
-
-
- -
- Commits +
+ +
+
+
+
+
+
+ +
+
+ Commits +
{totalCommits}
- {totalCommits}
-
-
-
- -
- Dateien + +
+
+
+
+
+
+ +
+
+ Dateien geändert +
+ {formatNumber(totalFilesChanged)} +
- {formatNumber(totalFilesChanged)}
-
-
-
- -
- Zeilen + + +
+
+
+
+
+
+ +
+
+ Zeilen hinzugefügt +
+ +{formatNumber(totalLinesAdded)} +
- +{formatNumber(totalLinesAdded)}
-
-
-
- -
- Zeilen - + +
+
- -{formatNumber(totalLinesRemoved)} +
+
+
+ +
+
+ Zeilen entfernt +
+ -{formatNumber(totalLinesRemoved)} +
+
+
+
+ + +
+
+
{totalDays}
+ Aktive Tage +
+
+
{avgCommitsPerDay}
+ Commits/Tag +
+
+
{maxStreak}
+ Max. Streak
-
-
- {totalDays} aktive Entwicklungstage - +
+ + +
+
+ { + monthPositions.map(({ month, weekIndex }, i) => { + const nextWeekIndex = monthPositions[i + 1]?.weekIndex || weeks.length; + const width = (nextWeekIndex - weekIndex) * 17; // 12px cell + 5px gap + return ( +
+ {monthLabels[month]} +
+ ); + }) + } +
+
+ -
-
- -
- {dayLabels.map((day, i) => ( -
+
+ +
+ { + dayLabels.map((day, i) => ( +
{day}
- ))} -
+ )) + } +
- - {weeks.map((week, weekIndex) => ( -
- {week.map((day) => ( -
0 ? 'cursor-pointer hover:ring-2 hover:ring-offset-1 hover:ring-green-500' : ''}`} - title={day.activity === -1 ? '' : `${formatDate(day.date)}: ${day.commits} Commits${day.posts.length > 0 ? '\n' + day.posts.join(', ') : ''}`} - /> - ))} -
- ))} + +
+ { + weeks.map((week, weekIndex) => ( +
+ {week.map((day) => ( +
0 ? 'cursor-pointer hover:ring-2 hover:ring-emerald-500 hover:ring-offset-2 dark:hover:ring-offset-gray-800 hover:scale-125' : ''}`} + title={ + day.activity === -1 + ? '' + : `${formatDate(day.date)}\n${day.commits} Commits${day.posts.length > 0 ? '\n\n' + day.posts.join('\n') : ''}` + } + data-slug={day.slug} + onclick={day.slug ? `window.location.href='/devlog/${day.slug}'` : ''} + /> + ))} +
+ )) + }
-
- Weniger - {activityColors.map((color) => ( -
- ))} - Mehr +
+ + Klicke auf einen Tag um den Devlog zu öffnen + +
+ Weniger + { + activityColors.map((color, i) => ( +
+ )) + } + Mehr +
-
- Contributors + { + contributors.length > 0 && ( +
+ + Contributors + -
- {contributors.map((contributor, index) => ( -
-
- {contributor.name.charAt(0)} -
-
-
- {contributor.name} - {contributor.handle && ( - @{contributor.handle} - )} -
-
- - - {contributor.commits} Commits - -
-
+
+ {contributors.map((contributor, index) => ( +
+
+
+ {contributor.name.charAt(0)} +
+ {index === 0 && ( +
+ +
+ )} +
+
+
+ + {contributor.name} + + {contributor.handle && ( + + + + )} +
+
+
+ + + {contributor.commits} Commits + +
+
+
+
+
+
+ + {Math.round((contributor.commits / totalCommits) * 100)}% + +
-
+ ))}
- ))} -
-
+
+ ) + }
-
+
- Letzte Aktivität +
+ Letzte Aktivität + + Alle anzeigen + +
-
@@ -320,3 +655,18 @@ const formatDate = (date: Date) => {
+ +