676 lines
25 KiB
JavaScript
676 lines
25 KiB
JavaScript
/**
|
||
* Modern Calculator - UX-polished service cost calculator
|
||
* Production-ready calculator with a11y, dark mode, localStorage, live price updates
|
||
*/
|
||
|
||
class ModernCalculator {
|
||
constructor() {
|
||
this.state = {
|
||
step: 0,
|
||
selectedService: null,
|
||
selectedComplexity: null,
|
||
selectedTimeline: null,
|
||
promoCode: '',
|
||
darkMode: this.getStoredTheme()
|
||
};
|
||
|
||
this.services = {
|
||
web: { key: 'web', basePrice: 500000, name: 'Веб-разработка' },
|
||
mobile: { key: 'mobile', basePrice: 800000, name: 'Мобильные приложения' },
|
||
design: { key: 'design', basePrice: 300000, name: 'UI/UX Дизайн' },
|
||
marketing: { key: 'marketing', basePrice: 200000, name: 'Цифровой маркетинг' }
|
||
};
|
||
|
||
this.complexity = {
|
||
simple: { key: 'simple', multiplier: 1, name: 'Простой' },
|
||
medium: { key: 'medium', multiplier: 1.5, name: 'Средний' },
|
||
complex: { key: 'complex', multiplier: 2.5, name: 'Сложный' }
|
||
};
|
||
|
||
this.timeline = {
|
||
extended: { key: 'extended', multiplier: 0.8, name: 'Увеличенные сроки' },
|
||
standard: { key: 'standard', multiplier: 1, name: 'Стандартные сроки' },
|
||
rush: { key: 'rush', multiplier: 1.5, name: 'Срочно' }
|
||
};
|
||
|
||
this.promoCodes = {
|
||
'HELLO10': 0.9,
|
||
'FRIENDS5': 0.95
|
||
};
|
||
|
||
this.storageKey = 'calculator_draft';
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.loadFromStorage();
|
||
this.setupEventListeners();
|
||
this.updateUI();
|
||
this.applyTheme();
|
||
this.setupKeyboardNavigation();
|
||
|
||
// Show sticky price display on page load
|
||
setTimeout(() => {
|
||
const stickyContainer = document.getElementById('stickyPriceContainer');
|
||
if (stickyContainer) {
|
||
stickyContainer.style.display = 'block';
|
||
}
|
||
}, 500);
|
||
}
|
||
|
||
// Storage management
|
||
loadFromStorage() {
|
||
try {
|
||
const saved = localStorage.getItem(this.storageKey);
|
||
if (saved) {
|
||
const savedState = JSON.parse(saved);
|
||
this.state = { ...this.state, ...savedState };
|
||
}
|
||
} catch (e) {
|
||
console.warn('Failed to load calculator state from localStorage');
|
||
}
|
||
}
|
||
|
||
saveToStorage() {
|
||
try {
|
||
localStorage.setItem(this.storageKey, JSON.stringify(this.state));
|
||
} catch (e) {
|
||
console.warn('Failed to save calculator state to localStorage');
|
||
}
|
||
}
|
||
|
||
getStoredTheme() {
|
||
const stored = localStorage.getItem('theme');
|
||
if (stored) return stored === 'dark';
|
||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||
}
|
||
|
||
// Theme management
|
||
applyTheme() {
|
||
document.documentElement.classList.toggle('dark', this.state.darkMode);
|
||
localStorage.setItem('theme', this.state.darkMode ? 'dark' : 'light');
|
||
|
||
const toggle = document.getElementById('darkModeToggle');
|
||
if (toggle) toggle.checked = this.state.darkMode;
|
||
}
|
||
|
||
// Price calculation
|
||
calculatePrice() {
|
||
if (!this.state.selectedService) return 0;
|
||
|
||
const basePrice = this.services[this.state.selectedService].basePrice;
|
||
const complexityMultiplier = this.state.selectedComplexity ?
|
||
this.complexity[this.state.selectedComplexity].multiplier : 1;
|
||
const timelineMultiplier = this.state.selectedTimeline ?
|
||
this.timeline[this.state.selectedTimeline].multiplier : 1;
|
||
const promoMultiplier = this.promoCodes[this.state.promoCode.toUpperCase()] || 1;
|
||
|
||
return Math.round(basePrice * complexityMultiplier * timelineMultiplier * promoMultiplier);
|
||
}
|
||
|
||
formatPrice(amount) {
|
||
return new Intl.NumberFormat('ko-KR', {
|
||
style: 'currency',
|
||
currency: 'KRW',
|
||
minimumFractionDigits: 0,
|
||
maximumFractionDigits: 0
|
||
}).format(amount);
|
||
}
|
||
|
||
// Translation helper
|
||
t(key) {
|
||
const translations = {
|
||
'calculator.result.estimated_price': 'Расчетная цена',
|
||
'calculator.complexity.simple': 'Простой',
|
||
'calculator.complexity.medium': 'Средний',
|
||
'calculator.complexity.complex': 'Сложный',
|
||
'calculator.timeline.extended': 'Увеличенные сроки',
|
||
'calculator.timeline.standard': 'Стандартные сроки',
|
||
'calculator.timeline.rush': 'Срочно',
|
||
'calculator.next_step': 'Далее',
|
||
'calculator.calculate': 'Рассчитать',
|
||
'services.web.title': 'Веб-разработка',
|
||
'services.mobile.title': 'Мобильные приложения',
|
||
'services.design.title': 'UI/UX Дизайн',
|
||
'services.marketing.title': 'Цифровой маркетинг'
|
||
};
|
||
|
||
if (window.calculatorTranslations && window.calculatorTranslations[key]) {
|
||
return window.calculatorTranslations[key];
|
||
}
|
||
|
||
return translations[key] || key;
|
||
}
|
||
|
||
// UI Updates
|
||
updatePriceDisplay() {
|
||
const price = this.calculatePrice();
|
||
const formattedPrice = this.formatPrice(price);
|
||
|
||
// Update main price displays
|
||
const priceElements = ['currentPrice', 'finalPrice'];
|
||
priceElements.forEach(id => {
|
||
const element = document.getElementById(id);
|
||
if (element) {
|
||
element.textContent = formattedPrice;
|
||
element.setAttribute('aria-live', 'polite');
|
||
}
|
||
});
|
||
|
||
// Update detailed breakdown
|
||
this.updatePriceBreakdown();
|
||
|
||
// Show sticky price display
|
||
const stickyContainer = document.getElementById('stickyPriceContainer');
|
||
if (stickyContainer) {
|
||
stickyContainer.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
updatePriceBreakdown() {
|
||
// Service selection
|
||
const selectedServiceDiv = document.getElementById('selectedService');
|
||
const serviceName = document.getElementById('serviceName');
|
||
const servicePrice = document.getElementById('servicePrice');
|
||
|
||
if (this.state.selectedService && selectedServiceDiv && serviceName && servicePrice) {
|
||
const service = this.services[this.state.selectedService];
|
||
serviceName.textContent = service.name;
|
||
servicePrice.textContent = this.formatPrice(service.basePrice);
|
||
selectedServiceDiv.classList.remove('hidden');
|
||
} else if (selectedServiceDiv) {
|
||
selectedServiceDiv.classList.add('hidden');
|
||
}
|
||
|
||
// Complexity
|
||
const selectedComplexityDiv = document.getElementById('selectedComplexity');
|
||
const complexityName = document.getElementById('complexityName');
|
||
const complexityMultiplier = document.getElementById('complexityMultiplier');
|
||
|
||
if (this.state.selectedComplexity && selectedComplexityDiv && complexityName && complexityMultiplier) {
|
||
const complexity = this.complexity[this.state.selectedComplexity];
|
||
complexityName.textContent = complexity.name;
|
||
complexityMultiplier.textContent = complexity.multiplier;
|
||
selectedComplexityDiv.classList.remove('hidden');
|
||
} else if (selectedComplexityDiv) {
|
||
selectedComplexityDiv.classList.add('hidden');
|
||
}
|
||
|
||
// Timeline
|
||
const selectedTimelineDiv = document.getElementById('selectedTimeline');
|
||
const timelineName = document.getElementById('timelineName');
|
||
const timelineMultiplier = document.getElementById('timelineMultiplier');
|
||
|
||
if (this.state.selectedTimeline && selectedTimelineDiv && timelineName && timelineMultiplier) {
|
||
const timeline = this.timeline[this.state.selectedTimeline];
|
||
timelineName.textContent = timeline.name;
|
||
timelineMultiplier.textContent = timeline.multiplier;
|
||
selectedTimelineDiv.classList.remove('hidden');
|
||
} else if (selectedTimelineDiv) {
|
||
selectedTimelineDiv.classList.add('hidden');
|
||
}
|
||
|
||
// Promo code
|
||
const appliedPromoDiv = document.getElementById('appliedPromo');
|
||
const promoCode = document.getElementById('promoCode');
|
||
const promoDiscount = document.getElementById('promoDiscount');
|
||
|
||
if (this.state.promoCode && this.promoCodes[this.state.promoCode.toUpperCase()] && appliedPromoDiv && promoCode && promoDiscount) {
|
||
const discount = this.promoCodes[this.state.promoCode.toUpperCase()];
|
||
const discountPercent = Math.round((1 - discount) * 100);
|
||
promoCode.textContent = this.state.promoCode.toUpperCase();
|
||
promoDiscount.textContent = `-${discountPercent}%`;
|
||
appliedPromoDiv.classList.remove('hidden');
|
||
} else if (appliedPromoDiv) {
|
||
appliedPromoDiv.classList.add('hidden');
|
||
}
|
||
|
||
// Final calculation
|
||
const finalCalculation = document.getElementById('finalCalculation');
|
||
if (finalCalculation) {
|
||
const price = this.calculatePrice();
|
||
finalCalculation.textContent = this.formatPrice(price);
|
||
}
|
||
}
|
||
|
||
updateStepIndicators() {
|
||
for (let i = 0; i < 3; i++) {
|
||
const indicator = document.getElementById(`step-indicator-${i}`);
|
||
const checkIcon = document.getElementById(`check-${i}`);
|
||
const numberSpan = document.getElementById(`number-${i}`);
|
||
const progressLine = document.getElementById(`progress-line-${i}`);
|
||
|
||
if (!indicator) continue;
|
||
|
||
// Reset classes
|
||
indicator.className = 'w-12 h-12 rounded-full flex items-center justify-center text-sm font-bold transition-all duration-300';
|
||
|
||
if (i < this.state.step) {
|
||
// Completed step
|
||
indicator.classList.add('bg-green-500', 'text-white', 'shadow-lg');
|
||
if (checkIcon) {
|
||
checkIcon.classList.remove('hidden');
|
||
numberSpan.classList.add('hidden');
|
||
}
|
||
if (progressLine) progressLine.style.width = '100%';
|
||
} else if (i === this.state.step) {
|
||
// Current step
|
||
indicator.classList.add('bg-blue-600', 'text-white', 'shadow-lg');
|
||
if (checkIcon) checkIcon.classList.add('hidden');
|
||
if (numberSpan) numberSpan.classList.remove('hidden');
|
||
if (progressLine) progressLine.style.width = '0%';
|
||
} else {
|
||
// Future step
|
||
indicator.classList.add('bg-gray-300', 'dark:bg-gray-600', 'text-gray-600', 'dark:text-gray-400');
|
||
if (checkIcon) checkIcon.classList.add('hidden');
|
||
if (numberSpan) numberSpan.classList.remove('hidden');
|
||
if (progressLine) progressLine.style.width = '0%';
|
||
}
|
||
}
|
||
|
||
// Update overall progress
|
||
const overallProgress = document.getElementById('overallProgress');
|
||
if (overallProgress) {
|
||
const progressPercentage = ((this.state.step + 1) / 3) * 100;
|
||
overallProgress.style.width = `${progressPercentage}%`;
|
||
}
|
||
}
|
||
|
||
updateStepContent() {
|
||
// Hide all steps
|
||
document.querySelectorAll('.step-content').forEach(step => {
|
||
step.classList.add('hidden');
|
||
});
|
||
|
||
// Show current step
|
||
const currentStep = document.getElementById(`step-${this.state.step + 1}`);
|
||
if (currentStep) {
|
||
currentStep.classList.remove('hidden');
|
||
}
|
||
|
||
// Update navigation buttons
|
||
this.updateNavigationButtons();
|
||
}
|
||
|
||
updateNavigationButtons() {
|
||
const prevBtn = document.getElementById('prevBtn');
|
||
const nextBtn = document.getElementById('nextBtn');
|
||
const nextBtnText = document.getElementById('nextBtnText');
|
||
|
||
if (prevBtn) {
|
||
if (this.state.step > 0) {
|
||
prevBtn.classList.remove('hidden');
|
||
} else {
|
||
prevBtn.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
if (nextBtn && nextBtnText) {
|
||
if (this.state.step < 2) {
|
||
nextBtn.classList.remove('hidden');
|
||
const canProceed = this.canProceedToNextStep();
|
||
nextBtn.disabled = !canProceed;
|
||
|
||
if (this.state.step === 1) {
|
||
nextBtnText.textContent = this.t('calculator.calculate');
|
||
} else {
|
||
nextBtnText.textContent = this.t('calculator.next_step');
|
||
}
|
||
} else {
|
||
nextBtn.classList.add('hidden');
|
||
}
|
||
}
|
||
}
|
||
|
||
canProceedToNextStep() {
|
||
switch (this.state.step) {
|
||
case 0: return !!this.state.selectedService;
|
||
case 1: return !!this.state.selectedComplexity && !!this.state.selectedTimeline;
|
||
case 2: return true;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
updateSelectionUI() {
|
||
// Update service selection
|
||
document.querySelectorAll('.service-card').forEach(card => {
|
||
const service = card.dataset.service;
|
||
const indicator = card.querySelector('.service-indicator');
|
||
|
||
if (service === this.state.selectedService) {
|
||
card.classList.add('selected');
|
||
if (indicator) indicator.classList.add('scale-100');
|
||
card.setAttribute('aria-pressed', 'true');
|
||
} else {
|
||
card.classList.remove('selected');
|
||
if (indicator) indicator.classList.remove('scale-100');
|
||
card.setAttribute('aria-pressed', 'false');
|
||
}
|
||
});
|
||
|
||
// Update complexity selection
|
||
document.querySelectorAll('.complexity-card').forEach(card => {
|
||
const complexity = card.dataset.complexity;
|
||
const cardDiv = card.querySelector('div');
|
||
|
||
if (complexity === this.state.selectedComplexity) {
|
||
cardDiv.classList.add('border-blue-500', 'bg-blue-50', 'dark:bg-blue-900');
|
||
cardDiv.classList.remove('border-transparent');
|
||
card.setAttribute('aria-pressed', 'true');
|
||
} else {
|
||
cardDiv.classList.remove('border-blue-500', 'bg-blue-50', 'dark:bg-blue-900');
|
||
cardDiv.classList.add('border-transparent');
|
||
card.setAttribute('aria-pressed', 'false');
|
||
}
|
||
});
|
||
|
||
// Update timeline selection
|
||
document.querySelectorAll('.timeline-card').forEach(card => {
|
||
const timeline = card.dataset.timeline;
|
||
const cardDiv = card.querySelector('div');
|
||
|
||
if (timeline === this.state.selectedTimeline) {
|
||
cardDiv.classList.add('border-blue-500', 'bg-blue-50', 'dark:bg-blue-900');
|
||
cardDiv.classList.remove('border-transparent');
|
||
card.setAttribute('aria-pressed', 'true');
|
||
} else {
|
||
cardDiv.classList.remove('border-blue-500', 'bg-blue-50', 'dark:bg-blue-900');
|
||
cardDiv.classList.add('border-transparent');
|
||
card.setAttribute('aria-pressed', 'false');
|
||
}
|
||
});
|
||
}
|
||
|
||
updatePriceBreakdown() {
|
||
const breakdown = document.getElementById('priceBreakdown');
|
||
if (!breakdown || this.state.step !== 2) return;
|
||
|
||
breakdown.innerHTML = '';
|
||
|
||
if (!this.state.selectedService) return;
|
||
|
||
const basePrice = this.services[this.state.selectedService].basePrice;
|
||
const complexityMultiplier = this.state.selectedComplexity ?
|
||
this.complexity[this.state.selectedComplexity].multiplier : 1;
|
||
const timelineMultiplier = this.state.selectedTimeline ?
|
||
this.timeline[this.state.selectedTimeline].multiplier : 1;
|
||
const promoMultiplier = this.promoCodes[this.state.promoCode.toUpperCase()] || 1;
|
||
const appliedPromo = this.promoCodes[this.state.promoCode.toUpperCase()] ?
|
||
this.state.promoCode.toUpperCase() : '';
|
||
|
||
// Base price
|
||
breakdown.appendChild(this.createPriceLineElement(
|
||
'Базовая стоимость',
|
||
basePrice
|
||
));
|
||
|
||
// Complexity adjustment
|
||
if (complexityMultiplier !== 1) {
|
||
breakdown.appendChild(this.createPriceLineElement(
|
||
'Сложность',
|
||
basePrice * complexityMultiplier,
|
||
`×${complexityMultiplier}`
|
||
));
|
||
}
|
||
|
||
// Timeline adjustment
|
||
if (timelineMultiplier !== 1) {
|
||
breakdown.appendChild(this.createPriceLineElement(
|
||
'Сроки',
|
||
basePrice * complexityMultiplier * timelineMultiplier,
|
||
`×${timelineMultiplier}`
|
||
));
|
||
}
|
||
|
||
// Promo code discount
|
||
if (appliedPromo) {
|
||
breakdown.appendChild(this.createPriceLineElement(
|
||
'Промокод',
|
||
basePrice * complexityMultiplier * timelineMultiplier * promoMultiplier,
|
||
appliedPromo
|
||
));
|
||
}
|
||
}
|
||
|
||
createPriceLineElement(label, amount, badge = null) {
|
||
const div = document.createElement('div');
|
||
div.className = 'flex justify-between items-center py-3 border-b border-gray-100 dark:border-gray-700 last:border-b-0';
|
||
|
||
div.innerHTML = `
|
||
<span class="flex items-center gap-2 text-gray-700 dark:text-gray-300">
|
||
${label}
|
||
${badge ? `<span class="text-xs font-medium text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900 px-2 py-1 rounded-full">${badge}</span>` : ''}
|
||
</span>
|
||
<span class="font-semibold text-gray-900 dark:text-white">${this.formatPrice(amount)}</span>
|
||
`;
|
||
|
||
return div;
|
||
}
|
||
|
||
updateUI() {
|
||
this.updateStepIndicators();
|
||
this.updateStepContent();
|
||
this.updateSelectionUI();
|
||
this.updatePriceDisplay();
|
||
this.updatePriceBreakdown();
|
||
this.saveToStorage();
|
||
}
|
||
|
||
// Event handlers
|
||
setupEventListeners() {
|
||
// Reset button
|
||
const resetButton = document.getElementById('resetButton');
|
||
if (resetButton) {
|
||
resetButton.addEventListener('click', () => {
|
||
this.resetCalculator();
|
||
});
|
||
}
|
||
|
||
// Dark mode toggle
|
||
const darkModeToggle = document.getElementById('darkModeToggle');
|
||
if (darkModeToggle) {
|
||
darkModeToggle.addEventListener('change', () => {
|
||
this.state.darkMode = darkModeToggle.checked;
|
||
this.applyTheme();
|
||
this.saveToStorage();
|
||
});
|
||
}
|
||
|
||
// Service selection
|
||
document.querySelectorAll('.service-card').forEach(card => {
|
||
card.addEventListener('click', () => {
|
||
const service = card.dataset.service;
|
||
this.state.selectedService = service;
|
||
this.updateUI();
|
||
|
||
// Announce selection for screen readers
|
||
this.announceForScreenReader(
|
||
`Выбрана услуга ${this.services[service].name}`
|
||
);
|
||
});
|
||
});
|
||
|
||
// Complexity selection
|
||
document.querySelectorAll('.complexity-card').forEach(card => {
|
||
card.addEventListener('click', () => {
|
||
const complexity = card.dataset.complexity;
|
||
this.state.selectedComplexity = complexity;
|
||
this.updateUI();
|
||
});
|
||
});
|
||
|
||
// Timeline selection
|
||
document.querySelectorAll('.timeline-card').forEach(card => {
|
||
card.addEventListener('click', () => {
|
||
const timeline = card.dataset.timeline;
|
||
this.state.selectedTimeline = timeline;
|
||
this.updateUI();
|
||
});
|
||
});
|
||
|
||
// Navigation buttons
|
||
const prevBtn = document.getElementById('prevBtn');
|
||
const nextBtn = document.getElementById('nextBtn');
|
||
|
||
if (prevBtn) {
|
||
prevBtn.addEventListener('click', () => {
|
||
if (this.state.step > 0) {
|
||
this.state.step--;
|
||
this.updateUI();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (nextBtn) {
|
||
nextBtn.addEventListener('click', () => {
|
||
if (this.state.step < 2 && this.canProceedToNextStep()) {
|
||
this.state.step++;
|
||
this.updateUI();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Promo code
|
||
const applyPromoBtn = document.getElementById('applyPromo');
|
||
if (applyPromoBtn) {
|
||
applyPromoBtn.addEventListener('click', this.applyPromoCode.bind(this));
|
||
}
|
||
|
||
const promoInput = document.getElementById('promoCode');
|
||
if (promoInput) {
|
||
promoInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
this.applyPromoCode();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Final actions
|
||
const getQuoteBtn = document.getElementById('getQuoteBtn');
|
||
if (getQuoteBtn) {
|
||
getQuoteBtn.addEventListener('click', this.submitQuote.bind(this));
|
||
}
|
||
|
||
const recalculateBtn = document.getElementById('recalculateBtn');
|
||
if (recalculateBtn) {
|
||
recalculateBtn.addEventListener('click', this.resetCalculator.bind(this));
|
||
}
|
||
}
|
||
|
||
setupKeyboardNavigation() {
|
||
// Add keyboard support for card selections
|
||
document.querySelectorAll('.service-card, .complexity-card, .timeline-card').forEach(card => {
|
||
card.setAttribute('tabindex', '0');
|
||
card.setAttribute('role', 'button');
|
||
|
||
card.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
card.click();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
applyPromoCode() {
|
||
const promoInput = document.getElementById('promoCode');
|
||
const promoStatus = document.getElementById('promoStatus');
|
||
|
||
if (!promoInput || !promoStatus) return;
|
||
|
||
const code = promoInput.value.trim().toUpperCase();
|
||
|
||
if (this.promoCodes[code]) {
|
||
this.state.promoCode = code;
|
||
promoStatus.textContent = 'Промокод применен';
|
||
promoStatus.className = 'mt-2 text-sm text-green-600 dark:text-green-400';
|
||
this.updateUI();
|
||
} else if (code) {
|
||
promoStatus.textContent = 'Неверный промокод';
|
||
promoStatus.className = 'mt-2 text-sm text-red-600 dark:text-red-400';
|
||
} else {
|
||
this.state.promoCode = '';
|
||
promoStatus.textContent = '';
|
||
this.updateUI();
|
||
}
|
||
}
|
||
|
||
submitQuote() {
|
||
const finalPrice = this.calculatePrice();
|
||
|
||
// In a real application, this would send data to the server
|
||
alert(`Заявка отправлена!\n\nИтоговая стоимость: ${this.formatPrice(finalPrice)}\n\nМы свяжемся с вами в ближайшее время.`);
|
||
|
||
// Clear the form
|
||
this.resetCalculator();
|
||
}
|
||
|
||
resetCalculator() {
|
||
this.state = {
|
||
step: 0,
|
||
selectedService: null,
|
||
selectedComplexity: null,
|
||
selectedTimeline: null,
|
||
promoCode: '',
|
||
darkMode: this.state.darkMode
|
||
};
|
||
|
||
// Clear promo code input and status
|
||
const promoInput = document.getElementById('promoCode');
|
||
const promoStatus = document.getElementById('promoStatus');
|
||
|
||
if (promoInput) promoInput.value = '';
|
||
if (promoStatus) promoStatus.textContent = '';
|
||
|
||
this.updateUI();
|
||
}
|
||
|
||
announceForScreenReader(message) {
|
||
const announcement = document.createElement('div');
|
||
announcement.setAttribute('aria-live', 'polite');
|
||
announcement.setAttribute('aria-atomic', 'true');
|
||
announcement.className = 'sr-only';
|
||
announcement.textContent = message;
|
||
|
||
document.body.appendChild(announcement);
|
||
|
||
setTimeout(() => {
|
||
document.body.removeChild(announcement);
|
||
}, 1000);
|
||
}
|
||
}
|
||
|
||
// Initialize calculator when DOM is loaded
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
new ModernCalculator();
|
||
});
|
||
|
||
// Add CSS classes for screen reader only content
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.sr-only {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
padding: 0;
|
||
margin: -1px;
|
||
overflow: hidden;
|
||
clip: rect(0, 0, 0, 0);
|
||
white-space: nowrap;
|
||
border: 0;
|
||
}
|
||
|
||
.service-card.selected .service-indicator {
|
||
transform: scale(1);
|
||
}
|
||
|
||
.service-card:focus,
|
||
.complexity-card:focus,
|
||
.timeline-card:focus {
|
||
outline: 2px solid #3b82f6;
|
||
outline-offset: 2px;
|
||
}
|
||
`;
|
||
|
||
document.head.appendChild(style); |