mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
💄 style(devlog): improve activity page design
- Show 90-day grid instead of 365 for better visibility - Add month labels above the contribution grid - Use European week start (Monday) - Add animated hero section with gradient backgrounds - Improve stat cards with hover effects and shadows - Add secondary stats: Active Days, Commits/Day, Max Streak - Make grid cells clickable to open respective devlog - Add crown icon for top contributor - Show percentage in contributor progress bars - Add date badges to recent activity items - Use glassmorphism effects and smooth animations
This commit is contained in:
parent
b9f0d841df
commit
a373784954
1 changed files with 511 additions and 161 deletions
|
|
@ -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<string, { commits: number; linesAdded: number; linesRemoved: number; posts: string[] }>();
|
||||
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<string, { name: string; handle?: string; commits: number }>();
|
||||
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;
|
||||
---
|
||||
|
||||
<Layout title="Aktivität - ManaCore Entwicklung">
|
||||
<div class="bg-gradient-to-b from-blue-50/30 via-white to-blue-50/30 dark:from-gray-900 dark:via-gray-900 dark:to-gray-900">
|
||||
<div
|
||||
class="min-h-screen bg-gradient-to-b from-gray-50 via-white to-gray-50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950"
|
||||
>
|
||||
<Navbar />
|
||||
|
||||
<!-- Hero Section -->
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 -bottom-32">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-blue-50/50 to-transparent dark:from-gray-900 dark:to-transparent"></div>
|
||||
<div class="absolute top-0 right-0 w-96 h-96 bg-green-500/10 dark:bg-green-500/5 rounded-full blur-3xl"></div>
|
||||
<div class="absolute bottom-0 left-0 w-96 h-96 bg-emerald-500/10 dark:bg-emerald-500/5 rounded-full blur-3xl"></div>
|
||||
<!-- Hero Section with animated background -->
|
||||
<div class="relative overflow-hidden">
|
||||
<div class="absolute inset-0">
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-emerald-500/5 via-transparent to-cyan-500/5 dark:from-emerald-500/10 dark:to-cyan-500/10"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-20 right-20 w-72 h-72 bg-emerald-400/20 dark:bg-emerald-500/10 rounded-full blur-3xl animate-pulse"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="absolute bottom-20 left-20 w-96 h-96 bg-cyan-400/20 dark:bg-cyan-500/10 rounded-full blur-3xl animate-pulse"
|
||||
style="animation-delay: 1s"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HeroSection
|
||||
title="Entwicklungsaktivität"
|
||||
subtitle="Übersicht der Entwicklungsarbeit am ManaCore Projekt. Jeder Tag zählt."
|
||||
background="none"
|
||||
minHeight="small"
|
||||
spacing="small"
|
||||
containerClass="py-16 relative z-10"
|
||||
centered={true}
|
||||
debug={false}
|
||||
/>
|
||||
<Container class="relative z-10 pt-24 pb-12">
|
||||
<div class="text-center max-w-3xl mx-auto">
|
||||
<div
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-emerald-100 dark:bg-emerald-900/50 text-emerald-700 dark:text-emerald-300 text-sm font-medium mb-6"
|
||||
>
|
||||
<Icon name="mdi:chart-timeline-variant" class="w-4 h-4" />
|
||||
Entwicklungsstatistik
|
||||
</div>
|
||||
<Heading
|
||||
as="h1"
|
||||
size="1"
|
||||
class="mb-4 bg-gradient-to-r from-gray-900 via-emerald-800 to-gray-900 dark:from-white dark:via-emerald-300 dark:to-white bg-clip-text text-transparent"
|
||||
>
|
||||
Entwicklungsaktivität
|
||||
</Heading>
|
||||
<Text size="lg" class="text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
Transparente Einblicke in die Entwicklung des ManaCore Ökosystems. Jeder Commit zählt.
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<Section spacing="medium" class="relative">
|
||||
<Section spacing="small" class="relative -mt-4">
|
||||
<Container>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12">
|
||||
<div class="bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-xl p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-green-500/20 to-emerald-500/20 flex items-center justify-center">
|
||||
<Icon name="mdi:source-commit" class="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400">Commits</Text>
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<!-- Commits -->
|
||||
<div
|
||||
class="group relative bg-white dark:bg-gray-800/80 backdrop-blur-xl rounded-2xl p-6 border border-gray-200/50 dark:border-gray-700/50 shadow-lg shadow-gray-200/50 dark:shadow-none hover:shadow-xl hover:shadow-emerald-500/10 transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-emerald-500/5 to-transparent rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center shadow-lg shadow-emerald-500/30"
|
||||
>
|
||||
<Icon name="mdi:source-commit" class="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400 mb-1">Commits</Text>
|
||||
<div class="text-3xl font-bold text-gray-900 dark:text-white">{totalCommits}</div>
|
||||
</div>
|
||||
<Heading as="h3" size="3" class="text-green-600 dark:text-green-400">{totalCommits}</Heading>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-xl p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500/20 to-cyan-500/20 flex items-center justify-center">
|
||||
<Icon name="mdi:file-document-outline" class="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400">Dateien</Text>
|
||||
<!-- Files -->
|
||||
<div
|
||||
class="group relative bg-white dark:bg-gray-800/80 backdrop-blur-xl rounded-2xl p-6 border border-gray-200/50 dark:border-gray-700/50 shadow-lg shadow-gray-200/50 dark:shadow-none hover:shadow-xl hover:shadow-blue-500/10 transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-transparent rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shadow-lg shadow-blue-500/30"
|
||||
>
|
||||
<Icon name="mdi:file-document-multiple" class="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400 mb-1">Dateien geändert</Text>
|
||||
<div class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{formatNumber(totalFilesChanged)}
|
||||
</div>
|
||||
</div>
|
||||
<Heading as="h3" size="3" class="text-blue-600 dark:text-blue-400">{formatNumber(totalFilesChanged)}</Heading>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-xl p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-emerald-500/20 to-green-500/20 flex items-center justify-center">
|
||||
<Icon name="mdi:plus" class="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
|
||||
</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400">Zeilen +</Text>
|
||||
<!-- Lines Added -->
|
||||
<div
|
||||
class="group relative bg-white dark:bg-gray-800/80 backdrop-blur-xl rounded-2xl p-6 border border-gray-200/50 dark:border-gray-700/50 shadow-lg shadow-gray-200/50 dark:shadow-none hover:shadow-xl hover:shadow-green-500/10 transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center shadow-lg shadow-green-500/30"
|
||||
>
|
||||
<Icon name="mdi:plus-thick" class="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400 mb-1">Zeilen hinzugefügt</Text
|
||||
>
|
||||
<div class="text-3xl font-bold text-green-600 dark:text-green-400">
|
||||
+{formatNumber(totalLinesAdded)}
|
||||
</div>
|
||||
</div>
|
||||
<Heading as="h3" size="3" class="text-emerald-600 dark:text-emerald-400">+{formatNumber(totalLinesAdded)}</Heading>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-xl p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-red-500/20 to-orange-500/20 flex items-center justify-center">
|
||||
<Icon name="mdi:minus" class="w-5 h-5 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400">Zeilen -</Text>
|
||||
<!-- Lines Removed -->
|
||||
<div
|
||||
class="group relative bg-white dark:bg-gray-800/80 backdrop-blur-xl rounded-2xl p-6 border border-gray-200/50 dark:border-gray-700/50 shadow-lg shadow-gray-200/50 dark:shadow-none hover:shadow-xl hover:shadow-red-500/10 transition-all duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-red-500/5 to-transparent rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
</div>
|
||||
<Heading as="h3" size="3" class="text-red-600 dark:text-red-400">-{formatNumber(totalLinesRemoved)}</Heading>
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl bg-gradient-to-br from-red-500 to-red-600 flex items-center justify-center shadow-lg shadow-red-500/30"
|
||||
>
|
||||
<Icon name="mdi:minus-thick" class="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400 mb-1">Zeilen entfernt</Text>
|
||||
<div class="text-3xl font-bold text-red-600 dark:text-red-400">
|
||||
-{formatNumber(totalLinesRemoved)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Stats Row -->
|
||||
<div class="grid grid-cols-3 gap-4 mb-12">
|
||||
<div
|
||||
class="bg-white/50 dark:bg-gray-800/50 backdrop-blur rounded-xl p-4 border border-gray-200/50 dark:border-gray-700/50 text-center"
|
||||
>
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-white">{totalDays}</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400">Aktive Tage</Text>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white/50 dark:bg-gray-800/50 backdrop-blur rounded-xl p-4 border border-gray-200/50 dark:border-gray-700/50 text-center"
|
||||
>
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-white">{avgCommitsPerDay}</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400">Commits/Tag</Text>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white/50 dark:bg-gray-800/50 backdrop-blur rounded-xl p-4 border border-gray-200/50 dark:border-gray-700/50 text-center"
|
||||
>
|
||||
<div class="text-2xl font-bold text-emerald-600 dark:text-emerald-400">{maxStreak}</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400">Max. Streak</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Grid -->
|
||||
<div class="bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-2xl p-6 border border-gray-200 dark:border-gray-700 mb-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<Heading as="h2" size="4">{totalDays} aktive Entwicklungstage</Heading>
|
||||
<a href="/devlog" class="text-sm text-mana-blue hover:underline flex items-center gap-1">
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800/80 backdrop-blur-xl rounded-2xl p-8 border border-gray-200/50 dark:border-gray-700/50 shadow-lg mb-8"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<Heading as="h2" size="4" class="mb-1">Aktivität der letzten 90 Tage</Heading>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400">
|
||||
{formatShortDate(startDate)} - {formatShortDate(today)}
|
||||
</Text>
|
||||
</div>
|
||||
<a
|
||||
href="/devlog"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-emerald-100 dark:bg-emerald-900/50 text-emerald-700 dark:text-emerald-300 text-sm font-medium hover:bg-emerald-200 dark:hover:bg-emerald-900 transition-colors"
|
||||
>
|
||||
Alle Devlogs
|
||||
<Icon name="mdi:arrow-right" class="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Month Labels -->
|
||||
<div class="mb-2 ml-10">
|
||||
<div class="flex" style={`gap: 5px;`}>
|
||||
{
|
||||
monthPositions.map(({ month, weekIndex }, i) => {
|
||||
const nextWeekIndex = monthPositions[i + 1]?.weekIndex || weeks.length;
|
||||
const width = (nextWeekIndex - weekIndex) * 17; // 12px cell + 5px gap
|
||||
return (
|
||||
<div
|
||||
style={`width: ${width}px;`}
|
||||
class="text-xs text-gray-500 dark:text-gray-400 font-medium"
|
||||
>
|
||||
{monthLabels[month]}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid -->
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-flex gap-1">
|
||||
<!-- Day labels -->
|
||||
<div class="flex flex-col gap-1 mr-2 text-xs text-gray-400">
|
||||
{dayLabels.map((day, i) => (
|
||||
<div class="h-3 flex items-center" style={i % 2 === 0 ? '' : 'visibility: hidden'}>
|
||||
<div class="flex gap-2">
|
||||
<!-- Day labels -->
|
||||
<div
|
||||
class="flex flex-col gap-[5px] text-xs text-gray-400 dark:text-gray-500 font-medium pr-2"
|
||||
>
|
||||
{
|
||||
dayLabels.map((day, i) => (
|
||||
<div
|
||||
class="h-3 flex items-center justify-end"
|
||||
style={i % 2 === 0 ? '' : 'visibility: hidden'}
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Weeks -->
|
||||
{weeks.map((week, weekIndex) => (
|
||||
<div class="flex flex-col gap-1">
|
||||
{week.map((day) => (
|
||||
<div
|
||||
class={`w-3 h-3 rounded-sm ${day.activity === -1 ? 'bg-transparent' : activityColors[day.activity]} ${day.commits > 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(', ') : ''}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<!-- Weeks -->
|
||||
<div class="flex gap-[5px]">
|
||||
{
|
||||
weeks.map((week, weekIndex) => (
|
||||
<div class="flex flex-col gap-[5px]">
|
||||
{week.map((day) => (
|
||||
<div
|
||||
class={`w-3 h-3 rounded-[3px] transition-all duration-200 ${day.activity === -1 ? 'bg-transparent' : activityColors[day.activity]} ${day.commits > 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}'` : ''}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="flex items-center justify-end gap-2 mt-4 text-xs text-gray-500">
|
||||
<span>Weniger</span>
|
||||
{activityColors.map((color) => (
|
||||
<div class={`w-3 h-3 rounded-sm ${color}`} />
|
||||
))}
|
||||
<span>Mehr</span>
|
||||
<div
|
||||
class="flex items-center justify-between mt-6 pt-6 border-t border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400">
|
||||
Klicke auf einen Tag um den Devlog zu öffnen
|
||||
</Text>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>Weniger</span>
|
||||
{
|
||||
activityColors.map((color, i) => (
|
||||
<div
|
||||
class={`w-3 h-3 rounded-[3px] ${color}`}
|
||||
title={
|
||||
[
|
||||
'Keine Aktivität',
|
||||
'1-19 Commits',
|
||||
'20-39 Commits',
|
||||
'40-59 Commits',
|
||||
'60+ Commits',
|
||||
][i]
|
||||
}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<span>Mehr</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contributors -->
|
||||
<div class="bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-2xl p-6 border border-gray-200 dark:border-gray-700">
|
||||
<Heading as="h2" size="4" class="mb-6">Contributors</Heading>
|
||||
{
|
||||
contributors.length > 0 && (
|
||||
<div class="bg-white dark:bg-gray-800/80 backdrop-blur-xl rounded-2xl p-8 border border-gray-200/50 dark:border-gray-700/50 shadow-lg mb-8">
|
||||
<Heading as="h2" size="4" class="mb-6">
|
||||
Contributors
|
||||
</Heading>
|
||||
|
||||
<div class="space-y-4">
|
||||
{contributors.map((contributor, index) => (
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-mana-blue to-purple-500 flex items-center justify-center text-white font-semibold">
|
||||
{contributor.name.charAt(0)}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<Text weight="semibold">{contributor.name}</Text>
|
||||
{contributor.handle && (
|
||||
<Text size="sm" class="text-gray-500">@{contributor.handle}</Text>
|
||||
)}
|
||||
</div>
|
||||
<div class="flex items-center gap-4 mt-1">
|
||||
<Text size="sm" class="text-gray-500">
|
||||
<Icon name="mdi:source-commit" class="w-4 h-4 inline mr-1" />
|
||||
{contributor.commits} Commits
|
||||
</Text>
|
||||
<div class="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-gradient-to-r from-green-500 to-emerald-500 rounded-full"
|
||||
style={`width: ${(contributor.commits / totalCommits) * 100}%`}
|
||||
/>
|
||||
<div class="space-y-6">
|
||||
{contributors.map((contributor, index) => (
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative">
|
||||
<div class="w-14 h-14 rounded-full bg-gradient-to-br from-emerald-500 via-cyan-500 to-blue-500 flex items-center justify-center text-white text-xl font-bold shadow-lg">
|
||||
{contributor.name.charAt(0)}
|
||||
</div>
|
||||
{index === 0 && (
|
||||
<div class="absolute -top-1 -right-1 w-6 h-6 bg-yellow-400 rounded-full flex items-center justify-center shadow">
|
||||
<Icon name="mdi:crown" class="w-4 h-4 text-yellow-800" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<Text weight="semibold" size="lg">
|
||||
{contributor.name}
|
||||
</Text>
|
||||
{contributor.handle && (
|
||||
<a
|
||||
href={`https://github.com/${contributor.handle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-gray-500 hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors"
|
||||
>
|
||||
<Icon name="mdi:github" class="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="mdi:source-commit" class="w-4 h-4 text-gray-400" />
|
||||
<Text size="sm" class="text-gray-600 dark:text-gray-400">
|
||||
{contributor.commits} Commits
|
||||
</Text>
|
||||
</div>
|
||||
<div class="flex-1 max-w-xs">
|
||||
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-gradient-to-r from-emerald-500 to-cyan-500 rounded-full transition-all duration-500"
|
||||
style={`width: ${(contributor.commits / totalCommits) * 100}%`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Text size="sm" class="text-gray-500 dark:text-gray-400 w-12 text-right">
|
||||
{Math.round((contributor.commits / totalCommits) * 100)}%
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<Section spacing="large" class="relative">
|
||||
<Section spacing="medium" class="relative">
|
||||
<Container>
|
||||
<Heading as="h2" size="3" class="mb-8">Letzte Aktivität</Heading>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<Heading as="h2" size="3">Letzte Aktivität</Heading>
|
||||
<a
|
||||
href="/devlog"
|
||||
class="text-emerald-600 dark:text-emerald-400 hover:underline text-sm font-medium"
|
||||
>
|
||||
Alle anzeigen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
{sortedPosts.slice(-5).reverse().map((post) => (
|
||||
<a
|
||||
href={`/devlog/${post.slug}`}
|
||||
class="block bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-xl p-4 border border-gray-200 dark:border-gray-700 hover:border-mana-blue/50 transition-colors"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Text weight="semibold" class="mb-1">{post.data.title}</Text>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span>
|
||||
<Icon name="mdi:calendar" class="w-4 h-4 inline mr-1" />
|
||||
{formatDate(post.data.date)}
|
||||
</span>
|
||||
<span>
|
||||
<Icon name="mdi:source-commit" class="w-4 h-4 inline mr-1" />
|
||||
{post.data.commits} Commits
|
||||
</span>
|
||||
{post.data.stats && (
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
+{formatNumber(post.data.stats.linesAdded)}
|
||||
</span>
|
||||
)}
|
||||
{post.data.stats && (
|
||||
<span class="text-red-600 dark:text-red-400">
|
||||
-{formatNumber(post.data.stats.linesRemoved)}
|
||||
</span>
|
||||
)}
|
||||
<div class="grid gap-4">
|
||||
{
|
||||
sortedPosts
|
||||
.slice(-5)
|
||||
.reverse()
|
||||
.map((post, index) => (
|
||||
<a
|
||||
href={`/devlog/${post.slug}`}
|
||||
class="group relative bg-white dark:bg-gray-800/80 backdrop-blur-xl rounded-xl p-6 border border-gray-200/50 dark:border-gray-700/50 shadow-sm hover:shadow-lg hover:shadow-emerald-500/10 transition-all duration-300 hover:-translate-y-0.5"
|
||||
>
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-emerald-500/5 to-cyan-500/5 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div class="relative flex items-center gap-6">
|
||||
<div class="hidden sm:flex flex-col items-center justify-center w-16 h-16 rounded-xl bg-gradient-to-br from-emerald-100 to-cyan-100 dark:from-emerald-900/50 dark:to-cyan-900/50">
|
||||
<div class="text-2xl font-bold text-emerald-600 dark:text-emerald-400">
|
||||
{post.data.date.getDate()}
|
||||
</div>
|
||||
<div class="text-xs text-emerald-600/70 dark:text-emerald-400/70 uppercase">
|
||||
{monthLabels[post.data.date.getMonth()]}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<Text
|
||||
weight="semibold"
|
||||
size="lg"
|
||||
class="mb-2 group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors truncate"
|
||||
>
|
||||
{post.data.title}
|
||||
</Text>
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<Icon name="mdi:source-commit" class="w-4 h-4" />
|
||||
{post.data.commits} Commits
|
||||
</span>
|
||||
{post.data.stats && (
|
||||
<>
|
||||
<span class="text-green-600 dark:text-green-400 font-medium">
|
||||
+{formatNumber(post.data.stats.linesAdded)}
|
||||
</span>
|
||||
<span class="text-red-600 dark:text-red-400 font-medium">
|
||||
-{formatNumber(post.data.stats.linesRemoved)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Icon
|
||||
name="mdi:chevron-right"
|
||||
class="w-6 h-6 text-gray-400 group-hover:text-emerald-500 group-hover:translate-x-1 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Icon name="mdi:chevron-right" class="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
|
|
@ -320,3 +655,18 @@ const formatDate = (date: Date) => {
|
|||
<Footer />
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
.animate-pulse {
|
||||
animation: pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue