Main functions

This commit is contained in:
2025-10-26 14:44:10 +09:00
parent 6ff35e26f4
commit 291fc63a4c
901 changed files with 79783 additions and 201383 deletions

View File

@@ -465,89 +465,408 @@ body {
/* Calculator Styles */
.calculator-step {
display: none;
animation: fadeInUp 0.5s ease-in-out;
}
.calculator-step.active {
display: block;
}
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* Step Indicators */
.calculator-progress-wrapper {
position: relative;
}
.step-indicator {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: #e5e7eb;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-bottom: 8px;
transition: all 0.3s ease;
}
.step-indicator.active .step-number {
background: linear-gradient(135deg, #3B82F6, #8B5CF6);
color: white;
transform: scale(1.1);
}
.step-indicator.completed .step-number {
background: linear-gradient(135deg, #10B981, #059669);
color: white;
transform: scale(1.05);
}
.step-indicator.completed .step-number::after {
content: '✓';
position: absolute;
font-size: 14px;
font-weight: bold;
}
.step-label {
font-size: 0.875rem;
color: #6b7280;
font-weight: 500;
text-align: center;
transition: color 0.3s ease;
}
.step-indicator.active .step-label {
color: #3B82F6;
font-weight: 600;
}
.step-indicator.completed .step-label {
color: #10B981;
font-weight: 600;
}
.step-connector {
flex: 1;
height: 2px;
background: #e5e7eb;
margin: 0 1rem;
position: relative;
top: -15px;
}
/* Selection indicators */
.selection-indicator {
transition: all 0.3s ease;
}
.service-option.selected .selection-indicator,
.complexity-option.selected .selection-indicator,
.timeline-option.selected .selection-indicator {
opacity: 1 !important;
}
.service-option.selected .selection-indicator .w-6,
.complexity-option.selected .selection-indicator .w-6,
.timeline-option.selected .selection-indicator .w-6 {
background: #3B82F6;
border-color: #3B82F6;
position: relative;
}
.service-option.selected .selection-indicator .w-6::after,
.complexity-option.selected .selection-indicator .w-6::after,
.timeline-option.selected .selection-indicator .w-6::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 12px;
font-weight: bold;
}
.service-option,
.complexity-option,
.timeline-option {
cursor: pointer;
transition: all 0.3s ease;
position: relative;
border-width: 2px;
}
/* Calculator specific improvements */
.calculator-step {
width: 100%;
}
/* Better spacing and padding for calculator */
.calculator-step .service-option,
.calculator-step .complexity-option,
.calculator-step .timeline-option {
margin: 0.5rem 0;
padding: 1.5rem;
}
/* Ensure text doesn't touch edges */
.calculator-step h2,
.calculator-step h3,
.calculator-step h4,
.calculator-step p {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
/* Responsive padding for service options */
@media (max-width: 640px) {
.calculator-step .service-option,
.calculator-step .complexity-option,
.calculator-step .timeline-option {
padding: 1rem;
margin: 0.25rem 0;
}
.calculator-step .space-y-4 > *,
.calculator-step .space-y-8 > * {
margin-top: 0.75rem;
}
}
/* Compact service options for full width layout */
.service-option.compact {
padding: 1rem 1.5rem;
border-radius: 12px;
margin-bottom: 0.5rem;
}
.service-option.compact .w-14 {
width: 3rem;
height: 3rem;
}
.service-option.compact h3 {
font-size: 1rem;
margin-bottom: 0.25rem;
}
.service-option.compact p {
font-size: 0.875rem;
line-height: 1.4;
}
/* Table-style layout for complexity and timeline */
.complexity-option,
.timeline-option {
min-height: 140px;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.complexity-option .w-16,
.timeline-option .w-16 {
width: 3.5rem;
height: 3.5rem;
margin: 0 auto 1rem auto;
}
.complexity-option h4,
.timeline-option h4 {
font-size: 1.1rem;
margin-bottom: 0.75rem;
}
.complexity-option p,
.timeline-option p {
font-size: 0.9rem;
line-height: 1.4;
margin-bottom: 0.5rem;
}
/* Enhanced button styles */
.next-step,
.prev-step,
.restart-calculator {
font-weight: 600;
letter-spacing: 0.025em;
white-space: nowrap;
}
/* Better result card styling */
#step-3 .relative {
max-width: none;
}
#step-3 #final-price {
word-break: break-word;
}
/* Service Options Hover Effects */
.service-option[data-service="web"]:hover {
border-color: #3B82F6 !important;
}
.service-option[data-service="mobile"]:hover {
border-color: #10B981 !important;
}
.service-option[data-service="design"]:hover {
border-color: #8B5CF6 !important;
}
.service-option[data-service="marketing"]:hover {
border-color: #F59E0B !important;
}
/* Complexity Options Hover Effects */
.complexity-option[data-value="simple"]:hover {
border-color: #10B981 !important;
}
.complexity-option[data-value="medium"]:hover {
border-color: #F59E0B !important;
}
.complexity-option[data-value="complex"]:hover {
border-color: #EF4444 !important;
}
/* Timeline Options Hover Effects */
.timeline-option[data-value="standard"]:hover {
border-color: #3B82F6 !important;
}
.timeline-option[data-value="rush"]:hover {
border-color: #F97316 !important;
}
.timeline-option[data-value="extended"]:hover {
border-color: #059669 !important;
}
.service-option:hover,
.complexity-option:hover,
.timeline-option:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(59, 130, 246, 0.15);
}
/* Selected states with specific colors */
.service-option[data-service="web"].selected {
border-color: #3B82F6 !important;
}
.service-option[data-service="mobile"].selected {
border-color: #10B981 !important;
}
.service-option[data-service="design"].selected {
border-color: #8B5CF6 !important;
}
.service-option[data-service="marketing"].selected {
border-color: #F59E0B !important;
}
.complexity-option[data-value="simple"].selected {
border-color: #10B981 !important;
}
.complexity-option[data-value="medium"].selected {
border-color: #F59E0B !important;
}
.complexity-option[data-value="complex"].selected {
border-color: #EF4444 !important;
}
.timeline-option[data-value="standard"].selected {
border-color: #3B82F6 !important;
}
.timeline-option[data-value="rush"].selected {
border-color: #F97316 !important;
}
.timeline-option[data-value="extended"].selected {
border-color: #059669 !important;
}
.service-option.selected,
.complexity-option.selected,
.timeline-option.selected {
border-color: #3B82F6 !important;
background-color: #EBF8FF !important;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
background: linear-gradient(135deg, #EBF8FF, #F0F9FF) !important;
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(59, 130, 246, 0.2);
}
.service-option.selected::after,
.complexity-option.selected::after,
.timeline-option.selected::after {
content: '✓';
position: absolute;
top: 0.5rem;
right: 0.5rem;
width: 24px;
height: 24px;
background: #3B82F6;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
/* Dark mode adjustments for selected states */
.dark .service-option.selected,
.dark .complexity-option.selected,
.dark .timeline-option.selected {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.1)) !important;
border-color: #60A5FA !important;
}
/* Price Display Animation */
#final-price {
animation: priceReveal 0.8s ease-in-out;
animation: priceReveal 1.2s ease-in-out;
}
@keyframes priceReveal {
0% {
opacity: 0;
transform: scale(0.8);
transform: scale(0.8) rotateY(180deg);
}
50% {
transform: scale(1.1);
transform: scale(1.1) rotateY(90deg);
}
100% {
opacity: 1;
transform: scale(1);
transform: scale(1) rotateY(0deg);
}
}
/* Calculator Progress Bar */
.calculator-progress {
width: 100%;
height: 4px;
height: 6px;
background: #e5e7eb;
border-radius: 2px;
margin-bottom: 2rem;
border-radius: 3px;
overflow: hidden;
position: relative;
}
.calculator-progress-bar {
height: 100%;
background: linear-gradient(90deg, #3B82F6, #8B5CF6);
border-radius: 2px;
transition: width 0.3s ease;
background: linear-gradient(90deg, #3B82F6, #8B5CF6, #06B6D4);
border-radius: 3px;
transition: width 0.6s ease-in-out;
width: 33.33%;
position: relative;
overflow: hidden;
}
.calculator-progress-bar::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: progressShine 2s ease-in-out infinite;
}
@keyframes progressShine {
0% { left: -100%; }
100% { left: 100%; }
}
.calculator-progress-bar.step-1 {
width: 33.33%;
}
@@ -559,6 +878,11 @@ body {
width: 100%;
}
/* Button hover effects */
.group:hover .fas {
transform: translateX(2px);
}
/* Calculator Mobile Improvements */
@media (max-width: 768px) {
.service-option,
@@ -568,7 +892,27 @@ body {
}
#final-price {
font-size: 2rem;
font-size: 2.5rem;
}
.step-connector {
display: none;
}
.calculator-progress-wrapper .flex {
flex-direction: column;
gap: 1rem;
}
.step-indicator {
flex-direction: row;
gap: 0.5rem;
}
.step-number {
width: 32px;
height: 32px;
margin-bottom: 0;
}
}

197
public/css/sticky-price.css Normal file
View File

@@ -0,0 +1,197 @@
/* Independent Floating Price Island */
#stickyPriceContainer {
position: fixed;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
z-index: 45;
display: none;
animation: slideUpIn 0.3s ease-out;
max-width: 56rem; /* max-w-4xl = 896px - match calculator width */
width: calc(100% - 2rem);
/* Enhanced backdrop blur effect */
-webkit-backdrop-filter: blur(12px) saturate(180%);
backdrop-filter: blur(12px) saturate(180%);
/* Smooth transitions */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sticky-price-island {
background: rgba(255, 255, 255, 0.95);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
padding: 1rem;
transition: all 0.3s ease;
}
.dark .sticky-price-island {
background: rgba(31, 41, 55, 0.95);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.sticky-price-island:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.dark .sticky-price-island:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
}
#stickyPriceContainer.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
#stickyPriceContainer.hide {
transform: translateX(-50%) translateY(100%);
opacity: 0;
}
/* Price breakdown sections */
.price-breakdown-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.dark .price-breakdown-item {
border-bottom-color: rgba(255, 255, 255, 0.05);
}
.price-breakdown-item:last-child {
border-bottom: none;
margin-top: 0.5rem;
padding-top: 0.75rem;
border-top: 2px solid rgba(59, 130, 246, 0.2);
font-weight: 600;
}
.breakdown-label {
font-size: 0.875rem;
color: #6b7280;
transition: color 0.2s ease;
}
.dark .breakdown-label {
color: #9ca3af;
}
.breakdown-value {
font-weight: 500;
color: #374151;
transition: color 0.2s ease;
}
.dark .breakdown-value {
color: #f3f4f6;
}
.breakdown-multiplier {
color: #059669;
font-size: 0.875rem;
}
.dark .breakdown-multiplier {
color: #10b981;
}
.breakdown-discount {
color: #dc2626;
font-size: 0.875rem;
}
.dark .breakdown-discount {
color: #ef4444;
}
/* Final price styling */
.final-price-value {
color: #1f2937;
font-size: 1.125rem;
font-weight: 700;
}
.dark .final-price-value {
color: #f9fafb;
}
/* Price update animation */
#currentPrice, #finalCalculation {
transition: all 0.2s ease;
}
.price-update {
animation: priceUpdate 0.3s ease-out;
}
@keyframes priceUpdate {
0% { transform: scale(1); }
50% { transform: scale(1.05); color: #3b82f6; }
100% { transform: scale(1); }
}
/* Animation */
@keyframes slideUpIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
/* Responsive adjustments */
@media (max-width: 640px) {
#stickyPriceContainer {
bottom: 0.5rem;
max-width: calc(100% - 2rem); /* Full width minus padding on mobile */
width: calc(100% - 2rem);
}
.sticky-price-island {
padding: 0.75rem;
border-radius: 12px;
}
.price-breakdown-item {
padding: 0.375rem 0;
}
.breakdown-label {
font-size: 0.8125rem;
}
.final-price-value {
font-size: 1rem;
}
}
/* Hide on very small screens to avoid overlap */
@media (max-width: 380px) {
#stickyPriceContainer {
display: none !important;
}
}
/* Blur effect support fallback */
@supports not (backdrop-filter: blur(12px)) {
.sticky-price-island {
background: rgba(255, 255, 255, 0.95);
}
.dark .sticky-price-island {
background: rgba(17, 24, 39, 0.95);
}
}

3898
public/css/tailwind.css Normal file

File diff suppressed because it is too large Load Diff

103
public/css/theme-toggle.css Normal file
View File

@@ -0,0 +1,103 @@
/* Animated Theme Toggle Styles */
.theme-toggle-slider {
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
background-color 0.3s ease;
}
.theme-sun-icon,
.theme-moon-icon {
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Initial states - Light theme */
.theme-sun-icon {
transform: rotate(0deg) scale(1);
opacity: 1;
}
.theme-moon-icon {
transform: rotate(-180deg) scale(0);
opacity: 0;
}
/* Dark theme states */
.dark .theme-sun-icon {
transform: rotate(180deg) scale(0);
opacity: 0;
}
.dark .theme-moon-icon {
transform: rotate(0deg) scale(1);
opacity: 1;
}
/* Toggle background enhancement with perfect centering */
.theme-toggle-bg {
background: linear-gradient(135deg, #e0f2fe 0%, #fff3e0 100%);
border: 2px solid #e5e7eb;
transition: all 0.3s ease;
position: relative;
display: flex;
align-items: center;
}
.dark .theme-toggle-bg {
background: linear-gradient(135deg, #374151 0%, #4b5563 100%);
border-color: #6b7280;
}
/* Enhanced slider styles with perfect centering */
.theme-toggle-slider {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
/* Позволяем Tailwind CSS управлять позиционированием */
}
.dark .theme-toggle-slider {
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
border-color: rgba(0, 0, 0, 0.2);
}
/* Hover effects */
label:hover .theme-toggle-slider {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Focus styles for accessibility */
input[type="checkbox"]:focus + label > div {
box-shadow: 0 0 0 2px #3b82f6, 0 0 0 4px rgba(59, 130, 246, 0.1);
}
/* Animation keyframes for icon rotation */
@keyframes iconSpinIn {
from {
transform: rotate(-180deg) scale(0);
opacity: 0;
}
to {
transform: rotate(0deg) scale(1);
opacity: 1;
}
}
@keyframes iconSpinOut {
from {
transform: rotate(0deg) scale(1);
opacity: 1;
}
to {
transform: rotate(180deg) scale(0);
opacity: 0;
}
}
/* Animation states */
.icon-animate-in {
animation: iconSpinIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.icon-animate-out {
animation: iconSpinOut 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

View File

@@ -0,0 +1,676 @@
/**
* 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);

View File

@@ -1,384 +0,0 @@
// Calculator Logic
class ProjectCalculator {
constructor() {
this.selectedServices = [];
this.selectedComplexity = null;
this.selectedTimeline = null;
this.currentStep = 1;
this.totalSteps = 3;
this.init();
}
init() {
this.setupEventListeners();
this.updateProgressBar();
}
setupEventListeners() {
// Service selection
document.querySelectorAll('.service-option').forEach(option => {
option.addEventListener('click', (e) => this.handleServiceSelection(e));
});
// Complexity selection
document.querySelectorAll('.complexity-option').forEach(option => {
option.addEventListener('click', (e) => this.handleComplexitySelection(e));
});
// Timeline selection
document.querySelectorAll('.timeline-option').forEach(option => {
option.addEventListener('click', (e) => this.handleTimelineSelection(e));
});
// Navigation buttons
document.querySelectorAll('.next-step').forEach(btn => {
btn.addEventListener('click', () => this.nextStep());
});
document.querySelectorAll('.prev-step').forEach(btn => {
btn.addEventListener('click', () => this.prevStep());
});
// Restart button
const restartBtn = document.querySelector('.restart-calculator');
if (restartBtn) {
restartBtn.addEventListener('click', () => this.restart());
}
}
handleServiceSelection(e) {
const option = e.currentTarget;
const service = option.dataset.service;
const price = parseInt(option.dataset.basePrice);
option.classList.toggle('selected');
if (option.classList.contains('selected')) {
this.selectedServices.push({ service, price });
this.animateSelection(option, true);
} else {
this.selectedServices = this.selectedServices.filter(s => s.service !== service);
this.animateSelection(option, false);
}
this.updateStepButton();
}
handleComplexitySelection(e) {
const option = e.currentTarget;
// Clear previous selections
document.querySelectorAll('.complexity-option').forEach(opt => {
opt.classList.remove('selected');
this.animateSelection(opt, false);
});
// Select current option
option.classList.add('selected');
this.animateSelection(option, true);
this.selectedComplexity = {
value: option.dataset.value,
multiplier: parseFloat(option.dataset.multiplier)
};
this.updateStepButton();
}
handleTimelineSelection(e) {
const option = e.currentTarget;
// Clear previous selections
document.querySelectorAll('.timeline-option').forEach(opt => {
opt.classList.remove('selected');
this.animateSelection(opt, false);
});
// Select current option
option.classList.add('selected');
this.animateSelection(option, true);
this.selectedTimeline = {
value: option.dataset.value,
multiplier: parseFloat(option.dataset.multiplier)
};
this.updateStepButton();
}
animateSelection(element, selected) {
if (selected) {
element.style.borderColor = '#3B82F6';
element.style.backgroundColor = '#EBF8FF';
element.style.transform = 'translateY(-2px)';
element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)';
} else {
element.style.borderColor = '';
element.style.backgroundColor = '';
element.style.transform = '';
element.style.boxShadow = '';
}
}
updateStepButton() {
const step1Button = document.querySelector('#step-1 .next-step');
const step2Button = document.querySelector('#step-2 .next-step');
if (step1Button) {
step1Button.disabled = this.selectedServices.length === 0;
step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6';
}
if (step2Button) {
const isValid = this.selectedComplexity && this.selectedTimeline;
step2Button.disabled = !isValid;
step2Button.style.opacity = isValid ? '1' : '0.6';
}
}
updateProgressBar() {
const progressBar = document.querySelector('.calculator-progress-bar');
if (progressBar) {
const progress = (this.currentStep / this.totalSteps) * 100;
progressBar.style.width = `${progress}%`;
progressBar.className = `calculator-progress-bar step-${this.currentStep}`;
}
}
nextStep() {
if (this.currentStep >= this.totalSteps) return;
const currentStepElement = document.querySelector('.calculator-step.active');
const nextStepElement = currentStepElement.nextElementSibling;
if (nextStepElement && nextStepElement.classList.contains('calculator-step')) {
// Animate out current step
currentStepElement.style.opacity = '0';
currentStepElement.style.transform = 'translateX(-20px)';
setTimeout(() => {
currentStepElement.classList.remove('active');
currentStepElement.style.display = 'none';
// Animate in next step
nextStepElement.classList.add('active');
nextStepElement.style.display = 'block';
nextStepElement.style.opacity = '0';
nextStepElement.style.transform = 'translateX(20px)';
setTimeout(() => {
nextStepElement.style.opacity = '1';
nextStepElement.style.transform = 'translateX(0)';
}, 50);
this.currentStep++;
this.updateProgressBar();
if (this.currentStep === 3) {
setTimeout(() => this.calculateFinalPrice(), 300);
}
}, 200);
}
}
prevStep() {
if (this.currentStep <= 1) return;
const currentStepElement = document.querySelector('.calculator-step.active');
const prevStepElement = currentStepElement.previousElementSibling;
if (prevStepElement && prevStepElement.classList.contains('calculator-step')) {
// Animate out current step
currentStepElement.style.opacity = '0';
currentStepElement.style.transform = 'translateX(20px)';
setTimeout(() => {
currentStepElement.classList.remove('active');
currentStepElement.style.display = 'none';
// Animate in previous step
prevStepElement.classList.add('active');
prevStepElement.style.display = 'block';
prevStepElement.style.opacity = '0';
prevStepElement.style.transform = 'translateX(-20px)';
setTimeout(() => {
prevStepElement.style.opacity = '1';
prevStepElement.style.transform = 'translateX(0)';
}, 50);
this.currentStep--;
this.updateProgressBar();
}, 200);
}
}
calculateFinalPrice() {
let total = 0;
// Calculate base price from services
this.selectedServices.forEach(service => {
total += service.price;
});
// Apply complexity multiplier
if (this.selectedComplexity) {
total *= this.selectedComplexity.multiplier;
}
// Apply timeline multiplier
if (this.selectedTimeline) {
total *= this.selectedTimeline.multiplier;
}
// Animate price reveal
const priceElement = document.getElementById('final-price');
if (priceElement) {
priceElement.style.opacity = '0';
priceElement.style.transform = 'scale(0.8)';
setTimeout(() => {
priceElement.textContent = '₩' + total.toLocaleString();
priceElement.style.opacity = '1';
priceElement.style.transform = 'scale(1)';
}, 300);
}
// Generate summary
setTimeout(() => this.generateSummary(total), 600);
}
generateSummary(total) {
const summary = document.getElementById('project-summary');
if (!summary) return;
let summaryHTML = '';
// Services
const servicesLabel = window.calculatorTranslations?.result?.selected_services || 'Selected Services';
const complexityLabel = window.calculatorTranslations?.result?.complexity || 'Complexity';
const timelineLabel = window.calculatorTranslations?.result?.timeline || 'Timeline';
summaryHTML += `<div class="mb-4"><strong>${servicesLabel}:</strong></div>`;
this.selectedServices.forEach(service => {
summaryHTML += `<div class="ml-4 mb-2 flex items-center">
<i class="fas fa-check-circle text-green-500 mr-2"></i>
${this.getServiceName(service.service)}
<span class="ml-auto font-semibold">₩${service.price.toLocaleString()}</span>
</div>`;
});
// Complexity
if (this.selectedComplexity) {
summaryHTML += `<div class="mt-4 mb-2 flex items-center">
<i class="fas fa-layer-group text-blue-500 mr-2"></i>
<strong>${complexityLabel}:</strong>
<span class="ml-2">${this.getComplexityName(this.selectedComplexity.value)}</span>
<span class="ml-auto text-sm text-gray-500">×${this.selectedComplexity.multiplier}</span>
</div>`;
}
// Timeline
if (this.selectedTimeline) {
summaryHTML += `<div class="mb-2 flex items-center">
<i class="fas fa-clock text-purple-500 mr-2"></i>
<strong>${timelineLabel}:</strong>
<span class="ml-2">${this.getTimelineName(this.selectedTimeline.value)}</span>
<span class="ml-auto text-sm text-gray-500">×${this.selectedTimeline.multiplier}</span>
</div>`;
}
// Animate summary appearance
summary.style.opacity = '0';
summary.innerHTML = summaryHTML;
setTimeout(() => {
summary.style.opacity = '1';
}, 200);
}
getServiceName(service) {
if (window.calculatorTranslations && window.calculatorTranslations.services) {
return window.calculatorTranslations.services[service] || service;
}
const names = {
web: 'Web Development',
mobile: 'Mobile App',
design: 'UI/UX Design',
marketing: 'Digital Marketing'
};
return names[service] || service;
}
getComplexityName(complexity) {
if (window.calculatorTranslations && window.calculatorTranslations.complexity) {
return window.calculatorTranslations.complexity[complexity] || complexity;
}
const names = {
simple: 'Simple',
medium: 'Medium',
complex: 'Complex'
};
return names[complexity] || complexity;
}
getTimelineName(timeline) {
if (window.calculatorTranslations && window.calculatorTranslations.timeline) {
return window.calculatorTranslations.timeline[timeline] || timeline;
}
const names = {
standard: 'Standard',
rush: 'Rush',
extended: 'Extended'
};
return names[timeline] || timeline;
}
restart() {
// Reset all selections
this.selectedServices = [];
this.selectedComplexity = null;
this.selectedTimeline = null;
this.currentStep = 1;
// Reset UI
document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => {
opt.classList.remove('selected');
this.animateSelection(opt, false);
});
// Reset steps
document.querySelectorAll('.calculator-step').forEach(step => {
step.classList.remove('active');
step.style.display = 'none';
step.style.opacity = '1';
step.style.transform = 'translateX(0)';
});
// Show first step
const firstStep = document.getElementById('step-1');
if (firstStep) {
firstStep.classList.add('active');
firstStep.style.display = 'block';
}
this.updateProgressBar();
this.updateStepButton();
}
}
// Initialize calculator when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Theme initialization
const theme = localStorage.getItem('theme') || 'light';
document.documentElement.className = theme === 'dark' ? 'dark' : '';
// Initialize calculator
if (document.querySelector('.calculator-step')) {
new ProjectCalculator();
}
});

View File

@@ -0,0 +1,52 @@
// Отладочная версия калькулятора
console.log('Debug calculator loaded');
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM loaded');
// Проверим наличие элементов
const priceDisplay = document.getElementById('priceDisplay');
const mobilePriceDisplay = document.getElementById('mobilePriceDisplay');
console.log('priceDisplay:', priceDisplay);
console.log('mobilePriceDisplay:', mobilePriceDisplay);
if (priceDisplay) {
console.log('priceDisplay classes:', priceDisplay.className);
// Тестируем показ блока
setTimeout(() => {
console.log('Showing price display...');
priceDisplay.classList.remove('hidden');
// Обновляем цену
const currentPrice = document.getElementById('currentPrice');
if (currentPrice) {
currentPrice.textContent = '₩500,000';
}
}, 2000);
}
// Добавим обработчики для карточек сервисов
document.querySelectorAll('.service-card').forEach(card => {
card.addEventListener('click', () => {
console.log('Service card clicked:', card.dataset.service);
if (priceDisplay) {
priceDisplay.classList.remove('hidden');
const currentPrice = document.getElementById('currentPrice');
if (currentPrice) {
currentPrice.textContent = '₩750,000';
}
}
if (mobilePriceDisplay) {
mobilePriceDisplay.classList.remove('hidden');
const mobilePriceValue = document.getElementById('mobilePriceValue');
if (mobilePriceValue) {
mobilePriceValue.textContent = '₩750,000';
}
}
});
});
});

View File

@@ -10,6 +10,102 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Theme Management
const themeToggle = document.getElementById('theme-toggle');
const html = document.documentElement;
const slider = document.querySelector('.theme-toggle-slider');
const sunIcon = document.querySelector('.theme-sun-icon');
const moonIcon = document.querySelector('.theme-moon-icon');
// Get current theme from server or localStorage
const serverTheme = html.classList.contains('dark') ? 'dark' : 'light';
const localTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Priority: localStorage > server > system preference
const currentTheme = localTheme || serverTheme || (prefersDark ? 'dark' : 'light');
console.log('Theme initialization:', { serverTheme, localTheme, prefersDark, currentTheme });
// Apply theme and update toggle with smooth animation
function applyTheme(isDark, animate = false) {
console.log('Applying theme:', isDark ? 'dark' : 'light', 'animate:', animate);
if (isDark) {
html.classList.add('dark');
if (themeToggle) themeToggle.checked = true;
if (slider) {
// Расчет: ширина контейнера (56px) - ширина ползунка (20px) - отступы (8px) = 28px движения
slider.style.transform = 'translateX(28px)';
}
if (sunIcon && moonIcon && animate) {
// Animated transition to dark
sunIcon.style.transform = 'rotate(180deg) scale(0)';
sunIcon.style.opacity = '0';
moonIcon.style.transform = 'rotate(0deg) scale(1)';
moonIcon.style.opacity = '1';
} else if (sunIcon && moonIcon) {
// Instant set for initial load
sunIcon.style.transform = 'rotate(180deg) scale(0)';
sunIcon.style.opacity = '0';
moonIcon.style.transform = 'rotate(0deg) scale(1)';
moonIcon.style.opacity = '1';
}
} else {
html.classList.remove('dark');
if (themeToggle) themeToggle.checked = false;
if (slider) {
slider.style.transform = 'translateX(0)';
}
if (sunIcon && moonIcon && animate) {
// Animated transition to light
moonIcon.style.transform = 'rotate(-180deg) scale(0)';
moonIcon.style.opacity = '0';
sunIcon.style.transform = 'rotate(0deg) scale(1)';
sunIcon.style.opacity = '1';
} else if (sunIcon && moonIcon) {
// Instant set for initial load
moonIcon.style.transform = 'rotate(-180deg) scale(0)';
moonIcon.style.opacity = '0';
sunIcon.style.transform = 'rotate(0deg) scale(1)';
sunIcon.style.opacity = '1';
}
}
}
// Initial theme application (без анимации при загрузке)
applyTheme(currentTheme === 'dark', false);
// Theme toggle handler
function toggleTheme() {
const isDark = html.classList.contains('dark');
const newTheme = isDark ? 'light' : 'dark';
console.log('Toggling theme from', isDark ? 'dark' : 'light', 'to', newTheme);
applyTheme(!isDark, true); // С анимацией при клике
localStorage.setItem('theme', newTheme);
// Send to server
fetch(`/theme/${newTheme}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
}).then(response => response.json())
.then(data => console.log('Theme synced to server:', data))
.catch(error => {
console.warn('Theme sync failed:', error);
});
}
if (themeToggle) {
themeToggle.addEventListener('change', toggleTheme);
console.log('Theme toggle listener attached');
} else {
console.warn('Theme toggle element not found');
}
// Mobile Navigation Toggle
const mobileMenuButton = document.querySelector('.mobile-menu-button');
const mobileMenu = document.querySelector('.mobile-menu');

289
public/js/price-island.js Normal file
View File

@@ -0,0 +1,289 @@
// Dynamic Price Island Controller
class PriceIsland {
constructor() {
this.island = document.getElementById('priceIsland');
this.container = document.getElementById('islandContainer');
this.content = document.getElementById('islandContent');
this.totalPrice = document.getElementById('totalPrice');
this.selectedServices = [];
this.projectDetails = {};
this.appliedPromo = null;
this.isExpanded = false;
this.init();
}
init() {
// Listen for calculator changes
document.addEventListener('calculatorStateChange', (e) => {
this.updateIsland(e.detail);
});
// Initialize promo code functionality
this.initPromoCode();
// Show island initially as compact
this.showCompact();
}
updateIsland(calculatorData) {
const { selectedServices, complexity, timeline, totalPrice } = calculatorData;
this.selectedServices = selectedServices || [];
this.projectDetails = { complexity, timeline };
// Update total price
this.updateTotalPrice(totalPrice || 0);
// Update content
this.updateContent();
// Expand if there's data to show
if (this.selectedServices.length > 0 || complexity || timeline) {
this.expand();
} else {
this.collapse();
}
}
updateTotalPrice(price) {
if (this.totalPrice) {
this.totalPrice.textContent = this.formatPrice(price);
this.totalPrice.classList.add('price-update');
setTimeout(() => {
this.totalPrice.classList.remove('price-update');
}, 300);
}
}
updateContent() {
this.updateSelectedServices();
this.updateProjectDetails();
this.updatePriceBreakdown();
}
updateSelectedServices() {
const container = document.getElementById('selectedServices');
const list = document.getElementById('servicesList');
if (this.selectedServices.length > 0) {
container.classList.remove('hidden');
list.innerHTML = this.selectedServices.map(service => `
<div class="flex justify-between items-center py-1">
<span class="text-sm text-gray-600 dark:text-gray-400">${service.name}</span>
<span class="text-sm font-medium text-gray-900 dark:text-white">${this.formatPrice(service.price)}</span>
</div>
`).join('');
} else {
container.classList.add('hidden');
}
}
updateProjectDetails() {
const container = document.getElementById('projectDetails');
const list = document.getElementById('detailsList');
const details = [];
if (this.projectDetails.complexity) {
details.push(`Сложность: ${this.projectDetails.complexity.name} (×${this.projectDetails.complexity.multiplier})`);
}
if (this.projectDetails.timeline) {
details.push(`Сроки: ${this.projectDetails.timeline.name} (×${this.projectDetails.timeline.multiplier})`);
}
if (details.length > 0) {
container.classList.remove('hidden');
list.innerHTML = details.map(detail => `
<div class="text-sm text-gray-600 dark:text-gray-400">${detail}</div>
`).join('');
} else {
container.classList.add('hidden');
}
}
updatePriceBreakdown() {
const container = document.getElementById('priceBreakdown');
const list = document.getElementById('breakdownList');
if (this.selectedServices.length > 0) {
container.classList.remove('hidden');
const basePrice = this.selectedServices.reduce((sum, service) => sum + service.price, 0);
const complexityMultiplier = this.projectDetails.complexity?.multiplier || 1;
const timelineMultiplier = this.projectDetails.timeline?.multiplier || 1;
const afterComplexity = basePrice * complexityMultiplier;
const final = afterComplexity * timelineMultiplier;
let breakdown = [
{ label: 'Базовая стоимость', value: basePrice }
];
if (complexityMultiplier !== 1) {
breakdown.push({
label: `Сложность (×${complexityMultiplier})`,
value: afterComplexity
});
}
if (timelineMultiplier !== 1) {
breakdown.push({
label: `Сроки (×${timelineMultiplier})`,
value: final
});
}
if (this.appliedPromo) {
const discount = final * (this.appliedPromo.discount / 100);
breakdown.push({
label: `Скидка ${this.appliedPromo.code} (-${this.appliedPromo.discount}%)`,
value: final - discount,
isDiscount: true
});
}
list.innerHTML = breakdown.map(item => `
<div class="flex justify-between items-center py-1 ${item.isDiscount ? 'text-green-600 dark:text-green-400' : ''}">
<span class="text-sm">${item.label}</span>
<span class="text-sm font-medium">${this.formatPrice(item.value)}</span>
</div>
`).join('');
} else {
container.classList.add('hidden');
}
}
initPromoCode() {
const promoBtn = document.getElementById('applyPromoBtn');
const promoInput = document.getElementById('promoInput');
const promoStatus = document.getElementById('promoStatus');
if (promoBtn && promoInput) {
promoBtn.addEventListener('click', () => {
const code = promoInput.value.trim().toUpperCase();
this.applyPromoCode(code);
});
promoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const code = promoInput.value.trim().toUpperCase();
this.applyPromoCode(code);
}
});
}
}
applyPromoCode(code) {
const promoStatus = document.getElementById('promoStatus');
// Simulate promo code validation
const validCodes = {
'WELCOME10': { discount: 10, name: 'Скидка новичка' },
'SAVE20': { discount: 20, name: 'Экономия 20%' },
'VIP15': { discount: 15, name: 'VIP скидка' }
};
if (validCodes[code]) {
this.appliedPromo = { code, ...validCodes[code] };
promoStatus.textContent = `✓ Применен: ${this.appliedPromo.name}`;
promoStatus.className = 'text-xs mt-1 text-green-600 dark:text-green-400';
// Trigger recalculation
this.updateContent();
this.recalculateTotal();
} else if (code) {
promoStatus.textContent = '✗ Неверный промокод';
promoStatus.className = 'text-xs mt-1 text-red-600 dark:text-red-400';
} else {
promoStatus.textContent = '';
}
}
recalculateTotal() {
if (this.selectedServices.length > 0) {
const basePrice = this.selectedServices.reduce((sum, service) => sum + service.price, 0);
const complexityMultiplier = this.projectDetails.complexity?.multiplier || 1;
const timelineMultiplier = this.projectDetails.timeline?.multiplier || 1;
let total = basePrice * complexityMultiplier * timelineMultiplier;
if (this.appliedPromo) {
total = total * (1 - this.appliedPromo.discount / 100);
}
this.updateTotalPrice(total);
// Also update mobile price if exists
const mobilePrice = document.getElementById('mobilePriceValue');
if (mobilePrice) {
mobilePrice.textContent = this.formatPrice(total);
}
}
}
expand() {
if (!this.isExpanded) {
this.isExpanded = true;
this.content.style.maxHeight = this.content.scrollHeight + 'px';
// Show promo section when expanded
const promoSection = document.getElementById('promoSection');
if (promoSection) {
promoSection.classList.remove('hidden');
}
}
}
collapse() {
if (this.isExpanded) {
this.isExpanded = false;
this.content.style.maxHeight = '0';
// Hide promo section when collapsed
const promoSection = document.getElementById('promoSection');
if (promoSection) {
promoSection.classList.add('hidden');
}
}
}
showCompact() {
this.island.classList.remove('hidden');
}
hide() {
this.island.classList.add('hidden');
}
formatPrice(price) {
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW',
maximumFractionDigits: 0
}).format(price);
}
}
// CSS for price update animation
const style = document.createElement('style');
style.textContent = `
.price-update {
animation: priceUpdate 0.3s ease-out;
}
@keyframes priceUpdate {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
`;
document.head.appendChild(style);
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.priceIsland = new PriceIsland();
});
// Export for use in calculator
window.PriceIsland = PriceIsland;

107
public/js/sticky-price.js Normal file
View File

@@ -0,0 +1,107 @@
// Enhanced Sticky Price Display
document.addEventListener('DOMContentLoaded', function() {
const priceDisplay = document.getElementById('priceDisplay');
const currentPrice = document.getElementById('currentPrice');
if (!priceDisplay) return;
// Show price display when calculator starts
function showPriceDisplay() {
if (priceDisplay.classList.contains('hidden')) {
priceDisplay.classList.remove('hidden');
priceDisplay.classList.add('visible');
}
}
// Enhanced scroll behavior
let scrollTimeout;
window.addEventListener('scroll', function() {
const scrollY = window.scrollY;
// Show/hide based on scroll position
if (scrollY > 100) {
showPriceDisplay();
priceDisplay.classList.add('scrolled');
} else {
priceDisplay.classList.remove('scrolled');
}
// Clear previous timeout
clearTimeout(scrollTimeout);
// Add subtle bounce effect
scrollTimeout = setTimeout(() => {
priceDisplay.style.transform = 'translateX(0) scale(1)';
}, 150);
});
// Price update animation
const originalUpdatePrice = window.updatePrice || function() {};
window.updatePrice = function(price, animate = true) {
if (currentPrice && animate) {
// Add updating animation
currentPrice.classList.add('updating');
// Update price after brief delay
setTimeout(() => {
currentPrice.textContent = price;
currentPrice.classList.remove('updating');
}, 150);
} else if (currentPrice) {
currentPrice.textContent = price;
}
// Show price display when price is calculated
showPriceDisplay();
// Call original function if it exists
if (typeof originalUpdatePrice === 'function') {
originalUpdatePrice(price, animate);
}
};
// Monitor calculator interactions
const calculator = document.querySelector('[data-calculator]') || document.querySelector('.calculator-container');
if (calculator) {
// Show price display when user interacts with calculator
const inputs = calculator.querySelectorAll('input, select, button');
inputs.forEach(input => {
input.addEventListener('change', showPriceDisplay);
input.addEventListener('click', showPriceDisplay);
});
}
// Enhanced visibility on mobile
function handleMobileVisibility() {
if (window.innerWidth <= 1024) {
priceDisplay.classList.remove('fixed');
priceDisplay.classList.add('relative');
} else {
priceDisplay.classList.remove('relative');
priceDisplay.classList.add('fixed');
}
}
// Initial check and resize listener
handleMobileVisibility();
window.addEventListener('resize', handleMobileVisibility);
// Smooth reveal animation
setTimeout(() => {
if (priceDisplay && !priceDisplay.classList.contains('hidden')) {
priceDisplay.style.transition = 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
}
}, 100);
// Auto-show after initial page load
setTimeout(showPriceDisplay, 1000);
});
// Helper function for other scripts to trigger price display
window.showStickyPrice = function() {
const priceDisplay = document.getElementById('priceDisplay');
if (priceDisplay) {
priceDisplay.classList.remove('hidden');
priceDisplay.classList.add('visible');
}
};

View File

@@ -1,7 +1,7 @@
// Service Worker for SmartSolTech PWA
const CACHE_NAME = 'smartsoltech-v1.0.1';
const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.1';
const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.1';
const CACHE_NAME = 'smartsoltech-v1.0.2';
const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.2';
const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.2';
// Files to cache immediately
const STATIC_FILES = [
@@ -101,8 +101,9 @@ self.addEventListener('fetch', event => {
event.respondWith(cacheFirst(request));
} else if (isAPIRequest(request.url)) {
event.respondWith(networkFirst(request));
} else if (isDynamicRoute(request.url)) {
event.respondWith(staleWhileRevalidate(request));
} else if (isDynamicRoute(request.url) || url.pathname === '/') {
// For main pages, always check network first to ensure language consistency
event.respondWith(networkFirst(request));
} else {
event.respondWith(networkFirst(request));
}