new site
This commit is contained in:
1 parent
f533ff8e36
commit
585951c8ce
1380 files changed
+1540268
No files matched your search
@@ -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
@@ -0,0 +1 @@
|
||||
/* Cookie Banner Management */ (function(){'use strict';class CookieBanner{constructor(){this.cookieKey='odoo_expertos_cookies';this.banner=null;this.init();}init(){const cookieChoice=localStorage.getItem(this.cookieKey);if (!cookieChoice){this.createBanner();requestAnimationFrame(()=>{requestAnimationFrame(()=>{this.showBanner();});});}}createBanner(){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>`;document.body.insertAdjacentHTML('beforeend',bannerHTML);this.banner=document.getElementById('cookieBanner');this.addBannerStyles();}showBanner(){if (this.banner){this.banner.classList.add('active');}}hideBanner(){if (this.banner){this.banner.classList.remove('active');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(){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);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=dLine truncated
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
// https://d3js.org v7.9.0 Copyright 2010-2023 Mike Bostock
|
||||
!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t.d3||{})}(this,(function(t){"use strict";function n(t,n){return null==t||null==n?NaN:t<n?-1:t>n?1:t>=n?0:NaN}function e(t,n){return null==t||null==n?NaN:n<t?-1:n>t?1:n>=t?0:NaN}function r(t){let r,o,a;function u(t,n,e=0,i=t.length){if(e<i){if(0!==r(n,n))return i;do{const r=e+i>>>1;o(t[r],n)<0?e=r+1:i=r}while(e<i)}return e}return 2!==t.length?(r=n,o=(e,r)=>n(t(e),r),a=(n,e)=>t(n)-e):(r=t===n||t===e?t:i,o=t,a=t),{left:u,center:function(t,n,e=0,r=t.length){const i=u(t,n,e,r-1);return i>e&&a(t[i-1],n)>-a(t[i],n)?i-1:i},right:function(t,n,e=0,i=t.length){if(e<i){if(0!==r(n,n))return i;do{const r=e+i>>>1;o(t[r],n)<=0?e=r+1:i=r}while(e<i)}return e}}}function i(){return 0}function o(t){return null===t?NaN:+t}const a=r(n),u=a.right,c=a.left,f=r(o).center;var s=u;const l=d(y),h=d((function(t){const n=y(t);return(t,e,r,i,o)=>{n(t,e,(r<<=2)+0,(i<<=2)+0,o<<=2),n(t,e,r+1,i+1,o),n(t,e,r+2,i+2,o),n(t,e,r+3,i+3,o)}}));function d(t){return function(n,e,r=e){if(!((e=+e)>=0))throw new RangeError("invalid rx");if(!((r=+r)>=0))throw new RangeError("invalid ry");let{data:i,width:o,height:a}=n;if(!((o=Math.floor(o))>=0))throw new RangeError("invalid width");if(!((a=Math.floor(void 0!==a?a:i.length/o))>=0))throw new RangeError("invalid height");if(!o||!a||!e&&!r)return n;const u=e&&t(e),c=r&&t(r),f=i.slice();return u&&c?(p(u,f,i,o,a),p(u,i,f,o,a),p(u,f,i,o,a),g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)):u?(p(u,i,f,o,a),p(u,f,i,o,a),p(u,i,f,o,a)):c&&(g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)),n}}function p(t,n,e,r,i){for(let o=0,a=r*i;o<a;)t(n,e,o,o+=r,1)}function g(t,n,e,r,i){for(let o=0,a=r*i;o<r;++o)t(n,e,o,o+a,r)}function y(t){const n=Math.floor(t);if(n===t)return function(t){const n=2*t+1;return(e,r,i,o,a)=>{if(!((o-=a)>=i))return;let u=t*r[i];const c=a*t;for(let t=i,n=i+c;t<n;t+=a)u+=r[Math.min(o,t)];for(let t=i,f=o;t<=f;t+=a)u+=r[Math.min(o,t+c)],e[t]=u/n,u-=r[Math.max(i,t-c)]}}(t);const e=t-n,r=2*t+1;return(t,i,o,a,u)=>{if(!((a-=u)>=o))return;let c=n*i[o];const f=u*n,s=f+u;for(let t=o,n=o+f;t<n;t+=u)c+=i[Math.min(a,t)];for(let n=o,l=a;n<=l;n+=u)c+=i[Math.min(a,n+f)],t[n]=(c+e*(i[Math.max(o,n-s)]+i[Math.min(a,n+s)]))/r,c-=i[Math.max(o,n-f)]}}function v(t,n){let e=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&++e;else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(i=+i)>=i&&++e}return e}function _(t){return 0|t.length}function b(t){return!(t>0)}function m(t){return"object"!=typeof t||"length"in t?t:Array.from(t)}function x(t,n){let e,r=0,i=0,o=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(e=n-i,i+=e/++r,o+=e*(n-i));else{let a=-1;for(let u of t)null!=(u=n(u,++a,t))&&(u=+u)>=u&&(e=u-i,i+=e/++r,o+=e*(u-i))}if(r>1)return o/(r-1)}function w(t,n){const e=x(t,n);return e?Math.sqrt(e):e}function M(t,n){let e,r;if(void 0===n)for(const n of t)null!=n&&(void 0===e?n>=n&&(e=r=n):(e>n&&(e=n),r<n&&(r=n)));else{let i=-1;for(let o of t)null!=(o=n(o,++i,t))&&(void 0===e?o>=o&&(e=r=o):(e>o&&(e=o),r<o&&(r=o)))}return[e,r]}class T{constructor(){this._partials=new Float64Array(32),this._n=0}add(t){const n=this._partials;let e=0;for(let r=0;r<this._n&&r<32;r++){const i=n[r],o=t+i,a=Math.abs(t)<Math.abs(i)?t-(o-i):i-(o-t);a&&(n[e++]=a),t=o}return n[e]=t,this._n=e+1,this}valueOf(){const t=this._partials;let n,e,r,i=this._n,o=0;if(i>0){for(o=t[--i];i>0&&(n=o,e=t[--i],o=n+e,r=e-(o-n),!r););i>0&&(r<0&&t[i-1]<0||r>0&&t[i-1]>0)&&(e=2*r,n=o+e,e==n-o&&(o=n))}return o}}class InternMap extends Map{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const[n,e]of t)this.set(n,e)}get(t){return super.get(A(this,t))}has(t){return super.has(A(this,t))}set(t,n){return super.set(S(this,t),n)}delete(t){return super.delete(E(this,t))}}class InternSet extends Set{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const n of t)this.add(n)}has(t){return super.has(A(this,t))}add(t){return super.add(S(this,t))}delete(t){return super.delete(E(this,t))}}function A({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):e}function S({_intern:t,_key:n},e){const r=n(e);return t.has(r)?t.get(r):(t.set(r,e),e)}function E({_intern:t,_key:n},e){const r=n(e);return t.has(r)&&(e=t.get(r),t.delete(r)),e}function N(t){return null!==t&&"object"==typeof t?t.valueOf():t}function k(t){return t}function C(t,...n){return F(t,k,k,n)}function P(t,...n){return F(t,Array.from,k,n)}function z(t,n){for(let e=1,r=n.length;e<r;++e)t=t.flatMap((t=>t.pop().map((([n,e])=>[...t,n,e]))));return t}function $(t,n,...e){return F(t,k,n,e)}function D(t,n,...e){return F(t,Array.from,n,e)}function R(t){if(1!==t.length)throw new Error("duplicate key");return t[0]}function F(t,n,e,r){return function t(i,o){if(o>=r.length)return e(i);const a=new InternMap,u=r[o++];let c=-1;for(const t of i){Line truncated
|
||||
@@ -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