diff --git a/index.html b/index.html
index 5a9030a..e546987 100644
--- a/index.html
+++ b/index.html
@@ -60,39 +60,19 @@
-
-
+
+
diff --git a/script.js b/script.js
index 5f2e48c..d395d2f 100644
--- a/script.js
+++ b/script.js
@@ -428,6 +428,130 @@ var si = 0,
renderBlog();
}
+ /* ---- Server Ping Banner ---- */
+ (function() {
+ var SERVERS = [
+ { host: 'meppel.odoo4projects.com', city: 'Amsterdam', region: 'Netherlands' },
+ { host: 'boston.odoo4projects.com', city: 'Boston', region: 'United States' },
+ { host: 'manchester.odoo4projects.com', city: 'Manchester', region: 'United Kingdom' },
+ { host: 'mumbai.odoo4projects.com', city: 'Mumbai', region: 'India' },
+ { host: 'saopaulo.odoo4projects.com', city: 'S\u00e3o Paulo', region: 'Brazil' },
+ { host: 'sydney.odoo4projects.com', city: 'Sydney', region: 'Australia' }
+ ];
+
+ var grid = document.getElementById('serverGrid');
+ var bannerStatus = document.getElementById('bannerStatus');
+ var optimalEl = document.getElementById('serverOptimal');
+ var cards = [];
+ var results = [];
+ var pending = SERVERS.length;
+
+ SERVERS.forEach(function(s, idx) {
+ var card = document.createElement('div');
+ card.className = 'server-card';
+ card.innerHTML =
+ 'Best
' +
+ '' + s.city + '
' +
+ '' + s.region + '
' +
+ '' +
+ '
' +
+ '
connecting' +
+ '
';
+ grid.appendChild(card);
+ cards.push({ el: card, server: s, idx: idx });
+ });
+
+ /* Stagger the card entrance animation */
+ cards.forEach(function(c, i) {
+ setTimeout(function() {
+ c.el.classList.add('visible');
+ }, 80 + i * 70);
+ });
+
+ function pingServer(host) {
+ return new Promise(function(resolve) {
+ var start = performance.now();
+ var img = new Image();
+ var timedOut = false;
+ var timer = setTimeout(function() {
+ timedOut = true;
+ img.src = '';
+ resolve({ time: -1, error: 'timeout' });
+ }, 8000);
+ img.onload = function() {
+ if (!timedOut) {
+ clearTimeout(timer);
+ resolve({ time: performance.now() - start });
+ }
+ };
+ img.onerror = function() {
+ if (!timedOut) {
+ clearTimeout(timer);
+ /* Even 404 measures round-trip time */
+ resolve({ time: performance.now() - start });
+ }
+ };
+ img.src = 'https://' + host + '/favicon.ico?_=' + Date.now() + '_' + Math.random().toString(36).slice(2,6);
+ });
+ }
+
+ function updateCardPing(card, time) {
+ var wrap = card.el.querySelector('.server-ping-wrap');
+ if (time < 0) {
+ wrap.innerHTML = 'unreachable';
+ } else {
+ var ms = time.toFixed(0);
+ var cls = 'done';
+ if (time < 100) cls += ' fast';
+ wrap.innerHTML =
+ '' + ms + '' +
+ 'ms';
+ }
+ }
+
+ function markBest() {
+ var valid = results.filter(function(r) { return r.time >= 0; });
+ if (valid.length === 0) return;
+ valid.sort(function(a, b) { return a.time - b.time; });
+ var best = valid[0];
+ cards.forEach(function(c) {
+ if (c.server.host === best.host) {
+ c.el.classList.add('server-best');
+ }
+ });
+ /* Update banner status */
+ bannerStatus.innerHTML =
+ ' All locations tested';
+ /* Show optimal server */
+ var bestCard = cards.filter(function(c) { return c.server.host === best.host; })[0];
+ if (bestCard) {
+ setTimeout(function() {
+ optimalEl.innerHTML =
+ '' +
+ '↑ ' +
+ 'Your optimal server location: ' + bestCard.server.city + ' (' + best.time.toFixed(0) + ' ms)' +
+ '
';
+ optimalEl.classList.add('visible');
+ }, 600);
+ }
+ }
+
+ /* Ping all servers in parallel */
+ cards.forEach(function(c) {
+ var idx = c.idx;
+ pingServer(c.server.host).then(function(result) {
+ result.host = c.server.host;
+ result.idx = idx;
+ results.push(result);
+ updateCardPing(c, result.time);
+ pending--;
+ if (pending === 0) {
+ markBest();
+ }
+ });
+ });
+ })();
+
fetch("blog/index.json?_=" + Date.now())
.then(function (r) {
return r.json();
diff --git a/style.css b/style.css
index e318c88..c194704 100644
--- a/style.css
+++ b/style.css
@@ -156,64 +156,228 @@ img { max-width: 100%; height: auto; vertical-align: middle; }
.hero-stat i { color: var(--brass); font-size: 0.75rem; }
.hero-actions { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 2rem; }
-/* ---- City Placards ---- */
-.city-placards {
- display: flex;
- gap: 0.75rem;
- padding: 0 1.25rem;
- margin: 0 auto;
- max-width: 1200px;
- width: 86%;
- overflow-x: auto;
- padding-bottom: 0.5rem;
- position: relative;
- z-index: 1;
-}
-.city-placard {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.3rem;
- text-decoration: none;
- flex-shrink: 0;
- width: 8rem;
- padding: 0.75rem 0.5rem 0.5rem;
+/* ---- Server Status Banner ---- */
+.server-banner {
+ background: var(--midnight);
+ padding: 2rem 0 1.5rem;
border-top: 1px solid var(--line-brass);
- transition: all 0.25s;
- border-radius: 0 0 var(--radius) var(--radius);
+ border-bottom: 1px solid var(--line-brass);
+ position: relative;
+ overflow: hidden;
}
-.city-placard:hover {
- background: rgba(201,162,39,0.08);
- border-top-color: var(--brass);
+.server-banner::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(ellipse 60% 80% at 50% 100%, rgba(201,162,39,0.06) 0%, transparent 70%);
+ pointer-events: none;
}
-.city-placard img {
- width: 3.5rem;
- height: 3.5rem;
- border-radius: 50%;
- object-fit: cover;
- border: 2px solid var(--line-brass);
- transition: border-color 0.25s;
+.server-banner-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 1.25rem;
+ flex-wrap: wrap;
+ gap: 0.5rem;
}
-.city-placard:hover img { border-color: var(--brass); }
-.city-placard .city-name {
+.server-banner-kicker {
font-family: Marcellus, Georgia, serif;
- font-size: 0.85rem;
- color: #fff;
- line-height: 1.2;
-}
-.city-placard .city-region {
- font-family: Inter, sans-serif;
- font-size: 0.62rem;
- letter-spacing: 0.06em;
+ font-size: 1rem;
+ letter-spacing: 0.08em;
text-transform: uppercase;
- color: rgba(255,255,255,0.5);
+ color: var(--brass);
+}
+.server-banner-status {
+ font-family: Inter, sans-serif;
+ font-size: 0.78rem;
+ color: rgba(255,255,255,0.55);
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+.status-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--brass);
+ display: inline-block;
+ animation: statusPulse 1.2s ease-in-out infinite;
+}
+@keyframes statusPulse {
+ 0%, 100% { opacity: 0.3; transform: scale(0.8); }
+ 50% { opacity: 1; transform: scale(1.2); }
+}
+.server-grid {
+ display: grid;
+ grid-template-columns: repeat(6, 1fr);
+ gap: 0.75rem;
+}
+.server-card {
+ background: rgba(255,255,255,0.035);
+ border: 1px solid rgba(201,162,39,0.12);
+ border-radius: var(--radius);
+ padding: 1rem 0.75rem;
+ text-align: center;
+ transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
+ position: relative;
+ opacity: 0;
+ transform: translateY(12px);
+}
+.server-card.visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+.server-card:hover {
+ background: rgba(255,255,255,0.06);
+ border-color: rgba(201,162,39,0.25);
+}
+.server-card .server-city {
+ font-family: Marcellus, Georgia, serif;
+ font-size: 1.05rem;
+ color: #fff;
+ margin-bottom: 0.15rem;
+}
+.server-card .server-region {
+ font-family: Inter, sans-serif;
+ font-size: 0.68rem;
+ color: rgba(255,255,255,0.45);
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ margin-bottom: 0.6rem;
+}
+.server-card .server-ping-wrap {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.35rem;
+ min-height: 1.6rem;
+}
+.server-card .server-ping-icon {
+ font-size: 0.65rem;
+ color: rgba(255,255,255,0.2);
+}
+.server-card .server-ping-ms {
+ font-family: Inter, sans-serif;
+ font-size: 1.15rem;
+ font-weight: 600;
+ color: rgba(255,255,255,0.3);
+ letter-spacing: -0.02em;
+ transition: color 0.5s ease;
+}
+.server-card .server-ping-ms.done {
+ color: #fff;
+}
+.server-card .server-ping-ms.fast {
+ color: #4ade80;
+}
+.server-card .server-ping-unit {
+ font-size: 0.7rem;
+ color: rgba(255,255,255,0.3);
+ font-weight: 400;
+}
+.server-card .ping-spinner {
+ width: 16px;
+ height: 16px;
+ border: 2px solid rgba(201,162,39,0.15);
+ border-top-color: var(--brass);
+ border-radius: 50%;
+ animation: pingSpin 0.7s linear infinite;
+}
+@keyframes pingSpin {
+ to { transform: rotate(360deg); }
+}
+.server-card .server-ping-label {
+ font-size: 0.62rem;
+ color: rgba(255,255,255,0.25);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+.server-card.server-best {
+ background: rgba(201,162,39,0.1);
+ border-color: var(--brass);
+ box-shadow: 0 0 25px rgba(201,162,39,0.15), inset 0 0 25px rgba(201,162,39,0.04);
+ transform: translateY(-3px);
+}
+.server-card.server-best .server-city {
+ color: var(--brass-bright);
+}
+.server-card.server-best .server-ping-ms.fast {
+ color: var(--brass-bright);
+}
+.server-card .best-badge {
+ position: absolute;
+ top: -0.5rem;
+ right: -0.5rem;
+ background: var(--brass);
+ color: var(--midnight);
+ font-size: 0.55rem;
+ font-weight: 700;
+ font-family: Inter, sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ padding: 0.2rem 0.5rem;
+ border-radius: var(--radius-pill);
+ opacity: 0;
+ transform: scale(0.5);
+ transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
+ pointer-events: none;
+}
+.server-card.server-best .best-badge {
+ opacity: 1;
+ transform: scale(1);
+}
+.server-optimal {
+ text-align: center;
+ margin-top: 1rem;
+ min-height: 2rem;
+ opacity: 0;
+ transform: translateY(10px);
+ transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1);
+}
+.server-optimal.visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+.server-optimal-inner {
+ display: inline-block;
+ font-family: Marcellus, Georgia, serif;
+ font-size: 1rem;
+ color: rgba(255,255,255,0.6);
+ letter-spacing: 0.02em;
+ border-top: 1px solid rgba(201,162,39,0.2);
+ padding-top: 0.75rem;
+}
+.server-optimal-inner strong {
+ color: var(--brass-bright);
+ font-weight: 400;
+}
+.server-optimal-inner .optimal-arrow {
+ display: inline-block;
+ margin: 0 0.3rem;
+ animation: optimalArrowBounce 1.6s ease-in-out infinite;
+}
+@keyframes optimalArrowBounce {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-3px); }
+}
+
+@media (max-width: 992px) {
+ .server-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+@media (max-width: 576px) {
+ .server-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ .server-banner-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
}
@media (max-width: 768px) {
.hero { min-height: auto; padding: 3rem 0 0; }
- .city-placards { width: 100%; padding: 0 1rem 0.5rem; gap: 0.5rem; }
- .city-placard { width: 6rem; padding: 0.5rem 0.25rem; }
- .city-placard img { width: 2.8rem; height: 2.8rem; }
.hero-actions { flex-direction: column; }
.hero-actions .btn { width: 100%; text-align: center; }
.hero-stats { gap: 0.75rem; }