| class StatsCounter extends HTMLElement { |
| static get observedAttributes() { |
| return ['value', 'label', 'icon']; |
| } |
| |
| constructor() { |
| super(); |
| this.attachShadow({ mode: 'open' }); |
| this.animated = false; |
| this.currentValue = 0; |
| } |
| |
| connectedCallback() { |
| this.render(); |
| this.observeIntersection(); |
| } |
| |
| attributeChangedCallback() { |
| this.render(); |
| } |
| |
| observeIntersection() { |
| const observer = new IntersectionObserver((entries) => { |
| entries.forEach(entry => { |
| if (entry.isIntersecting && !this.animated) { |
| this.animateCounter(); |
| this.animated = true; |
| } |
| }); |
| }, { |
| threshold: 0.5 |
| }); |
| |
| observer.observe(this); |
| } |
| |
| animateCounter() { |
| const value = parseInt(this.getAttribute('value') || '0'); |
| const duration = 2000; |
| const steps = 60; |
| const increment = value / steps; |
| const stepDuration = duration / steps; |
| |
| const counterElement = this.shadowRoot.querySelector('.counter-value'); |
| if (!counterElement) return; |
| |
| let current = 0; |
| const timer = setInterval(() => { |
| current += increment; |
| if (current >= value) { |
| current = value; |
| clearInterval(timer); |
| } |
| counterElement.textContent = Math.floor(current).toLocaleString(); |
| }, stepDuration); |
| } |
| |
| render() { |
| const value = this.getAttribute('value') || '0'; |
| const label = this.getAttribute('label') || 'Statistic'; |
| const icon = this.getAttribute('icon') || 'trending-up'; |
| |
| this.shadowRoot.innerHTML = ` |
| <style> |
| :host { |
| display: block; |
| } |
| |
| .stat-card { |
| transition: transform 0.3s ease; |
| } |
| |
| .stat-card:hover { |
| transform: scale(1.05); |
| } |
| |
| .counter-value { |
| font-variant-numeric: tabular-nums; |
| } |
| </style> |
| |
| <div class="stat-card text-center p-4"> |
| <div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-indigo-100 to-purple-100 mb-4"> |
| <i data-feather="${icon}" class="text-indigo-600 w-8 h-8"></i> |
| </div> |
| |
| <div class="counter-value text-3xl md:text-4xl font-bold text-gray-900 mb-2">0</div> |
| |
| <div class="text-gray-600 font-medium">${label}</div> |
| </div> |
| `; |
| |
| |
| setTimeout(() => { |
| if (typeof feather !== 'undefined') { |
| feather.replace(); |
| } |
| }, 100); |
| } |
| } |
|
|
| customElements.define('stats-counter', StatsCounter); |