new site
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
// D3.js Animations for Homepage - Training & Learning Focus
|
||||
function initD3Animation() {
|
||||
const container = d3.select("#d3-animation");
|
||||
if (container.empty()) return;
|
||||
|
||||
const containerRect = container.node().getBoundingClientRect();
|
||||
const width = containerRect.width;
|
||||
const height = containerRect.height || 400;
|
||||
|
||||
// Clear any existing SVG
|
||||
container.selectAll("*").remove();
|
||||
|
||||
const svg = container.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.attr("viewBox", `0 0 ${width} ${height}`)
|
||||
.style("background", "transparent")
|
||||
.style("overflow", "hidden");
|
||||
|
||||
// Create a main group for all elements
|
||||
const mainGroup = svg.append("g")
|
||||
.attr("class", "main-group");
|
||||
|
||||
// Create animated network visualization representing Odoo training modules
|
||||
const nodes = [
|
||||
{ id: "Fundamentos", group: 1, size: 35, level: 1 },
|
||||
{ id: "CRM Básico", group: 2, size: 25, level: 2 },
|
||||
{ id: "Ventas", group: 2, size: 25, level: 2 },
|
||||
{ id: "Inventario", group: 3, size: 25, level: 2 },
|
||||
{ id: "Contabilidad", group: 4, size: 30, level: 2 },
|
||||
{ id: "CRM Avanzado", group: 2, size: 20, level: 3 },
|
||||
{ id: "Reportes", group: 4, size: 20, level: 3 },
|
||||
{ id: "Integraciones", group: 3, size: 25, level: 3 },
|
||||
{ id: "Certificación", group: 1, size: 30, level: 4 },
|
||||
{ id: "Especialización", group: 1, size: 25, level: 4 }
|
||||
];
|
||||
|
||||
const links = [
|
||||
{ source: "Fundamentos", target: "CRM Básico", value: 3 },
|
||||
{ source: "Fundamentos", target: "Ventas", value: 3 },
|
||||
{ source: "Fundamentos", target: "Inventario", value: 3 },
|
||||
{ source: "Fundamentos", target: "Contabilidad", value: 3 },
|
||||
{ source: "CRM Básico", target: "CRM Avanzado", value: 2 },
|
||||
{ source: "Ventas", target: "CRM Avanzado", value: 2 },
|
||||
{ source: "Contabilidad", target: "Reportes", value: 3 },
|
||||
{ source: "Inventario", target: "Integraciones", value: 3 },
|
||||
{ source: "CRM Avanzado", target: "Certificación", value: 3 },
|
||||
{ source: "Reportes", target: "Certificación", value: 2 },
|
||||
{ source: "Integraciones", target: "Especialización", value: 2 },
|
||||
{ source: "Certificación", target: "Especialización", value: 3 }
|
||||
];
|
||||
|
||||
// Color scale with orange theme
|
||||
const color = d3.scaleOrdinal()
|
||||
.domain([1, 2, 3, 4])
|
||||
.range(["#FF6B35", "#FF8A50", "#FFA726", "#FFB74D"]);
|
||||
|
||||
// Add boundary force to keep nodes within container
|
||||
const boundaryForce = () => {
|
||||
nodes.forEach(node => {
|
||||
node.x = Math.max(node.size, Math.min(width - node.size, node.x));
|
||||
node.y = Math.max(node.size, Math.min(height - node.size, node.y));
|
||||
});
|
||||
};
|
||||
|
||||
// Create force simulation with boundaries
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force("link", d3.forceLink(links).id(d => d.id).distance(80))
|
||||
.force("charge", d3.forceManyBody().strength(-250))
|
||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||
.force("collision", d3.forceCollide().radius(d => d.size + 8))
|
||||
.force("boundary", boundaryForce);
|
||||
|
||||
// Create links
|
||||
const link = mainGroup.append("g")
|
||||
.attr("class", "links")
|
||||
.selectAll("line")
|
||||
.data(links)
|
||||
.enter().append("line")
|
||||
.attr("stroke", "#FF6B35")
|
||||
.attr("stroke-opacity", 0.3)
|
||||
.attr("stroke-width", d => Math.sqrt(d.value));
|
||||
|
||||
// Create nodes container
|
||||
const node = mainGroup.append("g")
|
||||
.attr("class", "nodes")
|
||||
.selectAll("g")
|
||||
.data(nodes)
|
||||
.enter().append("g")
|
||||
.call(d3.drag()
|
||||
.on("start", dragstarted)
|
||||
.on("drag", dragged)
|
||||
.on("end", dragended));
|
||||
|
||||
// Add circles
|
||||
node.append("circle")
|
||||
.attr("r", d => d.size)
|
||||
.attr("fill", d => color(d.group))
|
||||
.attr("stroke", "#1a1a1a")
|
||||
.attr("stroke-width", 2)
|
||||
.style("cursor", "pointer")
|
||||
.on("mouseover", function(event, d) {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr("r", d.size * 1.2);
|
||||
})
|
||||
.on("mouseout", function(event, d) {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr("r", d.size);
|
||||
});
|
||||
|
||||
// Add text labels
|
||||
node.append("text")
|
||||
.text(d => d.id)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dy", ".35em")
|
||||
.style("fill", "#1a1a1a")
|
||||
.style("font-size", "11px")
|
||||
.style("font-weight", "700")
|
||||
.style("pointer-events", "none");
|
||||
|
||||
// Add animated pulses
|
||||
node.append("circle")
|
||||
.attr("r", d => d.size)
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", d => color(d.group))
|
||||
.attr("stroke-width", 2)
|
||||
.attr("opacity", 0)
|
||||
.style("pointer-events", "none")
|
||||
.each(function(d, i) {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.delay(i * 200)
|
||||
.duration(2000)
|
||||
.attr("r", d => d.size * 2)
|
||||
.attr("opacity", 0)
|
||||
.on("end", function repeat() {
|
||||
d3.select(this)
|
||||
.attr("r", d.size)
|
||||
.attr("opacity", 0.5)
|
||||
.transition()
|
||||
.duration(2000)
|
||||
.attr("r", d.size * 2)
|
||||
.attr("opacity", 0)
|
||||
.on("end", repeat);
|
||||
});
|
||||
});
|
||||
|
||||
// Update positions on tick
|
||||
simulation.on("tick", () => {
|
||||
// Apply boundary force
|
||||
boundaryForce();
|
||||
|
||||
link
|
||||
.attr("x1", d => d.source.x)
|
||||
.attr("y1", d => d.source.y)
|
||||
.attr("x2", d => d.target.x)
|
||||
.attr("y2", d => d.target.y);
|
||||
|
||||
node
|
||||
.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
|
||||
// Drag functions with boundaries
|
||||
function dragstarted(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d.fx = Math.max(d.size, Math.min(width - d.size, event.x));
|
||||
d.fy = Math.max(d.size, Math.min(height - d.size, event.y));
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
}
|
||||
|
||||
// Add floating particles with orange theme
|
||||
const particles = mainGroup.append("g")
|
||||
.attr("class", "particles");
|
||||
|
||||
function createParticle() {
|
||||
const particle = particles.append("circle")
|
||||
.attr("r", Math.random() * 2 + 1)
|
||||
.attr("cx", Math.random() * width)
|
||||
.attr("cy", height + 5)
|
||||
.attr("fill", "#FF6B35")
|
||||
.attr("opacity", 0.2);
|
||||
|
||||
particle.transition()
|
||||
.duration(Math.random() * 8000 + 4000)
|
||||
.ease(d3.easeLinear)
|
||||
.attr("cy", -5)
|
||||
.attr("opacity", 0)
|
||||
.remove();
|
||||
}
|
||||
|
||||
// Create particles periodically
|
||||
setInterval(createParticle, 500);
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initD3Animation();
|
||||
|
||||
// Reinitialize on window resize
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', function() {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function() {
|
||||
initD3Animation();
|
||||
}, 250);
|
||||
});
|
||||
});
|
||||
|
||||
// Smooth scroll for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Vendored
+235
@@ -0,0 +1,235 @@
|
||||
// D3.js Animations for Homepage - Training & Learning Focus
|
||||
function initD3Animation() {
|
||||
const container = d3.select("#d3-animation");
|
||||
if (container.empty()) return;
|
||||
|
||||
const containerRect = container.node().getBoundingClientRect();
|
||||
const width = containerRect.width;
|
||||
const height = containerRect.height || 400;
|
||||
|
||||
// Clear any existing SVG
|
||||
container.selectAll("*").remove();
|
||||
|
||||
const svg = container.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.attr("viewBox", `0 0 ${width} ${height}`)
|
||||
.style("background", "transparent")
|
||||
.style("overflow", "hidden");
|
||||
|
||||
// Create a main group for all elements
|
||||
const mainGroup = svg.append("g")
|
||||
.attr("class", "main-group");
|
||||
|
||||
// Create animated network visualization representing Odoo training modules
|
||||
const nodes = [
|
||||
{ id: "Fundamentos", group: 1, size: 35, level: 1 },
|
||||
{ id: "CRM Básico", group: 2, size: 25, level: 2 },
|
||||
{ id: "Ventas", group: 2, size: 25, level: 2 },
|
||||
{ id: "Inventario", group: 3, size: 25, level: 2 },
|
||||
{ id: "Contabilidad", group: 4, size: 30, level: 2 },
|
||||
{ id: "CRM Avanzado", group: 2, size: 20, level: 3 },
|
||||
{ id: "Reportes", group: 4, size: 20, level: 3 },
|
||||
{ id: "Integraciones", group: 3, size: 25, level: 3 },
|
||||
{ id: "Certificación", group: 1, size: 30, level: 4 },
|
||||
{ id: "Especialización", group: 1, size: 25, level: 4 }
|
||||
];
|
||||
|
||||
const links = [
|
||||
{ source: "Fundamentos", target: "CRM Básico", value: 3 },
|
||||
{ source: "Fundamentos", target: "Ventas", value: 3 },
|
||||
{ source: "Fundamentos", target: "Inventario", value: 3 },
|
||||
{ source: "Fundamentos", target: "Contabilidad", value: 3 },
|
||||
{ source: "CRM Básico", target: "CRM Avanzado", value: 2 },
|
||||
{ source: "Ventas", target: "CRM Avanzado", value: 2 },
|
||||
{ source: "Contabilidad", target: "Reportes", value: 3 },
|
||||
{ source: "Inventario", target: "Integraciones", value: 3 },
|
||||
{ source: "CRM Avanzado", target: "Certificación", value: 3 },
|
||||
{ source: "Reportes", target: "Certificación", value: 2 },
|
||||
{ source: "Integraciones", target: "Especialización", value: 2 },
|
||||
{ source: "Certificación", target: "Especialización", value: 3 }
|
||||
];
|
||||
|
||||
// Color scale with orange theme
|
||||
const color = d3.scaleOrdinal()
|
||||
.domain([1, 2, 3, 4])
|
||||
.range(["#FF6B35", "#FF8A50", "#FFA726", "#FFB74D"]);
|
||||
|
||||
// Add boundary force to keep nodes within container
|
||||
const boundaryForce = () => {
|
||||
nodes.forEach(node => {
|
||||
node.x = Math.max(node.size, Math.min(width - node.size, node.x));
|
||||
node.y = Math.max(node.size, Math.min(height - node.size, node.y));
|
||||
});
|
||||
};
|
||||
|
||||
// Create force simulation with boundaries
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force("link", d3.forceLink(links).id(d => d.id).distance(80))
|
||||
.force("charge", d3.forceManyBody().strength(-250))
|
||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||
.force("collision", d3.forceCollide().radius(d => d.size + 8))
|
||||
.force("boundary", boundaryForce);
|
||||
|
||||
// Create links
|
||||
const link = mainGroup.append("g")
|
||||
.attr("class", "links")
|
||||
.selectAll("line")
|
||||
.data(links)
|
||||
.enter().append("line")
|
||||
.attr("stroke", "#FF6B35")
|
||||
.attr("stroke-opacity", 0.3)
|
||||
.attr("stroke-width", d => Math.sqrt(d.value));
|
||||
|
||||
// Create nodes container
|
||||
const node = mainGroup.append("g")
|
||||
.attr("class", "nodes")
|
||||
.selectAll("g")
|
||||
.data(nodes)
|
||||
.enter().append("g")
|
||||
.call(d3.drag()
|
||||
.on("start", dragstarted)
|
||||
.on("drag", dragged)
|
||||
.on("end", dragended));
|
||||
|
||||
// Add circles
|
||||
node.append("circle")
|
||||
.attr("r", d => d.size)
|
||||
.attr("fill", d => color(d.group))
|
||||
.attr("stroke", "#1a1a1a")
|
||||
.attr("stroke-width", 2)
|
||||
.style("cursor", "pointer")
|
||||
.on("mouseover", function(event, d) {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr("r", d.size * 1.2);
|
||||
})
|
||||
.on("mouseout", function(event, d) {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr("r", d.size);
|
||||
});
|
||||
|
||||
// Add text labels
|
||||
node.append("text")
|
||||
.text(d => d.id)
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("dy", ".35em")
|
||||
.style("fill", "#1a1a1a")
|
||||
.style("font-size", "11px")
|
||||
.style("font-weight", "700")
|
||||
.style("pointer-events", "none");
|
||||
|
||||
// Add animated pulses
|
||||
node.append("circle")
|
||||
.attr("r", d => d.size)
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", d => color(d.group))
|
||||
.attr("stroke-width", 2)
|
||||
.attr("opacity", 0)
|
||||
.style("pointer-events", "none")
|
||||
.each(function(d, i) {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.delay(i * 200)
|
||||
.duration(2000)
|
||||
.attr("r", d => d.size * 2)
|
||||
.attr("opacity", 0)
|
||||
.on("end", function repeat() {
|
||||
d3.select(this)
|
||||
.attr("r", d.size)
|
||||
.attr("opacity", 0.5)
|
||||
.transition()
|
||||
.duration(2000)
|
||||
.attr("r", d.size * 2)
|
||||
.attr("opacity", 0)
|
||||
.on("end", repeat);
|
||||
});
|
||||
});
|
||||
|
||||
// Update positions on tick
|
||||
simulation.on("tick", () => {
|
||||
// Apply boundary force
|
||||
boundaryForce();
|
||||
|
||||
link
|
||||
.attr("x1", d => d.source.x)
|
||||
.attr("y1", d => d.source.y)
|
||||
.attr("x2", d => d.target.x)
|
||||
.attr("y2", d => d.target.y);
|
||||
|
||||
node
|
||||
.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
|
||||
// Drag functions with boundaries
|
||||
function dragstarted(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d.fx = Math.max(d.size, Math.min(width - d.size, event.x));
|
||||
d.fy = Math.max(d.size, Math.min(height - d.size, event.y));
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
}
|
||||
|
||||
// Add floating particles with orange theme
|
||||
const particles = mainGroup.append("g")
|
||||
.attr("class", "particles");
|
||||
|
||||
function createParticle() {
|
||||
const particle = particles.append("circle")
|
||||
.attr("r", Math.random() * 2 + 1)
|
||||
.attr("cx", Math.random() * width)
|
||||
.attr("cy", height + 5)
|
||||
.attr("fill", "#FF6B35")
|
||||
.attr("opacity", 0.2);
|
||||
|
||||
particle.transition()
|
||||
.duration(Math.random() * 8000 + 4000)
|
||||
.ease(d3.easeLinear)
|
||||
.attr("cy", -5)
|
||||
.attr("opacity", 0)
|
||||
.remove();
|
||||
}
|
||||
|
||||
// Create particles periodically
|
||||
setInterval(createParticle, 500);
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initD3Animation();
|
||||
|
||||
// Reinitialize on window resize
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', function() {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function() {
|
||||
initD3Animation();
|
||||
}, 250);
|
||||
});
|
||||
});
|
||||
|
||||
// Smooth scroll for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,455 @@
|
||||
// Cookie Banner Management
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Cookie Banner Class
|
||||
class CookieBanner {
|
||||
constructor() {
|
||||
this.cookieKey = 'odoo_expertos_cookies';
|
||||
this.banner = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Check if user has already made a choice
|
||||
const cookieChoice = localStorage.getItem(this.cookieKey);
|
||||
|
||||
// If no choice has been made, show the banner
|
||||
if (!cookieChoice) {
|
||||
this.createBanner();
|
||||
// Use requestAnimationFrame for better timing
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this.showBanner();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createBanner() {
|
||||
// Create banner HTML
|
||||
const bannerHTML = `
|
||||
<div class="cookie-banner" id="cookieBanner">
|
||||
<div class="cookie-banner-content">
|
||||
<div class="cookie-banner-text">
|
||||
<h3>Política de Cookies</h3>
|
||||
<p>Utilizamos cookies para mejorar tu experiencia de aprendizaje en nuestra plataforma de formación Odoo.
|
||||
Las cookies nos ayudan a personalizar el contenido, recordar tu progreso y mejorar nuestros cursos.</p>
|
||||
</div>
|
||||
<div class="cookie-banner-buttons">
|
||||
<button class="cookie-btn cookie-btn-accept" onclick="cookieBannerInstance.acceptCookies()">
|
||||
Aceptar
|
||||
</button>
|
||||
<button class="cookie-btn cookie-btn-reject" onclick="cookieBannerInstance.rejectCookies()">
|
||||
Rechazar
|
||||
</button>
|
||||
<button class="cookie-btn cookie-btn-configure" onclick="cookieBannerInstance.configureCookies()">
|
||||
Configurar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add banner to the page
|
||||
document.body.insertAdjacentHTML('beforeend', bannerHTML);
|
||||
this.banner = document.getElementById('cookieBanner');
|
||||
|
||||
// Add CSS for cookie banner
|
||||
this.addBannerStyles();
|
||||
}
|
||||
|
||||
showBanner() {
|
||||
if (this.banner) {
|
||||
this.banner.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
hideBanner() {
|
||||
if (this.banner) {
|
||||
this.banner.classList.remove('active');
|
||||
// Remove banner from DOM after animation
|
||||
setTimeout(() => {
|
||||
if (this.banner && this.banner.parentNode) {
|
||||
this.banner.parentNode.removeChild(this.banner);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
acceptCookies() {
|
||||
this.saveCookieChoice({
|
||||
accepted: true,
|
||||
analytics: true,
|
||||
marketing: true,
|
||||
functional: true,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
this.hideBanner();
|
||||
this.enableCookies();
|
||||
}
|
||||
|
||||
rejectCookies() {
|
||||
this.saveCookieChoice({
|
||||
accepted: false,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
functional: false,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
this.hideBanner();
|
||||
this.disableCookies();
|
||||
}
|
||||
|
||||
configureCookies() {
|
||||
// Create configuration modal
|
||||
const configHTML = `
|
||||
<div class="cookie-config-modal" id="cookieConfigModal">
|
||||
<div class="cookie-config-content">
|
||||
<h2>Configurar Cookies</h2>
|
||||
<div class="cookie-config-section">
|
||||
<h3>Cookies Esenciales</h3>
|
||||
<p>Necesarias para el funcionamiento básico del sitio.</p>
|
||||
<label class="cookie-switch">
|
||||
<input type="checkbox" checked disabled>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="cookie-config-section">
|
||||
<h3>Cookies de Análisis</h3>
|
||||
<p>Nos ayudan a entender cómo utilizas nuestra plataforma de formación.</p>
|
||||
<label class="cookie-switch">
|
||||
<input type="checkbox" id="analyticsCookie">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="cookie-config-section">
|
||||
<h3>Cookies de Marketing</h3>
|
||||
<p>Utilizadas para mostrarte cursos relevantes.</p>
|
||||
<label class="cookie-switch">
|
||||
<input type="checkbox" id="marketingCookie">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="cookie-config-buttons">
|
||||
<button class="cookie-btn cookie-btn-accept" onclick="cookieBannerInstance.saveConfiguration()">
|
||||
Guardar Preferencias
|
||||
</button>
|
||||
<button class="cookie-btn cookie-btn-reject" onclick="cookieBannerInstance.closeConfiguration()">
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', configHTML);
|
||||
|
||||
// Add styles for modal
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.cookie-config-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-config-content {
|
||||
background: var(--bg-dark);
|
||||
border: 2px solid var(--primary-color);
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-config-content h2 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.cookie-config-section {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid rgba(255,107,53,0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cookie-config-section:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cookie-config-section h3 {
|
||||
color: var(--white);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.cookie-config-section p {
|
||||
color: var(--text-color);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0;
|
||||
flex: 1;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.cookie-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.cookie-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--light-bg);
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
input:disabled + .slider {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cookie-config-buttons {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
saveConfiguration() {
|
||||
const analytics = document.getElementById('analyticsCookie').checked;
|
||||
const marketing = document.getElementById('marketingCookie').checked;
|
||||
|
||||
this.saveCookieChoice({
|
||||
accepted: true,
|
||||
analytics: analytics,
|
||||
marketing: marketing,
|
||||
functional: true,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
this.closeConfiguration();
|
||||
this.hideBanner();
|
||||
|
||||
// Enable/disable cookies based on preferences
|
||||
if (analytics || marketing) {
|
||||
this.enableCookies();
|
||||
} else {
|
||||
this.disableCookies();
|
||||
}
|
||||
}
|
||||
|
||||
closeConfiguration() {
|
||||
const modal = document.getElementById('cookieConfigModal');
|
||||
if (modal && modal.parentNode) {
|
||||
modal.parentNode.removeChild(modal);
|
||||
}
|
||||
}
|
||||
|
||||
saveCookieChoice(choice) {
|
||||
localStorage.setItem(this.cookieKey, JSON.stringify(choice));
|
||||
}
|
||||
|
||||
addBannerStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.cookie-banner {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #1a1a1a;
|
||||
border-top: 3px solid #FF6B35;
|
||||
padding: 1.5rem;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.5s ease;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 -4px 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.cookie-banner.active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.cookie-banner-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cookie-banner-text {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.cookie-banner-text h3 {
|
||||
color: #FF6B35;
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.cookie-banner-text p {
|
||||
color: #e0e0e0;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cookie-banner-text a {
|
||||
color: #FF6B35;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cookie-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.cookie-btn-accept {
|
||||
background: #FF6B35;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cookie-btn-accept:hover {
|
||||
background: #e55a28;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px rgba(255,107,53,0.4);
|
||||
}
|
||||
|
||||
.cookie-btn-reject {
|
||||
background: transparent;
|
||||
color: #FF6B35;
|
||||
border: 2px solid #FF6B35;
|
||||
}
|
||||
|
||||
.cookie-btn-reject:hover {
|
||||
background: #FF6B35;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cookie-btn-configure {
|
||||
background: #333;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.cookie-btn-configure:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cookie-banner-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cookie-btn {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
enableCookies() {
|
||||
// Here you would enable your analytics, marketing cookies etc.
|
||||
console.log('Cookies enabled');
|
||||
}
|
||||
|
||||
disableCookies() {
|
||||
// Here you would disable non-essential cookies
|
||||
console.log('Non-essential cookies disabled');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize cookie banner when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.cookieBannerInstance = new CookieBanner();
|
||||
});
|
||||
} else {
|
||||
window.cookieBannerInstance = new CookieBanner();
|
||||
}
|
||||
})();
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
|
||||
// Directory Page Card Structure Fix
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Function to check and fix article card structure
|
||||
function fixArticleCards() {
|
||||
const articlesGrid = document.querySelector('.articles-grid');
|
||||
if (!articlesGrid) return;
|
||||
|
||||
// Get all article cards
|
||||
const cards = articlesGrid.querySelectorAll('.article-card');
|
||||
|
||||
cards.forEach(card => {
|
||||
// Check if card is an anchor tag
|
||||
if (card.tagName.toLowerCase() !== 'a') return;
|
||||
|
||||
// Check if card has proper structure
|
||||
const hasImage = card.querySelector('.article-card-image');
|
||||
const hasContent = card.querySelector('.article-card-content');
|
||||
|
||||
if (!hasImage || !hasContent) {
|
||||
console.warn('Article card structure issue detected:', card);
|
||||
|
||||
// Try to fix by finding orphaned content
|
||||
const nextSibling = card.nextElementSibling;
|
||||
if (nextSibling && nextSibling.classList.contains('article-card-content')) {
|
||||
// Move the content inside the card
|
||||
card.appendChild(nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure card has proper display properties
|
||||
card.style.display = 'flex';
|
||||
card.style.flexDirection = 'column';
|
||||
});
|
||||
}
|
||||
|
||||
// Run the fix
|
||||
fixArticleCards();
|
||||
|
||||
// Also run after a short delay in case of dynamic content
|
||||
setTimeout(fixArticleCards, 100);
|
||||
});
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
//Directory Page Card Structure Fix document.addEventListener('DOMContentLoaded',function(){function fixArticleCards(){const articlesGrid=document.querySelector('.articles-grid');if (!articlesGrid) return;const cards=articlesGrid.querySelectorAll('.article-card');cards.forEach(card=>{if (card.tagName.toLowerCase()!=='a') return;const hasImage=card.querySelector('.article-card-image');const hasContent=card.querySelector('.article-card-content');if (!hasImage||!hasContent){console.warn('Article card structure issue detected:',card);const nextSibling=card.nextElementSibling;if (nextSibling&&nextSibling.classList.contains('article-card-content')){card.appendChild(nextSibling);}}card.style.display='flex';card.style.flexDirection='column';});}fixArticleCards();setTimeout(fixArticleCards,100);});
|
||||
@@ -0,0 +1,299 @@
|
||||
// Language Router - Multi-language URL management for Odoo Expertos
|
||||
// Supports: ES (default, no prefix), EN, DE, FR
|
||||
|
||||
const LanguageRouter = {
|
||||
// Supported languages with their configurations
|
||||
languages: {
|
||||
es: { code: 'es', name: 'Español', prefix: '', flag: '🇪🇸', hreflang: 'es' },
|
||||
en: { code: 'en', name: 'English', prefix: '/en', flag: '🇬🇧', hreflang: 'en' },
|
||||
de: { code: 'de', name: 'Deutsch', prefix: '/de', flag: '🇩🇪', hreflang: 'de' },
|
||||
fr: { code: 'fr', name: 'Français', prefix: '/fr', flag: '🇫🇷', hreflang: 'fr' },
|
||||
pt: { code: 'pt', name: 'Português', prefix: '/pt', flag: '🇧🇷', hreflang: 'pt' },
|
||||
ar: { code: 'ar', name: 'العربية', prefix: '/ar', flag: '🇸🇦', hreflang: 'ar', rtl: true }
|
||||
},
|
||||
|
||||
// Navigation labels per language
|
||||
navLabels: {
|
||||
es: { home: 'Inicio', odoo: 'Odoo', odooIA: 'Odoo IA', hosting: 'Hosting Odoo' },
|
||||
en: { home: 'Home', odoo: 'Odoo', odooIA: 'Odoo AI', hosting: 'Odoo Hosting' },
|
||||
de: { home: 'Startseite', odoo: 'Odoo', odooIA: 'Odoo KI', hosting: 'Odoo Hosting' },
|
||||
fr: { home: 'Accueil', odoo: 'Odoo', odooIA: 'Odoo IA', hosting: 'Hébergement Odoo' },
|
||||
pt: { home: 'Início', odoo: 'Odoo', odooIA: 'Odoo IA', hosting: 'Hospedagem Odoo' },
|
||||
ar: { home: 'الرئيسية', odoo: 'Odoo', odooIA: 'Odoo AI', hosting: 'استضافة Odoo' }
|
||||
},
|
||||
|
||||
// Footer labels per language
|
||||
footerLabels: {
|
||||
es: {
|
||||
contact: 'Contacto',
|
||||
quickLinks: 'Enlaces Rápidos',
|
||||
legal: 'Legal',
|
||||
modules: 'Módulos Odoo',
|
||||
benefits: 'Beneficios',
|
||||
community: 'Comunidad',
|
||||
legalNotice: 'Aviso Legal',
|
||||
privacy: 'Política de Privacidad',
|
||||
cookies: 'Política de Cookies',
|
||||
terms: 'Términos de Uso',
|
||||
copyright: 'Todos los derechos reservados.',
|
||||
experts: 'Expertos en soluciones Odoo ERP'
|
||||
},
|
||||
en: {
|
||||
contact: 'Contact',
|
||||
quickLinks: 'Quick Links',
|
||||
legal: 'Legal',
|
||||
modules: 'Odoo Modules',
|
||||
benefits: 'Benefits',
|
||||
community: 'Community',
|
||||
legalNotice: 'Legal Notice',
|
||||
privacy: 'Privacy Policy',
|
||||
cookies: 'Cookie Policy',
|
||||
terms: 'Terms of Use',
|
||||
copyright: 'All rights reserved.',
|
||||
experts: 'Odoo ERP Solutions Experts'
|
||||
},
|
||||
de: {
|
||||
contact: 'Kontakt',
|
||||
quickLinks: 'Schnelllinks',
|
||||
legal: 'Rechtliches',
|
||||
modules: 'Odoo Module',
|
||||
benefits: 'Vorteile',
|
||||
community: 'Community',
|
||||
legalNotice: 'Impressum',
|
||||
privacy: 'Datenschutzerklärung',
|
||||
cookies: 'Cookie-Richtlinie',
|
||||
terms: 'Nutzungsbedingungen',
|
||||
copyright: 'Alle Rechte vorbehalten.',
|
||||
experts: 'Odoo ERP Lösungsexperten'
|
||||
},
|
||||
fr: {
|
||||
contact: 'Contact',
|
||||
quickLinks: 'Liens Rapides',
|
||||
legal: 'Mentions Légales',
|
||||
modules: 'Modules Odoo',
|
||||
benefits: 'Avantages',
|
||||
community: 'Communauté',
|
||||
legalNotice: 'Mentions Légales',
|
||||
privacy: 'Politique de Confidentialité',
|
||||
cookies: 'Politique des Cookies',
|
||||
terms: "Conditions d'Utilisation",
|
||||
copyright: 'Tous droits réservés.',
|
||||
experts: 'Experts en solutions Odoo ERP'
|
||||
},
|
||||
pt: {
|
||||
contact: 'Contato',
|
||||
quickLinks: 'Links Rápidos',
|
||||
legal: 'Legal',
|
||||
modules: 'Módulos Odoo',
|
||||
benefits: 'Benefícios',
|
||||
community: 'Comunidade',
|
||||
legalNotice: 'Aviso Legal',
|
||||
privacy: 'Política de Privacidade',
|
||||
cookies: 'Política de Cookies',
|
||||
terms: 'Termos de Uso',
|
||||
copyright: 'Todos os direitos reservados.',
|
||||
experts: 'Especialistas em soluções Odoo ERP'
|
||||
},
|
||||
ar: {
|
||||
contact: 'اتصل بنا',
|
||||
quickLinks: 'روابط سريعة',
|
||||
legal: 'قانوني',
|
||||
modules: 'وحدات Odoo',
|
||||
benefits: 'المزايا',
|
||||
community: 'المجتمع',
|
||||
legalNotice: 'إشعار قانوني',
|
||||
privacy: 'سياسة الخصوصية',
|
||||
cookies: 'سياسة ملفات تعريف الارتباط',
|
||||
terms: 'شروط الاستخدام',
|
||||
copyright: 'جميع الحقوق محفوظة.',
|
||||
experts: 'خبراء حلول Odoo ERP'
|
||||
}
|
||||
},
|
||||
|
||||
// Detect current language from URL
|
||||
detectLanguage: function() {
|
||||
const path = window.location.pathname;
|
||||
if (path.startsWith('/en/') || path === '/en') return 'en';
|
||||
if (path.startsWith('/de/') || path === '/de') return 'de';
|
||||
if (path.startsWith('/fr/') || path === '/fr') return 'fr';
|
||||
if (path.startsWith('/pt/') || path === '/pt') return 'pt';
|
||||
if (path.startsWith('/ar/') || path === '/ar') return 'ar';
|
||||
return 'es'; // Default
|
||||
},
|
||||
|
||||
// Get current language config
|
||||
getCurrentLanguage: function() {
|
||||
const lang = this.detectLanguage();
|
||||
return this.languages[lang];
|
||||
},
|
||||
|
||||
// Get base path (without language prefix)
|
||||
getBasePath: function() {
|
||||
const path = window.location.pathname;
|
||||
const lang = this.detectLanguage();
|
||||
|
||||
if (lang === 'es') return path;
|
||||
|
||||
const prefix = this.languages[lang].prefix;
|
||||
if (path.startsWith(prefix)) {
|
||||
const basePath = path.substring(prefix.length);
|
||||
return basePath || '/';
|
||||
}
|
||||
return path;
|
||||
},
|
||||
|
||||
// Build URL for a specific language
|
||||
buildLanguageUrl: function(targetLang) {
|
||||
const basePath = this.getBasePath();
|
||||
const config = this.languages[targetLang];
|
||||
|
||||
if (targetLang === 'es') {
|
||||
return basePath;
|
||||
}
|
||||
|
||||
return config.prefix + (basePath === '/' ? '/' : basePath);
|
||||
},
|
||||
|
||||
// Generate all alternate URLs for hreflang
|
||||
getAlternateUrls: function() {
|
||||
const basePath = this.getBasePath();
|
||||
const baseUrl = 'https://odoo-expertos.com';
|
||||
const alternates = [];
|
||||
|
||||
Object.keys(this.languages).forEach(lang => {
|
||||
const config = this.languages[lang];
|
||||
let url;
|
||||
|
||||
if (lang === 'es') {
|
||||
url = baseUrl + basePath;
|
||||
} else {
|
||||
url = baseUrl + config.prefix + (basePath === '/' ? '/' : basePath);
|
||||
}
|
||||
|
||||
alternates.push({
|
||||
lang: config.hreflang,
|
||||
url: url
|
||||
});
|
||||
});
|
||||
|
||||
// Add x-default (Spanish)
|
||||
alternates.push({
|
||||
lang: 'x-default',
|
||||
url: baseUrl + basePath
|
||||
});
|
||||
|
||||
return alternates;
|
||||
},
|
||||
|
||||
// Generate hreflang link tags HTML
|
||||
generateHreflangTags: function() {
|
||||
const alternates = this.getAlternateUrls();
|
||||
return alternates.map(alt =>
|
||||
`<link rel="alternate" hreflang="${alt.lang}" href="${alt.url}" />`
|
||||
).join('\n ');
|
||||
},
|
||||
|
||||
// Inject hreflang tags into document head
|
||||
injectHreflangTags: function() {
|
||||
// Remove existing hreflang tags
|
||||
document.querySelectorAll('link[hreflang]').forEach(el => el.remove());
|
||||
|
||||
const alternates = this.getAlternateUrls();
|
||||
alternates.forEach(alt => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'alternate';
|
||||
link.hreflang = alt.lang;
|
||||
link.href = alt.url;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
},
|
||||
|
||||
// Get navigation labels for current language
|
||||
getNavLabels: function() {
|
||||
const lang = this.detectLanguage();
|
||||
return this.navLabels[lang];
|
||||
},
|
||||
|
||||
// Get footer labels for current language
|
||||
getFooterLabels: function() {
|
||||
const lang = this.detectLanguage();
|
||||
return this.footerLabels[lang];
|
||||
},
|
||||
|
||||
// Get link prefix for current language
|
||||
getLinkPrefix: function() {
|
||||
const lang = this.detectLanguage();
|
||||
return this.languages[lang].prefix;
|
||||
},
|
||||
|
||||
// Switch to another language
|
||||
switchLanguage: function(targetLang) {
|
||||
const newUrl = this.buildLanguageUrl(targetLang);
|
||||
window.location.href = newUrl;
|
||||
},
|
||||
|
||||
// Create language switcher HTML
|
||||
createLanguageSwitcher: function() {
|
||||
const currentLang = this.detectLanguage();
|
||||
const currentConfig = this.languages[currentLang];
|
||||
|
||||
let dropdownItems = '';
|
||||
Object.keys(this.languages).forEach(lang => {
|
||||
const config = this.languages[lang];
|
||||
const isActive = lang === currentLang ? ' active' : '';
|
||||
const url = this.buildLanguageUrl(lang);
|
||||
dropdownItems += `
|
||||
<a href="${url}" class="lang-option${isActive}" data-lang="${lang}">
|
||||
<span class="lang-flag">${config.flag}</span>
|
||||
<span class="lang-name">${config.name}</span>
|
||||
</a>`;
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="language-switcher">
|
||||
<button class="lang-toggle" aria-label="Select language">
|
||||
<span class="lang-flag">${currentConfig.flag}</span>
|
||||
<span class="lang-code">${currentLang.toUpperCase()}</span>
|
||||
<svg class="lang-arrow" width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
||||
<path d="M2 4L6 8L10 4" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="lang-dropdown">
|
||||
${dropdownItems}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
// Initialize language switcher functionality
|
||||
initLanguageSwitcher: function() {
|
||||
document.addEventListener('click', (e) => {
|
||||
const toggle = e.target.closest('.lang-toggle');
|
||||
const switcher = document.querySelector('.language-switcher');
|
||||
|
||||
if (toggle) {
|
||||
e.preventDefault();
|
||||
switcher.classList.toggle('open');
|
||||
} else if (!e.target.closest('.language-switcher')) {
|
||||
if (switcher) switcher.classList.remove('open');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Initialize all language features
|
||||
init: function() {
|
||||
this.injectHreflangTags();
|
||||
this.initLanguageSwitcher();
|
||||
|
||||
// Set html lang attribute
|
||||
const lang = this.detectLanguage();
|
||||
document.documentElement.lang = lang;
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => LanguageRouter.init());
|
||||
} else {
|
||||
LanguageRouter.init();
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Upstash Client-Side Cache Integration
|
||||
* Provides intelligent caching for API calls and dynamic content
|
||||
*/
|
||||
|
||||
class UpstashClient {
|
||||
constructor() {
|
||||
this.cacheEndpoint = '/api/cache';
|
||||
this.localCache = new Map();
|
||||
this.pendingRequests = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from cache or fetch if not available
|
||||
*/
|
||||
async get(key, fetcher, ttl = 3600) {
|
||||
// Check local memory cache first
|
||||
const localData = this.localCache.get(key);
|
||||
if (localData && Date.now() - localData.timestamp < localData.ttl * 1000) {
|
||||
console.log(`Local cache HIT: ${key}`);
|
||||
return localData.data;
|
||||
}
|
||||
|
||||
// Check if request is already pending
|
||||
if (this.pendingRequests.has(key)) {
|
||||
console.log(`Waiting for pending request: ${key}`);
|
||||
return this.pendingRequests.get(key);
|
||||
}
|
||||
|
||||
// Create promise for this request
|
||||
const promise = this._fetchWithCache(key, fetcher, ttl);
|
||||
this.pendingRequests.set(key, promise);
|
||||
|
||||
try {
|
||||
const result = await promise;
|
||||
return result;
|
||||
} finally {
|
||||
this.pendingRequests.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
async _fetchWithCache(key, fetcher, ttl) {
|
||||
try {
|
||||
// Try to get from Upstash cache
|
||||
const cached = await this._getFromUpstash(key);
|
||||
if (cached) {
|
||||
console.log(`Upstash cache HIT: ${key}`);
|
||||
// Store in local cache
|
||||
this.localCache.set(key, {
|
||||
data: cached,
|
||||
timestamp: Date.now(),
|
||||
ttl: ttl
|
||||
});
|
||||
return cached;
|
||||
}
|
||||
|
||||
console.log(`Cache MISS: ${key} - fetching fresh data`);
|
||||
|
||||
// Fetch fresh data
|
||||
const freshData = await fetcher();
|
||||
|
||||
// Store in both Upstash and local cache
|
||||
await this._setInUpstash(key, freshData, ttl);
|
||||
this.localCache.set(key, {
|
||||
data: freshData,
|
||||
timestamp: Date.now(),
|
||||
ttl: ttl
|
||||
});
|
||||
|
||||
return freshData;
|
||||
} catch (error) {
|
||||
console.error(`Cache error for ${key}:`, error);
|
||||
// Fallback to fetcher on error
|
||||
return fetcher();
|
||||
}
|
||||
}
|
||||
|
||||
async _getFromUpstash(key) {
|
||||
try {
|
||||
const response = await fetch(`${this.cacheEndpoint}?action=get&key=${encodeURIComponent(key)}`);
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upstash get error:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async _setInUpstash(key, value, ttl) {
|
||||
try {
|
||||
await fetch(this.cacheEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'set',
|
||||
key: key,
|
||||
value: JSON.stringify(value),
|
||||
ttl: ttl
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upstash set error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache for a specific key or pattern
|
||||
*/
|
||||
async invalidate(pattern) {
|
||||
// Clear local cache
|
||||
if (pattern === '*') {
|
||||
this.localCache.clear();
|
||||
} else {
|
||||
for (const [key] of this.localCache) {
|
||||
if (key.includes(pattern)) {
|
||||
this.localCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate Upstash cache
|
||||
try {
|
||||
await fetch('/api/cache/invalidate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ pattern })
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Cache invalidation error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload data into cache
|
||||
*/
|
||||
async preload(key, fetcher, ttl = 3600) {
|
||||
// Don't wait for result
|
||||
this.get(key, fetcher, ttl).catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
async getStats() {
|
||||
try {
|
||||
const response = await fetch('/api/cache/stats');
|
||||
if (response.ok) {
|
||||
const stats = await response.json();
|
||||
stats.localCacheSize = this.localCache.size;
|
||||
return stats;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Stats error:', error);
|
||||
}
|
||||
return { localCacheSize: this.localCache.size };
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize global cache client
|
||||
window.upstashCache = new UpstashClient();
|
||||
|
||||
// Intelligent prefetching based on user behavior
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Prefetch on link hover
|
||||
let prefetchedUrls = new Set();
|
||||
|
||||
document.querySelectorAll('a[href^="/"]').forEach(link => {
|
||||
link.addEventListener('mouseenter', () => {
|
||||
const href = link.getAttribute('href');
|
||||
if (!prefetchedUrls.has(href)) {
|
||||
prefetchedUrls.add(href);
|
||||
|
||||
// Prefetch the page content
|
||||
const cacheKey = `page:${href}`;
|
||||
window.upstashCache.preload(cacheKey, async () => {
|
||||
const response = await fetch(href);
|
||||
return response.text();
|
||||
}, 3600);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Cache API responses
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(url, options = {}) {
|
||||
// Only cache GET requests to our API
|
||||
if (typeof url === 'string' &&
|
||||
url.startsWith('/api/') &&
|
||||
(!options.method || options.method === 'GET') &&
|
||||
!url.includes('/cache')) {
|
||||
|
||||
const cacheKey = `api:${url}`;
|
||||
return window.upstashCache.get(cacheKey, () => originalFetch(url, options), 1800);
|
||||
}
|
||||
|
||||
return originalFetch(url, options);
|
||||
};
|
||||
|
||||
// Performance monitoring
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.entryType === 'navigation') {
|
||||
console.log('Page Load Performance:', {
|
||||
domContentLoaded: entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart,
|
||||
loadComplete: entry.loadEventEnd - entry.loadEventStart,
|
||||
totalTime: entry.loadEventEnd - entry.fetchStart
|
||||
});
|
||||
|
||||
// Report to analytics if needed
|
||||
if (window.gtag) {
|
||||
window.gtag('event', 'page_load_time', {
|
||||
value: Math.round(entry.loadEventEnd - entry.fetchStart),
|
||||
metric_name: 'load_time'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe({ entryTypes: ['navigation'] });
|
||||
}
|
||||
|
||||
// Cache warming for critical resources
|
||||
const criticalResources = [
|
||||
'/odoo/',
|
||||
'/odoo-ia/',
|
||||
'/odoo-hosting/'
|
||||
];
|
||||
|
||||
// Warm cache in idle time
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => {
|
||||
criticalResources.forEach(resource => {
|
||||
const cacheKey = `page:${resource}`;
|
||||
window.upstashCache.preload(cacheKey, async () => {
|
||||
const response = await fetch(resource);
|
||||
return response.text();
|
||||
}, 7200); // 2 hours for directory pages
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Export for use in other scripts
|
||||
window.UpstashClient = UpstashClient;
|
||||
|
||||
// Log cache status
|
||||
console.log('🚀 Upstash client-side caching initialized');
|
||||
window.upstashCache.getStats().then(stats => {
|
||||
console.log('📊 Cache stats:', stats);
|
||||
});
|
||||
Reference in New Issue
Block a user