More udpates to documentation generation
This commit is contained in:
132
mkdocs/site/javascripts/gitea-widget.js
Normal file
132
mkdocs/site/javascripts/gitea-widget.js
Normal file
@@ -0,0 +1,132 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const widgets = document.querySelectorAll('.gitea-widget');
|
||||
|
||||
widgets.forEach(widget => {
|
||||
const repo = widget.dataset.repo;
|
||||
|
||||
// Auto-generate data file path from repo name
|
||||
const dataFile = `/assets/repo-data/${repo.replace('/', '-')}.json`;
|
||||
const showDescription = widget.dataset.showDescription !== 'false';
|
||||
const showLanguage = widget.dataset.showLanguage !== 'false';
|
||||
const showLastUpdate = widget.dataset.showLastUpdate !== 'false';
|
||||
|
||||
// Show loading state
|
||||
widget.innerHTML = `
|
||||
<div class="gitea-widget-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>Loading ${repo}...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Fetch repository data
|
||||
fetch(dataFile)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not load data for ${repo}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
renderWidget(widget, data, { showDescription, showLanguage, showLastUpdate });
|
||||
})
|
||||
.catch(error => {
|
||||
renderError(widget, repo, error.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function renderWidget(widget, data, options) {
|
||||
const lastUpdate = new Date(data.updated_at).toLocaleDateString();
|
||||
const language = data.language || 'Not specified';
|
||||
|
||||
widget.innerHTML = `
|
||||
<div class="gitea-widget-container">
|
||||
<div class="gitea-widget-header">
|
||||
<div class="gitea-widget-title">
|
||||
<svg class="gitea-icon" viewBox="0 0 16 16" width="16" height="16">
|
||||
<path fill="currentColor" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>
|
||||
</svg>
|
||||
<a href="${data.html_url}" target="_blank" rel="noopener noreferrer" class="repo-link">
|
||||
${data.full_name}
|
||||
</a>
|
||||
</div>
|
||||
<div class="gitea-widget-stats">
|
||||
<span class="stat-item" title="Stars">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14">
|
||||
<path fill="currentColor" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
|
||||
</svg>
|
||||
${data.stars_count.toLocaleString()}
|
||||
</span>
|
||||
<span class="stat-item" title="Forks">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14">
|
||||
<path fill="currentColor" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25V5.372a2.25 2.25 0 10-1.5 0v.878A.75.75 0 0110.25 7H5.75A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
|
||||
</svg>
|
||||
${data.forks_count.toLocaleString()}
|
||||
</span>
|
||||
<span class="stat-item" title="Open Issues">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14">
|
||||
<path fill="currentColor" d="M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"/>
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z"/>
|
||||
</svg>
|
||||
${data.open_issues_count.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
${options.showDescription && data.description ? `
|
||||
<div class="gitea-widget-description">
|
||||
${data.description}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="gitea-widget-footer">
|
||||
${options.showLanguage ? `
|
||||
<span class="language-info">
|
||||
<span class="language-dot" style="background-color: ${getLanguageColor(language)}"></span>
|
||||
${language}
|
||||
</span>
|
||||
` : ''}
|
||||
${options.showLastUpdate ? `
|
||||
<span class="last-update">Updated ${lastUpdate}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderError(widget, repo, error) {
|
||||
widget.innerHTML = `
|
||||
<div class="gitea-widget-error">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16">
|
||||
<path fill="currentColor" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"/>
|
||||
</svg>
|
||||
<span>Failed to load ${repo}</span>
|
||||
<small>${error}</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getLanguageColor(language) {
|
||||
const colors = {
|
||||
'JavaScript': '#f1e05a',
|
||||
'TypeScript': '#2b7489',
|
||||
'Python': '#3572A5',
|
||||
'Java': '#b07219',
|
||||
'C++': '#f34b7d',
|
||||
'C': '#555555',
|
||||
'C#': '#239120',
|
||||
'PHP': '#4F5D95',
|
||||
'Ruby': '#701516',
|
||||
'Go': '#00ADD8',
|
||||
'Rust': '#dea584',
|
||||
'Swift': '#ffac45',
|
||||
'Kotlin': '#F18E33',
|
||||
'Scala': '#c22d40',
|
||||
'Shell': '#89e051',
|
||||
'HTML': '#e34c26',
|
||||
'CSS': '#563d7c',
|
||||
'Vue': '#2c3e50',
|
||||
'React': '#61dafb',
|
||||
'Dockerfile': '#384d54',
|
||||
'Markdown': '#083fa1'
|
||||
};
|
||||
return colors[language] || '#586069';
|
||||
}
|
||||
167
mkdocs/site/javascripts/github-widget.js
Normal file
167
mkdocs/site/javascripts/github-widget.js
Normal file
@@ -0,0 +1,167 @@
|
||||
let widgetsInitialized = new Set();
|
||||
|
||||
function initializeGitHubWidgets() {
|
||||
const widgets = document.querySelectorAll('.github-widget');
|
||||
|
||||
widgets.forEach(widget => {
|
||||
// Skip if already initialized
|
||||
const widgetId = widget.dataset.repo + '-' + Math.random().toString(36).substr(2, 9);
|
||||
if (widget.dataset.initialized) return;
|
||||
|
||||
widget.dataset.initialized = 'true';
|
||||
const repo = widget.dataset.repo;
|
||||
|
||||
// Auto-generate data file path from repo name
|
||||
const dataFile = `/assets/repo-data/${repo.replace('/', '-')}.json`;
|
||||
const showDescription = widget.dataset.showDescription !== 'false';
|
||||
const showLanguage = widget.dataset.showLanguage !== 'false';
|
||||
const showLastUpdate = widget.dataset.showLastUpdate !== 'false';
|
||||
|
||||
// Show loading state
|
||||
widget.innerHTML = `
|
||||
<div class="github-widget-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>Loading ${repo}...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Fetch repository data from pre-generated JSON file
|
||||
fetch(dataFile)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not load data for ${repo}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
const lastUpdate = new Date(data.updated_at).toLocaleDateString();
|
||||
const language = data.language || 'Not specified';
|
||||
|
||||
widget.innerHTML = `
|
||||
<div class="github-widget-container">
|
||||
<div class="github-widget-header">
|
||||
<div class="github-widget-title">
|
||||
<svg class="github-icon" viewBox="0 0 16 16" width="16" height="16">
|
||||
<path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
<a href="${data.html_url}" target="_blank" rel="noopener noreferrer" class="repo-link">
|
||||
${data.full_name}
|
||||
</a>
|
||||
</div>
|
||||
<div class="github-widget-stats">
|
||||
<span class="stat-item" title="Stars">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14">
|
||||
<path fill="currentColor" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
|
||||
</svg>
|
||||
${data.stars_count.toLocaleString()}
|
||||
</span>
|
||||
<span class="stat-item" title="Forks">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14">
|
||||
<path fill="currentColor" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25V5.372a2.25 2.25 0 10-1.5 0v.878A.75.75 0 0110.25 7H5.75A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
|
||||
</svg>
|
||||
${data.forks_count.toLocaleString()}
|
||||
</span>
|
||||
<span class="stat-item" title="Open Issues">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14">
|
||||
<path fill="currentColor" d="M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"/>
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z"/>
|
||||
</svg>
|
||||
${data.open_issues_count.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
${showDescription && data.description ? `
|
||||
<div class="github-widget-description">
|
||||
${data.description}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="github-widget-footer">
|
||||
${showLanguage ? `
|
||||
<span class="language-info">
|
||||
<span class="language-dot" style="background-color: ${getLanguageColor(language)}"></span>
|
||||
${language}
|
||||
</span>
|
||||
` : ''}
|
||||
${showLastUpdate ? `
|
||||
<span class="last-update">Updated ${lastUpdate}</span>
|
||||
` : ''}
|
||||
${data.license ? `
|
||||
<span class="license-info">${data.license.spdx_id || data.license.name}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading repository data:', error);
|
||||
widget.innerHTML = `
|
||||
<div class="github-widget-error">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16">
|
||||
<path fill="currentColor" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"/>
|
||||
</svg>
|
||||
<span>Failed to load ${repo}</span>
|
||||
<small>${error.message}</small>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize on DOM ready
|
||||
document.addEventListener('DOMContentLoaded', initializeGitHubWidgets);
|
||||
|
||||
// Watch for DOM changes (MkDocs Material dynamic content)
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let shouldReinitialize = false;
|
||||
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
// Check if any github-widget elements were added
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === 1 && // Element node
|
||||
(node.classList?.contains('github-widget') ||
|
||||
node.querySelector?.('.github-widget'))) {
|
||||
shouldReinitialize = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldReinitialize) {
|
||||
// Small delay to ensure DOM is stable
|
||||
setTimeout(initializeGitHubWidgets, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Language color mapping (simplified version)
|
||||
function getLanguageColor(language) {
|
||||
const colors = {
|
||||
'JavaScript': '#f1e05a',
|
||||
'TypeScript': '#2b7489',
|
||||
'Python': '#3572A5',
|
||||
'Java': '#b07219',
|
||||
'C++': '#f34b7d',
|
||||
'C': '#555555',
|
||||
'C#': '#239120',
|
||||
'PHP': '#4F5D95',
|
||||
'Ruby': '#701516',
|
||||
'Go': '#00ADD8',
|
||||
'Rust': '#dea584',
|
||||
'Swift': '#ffac45',
|
||||
'Kotlin': '#F18E33',
|
||||
'Scala': '#c22d40',
|
||||
'Shell': '#89e051',
|
||||
'HTML': '#e34c26',
|
||||
'CSS': '#563d7c',
|
||||
'Vue': '#2c3e50',
|
||||
'React': '#61dafb',
|
||||
'Dockerfile': '#384d54'
|
||||
};
|
||||
return colors[language] || '#586069';
|
||||
}
|
||||
338
mkdocs/site/javascripts/home.js
Normal file
338
mkdocs/site/javascripts/home.js
Normal file
@@ -0,0 +1,338 @@
|
||||
// Changemaker Lite - Minimal Interactions
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Terminal copy functionality
|
||||
const terminals = document.querySelectorAll('.terminal-box');
|
||||
terminals.forEach(terminal => {
|
||||
terminal.addEventListener('click', function() {
|
||||
const code = this.textContent.trim();
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
// Quick visual feedback
|
||||
this.style.background = '#0a0a0a';
|
||||
setTimeout(() => {
|
||||
this.style.background = '#000';
|
||||
}, 200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Smooth scroll for anchors
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Reduced motion support
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
document.documentElement.style.scrollBehavior = 'auto';
|
||||
const style = document.createElement('style');
|
||||
style.textContent = '*, *::before, *::after { animation: none !important; transition: none !important; }';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
});
|
||||
|
||||
// Changemaker Lite - Smooth Grid Interactions
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Smooth scroll for anchors
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add stagger animation to grid cards on scroll
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry, index) => {
|
||||
if (entry.isIntersecting) {
|
||||
setTimeout(() => {
|
||||
entry.target.style.opacity = '1';
|
||||
entry.target.style.transform = 'translateY(0)';
|
||||
}, index * 50);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// Observe all grid cards (exclude site-card and stat-card which have their own observer)
|
||||
document.querySelectorAll('.grid-card:not(.site-card):not(.stat-card)').forEach((card, index) => {
|
||||
card.style.opacity = '0';
|
||||
card.style.transform = 'translateY(20px)';
|
||||
card.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
||||
observer.observe(card);
|
||||
});
|
||||
|
||||
// Neon hover effect for service cards
|
||||
document.querySelectorAll('.service-card').forEach(card => {
|
||||
card.addEventListener('mouseenter', function(e) {
|
||||
const rect = this.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const ripple = document.createElement('div');
|
||||
ripple.style.position = 'absolute';
|
||||
ripple.style.left = x + 'px';
|
||||
ripple.style.top = y + 'px';
|
||||
ripple.style.width = '0';
|
||||
ripple.style.height = '0';
|
||||
ripple.style.borderRadius = '50%';
|
||||
ripple.style.background = 'rgba(91, 206, 250, 0.3)';
|
||||
ripple.style.transform = 'translate(-50%, -50%)';
|
||||
ripple.style.pointerEvents = 'none';
|
||||
ripple.style.transition = 'width 0.6s, height 0.6s, opacity 0.6s';
|
||||
|
||||
this.appendChild(ripple);
|
||||
|
||||
setTimeout(() => {
|
||||
ripple.style.width = '200px';
|
||||
ripple.style.height = '200px';
|
||||
ripple.style.opacity = '0';
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
ripple.remove();
|
||||
}, 600);
|
||||
});
|
||||
});
|
||||
|
||||
// Animated counter for hero stats (the smaller stat grid)
|
||||
const animateValue = (element, start, end, duration) => {
|
||||
const range = end - start;
|
||||
const increment = range / (duration / 16);
|
||||
let current = start;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (current >= end) {
|
||||
current = end;
|
||||
clearInterval(timer);
|
||||
}
|
||||
element.textContent = Math.round(current);
|
||||
}, 16);
|
||||
};
|
||||
|
||||
// Animate hero stat numbers on scroll (only for .stat-item, not .stat-card)
|
||||
const heroStatObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const statNumber = entry.target.querySelector('.stat-number');
|
||||
if (statNumber && !statNumber.animated) {
|
||||
statNumber.animated = true;
|
||||
const value = parseInt(statNumber.textContent);
|
||||
if (!isNaN(value)) {
|
||||
statNumber.textContent = '0';
|
||||
animateValue(statNumber, 0, value, 1000);
|
||||
}
|
||||
}
|
||||
heroStatObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
document.querySelectorAll('.stat-item').forEach(stat => {
|
||||
heroStatObserver.observe(stat);
|
||||
});
|
||||
|
||||
// Animated counter for proof stats - improved version
|
||||
const proofStatObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const statCard = entry.target;
|
||||
const counter = statCard.querySelector('.stat-counter');
|
||||
const statValue = statCard.getAttribute('data-stat');
|
||||
|
||||
if (counter && statValue && !counter.hasAttribute('data-animated')) {
|
||||
counter.setAttribute('data-animated', 'true');
|
||||
|
||||
// Add counting class for visual effect
|
||||
counter.classList.add('counting');
|
||||
|
||||
// Special handling for different stat types
|
||||
if (statValue === '1') {
|
||||
// AI Ready - just animate the text
|
||||
counter.style.opacity = '0';
|
||||
counter.style.transform = 'scale(0.5)';
|
||||
setTimeout(() => {
|
||||
counter.style.transition = 'all 0.8s ease';
|
||||
counter.style.opacity = '1';
|
||||
counter.style.transform = 'scale(1)';
|
||||
}, 200);
|
||||
} else if (statValue === '30' || statValue === '2') {
|
||||
// Time values like "30min", "2hr"
|
||||
const originalText = counter.textContent;
|
||||
counter.textContent = '0';
|
||||
counter.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
counter.style.transition = 'all 0.8s ease';
|
||||
counter.style.opacity = '1';
|
||||
counter.textContent = originalText;
|
||||
}, 200);
|
||||
} else {
|
||||
// Numeric values - animate the counting
|
||||
const value = parseInt(statValue);
|
||||
const originalText = counter.textContent;
|
||||
counter.textContent = '0';
|
||||
|
||||
// Simple counting animation
|
||||
let current = 0;
|
||||
const increment = value / 30; // 30 steps
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (current >= value) {
|
||||
current = value;
|
||||
clearInterval(timer);
|
||||
// Restore original formatted text
|
||||
setTimeout(() => {
|
||||
counter.textContent = originalText;
|
||||
}, 200);
|
||||
} else {
|
||||
counter.textContent = Math.floor(current).toLocaleString();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
proofStatObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
threshold: 0.3,
|
||||
rootMargin: '0px 0px -20px 0px'
|
||||
});
|
||||
|
||||
// Observe proof stat cards (only those with data-stat attribute)
|
||||
document.querySelectorAll('.stat-card[data-stat]').forEach(stat => {
|
||||
proofStatObserver.observe(stat);
|
||||
});
|
||||
|
||||
// Site card hover effects
|
||||
document.querySelectorAll('.site-card').forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
const icon = this.querySelector('.site-icon');
|
||||
if (icon) {
|
||||
icon.style.animation = 'none';
|
||||
setTimeout(() => {
|
||||
icon.style.animation = 'site-float 3s ease-in-out infinite';
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Staggered animation for proof cards - improved
|
||||
const proofCardObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const container = entry.target.closest('.sites-grid, .stats-grid');
|
||||
if (container) {
|
||||
const allCards = Array.from(container.children);
|
||||
const index = allCards.indexOf(entry.target);
|
||||
|
||||
setTimeout(() => {
|
||||
entry.target.style.opacity = '1';
|
||||
entry.target.style.transform = 'translateY(0)';
|
||||
}, index * 150); // Slightly longer delay for better effect
|
||||
}
|
||||
|
||||
proofCardObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
threshold: 0.2,
|
||||
rootMargin: '0px 0px -30px 0px'
|
||||
});
|
||||
|
||||
// Observe site cards and stat cards for stagger animation
|
||||
document.querySelectorAll('.site-card, .stat-card').forEach((card) => {
|
||||
// Set initial state
|
||||
card.style.opacity = '0';
|
||||
card.style.transform = 'translateY(30px)';
|
||||
card.style.transition = 'opacity 0.8s ease, transform 0.8s ease';
|
||||
proofCardObserver.observe(card);
|
||||
});
|
||||
|
||||
// Add parallax effect to hero section
|
||||
let ticking = false;
|
||||
function updateParallax() {
|
||||
const scrolled = window.pageYOffset;
|
||||
const hero = document.querySelector('.hero-grid');
|
||||
if (hero) {
|
||||
hero.style.transform = `translateY(${scrolled * 0.3}px)`;
|
||||
}
|
||||
ticking = false;
|
||||
}
|
||||
|
||||
function requestTick() {
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(updateParallax);
|
||||
ticking = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only add parallax on desktop
|
||||
if (window.innerWidth > 768) {
|
||||
window.addEventListener('scroll', requestTick);
|
||||
}
|
||||
|
||||
// Button ripple effect
|
||||
document.querySelectorAll('.btn').forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
const rect = this.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const ripple = document.createElement('span');
|
||||
ripple.style.position = 'absolute';
|
||||
ripple.style.left = x + 'px';
|
||||
ripple.style.top = y + 'px';
|
||||
ripple.className = 'btn-ripple';
|
||||
|
||||
this.appendChild(ripple);
|
||||
|
||||
setTimeout(() => {
|
||||
ripple.remove();
|
||||
}, 600);
|
||||
});
|
||||
});
|
||||
|
||||
// Reduced motion support
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
document.documentElement.style.scrollBehavior = 'auto';
|
||||
window.removeEventListener('scroll', requestTick);
|
||||
}
|
||||
});
|
||||
|
||||
// Add CSS for button ripple
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.btn-ripple {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: rgba(111, 66, 193, 0.5); /* mkdocs purple */
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
animation: ripple-animation 0.6s ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes ripple-animation {
|
||||
to {
|
||||
transform: translate(-50%, -50%) scale(10);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
Reference in New Issue
Block a user