Initial commit with all files
This commit is contained in:
6
js/all.min.js
vendored
Normal file
6
js/all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
45
js/echarts.min.js
vendored
Normal file
45
js/echarts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
js/fade-up.js
Normal file
4
js/fade-up.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Анимация появления блоков при загрузке
|
||||
document.querySelectorAll('.fade-up').forEach((el,i) => {
|
||||
setTimeout(() => el.classList.add('animate-fadeUp'), 70+i*90)
|
||||
});
|
||||
109
js/features.js
Normal file
109
js/features.js
Normal file
@@ -0,0 +1,109 @@
|
||||
const originalFeaturesData = [
|
||||
{icon:'shield-halved', color:'#0ff'},
|
||||
{icon:'user-secret', color:'#6efaff'},
|
||||
{icon:'envelope-open-text', color:'#91eaff'},
|
||||
{icon:'cloud', color:'#fd7cff'},
|
||||
{icon:'file-shield', color:'#fedd60'},
|
||||
{icon:'coins', color:'#41fdc2'},
|
||||
{icon:'eye-slash', color:'#a6fca8'},
|
||||
{icon:'users', color:'#dc9afd'},
|
||||
];
|
||||
|
||||
const originalProductsData = [
|
||||
{color:'#0ff', data:[1,1,1,1,1,1,1,1]},
|
||||
{color:'#38bdf8', data:[1,1,0,0,0,0,0,1]},
|
||||
{color:'#8b5cf6', data:[1,1,0,0,0,0,0,0]},
|
||||
];
|
||||
|
||||
|
||||
let languageData = {};
|
||||
|
||||
async function fetchLanguageData(lang) {
|
||||
try {
|
||||
let response = await fetch(`/lang/${lang}/json/${lang}.json`);
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to load language data for ${lang}: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
languageData = await response.json();
|
||||
return languageData;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching language data for ${lang}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderFeatures(data) {
|
||||
const container = document.getElementById('featuresContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
data.features.forEach((feature, index) => {
|
||||
const featureRow = document.createElement('div');
|
||||
featureRow.className = 'feature-row grid grid-cols-4 gap-4 py-3 border-b border-gray-700/50';
|
||||
featureRow.id = `feature-${index}`;
|
||||
|
||||
// Feature name
|
||||
const nameCell = document.createElement('div');
|
||||
nameCell.className = 'col-span-1 text-gray-300 text-right pr-4 flex items-center justify-end';
|
||||
const icon = originalFeaturesData[index].icon;
|
||||
const color = originalFeaturesData[index].color;
|
||||
nameCell.innerHTML = `<i class="fas fa-${icon} mr-2" style="color:${color}"></i>${feature.name}`;
|
||||
|
||||
// BlackBox
|
||||
const bbCell = document.createElement('div');
|
||||
bbCell.className = 'col-span-1 text-center flex items-center justify-center';
|
||||
const bbData = originalProductsData[0].data[index];
|
||||
bbCell.innerHTML = bbData
|
||||
? `<i class="fas fa-check-circle check-icon text-2xl text-cyan-400"></i>`
|
||||
: `<i class="fas fa-times-circle cross-icon text-xl text-gray-500"></i>`;
|
||||
|
||||
|
||||
// VPN Router
|
||||
const routerCell = document.createElement('div');
|
||||
routerCell.className = 'col-span-1 text-center flex items-center justify-center';
|
||||
const routerData = originalProductsData[1].data[index];
|
||||
routerCell.innerHTML = routerData
|
||||
? `<i class="fas fa-check-circle check-icon text-xl text-blue-400 opacity-70"></i>`
|
||||
: `<i class="fas fa-times-circle cross-icon text-xl text-gray-500"></i>`;
|
||||
|
||||
// VPN Service
|
||||
const vpnCell = document.createElement('div');
|
||||
vpnCell.className = 'col-span-1 text-center flex items-center justify-center';
|
||||
const vpnData = originalProductsData[2].data[index];
|
||||
vpnCell.innerHTML = vpnData
|
||||
? `<i class="fas fa-check-circle check-icon text-xl text-purple-400 opacity-70"></i>`
|
||||
: `<i class="fas fa-times-circle cross-icon text-xl text-gray-500"></i>`;
|
||||
|
||||
featureRow.appendChild(nameCell);
|
||||
featureRow.appendChild(bbCell);
|
||||
featureRow.appendChild(routerCell);
|
||||
featureRow.appendChild(vpnCell);
|
||||
container.appendChild(featureRow);
|
||||
});
|
||||
}
|
||||
|
||||
function setupHoverEffects(data) {
|
||||
document.querySelectorAll('#featuresContainer > div').forEach((row, index) => {
|
||||
row.addEventListener('mouseenter', () => {
|
||||
// Update hint
|
||||
document.getElementById('dynCompareHint').innerHTML =
|
||||
`<i class="fas fa-lightbulb text-cyan-300"></i> <span class="font-semibold">${data.features[index].name}:</span> ${data.features[index].hint}`;
|
||||
});
|
||||
|
||||
row.addEventListener('mouseleave', () => {
|
||||
// Clear hint
|
||||
document.getElementById('dynCompareHint').innerHTML = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
const currentPath = window.location.pathname;
|
||||
const lang = currentPath.split('/')[1] || 'en'; // Get language from URL, default to 'en'
|
||||
|
||||
const data = await fetchLanguageData(lang);
|
||||
if (data) {
|
||||
renderFeatures(data);
|
||||
setupHoverEffects(data);
|
||||
}
|
||||
});
|
||||
153
js/flipbook.js
Normal file
153
js/flipbook.js
Normal file
@@ -0,0 +1,153 @@
|
||||
// Навигация флипбука
|
||||
let currentPage = 3;
|
||||
const totalPages = 6;
|
||||
const flipbookContainer = document.querySelector('.flipbook-container');
|
||||
const pages = document.querySelectorAll('.flipbook-page');
|
||||
const indicator = document.querySelector('.flipbook-indicator');
|
||||
|
||||
let startTranslateX = 0; // Initial translateX of the active page
|
||||
const pageSpacing = 16 * 1; // 1rem margin on each side of the page (0.5rem * 2)
|
||||
|
||||
function updatePages() {
|
||||
pages.forEach(page => {
|
||||
const pageNum = parseInt(page.dataset.page);
|
||||
const diff = pageNum - currentPage;
|
||||
|
||||
// Apply original classes for animation on both mobile and desktop
|
||||
page.classList.remove('active', 'left', 'right', 'far-left', 'far-right', 'far-far-right', 'far-far-left');
|
||||
if (diff === 0) {
|
||||
page.classList.add('active');
|
||||
} else if (diff === -1) {
|
||||
page.classList.add('left');
|
||||
} else if (diff === -2) {
|
||||
page.classList.add('far-left');
|
||||
} else if (diff === -3) {
|
||||
page.classList.add('far-far-left');
|
||||
} else if (diff === 1) {
|
||||
page.classList.add('right');
|
||||
} else if (diff === 2) {
|
||||
page.classList.add('far-right');
|
||||
} else if (diff === 3) {
|
||||
page.classList.add('far-far-right');
|
||||
}
|
||||
|
||||
// Calculate and apply translateX for each page based on its position relative to the active page
|
||||
// This is the core change for custom scrolling
|
||||
let translateX;
|
||||
// Calculate horizontal translation for each page
|
||||
if (window.innerWidth <= 768) {
|
||||
// Mobile: Adjusted translateX with tighter spacing
|
||||
translateX = (pageNum - currentPage) * (page.offsetWidth * 0.6 + pageSpacing) - 15;
|
||||
// Centering is handled by flexbox on the container
|
||||
} else {
|
||||
// Desktop: Adjusted translateX for spacing and to contribute to the curve effect
|
||||
// Multiplier (0.9) controls horizontal spacing between pages
|
||||
const desktopSpacing = page.offsetWidth * 0.9 + pageSpacing;
|
||||
translateX = (pageNum - currentPage) * desktopSpacing;
|
||||
// Centering is handled by flexbox on the container
|
||||
}
|
||||
|
||||
|
||||
// Apply combined transform: translateX, rotateY (for flip effect), and scale (for perspective)
|
||||
// rotateY: Controls the rotation around the Y-axis (the "flip")
|
||||
// Multipliers (12 for mobile, 20 for desktop) control the degree of rotation per page difference
|
||||
// scale: Controls the size of the page, creating a perspective effect
|
||||
// Multipliers (0.08 for mobile, 0.05 for desktop) control how much smaller pages get further away
|
||||
page.style.transform = `translateX(${translateX}px) rotateY(${diff * (window.innerWidth <= 768 ? 12 : 20)}deg) scale(${1 - Math.abs(diff) * (window.innerWidth <= 768 ? 0.08 : 0.05)})`;
|
||||
|
||||
// Adjust z-index based on distance from active page to control stacking order
|
||||
page.style.zIndex = totalPages - Math.abs(diff);
|
||||
|
||||
// Adjust opacity based on distance to create a fading effect for far pages
|
||||
// Multipliers (0.15 for mobile, 0.1 for desktop) control how much pages fade
|
||||
page.style.opacity = 1 - Math.abs(diff) * (window.innerWidth <= 768 ? 0.15 : 0.1);
|
||||
});
|
||||
|
||||
// Update indicator position
|
||||
let position;
|
||||
if (window.innerWidth <= 768) {
|
||||
// Indicator position for mobile (centered)
|
||||
position = ((currentPage - 1) / (totalPages - 1)) * 100 - 50;
|
||||
} else {
|
||||
// Indicator position for desktop (adjusted for wider range)
|
||||
position = ((currentPage - 1) / (totalPages - 1)) * 200 - 100;
|
||||
}
|
||||
indicator.style.transform = `translateX(${position}%)`;
|
||||
}
|
||||
|
||||
// Click navigation (keep original logic)
|
||||
pages.forEach(page => {
|
||||
page.addEventListener('click', (e) => {
|
||||
if (!page.classList.contains('active')) {
|
||||
currentPage = parseInt(page.dataset.page);
|
||||
updatePages();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Mouse hover navigation (keep original logic)
|
||||
let hoverTimer;
|
||||
pages.forEach(page => {
|
||||
page.addEventListener('mouseenter', () => {
|
||||
if (!page.classList.contains('active')) {
|
||||
clearTimeout(hoverTimer);
|
||||
hoverTimer = setTimeout(() => {
|
||||
currentPage = parseInt(page.dataset.page);
|
||||
updatePages();
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
page.addEventListener('mouseleave', () => {
|
||||
clearTimeout(hoverTimer);
|
||||
});
|
||||
});
|
||||
|
||||
// Touch swipe detection (removed for mobile as per user request)
|
||||
// The following touch event listeners and related variables are removed.
|
||||
// Navigation on mobile will now be handled by clicking on the cards.
|
||||
|
||||
// Handle window resize to switch between mobile/desktop layouts
|
||||
window.addEventListener('resize', updatePages);
|
||||
|
||||
// Initialize
|
||||
updatePages();
|
||||
|
||||
// Product details data - will be populated from HTML
|
||||
const productDetails = {};
|
||||
|
||||
// Initialize product details from data attributes
|
||||
function initProductDetails() {
|
||||
const productElements = document.querySelectorAll('.flipbook-page');
|
||||
productElements.forEach(page => {
|
||||
const pageNum = parseInt(page.dataset.page);
|
||||
const title = page.querySelector('.product-title').textContent;
|
||||
const description = page.querySelector('.product-description').textContent;
|
||||
productDetails[pageNum] = { title, description };
|
||||
});
|
||||
}
|
||||
|
||||
// Show product details modal
|
||||
function showProductDetails(pageNum) {
|
||||
const details = productDetails[pageNum];
|
||||
if (details) {
|
||||
document.getElementById('modalTitle').textContent = details.title;
|
||||
document.getElementById('modalDescription').textContent = details.description;
|
||||
document.getElementById('productModal').classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
function hideProductDetails() {
|
||||
document.getElementById('productModal').classList.remove('show');
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
document.getElementById('productModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
hideProductDetails();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
initProductDetails();
|
||||
updatePages();
|
||||
21
js/lang-detect-redirect.js
Normal file
21
js/lang-detect-redirect.js
Normal file
@@ -0,0 +1,21 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const currentPath = window.location.pathname;
|
||||
const supportedLanguages = ['en', 'ru', 'es'];
|
||||
const browserLang = (navigator.language || navigator.userLanguage).split('-')[0];
|
||||
|
||||
// Check if the current path already includes a supported language prefix
|
||||
const isOnLangPage = currentPath.startsWith('/en/') ||
|
||||
currentPath.startsWith('/ru/') ||
|
||||
currentPath.startsWith('/es/');
|
||||
|
||||
if (!isOnLangPage) {
|
||||
// Redirect to the browser's language version if supported, otherwise default to English
|
||||
const targetLang = supportedLanguages.includes(browserLang) ? browserLang : 'en';
|
||||
// Construct the new URL, preserving the rest of the path if any
|
||||
let newPath = currentPath;
|
||||
if (!currentPath.startsWith('/en/') && !currentPath.startsWith('/ru/') && !currentPath.startsWith('/es/')) {
|
||||
newPath = '/' + targetLang + currentPath;
|
||||
}
|
||||
window.location.replace(newPath);
|
||||
}
|
||||
});
|
||||
80
js/language-switcher.js
Normal file
80
js/language-switcher.js
Normal file
@@ -0,0 +1,80 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const supportedLanguages = {
|
||||
'en': 'English',
|
||||
'ru': 'Русский',
|
||||
'es': 'Español'
|
||||
};
|
||||
|
||||
const switcherContainer = document.createElement('div');
|
||||
switcherContainer.id = 'language-switcher';
|
||||
switcherContainer.style.position = 'fixed';
|
||||
switcherContainer.style.bottom = '20px';
|
||||
switcherContainer.style.right = '20px';
|
||||
switcherContainer.style.zIndex = '1000';
|
||||
switcherContainer.style.cursor = 'pointer';
|
||||
|
||||
const currentLang = window.location.pathname.split('/')[1];
|
||||
const currentLangDisplay = supportedLanguages[currentLang] || 'Language'; // Fallback if not on a supported lang path
|
||||
|
||||
switcherContainer.innerHTML = `
|
||||
<div id="language-icon" style="width: 40px; height: 40px; border-radius: 50%; overflow: hidden; background-color: #007bff; text-align: center; line-height: 40px; font-weight: bold;">
|
||||
<img src="https://flagcdn.com/w40/${currentLang === 'en' ? 'gb' : currentLang}.png" alt="${currentLang.toUpperCase()}" style="width: 100%; height: 100%; object-fit: cover;">
|
||||
</div>
|
||||
<div id="language-options-container" class="language-options-container" style="position: absolute; bottom: 50px; right: 0;">
|
||||
${Object.keys(supportedLanguages).map(lang => `
|
||||
<div class="language-option" data-lang="${lang}" style="width: 40px; height: 40px; border-radius: 50%; overflow: hidden; margin-bottom: 10px; cursor: pointer;">
|
||||
<img src="https://flagcdn.com/w40/${lang === 'en' ? 'gb' : lang}.png" alt="${supportedLanguages[lang]}" style="width: 100%; height: 100%; object-fit: cover;">
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(switcherContainer);
|
||||
|
||||
const languageIcon = document.getElementById('language-icon');
|
||||
const languageOptionsContainer = document.getElementById('language-options-container');
|
||||
|
||||
languageIcon.addEventListener('click', function(event) {
|
||||
// Close any other open language switchers first
|
||||
document.querySelectorAll('.language-options-container.show').forEach(el => {
|
||||
if (el !== languageOptionsContainer) {
|
||||
el.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle current switcher
|
||||
languageOptionsContainer.classList.toggle('show');
|
||||
|
||||
// Prevent event bubbling and default behavior
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(event) {
|
||||
if (!switcherContainer.contains(event.target)) {
|
||||
languageOptionsContainer.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Close language switcher when other modals open
|
||||
document.addEventListener('modalOpened', function() {
|
||||
languageOptionsContainer.classList.remove('show');
|
||||
});
|
||||
|
||||
languageOptionsContainer.querySelectorAll('.language-option').forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
const lang = this.getAttribute('data-lang');
|
||||
const currentPathname = window.location.pathname;
|
||||
const pathParts = currentPathname.split('/').filter(part => part !== '');
|
||||
|
||||
// Remove existing language prefix if present
|
||||
if (supportedLanguages[pathParts[0]]) {
|
||||
pathParts.shift();
|
||||
}
|
||||
|
||||
// Construct new path with selected language
|
||||
const newPath = '/' + lang + '/' + pathParts.join('/');
|
||||
window.location.href = newPath;
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user