Skip to content

Instantly share code, notes, and snippets.

@aldoyh
Last active July 5, 2025 22:58
Show Gist options
  • Save aldoyh/89fab96d7f5bf7200a187b6a96bedbb4 to your computer and use it in GitHub Desktop.
Save aldoyh/89fab96d7f5bf7200a187b6a96bedbb4 to your computer and use it in GitHub Desktop.
Interactive GitHub Streak Stats Clone in PHP
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive GitHub Streak Stats Clone</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!--
Chosen Palette: Golden Dusk. This palette is built around warm yellows and deep, dark tones. The base `--background` and `--card` colors are very light, desaturated yellows, providing a soft, luminous foundation in light mode. In dark mode, these shift to very dark, rich brown-blacks. `--foreground` and `--card-foreground` utilize a very dark, rich brown-black (`271C21`) for excellent readability in light mode, becoming a light, desaturated yellow-gray in dark mode. The bright golden yellow (`F1D61B`) serves as the `--primary` accent, drawing attention to key interactive elements and data in both modes. Secondary colors derived from `D3CF6A` and `B09023` create a coherent gradient for contribution levels and subtle UI elements, with their light/dark variations adjusted for each theme. The `C73E7F` (magenta) is noted but not directly integrated into the core harmonious palette to maintain consistency, focusing on the yellow/dark theme as the primary visual identity.
-->
<!--
Application Structure Plan: The application is designed as a single-page interactive dashboard combined with an explainer document. The structure prioritizes user engagement by presenting a live, functional demo at the top, immediately satisfying the core user goal. Below the demo, a tabbed "Technical Deep Dive" section organizes the dense information from the source report into logical, non-linear chunks (Data, Backend, Frontend). This allows users to explore the technical details at their own pace without being overwhelmed by a long, scrolling document. This dual structure (demo + explainer) serves both users who want to use the tool and those who want to understand how it's built, directly reflecting the dual nature of the source report.
-->
<!--
Visualization & Content Choices:
- Report Info: Key Metrics (Current/Longest Streak, Total Contributions) -> Goal: Inform -> Viz: Large, animated numbers in a card layout -> Interaction: GSAP count-up animation on load -> Justification: Immediately draws attention to the primary data and adds a dynamic, polished feel.
- Report Info: Daily Contribution Data -> Goal: Show Patterns over Time -> Viz: HTML/CSS Grid of colored squares (no SVG/Canvas for this part) -> Interaction: Hover to show a tooltip with date/count -> Justification: Faithfully replicates the familiar GitHub contribution graph for intuitive understanding. An HTML grid is simpler, more accessible, and easier to make interactive for this specific grid-based layout than a canvas.
- Report Info: Contributions by Day of Week -> Goal: Compare -> Viz: Chart.js Bar Chart -> Interaction: Hover over bars to see exact totals -> Justification: Provides a different analytical view of the data, fulfilling the Chart.js requirement with a visualization that is well-suited for categorical comparison.
- Report Info: Technical Concepts -> Goal: Organize & Explain -> Viz: Tabbed content sections and a flowchart-style diagram made with HTML/Tailwind -> Interaction: Click tabs to switch content -> Justification: Breaks down complex information into manageable, user-selected topics, improving comprehension over a linear text block.
-->
<!-- CONFIRMATION: NO SVG graphics used. NO Mermaid JS used. -->
<style>
:root {
/* Golden Dusk Light Mode Palette */
--background: 57 20% 97%; /* Very light, slightly warm yellow-gray */
--foreground: 21 14% 13%; /* Dark, rich brown-black from 271C21 */
--card: 57 15% 99%; /* Slightly lighter yellow-white for cards */
--card-foreground: 21 14% 13%; /* Same dark foreground */
--primary: 50 89% 52%; /* Bright golden yellow from F1D61B */
--primary-foreground: 0 0% 100%; /* White text on primary */
--muted: 57 15% 75%; /* Lighter, desaturated yellow-green from D3CF6A */
--muted-foreground: 21 10% 40%; /* Slightly lighter dark for muted text */
--border: 57 20% 85%; /* Warm, light border */
/* Contribution Levels (Light Mode) - Adjusted for contrast */
--contribution-level-0: 57 15% 94%; /* Very light, almost grey-yellow */
--contribution-level-1: 57 30% 85%; /* Distinctly lighter D3CF6A range */
--contribution-level-2: 57 50% 70%; /* Mid D3CF6A range */
--contribution-level-3: 50 70% 55%; /* Closer to F1D61B */
--contribution-level-4: 42 66% 35%; /* Darkest B09023 range */
}
html.dark {
/* Golden Dusk Dark Mode Palette */
--background: 320 19% 13%; /* Dark brown-black from 271C21 */
--foreground: 57 15% 90%; /* Light yellow-gray for text */
--card: 320 15% 15%; /* Slightly lighter background for cards */
--card-foreground: 57 15% 90%; /* Same light foreground */
--primary: 50 89% 52%; /* Bright golden yellow from F1D61B (unchanged) */
--primary-foreground: 0 0% 100%; /* White text on primary (unchanged) */
--muted: 320 10% 30%; /* Darker muted tone */
--muted-foreground: 57 10% 70%; /* Lighter muted text */
--border: 320 10% 25%; /* Darker border */
/* Contribution Levels (Dark Mode) - Adjusted for contrast */
--contribution-level-0: 320 15% 18%; /* Darkest, almost black */
--contribution-level-1: 320 20% 25%; /* A subtle step up, dark grey/brown */
--contribution-level-2: 42 66% 35%; /* B09023 - good mid-range for dark mode */
--contribution-level-3: 50 70% 55%; /* F1D61B, more prominent */
--contribution-level-4: 57 40% 80%; /* Lighter D3CF6A, very visible and contrasting */
}
body {
font-family: 'Inter', sans-serif;
background-color: hsl(var(--background));
color: hsl(var(--foreground));
transition: background-color 0.3s ease, color 0.3s ease; /* Smooth transition for theme change */
}
.card {
background-color: hsl(var(--card));
color: hsl(var(--card-foreground));
border: 1px solid hsl(var(--border));
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
.btn-primary {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
transition: background-color 0.3s ease, color 0.3s ease;
}
.text-primary {
color: hsl(var(--primary));
transition: color 0.3s ease;
}
.text-muted-foreground {
color: hsl(var(--muted-foreground));
transition: color 0.3s ease;
}
.border-base {
border-color: hsl(var(--border));
transition: border-color 0.3s ease;
}
.contribution-graph-grid {
display: grid;
grid-template-columns: repeat(53, 1fr);
grid-template-rows: repeat(7, 1fr);
grid-auto-flow: column;
gap: 3px;
}
.contribution-day {
aspect-ratio: 1 / 1;
border-radius: 2px;
background-color: hsl(var(--border));
transition: background-color 0.3s ease;
}
.chart-container {
position: relative;
width: 100%;
height: 250px;
max-height: 250px;
}
@media (min-width: 768px) {
.chart-container {
height: 300px;
max-height: 300px;
}
}
</style>
</head>
<body class="antialiased">
<div id="tooltip" class="fixed hidden px-3 py-1 text-sm font-semibold text-white bg-gray-900 rounded-md shadow-lg pointer-events-none z-50"></div>
<div class="container mx-auto p-4 md:p-8 max-w-5xl">
<header class="text-center mb-8 md:mb-12 relative">
<h1 class="text-3xl md:text-4xl font-extrabold tracking-tight">GitHub Streak Stats Reimagined</h1>
<p class="mt-3 text-lg text-muted-foreground max-w-3xl mx-auto">An interactive dashboard and technical breakdown based on the full-stack implementation report for a GitHub streak statistics clone.</p>
<button id="theme-toggle" class="absolute top-0 right-0 p-2 rounded-full bg-card hover:bg-muted transition-colors">
<!-- Sun icon for light mode, Moon icon for dark mode -->
<span class="light-icon hidden text-primary" title="Switch to Dark Mode">☀️</span>
<span class="dark-icon text-primary" title="Switch to Light Mode">🌙</span>
</button>
</header>
<!-- Placeholder for Unsplash Image - Replace with actual image URL -->
<div class="mb-12 max-w-3xl mx-auto">
<img src="https://placehold.co/800x450/271C21/F1D61B?text=Code+Statistics+%0A+%26+Development" alt="Abstract image representing code statistics and development" class="w-full h-auto rounded-xl shadow-lg object-cover">
<!-- Example Unsplash Image URL (replace with a real one that fits your theme): -->
<!-- <img src="https://images.unsplash.com/photo-1596496191712-42177372e1ed?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="Developer coding on a laptop" class="w-full h-auto rounded-xl shadow-lg object-cover"> -->
</div>
<main>
<!-- Live Demo Section -->
<section id="demo" class="mb-12">
<div class="card rounded-xl shadow-lg p-4 md:p-6">
<h2 class="text-2xl font-bold tracking-tight mb-4">Live Demo</h2>
<p class="text-muted-foreground mb-6">Enter a GitHub username to generate their contribution stats. Since this is a frontend-only demo, it uses mocked data but simulates the full functionality, including fetching, caching delays, and dynamic rendering.</p>
<form id="user-form" class="flex flex-col sm:flex-row gap-3 mb-6">
<input type="text" id="username-input" placeholder="e.g., torvalds" class="w-full px-4 py-2 bg-card text-foreground border border-base rounded-lg focus:ring-2 focus:ring-[hsl(var(--primary))] focus:outline-none transition-shadow" value="aldoyh">
<button type="submit" class="btn-primary font-semibold px-6 py-2 rounded-lg hover:opacity-90 transition-opacity whitespace-nowrap">
Get Stats
</button>
</form>
<div id="stats-card-container">
<!-- Stats card will be injected here -->
</div>
</div>
</section>
<!-- Technical Deep Dive Section -->
<section id="deep-dive">
<h2 class="text-3xl font-bold tracking-tight text-center mb-8">Technical Deep Dive</h2>
<div class="card rounded-xl shadow-lg p-4 md:p-6">
<p class="text-muted-foreground mb-6 text-center">The original report outlines a comprehensive strategy for building this tool. The following sections provide an interactive summary of the key architectural components, allowing you to explore how data is acquired, processed by the backend, and rendered on the frontend.</p>
<div class="border-b border-base mb-6">
<nav id="tabs" class="flex flex-wrap -mb-px justify-center" aria-label="Tabs">
<button class="tab-btn active text-primary border-primary whitespace-nowrap border-b-2 font-semibold px-4 py-3">
📊 Data Strategy
</button>
<button class="tab-btn text-muted-foreground hover:text-foreground border-transparent whitespace-nowrap border-b-2 font-semibold px-4 py-3">
⚙️ Backend Logic
</button>
<button class="tab-btn text-muted-foreground hover:text-foreground border-transparent whitespace-nowrap border-b-2 font-semibold px-4 py-3">
✨ Frontend UI
</button>
</nav>
</div>
<div id="tab-content">
<!-- Data Strategy Content -->
<div id="tab-data" class="tab-pane active">
<h3 class="text-xl font-bold mb-3">Data Acquisition: API vs. Scraping</h3>
<p class="text-muted-foreground mb-4">The most reliable method for fetching contribution data is the official GitHub GraphQL API. It provides structured, accurate data and avoids the fragility of web scraping, which can break whenever GitHub updates its website layout. The API allows for precise queries, requesting only the data needed, which is more efficient.</p>
<div class="grid md:grid-cols-2 gap-6">
<div class="bg-[hsl(var(--card))] p-4 rounded-lg border border-base">
<h4 class="font-semibold mb-2">GraphQL API Query Structure</h4>
<p class="text-sm text-muted-foreground mb-3">A specific query is crafted to fetch the `contributionCalendar` over a date range, returning daily counts and color levels.</p>
<pre class="text-xs bg-[hsl(var(--muted))] text-[hsl(var(--foreground))] p-3 rounded-md overflow-x-auto"><code>query($user: String!) {
user(login: $user) {
contributionsCollection {
contributionCalendar {
totalContributions
weeks {
contributionDays {
contributionCount
color
date
}
}
}
}
}
}</code></pre>
</div>
<div class="bg-[hsl(var(--card))] p-4 rounded-lg border border-base">
<h4 class="font-semibold mb-2">Contribution Distribution by Day</h4>
<p class="text-sm text-muted-foreground mb-3">Analyzing the fetched data can reveal patterns, like which days of the week are most active. The bar chart below visualizes this distribution based on the currently loaded user's data.</p>
<div class="chart-container">
<canvas id="dayOfWeekChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Backend Logic Content -->
<div id="tab-backend" class="tab-pane hidden">
<h3 class="text-xl font-bold mb-3">PHP Backend & Caching</h3>
<p class="text-muted-foreground mb-6">The backend's role is to securely query the GitHub API, process the data to calculate stats, and cache the results to avoid hitting rate limits. This ensures the application is fast and efficient.</p>
<div class="w-full overflow-x-auto">
<div class="flex flex-col md:flex-row items-center justify-center gap-4 text-center text-sm min-w-[600px]">
<div class="p-3 bg-[hsl(var(--contribution-level-0))] border border-[hsl(var(--contribution-level-1))] rounded-lg"><strong>1. Frontend Request</strong><br><span class="text-xs">/api.php?user=name</span></div>
<div class="font-bold text-xl text-[hsl(var(--primary))]">&rarr;</div>
<div class="p-3 bg-[hsl(var(--contribution-level-1))] border border-[hsl(var(--contribution-level-2))] rounded-lg"><strong>2. Check Cache</strong><br><span class="text-xs">Is `cache/name.json` valid?</span></div>
<div class="font-bold text-xl text-[hsl(var(--primary))]">&rarr;</div>
<div class="p-3 bg-[hsl(var(--contribution-level-2))] border border-[hsl(var(--contribution-level-3))] rounded-lg"><strong>3. GitHub API Call</strong><br><span class="text-xs">(If cache is invalid)</span></div>
<div class="font-bold text-xl text-[hsl(var(--primary))]">&rarr;</div>
<div class="p-3 bg-[hsl(var(--contribution-level-3))] border border-[hsl(var(--contribution-level-4))] rounded-lg"><strong>4. Process & Cache</strong><br><span class="text-xs">Calculate streaks, save JSON</span></div>
<div class="font-bold text-xl text-[hsl(var(--primary))]">&rarr;</div>
<div class="p-3 bg-[hsl(var(--contribution-level-4))] border border-[hsl(var(--primary))] rounded-lg"><strong>5. Return JSON</strong><br><span class="text-xs">Serve data to frontend</span></div>
</div>
</div>
</div>
<!-- Frontend UI Content -->
<div id="tab-frontend" class="tab-pane hidden">
<h3 class="text-xl font-bold mb-3">Frontend: ShadCN UI & GSAP Animation</h3>
<p class="text-muted-foreground mb-4">The frontend is built with HTML and styled using Tailwind CSS, following the principles of ShadCN UI for a clean, component-based structure. The GSAP library is used to create smooth, engaging animations for a premium user experience.</p>
<div class="grid md:grid-cols-2 gap-6">
<div>
<h4 class="font-semibold mb-2">Dynamic Rendering</h4>
<p class="text-sm text-muted-foreground mb-3">JavaScript fetches the JSON data from the backend and dynamically populates the UI. The contribution graph is built by creating a grid of `div` elements, colored according to the data.</p>
</div>
<div>
<h4 class="font-semibold mb-2">Key Animations</h4>
<ul class="list-disc list-inside text-muted-foreground text-sm space-y-2">
<li><strong>Number Count-Up:</strong> The main statistics animate from 0 to their final value to draw attention.</li>
<li><strong>Staggered Fade-In:</strong> The contribution graph squares fade in sequentially for a visually appealing effect.</li>
<li><strong>Smooth Transitions:</strong> The entire card fades in smoothly when data is loaded, preventing jarring content shifts.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const htmlElement = document.documentElement;
const themeToggle = document.getElementById('theme-toggle');
const lightIcon = document.querySelector('.light-icon');
const darkIcon = document.querySelector('.dark-icon');
const userForm = document.getElementById('user-form');
const usernameInput = document.getElementById('username-input');
const statsCardContainer = document.getElementById('stats-card-container');
const tooltip = document.getElementById('tooltip');
const tabs = document.querySelectorAll('.tab-btn');
const tabPanes = document.querySelectorAll('.tab-pane');
let dayOfWeekChart = null;
// --- Theme Management ---
function setPreferredTheme(theme) {
if (theme === 'dark') {
htmlElement.classList.add('dark');
lightIcon.classList.remove('hidden');
darkIcon.classList.add('hidden');
} else {
htmlElement.classList.remove('dark');
lightIcon.classList.add('hidden');
darkIcon.classList.remove('hidden');
}
localStorage.setItem('theme', theme);
}
// Initialize theme: default to dark, or load from localStorage
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setPreferredTheme(savedTheme);
} else {
setPreferredTheme('dark'); // Default to dark mode
}
themeToggle.addEventListener('click', () => {
if (htmlElement.classList.contains('dark')) {
setPreferredTheme('light');
} else {
setPreferredTheme('dark');
}
});
// --- Mock Data Generation ---
function generateMockData(username) {
const endDate = new Date();
const startDate = new Date();
startDate.setFullYear(endDate.getFullYear() - 1);
let contributionDays = [];
let currentDate = new Date(startDate);
let totalContributions = 0;
let longestStreak = 0;
let currentStreak = 0;
let tempStreak = 0;
// Re-map colors to reflect dark/light mode for consistency, but Chart.js will pick up CSS vars
const colors = [
'var(--contribution-level-0)',
'var(--contribution-level-1)',
'var(--contribution-level-2)',
'var(--contribution-level-3)',
'var(--contribution-level-4)'
];
while (currentDate <= endDate) {
const hasContribution = Math.random() > 0.3; // 70% chance of contribution
const contributionCount = hasContribution ? Math.floor(Math.random() * 20) + 1 : 0;
totalContributions += contributionCount;
let colorIndex = 0;
if (contributionCount > 15) colorIndex = 4;
else if (contributionCount > 9) colorIndex = 3;
else if (contributionCount > 4) colorIndex = 2;
else if (contributionCount > 0) colorIndex = 1;
contributionDays.push({
date: currentDate.toISOString().split('T')[0],
count: contributionCount,
color: `hsl(${getComputedStyle(document.documentElement).getPropertyValue(colors[colorIndex]).trim()})`,
level: colorIndex
});
if (contributionCount > 0) {
tempStreak++;
} else {
if (tempStreak > longestStreak) {
longestStreak = tempStreak;
}
tempStreak = 0;
}
currentDate.setDate(currentDate.getDate() + 1);
}
if (tempStreak > longestStreak) {
longestStreak = tempStreak;
}
// Calculate current streak from the end
let tempCurrentStreak = 0;
for (let i = contributionDays.length - 1; i >= 0; i--) {
if (contributionDays[i].count > 0) {
tempCurrentStreak++;
} else {
const today = new Date().toISOString().split('T')[0];
if (contributionDays[i].date === today && i > 0 && contributionDays[i-1].count > 0) {
continue;
}
break;
}
}
currentStreak = tempCurrentStreak;
return {
username,
totalContributions,
longestStreak,
currentStreak,
contributionDays
};
}
async function fetchMockData(username) {
statsCardContainer.innerHTML = `<div class="text-center p-8"><p class="text-muted-foreground">Fetching stats for ${username}...</p></div>`;
return new Promise(resolve => {
setTimeout(() => {
resolve(generateMockData(username));
}, 1500);
});
}
// --- Rendering Functions ---
function renderStatsCard(data) {
const cardHTML = `
<div id="stats-card" class="card rounded-xl border-base shadow-md p-6 opacity-0">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-bold">GitHub Contribution Streak</h3>
<span class="text-sm font-semibold text-muted-foreground">${data.username}</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6 text-center mb-8">
<div>
<p id="current-streak" class="text-5xl font-extrabold text-primary">0</p>
<p class="text-sm text-muted-foreground mt-1">Current Streak</p>
</div>
<div>
<p id="longest-streak" class="text-5xl font-extrabold">0</p>
<p class="text-sm text-muted-foreground mt-1">Longest Streak</p>
</div>
<div>
<p id="total-contributions" class="text-5xl font-extrabold">0</p>
<p class="text-sm text-muted-foreground mt-1">Total Contributions</p>
</div>
</div>
<div>
<h4 class="font-semibold text-center mb-4">Contribution Graph (Last Year)</h4>
<div id="contribution-graph" class="contribution-graph-grid">
<!-- Contribution days will be injected here -->
</div>
</div>
</div>
`;
statsCardContainer.innerHTML = cardHTML;
renderContributionGraph(data.contributionDays);
animateStats(data);
renderDayOfWeekChart(data.contributionDays);
}
function renderContributionGraph(days) {
const graphContainer = document.getElementById('contribution-graph');
if (!graphContainer) return;
const fragment = document.createDocumentFragment();
// Pad with empty days to align the graph correctly
const firstDay = new Date(days[0].date).getDay();
for (let i = 0; i < firstDay; i++) {
const pad = document.createElement('div');
fragment.appendChild(pad);
}
days.forEach(day => {
const dayEl = document.createElement('div');
dayEl.className = 'contribution-day';
// Directly use day.color which is already set based on theme
dayEl.style.backgroundColor = day.color;
dayEl.dataset.count = day.count;
dayEl.dataset.date = day.date;
fragment.appendChild(dayEl);
});
graphContainer.appendChild(fragment);
graphContainer.addEventListener('mouseover', (e) => {
if (e.target.classList.contains('contribution-day') && e.target.dataset.date) {
tooltip.classList.remove('hidden');
tooltip.textContent = `${e.target.dataset.count} contributions on ${e.target.dataset.date}`;
}
});
graphContainer.addEventListener('mousemove', (e) => {
if (!tooltip.classList.contains('hidden')) {
tooltip.style.left = `${e.pageX + 15}px`;
tooltip.style.top = `${e.pageY + 15}px`;
}
});
graphContainer.addEventListener('mouseout', () => {
tooltip.classList.add('hidden');
});
}
function renderDayOfWeekChart(days) {
const ctx = document.getElementById('dayOfWeekChart');
if (!ctx) return;
const contributionsByDay = [0, 0, 0, 0, 0, 0, 0]; // Sun - Sat
days.forEach(day => {
const date = new Date(day.date);
contributionsByDay[date.getDay()] += day.count;
});
const chartBackgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--contribution-level-3').trim(); // Use a middle contribution level color
const chartBorderColor = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim(); // Use primary accent for border
const data = {
labels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
datasets: [{
label: 'Total Contributions',
data: contributionsByDay,
backgroundColor: `hsl(${chartBackgroundColor})`,
borderColor: `hsl(${chartBorderColor})`,
borderWidth: 2,
borderRadius: 4,
}]
};
if (dayOfWeekChart) {
dayOfWeekChart.destroy();
}
dayOfWeekChart = new Chart(ctx, {
type: 'bar',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
displayColors: false,
backgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--muted').trim(), /* Use muted background for tooltip */
titleFont: { size: 0 },
bodyFont: { size: 14, weight: 'bold' },
bodyColor: getComputedStyle(document.documentElement).getPropertyValue('--foreground').trim()
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: getComputedStyle(document.documentElement).getPropertyValue('--border').trim()
},
ticks: {
color: getComputedStyle(document.documentElement).getPropertyValue('--muted-foreground').trim()
}
},
x: {
grid: {
display: false
},
ticks: {
color: getComputedStyle(document.documentElement).getPropertyValue('--muted-foreground').trim()
}
}
}
}
});
}
// --- Animation Functions ---
function animateStats(data) {
const statsCard = document.getElementById('stats-card');
gsap.to(statsCard, { opacity: 1, duration: 0.5 });
const currentStreak = { val: 0 };
gsap.to(currentStreak, { val: data.currentStreak, duration: 1.5, ease: 'power2.out', onUpdate: () => {
document.getElementById('current-streak').textContent = Math.round(currentStreak.val);
}});
const longestStreak = { val: 0 };
gsap.to(longestStreak, { val: data.longestStreak, duration: 1.5, ease: 'power2.out', onUpdate: () => {
document.getElementById('longest-streak').textContent = Math.round(longestStreak.val);
}});
const totalContributions = { val: 0 };
gsap.to(totalContributions, { val: data.totalContributions, duration: 1.5, ease: 'power2.out', onUpdate: () => {
document.getElementById('total-contributions').textContent = Math.round(totalContributions.val);
}});
gsap.from(".contribution-day", {
opacity: 0,
scale: 0.5,
duration: 0.3,
stagger: 0.005,
ease: 'back.out(1.7)',
delay: 0.5
});
}
// --- Event Handlers ---
userForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = usernameInput.value.trim();
if (username) {
const data = await fetchMockData(username);
renderStatsCard(data);
}
});
// Re-render chart on theme change to pick up new CSS variables
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class' && mutation.target === htmlElement) {
const username = usernameInput.value.trim();
if (username) {
// Re-generate mock data to get updated HSL colors based on the current theme
const data = generateMockData(username);
renderDayOfWeekChart(data.contributionDays);
}
}
});
});
observer.observe(htmlElement, { attributes: true });
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(item => {
item.classList.remove('active', 'text-primary', 'border-primary');
item.classList.add('text-muted-foreground', 'hover:text-foreground', 'border-transparent');
});
tab.classList.add('active', 'text-primary', 'border-primary');
tab.classList.remove('text-muted-foreground', 'hover:text-foreground', 'border-transparent');
tabPanes.forEach(pane => {
pane.classList.add('hidden');
});
const activePaneId = tab.textContent.trim().toLowerCase().split(' ')[1]; // "data", "logic", "ui"
document.getElementById(`tab-${activePaneId}`).classList.remove('hidden');
});
});
// --- Initial Load ---
userForm.dispatchEvent(new Event('submit'));
});
</script>
<!-- VIBE CODED BY @ALDOYH -->
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment