/* ===== Navbar scroll — pill shape on scroll ===== */
const nav = document.getElementById('navbar');
window.addEventListener('scroll', () => {
nav.classList.toggle('scrolled', window.scrollY > 40);
}, { passive: true });
/* ===== Active nav link on scroll ===== */
const sectionWrappers = document.querySelectorAll('.section-wrapper[id]');
const navLinks = document.querySelectorAll('.nav-links a');
function updateActiveNav() {
const readingLine = window.scrollY + window.innerHeight / 3;
let current = '';
sectionWrappers.forEach(s => {
const rect = s.getBoundingClientRect();
const absoluteTop = rect.top + window.scrollY;
if (absoluteTop <= readingLine) {
current = s.id;
}
});
// Update nav links
navLinks.forEach(l => {
const href = l.getAttribute('href');
const isActive = href === '#' + current;
l.classList.toggle('active', isActive);
});
// Mobile: show active section name
nav.setAttribute('data-active-section', current || '');
}
window.addEventListener('scroll', updateActiveNav, { passive: true });
updateActiveNav();
/* ===== Smooth scroll with offset for fixed nav ===== */
document.querySelectorAll('a[href^="#"]').forEach(link => {
link.addEventListener('click', e => {
const target = document.querySelector(link.getAttribute('href'));
if (!target) return;
e.preventDefault();
const rect = target.getBoundingClientRect();
const absoluteTop = rect.top + window.scrollY;
const offset = 100;
window.scrollTo({
top: absoluteTop - offset,
behavior: 'smooth'
});
});
});
/* ===== Hero parallax grid & spotlight ===== */
const heroBg = document.querySelector('.hero-bg');
document.addEventListener('mousemove', e => {
const x = e.clientX;
const y = e.clientY;
// Hero parallax
if (heroBg) {
heroBg.style.setProperty('--bg-x', (x * 0.02) + 'px');
heroBg.style.setProperty('--bg-y', (y * 0.02) + 'px');
heroBg.style.setProperty('--mouse-x', x + 'px');
heroBg.style.setProperty('--mouse-y', y + 'px');
}
// Card spotlight tracking
document.querySelectorAll('.card, .minimal-card').forEach(card => {
const r = card.getBoundingClientRect();
card.style.setProperty('--mouse-x', (x - r.left) + 'px');
card.style.setProperty('--mouse-y', (y - r.top) + 'px');
});
}, { passive: true });
/* ===== Typewriter — character-by-character reveal ===== */
function typewrite(el, delay, speed) {
speed = speed || 30;
const text = el.textContent;
el.innerHTML = '';
const chars = [];
for (const ch of text) {
const span = document.createElement('span');
span.classList.add('char');
span.textContent = ch;
el.appendChild(span);
chars.push(span);
}
// Insert a real cursor element that moves with the text
const cursor = document.createElement('span');
cursor.classList.add('typing-cursor');
cursor.textContent = '|';
return new Promise(resolve => {
chars.forEach((span, i) => {
setTimeout(() => {
span.classList.add('visible');
// Move cursor right after the latest visible char
span.after(cursor);
if (i === chars.length - 1) {
resolve();
}
}, delay + i * speed);
});
if (chars.length === 0) resolve();
});
}
// Typewrite hero elements sequentially: subtitle starts after title finishes
(async function () {
const heroTitle = document.getElementById('hero-title');
const heroSub = document.getElementById('hero-subtitle');
// Hide subtitle until its turn
if (heroSub) heroSub.style.visibility = 'hidden';
if (heroTitle) {
await typewrite(heroTitle, 300);
heroTitle.querySelector('.typing-cursor')?.remove();
}
if (heroSub) {
heroSub.style.visibility = 'visible';
await typewrite(heroSub, 200, 12);
heroSub.querySelector('.typing-cursor')?.remove();
}
// Fade in hero CTA after both animations complete
setTimeout(() => {
document.querySelectorAll('.hero-fade-up').forEach(el => el.classList.add('visible'));
}, 200);
})();
/* ===== Intersection Observer — fade-up on scroll ===== */
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('visible');
observer.unobserve(e.target);
}
});
}, { threshold: 0.1, rootMargin: '-50px' });
document.querySelectorAll('.animate-up').forEach(el => observer.observe(el));
/* ===== Playground Logic ===== */
const COLORS = {
warmup: '#34a853', beginner: '#1a73e8', intermediate: '#f9ab00',
advanced: '#ea4335', expert: '#7627bb'
};
const COLOR_BG = {
warmup: '#e6f4ea', beginner: '#e8f0fe', intermediate: '#fef7e0',
advanced: '#fce8e6', expert: '#f3e8fd'
};
let stepCount = 0;
// Services that have official AWS SVG files in /static/img/aws/
const SVC_IMG_FILES = ['s3', 'sqs', 'sns', 'lambda', 'dynamodb', 'iam', 'ec2', 'rds', 'cloudformation', 'cloudwatch', 'route53', 'apigateway', 'apigateway_v1', 'elasticache', 'elbv2', 'events', 'ssm', 'cognito-idp', 'glue', 'firehose', 'athena', 'emr', 'efs', 'ebs', 'kinesis', 'logs', 'monitoring', 'ses', 'ses_v2', 'acm', 'wafv2', 'states', 'secretsmanager', 'ecs', 'elasticmapreduce', 'elasticloadbalancing', 'elasticfilesystem'];
const DEFAULT_ICON = '';
}
return '';
}
// Cache infra data for modal drill-down
let _lastInfraServices = {};
async function refreshState() {
try {
const res = await fetch('/web/state');
const state = await res.json();
// Update sidebar stats
document.getElementById('stateSteps').textContent = state.tracker ? state.tracker.step_count : '0';
document.getElementById('stateHints').textContent = state.tracker ? state.tracker.hints_used : '0';
const chaosEl = document.getElementById('stateChaos');
if (state.chaos_occurred) {
chaosEl.textContent = 'Active';
chaosEl.className = 'state-value chaos-active';
} else {
chaosEl.textContent = 'None';
chaosEl.className = 'state-value chaos-inactive';
}
// Render infra tiles
const grid = document.getElementById('infraGrid');
const services = state.infra_state && state.infra_state.services ? state.infra_state.services : {};
_lastInfraServices = services;
const svcKeys = Object.keys(services);
if (svcKeys.length === 0) {
grid.innerHTML = '
No data.
'; return; } let html = ''; for (const svc of svcKeys) { const data = services[svc]; let totalCount = 0; for (const [, resData] of Object.entries(data)) { if (resData && typeof resData === 'object') { if (typeof resData.count === 'number') { totalCount += resData.count; } else if (Array.isArray(resData)) { totalCount += resData.length; } else { // Nested object keyed by ID (e.g. apigateway_v1 rest_apis) const keys = Object.keys(resData); if (keys.length > 0) totalCount += keys.length; } } } const hasRes = totalCount > 0; html += 'No resources in this service.
'; document.getElementById('infra-modal').classList.add('open'); document.body.style.overflow = 'hidden'; } function closeInfraModal() { document.getElementById('infra-modal').classList.remove('open'); document.body.style.overflow = ''; } // Command log modal let _logEntries = []; function openLogModal(index) { const entry = _logEntries[index]; if (!entry) return; document.getElementById('log-modal-title').textContent = 'Step #' + entry.step; document.getElementById('log-modal-cmd').textContent = entry.command; document.getElementById('log-modal-status').innerHTML = entry.success ? 'Success' : 'Failed'; document.getElementById('log-modal-reward').textContent = (entry.reward >= 0 ? '+' : '') + entry.reward.toFixed(2); document.getElementById('log-modal-output').textContent = entry.output || 'No output'; document.getElementById('log-modal').classList.add('open'); document.body.style.overflow = 'hidden'; } function closeLogModal() { document.getElementById('log-modal').classList.remove('open'); document.body.style.overflow = ''; } // Close modals on Escape / backdrop click document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { closeInfraModal(); closeLogModal(); } }); ['infra-modal', 'log-modal'].forEach(function (id) { var el = document.getElementById(id); if (el) el.addEventListener('click', function (e) { if (e.target.id === id) { closeInfraModal(); closeLogModal(); } }); }); function setStatus(msg, type) { const bar = document.getElementById('statusBar'); bar.className = 'status-bar ' + (type || ''); bar.innerHTML = msg; } function setLoading(btn, loading) { if (loading) { btn.disabled = true; btn.dataset.orig = btn.textContent; } btn.innerHTML = loading ? '' + (btn.dataset.orig || '') : (btn.dataset.orig || btn.textContent); } function escHtml(s) { return s.replace(/&/g, '&').replace(//g, '>'); } async function resetEnv() { const btn = document.getElementById('resetBtn'); setLoading(btn, true); setStatus('Resetting environment...', 'info'); try { const res = await fetch('/web/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); const data = await res.json(); const obs = data.observation; stepCount = 0; const task = obs.task; const box = document.getElementById('taskBox'); if (task) { const color = COLORS[task.difficulty] || '#5f6368'; const bg = COLOR_BG[task.difficulty] || '#f1f3f4'; box.className = 'task-box'; box.style.borderLeftColor = color; box.innerHTML = '' + escHtml(task.description) + '
'; } document.getElementById('outputBox').textContent = obs.command_output || ''; document.getElementById('logBody').innerHTML = '