feat: add dark mode, navigation, and Web Vitals tracking to marketplace

Backend:
- Simplify DatabaseConfig: remove effective_url property and project root finder
- Update to Pydantic v2 model_config (replace nested Config class)
- Add web_vitals router to main.py and __init__.py
- Fix ExplorerService datetime handling (ensure timezone-naive comparisons)
- Fix status_label extraction to handle both enum and string job states

Frontend (Marketplace):
- Add dark mode toggle with system preference detection
This commit is contained in:
oib
2026-02-15 19:02:51 +01:00
parent 72e21fd07f
commit 7062b2cc78
26 changed files with 1945 additions and 769 deletions

View File

@@ -244,7 +244,7 @@
</a>
</div>
<div class="link-item">
<a href="mailto:aitbc@bubuit.net">
<a href="mailto:andreas.fleckl@bubuit.net">
<i class="fas fa-envelope"></i> Contact Support
</a>
</div>

File diff suppressed because one or more lines are too long

516
website/assets/css/main.css Normal file
View File

@@ -0,0 +1,516 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #2563eb;
--secondary-color: #1e40af;
--accent-color: #3b82f6;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--text-dark: #1f2937;
--text-light: #6b7280;
--bg-light: #f9fafb;
--bg-white: #ffffff;
--gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
[data-theme="dark"] {
--primary-color: #3b82f6;
--secondary-color: #2563eb;
--accent-color: #60a5fa;
--text-dark: #f9fafb;
--text-light: #d1d5db;
--bg-light: #111827;
--bg-white: #1f2937;
--gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: var(--text-dark);
background-color: var(--bg-light);
transition: background-color 0.3s ease, color 0.3s ease;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
header {
background: var(--bg-white);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
position: fixed;
width: 100%;
top: 0;
z-index: 1000;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: var(--primary-color);
text-decoration: none;
}
.nav-links {
display: flex;
gap: 2rem;
list-style: none;
}
.nav-links a {
color: var(--text-dark);
text-decoration: none;
transition: color 0.3s;
}
.nav-links a:hover {
color: var(--primary-color);
}
.dark-mode-toggle {
background: none;
border: 1px solid var(--text-light);
color: var(--text-dark);
cursor: pointer;
font-size: 1.2rem;
transition: color 0.3s, border-color 0.3s;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.dark-mode-toggle:hover {
color: var(--primary-color);
background: rgba(59, 130, 246, 0.1);
}
/* Hero Section */
.hero {
background: var(--gradient);
color: white;
padding: 100px 0 80px;
text-align: center;
}
.hero h1 {
font-size: 3.5rem;
margin-bottom: 1rem;
animation: fadeInUp 0.8s ease;
}
.hero p {
font-size: 1.25rem;
margin-bottom: 2rem;
opacity: 0.9;
animation: fadeInUp 0.8s ease 0.2s both;
}
.cta-button {
display: inline-block;
padding: 12px 30px;
background: var(--bg-white);
color: var(--primary-color);
text-decoration: none;
border-radius: 5px;
font-weight: 600;
transition: transform 0.3s, box-shadow 0.3s;
animation: fadeInUp 0.8s ease 0.4s both;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
/* Features Section */
.features {
padding: 80px 0;
background: var(--bg-light);
}
.section-title {
text-align: center;
font-size: 2.5rem;
margin-bottom: 3rem;
color: var(--text-dark);
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
.feature-card {
background: var(--bg-white);
padding: 2rem;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
transition: transform 0.3s;
}
.feature-card:hover {
transform: translateY(-5px);
}
.feature-icon {
font-size: 3rem;
color: var(--primary-color);
margin-bottom: 1rem;
}
.feature-card h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--text-dark);
}
.feature-card p {
color: var(--text-light);
line-height: 1.8;
margin-bottom: 1.5rem;
flex-grow: 1;
}
.feature-link {
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s;
}
.feature-link:hover {
color: var(--secondary-color);
transform: translateX(5px);
}
/* Architecture Section */
.architecture {
padding: 80px 0;
background: var(--bg-white);
}
/* Header styles matching Exchange */
.gradient-bg {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
}
.nav-button {
background: transparent !important;
color: white !important;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.nav-button:hover {
background: rgba(59, 130, 246, 0.1) !important;
color: var(--primary-color) !important;
}
.nav-button:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
}
.architecture-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin-top: 3rem;
}
.arch-component {
text-align: center;
padding: 1.5rem;
border: 2px solid var(--text-light);
border-radius: 10px;
background: var(--bg-white);
transition: border-color 0.3s;
}
.arch-component:hover {
border-color: var(--primary-color);
}
.arch-component i {
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 1rem;
}
/* Stats Section */
.stats {
padding: 60px 0;
background: var(--gradient);
color: white;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
text-align: center;
}
.stat-item h3 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.stat-item p {
font-size: 1.1rem;
opacity: 0.9;
}
/* Documentation Section */
.documentation {
padding: 80px 0;
background: var(--bg-light);
}
.section-subtitle {
text-align: center;
font-size: 1.2rem;
color: var(--text-light);
margin-bottom: 3rem;
}
.docs-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
.doc-card {
background: var(--bg-white);
padding: 2.5rem;
border-radius: 15px;
text-align: center;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.doc-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
}
.doc-card:nth-child(1)::before {
background: var(--primary-color);
}
.doc-card:nth-child(2)::before {
background: var(--success-color);
}
.doc-card:nth-child(3)::before {
background: var(--warning-color);
}
.doc-card:hover {
transform: translateY(-10px);
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
}
.doc-icon {
width: 70px;
height: 70px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
color: white;
margin: 0 auto 1.5rem;
}
.doc-card:nth-child(1) .doc-icon {
background: var(--primary-color);
}
.doc-card:nth-child(2) .doc-icon {
background: var(--success-color);
}
.doc-card:nth-child(3) .doc-icon {
background: var(--warning-color);
}
.doc-card h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--text-dark);
}
.doc-card p {
color: var(--text-light);
margin-bottom: 2rem;
}
.doc-link {
gap: 0.5rem;
color: var(--text-dark);
text-decoration: none;
font-weight: 600;
transition: all 0.3s;
}
.doc-card:nth-child(1) .doc-link:hover {
color: var(--primary-color);
}
.doc-card:nth-child(2) .doc-link:hover {
color: var(--success-color);
}
.doc-card:nth-child(3) .doc-link:hover {
color: var(--warning-color);
}
.docs-cta {
text-align: center;
}
/* Roadmap Section */
.roadmap {
padding: 80px 0;
background: var(--bg-light);
}
.roadmap-timeline {
max-width: 800px;
margin: 0 auto;
position: relative;
}
.roadmap-item {
display: flex;
margin-bottom: 3rem;
position: relative;
}
.roadmap-item::before {
content: '';
position: absolute;
left: 20px;
top: 30px;
bottom: -30px;
width: 2px;
background: var(--primary-color);
}
.roadmap-item:last-child::before {
display: none;
}
.roadmap-marker {
width: 40px;
height: 40px;
background: var(--primary-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
margin-right: 2rem;
flex-shrink: 0;
}
.roadmap-content {
background: var(--bg-white);
padding: 1.5rem;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
flex-grow: 1;
}
.roadmap-content h4 {
color: var(--primary-color);
margin-bottom: 0.5rem;
}
/* Footer */
footer {
background: var(--text-dark);
color: white;
padding: 40px 0;
text-align: center;
}
.footer-links {
display: flex;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.footer-links a {
color: white;
text-decoration: none;
transition: color 0.3s;
}
.footer-links a:hover {
color: var(--accent-color);
}
/* Animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive */
@media (max-width: 768px) {
.nav-links {
display: none;
}
.hero h1 {
font-size: 2.5rem;
}
.features-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,2 @@
/* Tailwind CSS placeholder - replace with actual Tailwind build if needed */
/* For now, using custom CSS in main.css */

View File

View File

@@ -0,0 +1,53 @@
// Lightweight privacy-focused analytics for AITBC
// No cookies, no tracking, just basic page view metrics
(function() {
'use strict';
const script = document.currentScript;
const host = script.getAttribute('data-host') || window.location.origin;
const website = script.getAttribute('data-website') || 'default';
// Send page view on load
function sendPageView() {
const data = {
url: window.location.href,
pathname: window.location.pathname,
hostname: window.location.hostname,
referrer: document.referrer,
screenWidth: window.screen.width,
screenHeight: window.screen.height,
language: navigator.language,
website: website,
timestamp: new Date().toISOString()
};
// Use sendBeacon for reliable delivery
if (navigator.sendBeacon) {
navigator.sendBeacon(`${host}/api/analytics`, JSON.stringify(data));
} else {
// Fallback to fetch
fetch(`${host}/api/analytics`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
keepalive: true
}).catch(() => {});
}
}
// Send initial page view
if (document.readyState === 'complete') {
sendPageView();
} else {
window.addEventListener('load', sendPageView);
}
// Track route changes for SPA
let lastPath = window.location.pathname;
setInterval(() => {
if (window.location.pathname !== lastPath) {
lastPath = window.location.pathname;
sendPageView();
}
}, 1000);
})();

307
website/assets/js/main.js Normal file
View File

@@ -0,0 +1,307 @@
// Smooth scrolling for navigation links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
if (targetId && targetId !== '#') {
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth'
});
}
}
});
});
// Add animation on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
// Observe all feature cards
document.querySelectorAll('.feature-card, .arch-component').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
observer.observe(el);
});
// Dark mode functionality with enhanced persistence and system preference detection
function toggleDarkMode() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
}
function setTheme(theme) {
// Apply theme immediately
document.documentElement.setAttribute('data-theme', theme);
// Save to localStorage for persistence
localStorage.setItem('theme', theme);
// Update button display if it exists
updateThemeButton(theme);
// Send analytics event
if (window.analytics) {
window.analytics.track('theme_changed', { theme });
}
}
function updateThemeButton(theme) {
const emoji = document.getElementById('darkModeEmoji');
const text = document.getElementById('darkModeText');
if (emoji && text) {
if (theme === 'dark') {
emoji.textContent = '🌙';
text.textContent = 'Dark';
} else {
emoji.textContent = '☀️';
text.textContent = 'Light';
}
}
}
function getPreferredTheme() {
// 1. Check localStorage first (user preference)
const saved = localStorage.getItem('theme');
if (saved) {
return saved;
}
// 2. Check system preference
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
// 3. Default to dark (AITBC brand preference)
return 'dark';
}
function initializeTheme() {
const theme = getPreferredTheme();
setTheme(theme);
// Listen for system preference changes
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
// Only auto-switch if user hasn't manually set a preference
if (!localStorage.getItem('theme')) {
setTheme(e.matches ? 'dark' : 'light');
}
});
}
}
// Initialize theme immediately (before DOM loads)
initializeTheme();
// Touch gesture support for mobile navigation
class TouchNavigation {
constructor() {
this.touchStartX = 0;
this.touchStartY = 0;
this.touchEndX = 0;
this.touchEndY = 0;
this.minSwipeDistance = 50;
this.maxVerticalDistance = 100;
// Get all major sections for navigation
this.sections = ['hero', 'features', 'architecture', 'achievements', 'documentation'];
this.currentSectionIndex = 0;
this.bindEvents();
this.setupMobileOptimizations();
}
bindEvents() {
document.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: false });
document.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
document.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: false });
}
handleTouchStart(e) {
this.touchStartX = e.touches[0].clientX;
this.touchStartY = e.touches[0].clientY;
}
handleTouchMove(e) {
// Prevent scrolling when detecting horizontal swipes
const touchCurrentX = e.touches[0].clientX;
const touchCurrentY = e.touches[0].clientY;
const deltaX = Math.abs(touchCurrentX - this.touchStartX);
const deltaY = Math.abs(touchCurrentY - this.touchStartY);
// If horizontal movement is greater than vertical, prevent default scrolling
if (deltaX > deltaY && deltaX > 10) {
e.preventDefault();
}
}
handleTouchEnd(e) {
this.touchEndX = e.changedTouches[0].clientX;
this.touchEndY = e.changedTouches[0].clientY;
const deltaX = this.touchEndX - this.touchStartX;
const deltaY = Math.abs(this.touchEndY - this.touchStartY);
// Only process swipe if vertical movement is minimal
if (deltaY < this.maxVerticalDistance && Math.abs(deltaX) > this.minSwipeDistance) {
if (deltaX > 0) {
this.swipeRight();
} else {
this.swipeLeft();
}
}
}
swipeLeft() {
// Navigate to next section
const nextIndex = Math.min(this.currentSectionIndex + 1, this.sections.length - 1);
this.navigateToSection(nextIndex);
}
swipeRight() {
// Navigate to previous section
const prevIndex = Math.max(this.currentSectionIndex - 1, 0);
this.navigateToSection(prevIndex);
}
navigateToSection(index) {
const sectionId = this.sections[index];
const element = document.getElementById(sectionId);
if (element) {
this.currentSectionIndex = index;
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
// Update URL hash without triggering scroll
history.replaceState(null, null, `#${sectionId}`);
}
}
setupMobileOptimizations() {
// Add touch-friendly interactions
this.setupTouchButtons();
this.setupScrollOptimizations();
this.setupMobileMenu();
}
setupTouchButtons() {
// Make buttons more touch-friendly
const buttons = document.querySelectorAll('button, .cta-button, .nav-button');
buttons.forEach(button => {
button.addEventListener('touchstart', () => {
button.style.transform = 'scale(0.98)';
}, { passive: true });
button.addEventListener('touchend', () => {
button.style.transform = '';
}, { passive: true });
});
}
setupScrollOptimizations() {
// Improve momentum scrolling on iOS
if ('webkitOverflowScrolling' in document.body.style) {
document.body.style.webkitOverflowScrolling = 'touch';
}
// Add smooth scrolling for anchor links with touch feedback
document.querySelectorAll('a[href^="#"]').forEach(link => {
link.addEventListener('touchstart', () => {
link.style.opacity = '0.7';
}, { passive: true });
link.addEventListener('touchend', () => {
link.style.opacity = '';
}, { passive: true });
});
}
setupMobileMenu() {
// Create mobile menu toggle if nav is hidden on mobile
const nav = document.querySelector('nav');
if (nav && window.innerWidth < 768) {
this.createMobileMenu();
}
}
createMobileMenu() {
// Create hamburger menu for mobile
const header = document.querySelector('header');
if (!header) return;
const mobileMenuBtn = document.createElement('button');
mobileMenuBtn.className = 'mobile-menu-btn';
mobileMenuBtn.innerHTML = '☰';
mobileMenuBtn.setAttribute('aria-label', 'Toggle mobile menu');
const nav = document.querySelector('nav');
if (nav) {
nav.style.display = 'none';
mobileMenuBtn.addEventListener('click', () => {
const isOpen = nav.style.display !== 'none';
nav.style.display = isOpen ? 'none' : 'flex';
mobileMenuBtn.innerHTML = isOpen ? '☰' : '✕';
});
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!header.contains(e.target)) {
nav.style.display = 'none';
mobileMenuBtn.innerHTML = '☰';
}
});
header.appendChild(mobileMenuBtn);
}
}
updateCurrentSection() {
// Update current section based on scroll position
const scrollY = window.scrollY + window.innerHeight / 2;
this.sections.forEach((sectionId, index) => {
const element = document.getElementById(sectionId);
if (element) {
const rect = element.getBoundingClientRect();
const elementTop = rect.top + window.scrollY;
const elementBottom = elementTop + rect.height;
if (scrollY >= elementTop && scrollY < elementBottom) {
this.currentSectionIndex = index;
}
}
});
}
}
// Initialize touch navigation when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new TouchNavigation();
// Update current section on scroll
window.addEventListener('scroll', () => {
if (window.touchNav) {
window.touchNav.updateCurrentSection();
}
}, { passive: true });
});

69
website/assets/js/sw.js Normal file
View File

@@ -0,0 +1,69 @@
const CACHE_NAME = 'aitbc-v1';
const urlsToCache = [
'/',
'/index.html',
'/assets/css/main.css',
'/assets/css/font-awesome.min.css',
'/assets/js/main.js',
'/favicon.ico',
'/explorer/',
'/marketplace/',
'/docs/'
];
// Install event - cache assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Caching assets...');
return cache.addAll(urlsToCache);
})
.catch(err => console.log('Cache failed:', err))
);
self.skipWaiting();
});
// Fetch event - serve from cache or network
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Return cached version or fetch from network
if (response) {
return response;
}
return fetch(event.request)
.then(networkResponse => {
// Cache successful network responses
if (networkResponse && networkResponse.status === 200) {
const clonedResponse = networkResponse.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, clonedResponse);
});
}
return networkResponse;
})
.catch(() => {
// Return offline fallback for HTML requests
if (event.request.mode === 'navigate') {
return caches.match('/index.html');
}
});
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
self.clients.claim();
});

View File

@@ -0,0 +1,158 @@
// Web Vitals monitoring for AITBC
// Tracks Core Web Vitals: LCP, FID, CLS, TTFB, FCP
(function() {
'use strict';
function sendToAnalytics(metric) {
const data = {
name: metric.name,
value: Math.round(metric.value),
id: metric.id,
delta: Math.round(metric.delta),
entries: metric.entries.map(e => ({
name: e.name,
startTime: e.startTime,
duration: e.duration
})),
url: window.location.href,
timestamp: new Date().toISOString()
};
// Send to analytics endpoint
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/web-vitals', JSON.stringify(data));
}
// Also log to console in development
console.log(`[Web Vitals] ${metric.name}: ${metric.value}`, metric);
}
// Largest Contentful Paint (LCP)
function observeLCP() {
try {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
sendToAnalytics({
name: 'LCP',
value: lastEntry.renderTime || lastEntry.loadTime,
id: lastEntry.id,
delta: 0,
entries: entries
});
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
} catch (e) {}
}
// First Input Delay (FID)
function observeFID() {
try {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
sendToAnalytics({
name: 'FID',
value: entry.processingStart - entry.startTime,
id: entry.id,
delta: 0,
entries: [entry]
});
});
});
observer.observe({ entryTypes: ['first-input'] });
} catch (e) {}
}
// Cumulative Layout Shift (CLS)
function observeCLS() {
try {
let clsValue = 0;
const observer = new PerformanceObserver((list) => {
try {
const entries = list.getEntries();
entries.forEach(entry => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
sendToAnalytics({
name: 'CLS',
value: clsValue,
id: 'cls',
delta: 0,
entries: entries
});
} catch (e) {
// CLS measurement failed, but don't crash the whole script
console.log('CLS measurement error:', e.message);
}
});
// Try to observe layout-shift, but handle if it's not supported
try {
observer.observe({ entryTypes: ['layout-shift'] });
} catch (e) {
console.log('Layout-shift observation not supported:', e.message);
// CLS will not be measured, but other metrics will still work
}
} catch (e) {
console.log('CLS observer setup failed:', e.message);
}
}
// Time to First Byte (TTFB)
function measureTTFB() {
try {
const nav = performance.getEntriesByType('navigation')[0];
if (nav) {
sendToAnalytics({
name: 'TTFB',
value: nav.responseStart,
id: 'ttfb',
delta: 0,
entries: [nav]
});
}
} catch (e) {}
}
// First Contentful Paint (FCP)
function observeFCP() {
try {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
if (entry.name === 'first-contentful-paint') {
sendToAnalytics({
name: 'FCP',
value: entry.startTime,
id: entry.id,
delta: 0,
entries: [entry]
});
}
});
});
observer.observe({ entryTypes: ['paint'] });
} catch (e) {}
}
// Initialize when page loads
if (document.readyState === 'complete') {
observeLCP();
observeFID();
observeCLS();
observeFCP();
measureTTFB();
} else {
window.addEventListener('load', () => {
observeLCP();
observeFID();
observeCLS();
observeFCP();
measureTTFB();
});
}
})();

View File

@@ -388,7 +388,7 @@ curl -X GET https://aitbc.bubuit.net/api/v1/jobs/JOB_ID \
<ul>
<li><strong>Documentation</strong>: <a href="full-documentation.html">Full API reference</a></li>
<li><strong>Community</strong>: <a href="https://discord.gg/aitbc">Join our Discord</a></li>
<li><strong>Email</strong>: <a href="mailto:aitbc@bubuit.net">aitbc@bubuit.net</a></li>
<li><strong>Email</strong>: <a href="mailto:andreas.fleckl@bubuit.net">andreas.fleckl@bubuit.net</a></li>
<li><strong>Status</strong>: <a href="https://status.aitbc.bubuit.net">System status</a></li>
</ul>
@@ -415,7 +415,7 @@ curl -X GET https://aitbc.bubuit.net/api/v1/jobs/JOB_ID \
<h2>Frequently Asked Questions</h2>
<div class="alert alert-info">
<strong>Question not answered?</strong> Contact us at <a href="mailto:aitbc@bubuit.net">aitbc@bubuit.net</a>
<strong>Question not answered?</strong> Contact us at <a href="mailto:andreas.fleckl@bubuit.net">andreas.fleckl@bubuit.net</a>
</div>
<h3>General</h3>

View File

@@ -585,7 +585,7 @@ gosec ./...</code></pre>
<li>Medium: $1,000 - $10,000</li>
<li>Low: $100 - $1,000</li>
</ul>
<p>Report vulnerabilities at: <a href="mailto:aitbc@bubuit.net">aitbc@bubuit.net</a></p>
<p>Report vulnerabilities at: <a href="mailto:andreas.fleckl@bubuit.net">andreas.fleckl@bubuit.net</a></p>
</section>
<section id="reference">
@@ -641,7 +641,7 @@ gosec ./...</code></pre>
<li>Documentation: <a href="full-documentation.html">Full Documentation</a></li>
<li>Community: <a href="https://discord.gg/aitbc">Discord</a></li>
<li>Issues: <a href="https://github.com/oib/AITBC/issues">GitHub Issues</a></li>
<li>Email: <a href="mailto:aitbc@bubuit.net">aitbc@bubuit.net</a></li>
<li>Email: <a href="mailto:andreas.fleckl@bubuit.net">andreas.fleckl@bubuit.net</a></li>
</ul>
</section>
</div>

View File

@@ -3,9 +3,12 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/docs.css">
<link rel="stylesheet" href="../assets/css/docs.css">
<link rel="preload" href="../assets/css/font-awesome.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="../assets/css/font-awesome.min.css"></noscript>
<!-- Font Awesome CDN fallback -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" media="print" onload="this.media='all'; this.onload=null;">
<title>Documentation - AITBC</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<!-- Header -->
@@ -172,11 +175,7 @@
<i class="fas fa-git"></i>
<span>Source Code</span>
</a>
<a href="https://discord.gg/aitbc" class="link-item">
<i class="fab fa-discord"></i>
<span>Community</span>
</a>
<a href="mailto:aitbc@bubuit.net" class="link-item">
<a href="mailto:andreas.fleckl@bubuit.net" class="link-item">
<i class="fas fa-envelope"></i>
<span>Support</span>
</a>
@@ -186,17 +185,11 @@
<!-- Help Section -->
<section class="help-section">
<h2>Need Help?</h2>
<p>Can't find what you're looking for? Our community is here to help!</p>
<p>Can't find what you're looking for? Our support team is here to help!</p>
<div class="help-buttons">
<a href="https://discord.gg/aitbc" class="btn">
<i class="fab fa-discord"></i> Join Discord
</a>
<a href="mailto:aitbc@bubuit.net" class="btn btn-outline">
<a href="mailto:andreas.fleckl@bubuit.net" class="btn btn-outline">
<i class="fas fa-envelope"></i> Email Support
</a>
<a href="https://discord.gg/aitbc" class="btn btn-outline">
<i class="fas fa-comments"></i> Live Chat
</a>
</div>
</section>
</div>

View File

@@ -260,7 +260,7 @@ docker run -d \
<ul>
<li>Check the <a href="full-documentation.html">issue tracker</a></li>
<li>Join our <a href="https://discord.gg/aitbc">Discord</a></li>
<li>Contact: <a href="mailto:aitbc@bubuit.net">aitbc@bubuit.net</a></li>
<li>Contact: <a href="mailto:andreas.fleckl@bubuit.net">andreas.fleckl@bubuit.net</a></li>
</ul>
</section>
</div>

View File

@@ -315,7 +315,7 @@ nano ~/.aitbc/miner.toml</div>
<li>Check the logs: <code>./aitbc-miner logs</code></li>
<li>Visit our Discord community</li>
<li>Search issues on Gitea</li>
<li>Email support: aitbc@bubuit.net</li>
<li>Email support: andreas.fleckl@bubuit.net</li>
</ul>
</section>

View File

@@ -260,7 +260,7 @@ BITCOIN_RPC_PASS=password</code></pre>
<h3>Contact</h3>
<ul>
<li>Email: <a href="mailto:aitbc@bubuit.net">aitbc@bubuit.net</a></li>
<li>Email: <a href="mailto:andreas.fleckl@bubuit.net">andreas.fleckl@bubuit.net</a></li>
<li>Discord: <a href="https://discord.gg/aitbc" target="_blank">discord.gg/aitbc</a></li>
</ul>
</section>

View File

@@ -8,526 +8,23 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<link rel="stylesheet" href="/assets/css/tailwind.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #2563eb;
--secondary-color: #1e40af;
--accent-color: #3b82f6;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--text-dark: #1f2937;
--text-light: #6b7280;
--bg-light: #f9fafb;
--bg-white: #ffffff;
--gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
[data-theme="dark"] {
--primary-color: #3b82f6;
--secondary-color: #2563eb;
--accent-color: #60a5fa;
--text-dark: #f9fafb;
--text-light: #d1d5db;
--bg-light: #111827;
--bg-white: #1f2937;
--gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: var(--text-dark);
background-color: var(--bg-light);
transition: background-color 0.3s ease, color 0.3s ease;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
header {
background: var(--bg-white);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
position: fixed;
width: 100%;
top: 0;
z-index: 1000;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: var(--primary-color);
text-decoration: none;
}
.nav-links {
display: flex;
gap: 2rem;
list-style: none;
}
.nav-links a {
color: var(--text-dark);
text-decoration: none;
transition: color 0.3s;
}
.nav-links a:hover {
color: var(--primary-color);
}
.dark-mode-toggle {
background: none;
border: 1px solid var(--text-light);
color: var(--text-dark);
cursor: pointer;
font-size: 1.2rem;
transition: color 0.3s, border-color 0.3s;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.dark-mode-toggle:hover {
color: var(--primary-color);
background: rgba(59, 130, 246, 0.1);
}
/* Hero Section */
.hero {
background: var(--gradient);
color: white;
padding: 100px 0 80px;
text-align: center;
}
.hero h1 {
font-size: 3.5rem;
margin-bottom: 1rem;
animation: fadeInUp 0.8s ease;
}
.hero p {
font-size: 1.25rem;
margin-bottom: 2rem;
opacity: 0.9;
animation: fadeInUp 0.8s ease 0.2s both;
}
.cta-button {
display: inline-block;
padding: 12px 30px;
background: var(--bg-white);
color: var(--primary-color);
text-decoration: none;
border-radius: 5px;
font-weight: 600;
transition: transform 0.3s, box-shadow 0.3s;
animation: fadeInUp 0.8s ease 0.4s both;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
/* Features Section */
.features {
padding: 80px 0;
background: var(--bg-light);
}
.section-title {
text-align: center;
font-size: 2.5rem;
margin-bottom: 3rem;
color: var(--text-dark);
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
.feature-card {
background: var(--bg-white);
padding: 2rem;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
transition: transform 0.3s;
}
.feature-card:hover {
transform: translateY(-5px);
}
.feature-icon {
font-size: 3rem;
color: var(--primary-color);
margin-bottom: 1rem;
}
.feature-card h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--text-dark);
}
.feature-card p {
color: var(--text-light);
line-height: 1.8;
margin-bottom: 1.5rem;
flex-grow: 1;
}
.feature-link {
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s;
}
.feature-link:hover {
color: var(--secondary-color);
transform: translateX(5px);
}
/* Architecture Section */
.architecture {
padding: 80px 0;
background: var(--bg-white);
}
/* Header styles matching Exchange */
.gradient-bg {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
}
.nav-button {
background: transparent !important;
color: white !important;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.nav-button:hover {
background: rgba(59, 130, 246, 0.1) !important;
color: var(--primary-color) !important;
}
.nav-button:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
}
.architecture-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin-top: 3rem;
}
.arch-component {
text-align: center;
padding: 1.5rem;
border: 2px solid var(--text-light);
border-radius: 10px;
background: var(--bg-white);
transition: border-color 0.3s;
}
.arch-component:hover {
border-color: var(--primary-color);
}
.arch-component i {
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 1rem;
}
/* Stats Section */
.stats {
padding: 60px 0;
background: var(--gradient);
color: white;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
text-align: center;
}
.stat-item h3 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.stat-item p {
font-size: 1.1rem;
opacity: 0.9;
}
/* Documentation Section */
.documentation {
padding: 80px 0;
background: var(--bg-light);
}
.section-subtitle {
text-align: center;
font-size: 1.2rem;
color: var(--text-light);
margin-bottom: 3rem;
}
.docs-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
.doc-card {
background: var(--bg-white);
padding: 2.5rem;
border-radius: 15px;
text-align: center;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.doc-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
}
.doc-card:nth-child(1)::before {
background: var(--primary-color);
}
.doc-card:nth-child(2)::before {
background: var(--success-color);
}
.doc-card:nth-child(3)::before {
background: var(--warning-color);
}
.doc-card:hover {
transform: translateY(-10px);
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
}
.doc-icon {
width: 70px;
height: 70px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
color: white;
margin: 0 auto 1.5rem;
}
.doc-card:nth-child(1) .doc-icon {
background: var(--primary-color);
}
.doc-card:nth-child(2) .doc-icon {
background: var(--success-color);
}
.doc-card:nth-child(3) .doc-icon {
background: var(--warning-color);
}
.doc-card h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--text-dark);
}
.doc-card p {
color: var(--text-light);
margin-bottom: 2rem;
}
.doc-link {
gap: 0.5rem;
color: var(--text-dark);
text-decoration: none;
font-weight: 600;
transition: all 0.3s;
}
.doc-card:nth-child(1) .doc-link:hover {
color: var(--primary-color);
}
.doc-card:nth-child(2) .doc-link:hover {
color: var(--success-color);
}
.doc-card:nth-child(3) .doc-link:hover {
color: var(--warning-color);
}
.docs-cta {
text-align: center;
}
/* Roadmap Section */
.roadmap {
padding: 80px 0;
background: var(--bg-light);
}
.roadmap-timeline {
max-width: 800px;
margin: 0 auto;
position: relative;
}
.roadmap-item {
display: flex;
margin-bottom: 3rem;
position: relative;
}
.roadmap-item::before {
content: '';
position: absolute;
left: 20px;
top: 30px;
bottom: -30px;
width: 2px;
background: var(--primary-color);
}
.roadmap-item:last-child::before {
display: none;
}
.roadmap-marker {
width: 40px;
height: 40px;
background: var(--primary-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
margin-right: 2rem;
flex-shrink: 0;
}
.roadmap-content {
background: var(--bg-white);
padding: 1.5rem;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
flex-grow: 1;
}
.roadmap-content h4 {
color: var(--primary-color);
margin-bottom: 0.5rem;
}
/* Footer */
footer {
background: var(--text-dark);
color: white;
padding: 40px 0;
text-align: center;
}
.footer-links {
display: flex;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.footer-links a {
color: white;
text-decoration: none;
transition: color 0.3s;
}
.footer-links a:hover {
color: var(--accent-color);
}
/* Animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive */
@media (max-width: 768px) {
.nav-links {
display: none;
}
.hero h1 {
font-size: 2.5rem;
}
.features-grid {
grid-template-columns: 1fr;
}
}
<link rel="preconnect" href="https://aitbc.bubuit.net">
<link rel="dns-prefetch" href="//aitbc.bubuit.net">
<meta name="theme-color" content="#2563eb">
<meta property="og:title" content="AITBC - Production-Ready AI Blockchain Platform">
<meta property="og:description" content="Production-ready AI blockchain platform with 7 live components, 30+ GPU services">
<meta property="og:type" content="website">
<meta property="og:url" content="https://aitbc.bubuit.net">
<meta name="twitter:card" content="summary_large_image">
<style>
/* Critical CSS for above-the-fold content */
:root{--primary-color:#2563eb;--secondary-color:#1e40af;--accent-color:#3b82f6;--text-dark:#1f2937;--bg-white:#ffffff;--gradient:linear-gradient(135deg,#667eea 0%,#764ba2 100%)}[data-theme="dark"]{--primary-color:#3b82f6;--secondary-color:#2563eb;--accent-color:#60a5fa;--text-dark:#f9fafb;--bg-white:#1f2937;--gradient:linear-gradient(135deg,#667eea 0%,#764ba2 100%)}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;line-height:1.6;color:var(--text-dark);background-color:var(--bg-light);transition:background-color .3s ease,color .3s ease}.container{max-width:1200px;margin:0 auto;padding:0 20px}header{background:var(--bg-white);box-shadow:0 1px 3px rgba(0,0,0,.1);position:fixed;width:100%;top:0;z-index:1000}nav{display:flex;justify-content:space-between;align-items:center;padding:1rem 0}.logo{font-size:1.5rem;font-weight:700;color:var(--primary-color);text-decoration:none}.nav-button{background:transparent!important;color:var(--text-dark)!important;padding:.5rem .75rem;border-radius:.5rem;font-weight:500;transition:all .2s ease;text-decoration:none;display:inline-flex;align-items:center}.nav-button:hover{background:rgba(59,130,246,.1)!important;color:var(--primary-color)!important}.hero{background:var(--gradient);color:#fff;padding:100px 0 80px;text-align:center}.hero h1{font-size:3.5rem;margin-bottom:1rem;animation:fadeInUp .8s ease}.hero p{font-size:1.25rem;margin-bottom:2rem;opacity:.9;animation:fadeInUp .8s ease .2s both}.cta-button{display:inline-block;padding:12px 30px;background:var(--bg-white);color:var(--primary-color);text-decoration:none;border-radius:5px;font-weight:600;transition:transform .3s,box-shadow .3s;animation:fadeInUp .8s ease .4s both}.cta-button:hover{transform:translateY(-2px);box-shadow:0 10px 20px rgba(0,0,0,.1)}@keyframes fadeInUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@media (max-width:768px){.hero h1{font-size:2.5rem}}
</style>
<link rel="preload" href="/assets/css/font-awesome.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" href="/assets/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/assets/css/font-awesome.min.css"><link rel="stylesheet" href="/assets/css/main.css"></noscript>
<!-- Font Awesome CDN fallback -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" media="print" onload="this.media='all'; this.onload=null;">
</head>
<body>
<!-- Header -->
@@ -539,8 +36,10 @@
<h1 class="text-2xl font-bold" style="color: var(--text-dark);">AITBC</h1>
</div>
<nav class="flex items-center space-x-6">
<a href="/explorer/" class="nav-button" style="background: transparent !important; color: var(--text-dark) !important;">Explorer</a>
<a href="/marketplace/" class="nav-button" style="background: transparent !important; color: var(--text-dark) !important;">Marketplace</a>
<a href="/Exchange/" class="nav-button" style="background: transparent !important; color: var(--text-dark) !important;">Exchange</a>
<a href="docs/index.html" class="nav-button" style="background: transparent !important; color: var(--text-dark) !important;">Documentation</a>
<a href="docs/index.html" class="nav-button" style="background: transparent !important; color: var(--text-dark) !important;">Docs</a>
<button onclick="toggleDarkMode()" class="nav-button" title="Toggle dark mode" style="background: rgba(59, 130, 246, 0.1) !important; color: var(--primary-color) !important; border: 2px solid var(--primary-color); padding: 0.5rem 1rem;">
<span id="darkModeEmoji">🌙</span>
<span id="darkModeText" style="margin-left: 0.5rem;">Dark</span>
@@ -551,7 +50,7 @@
</header>
<!-- Hero Section -->
<section class="hero">
<section class="hero" id="hero">
<div class="container">
<h1>Production-Ready AI Blockchain Platform</h1>
<p>7 Live Components • 30+ GPU Services • Stage 11 Complete</p>
@@ -784,93 +283,45 @@
<footer>
<div class="container">
<ul class="nav-links">
<li><a href="/Exchange/">Marketplace</a></li>
<li><a href="/explorer/">Explorer</a></li>
<li><a href="/marketplace/">Marketplace</a></li>
<li><a href="/Exchange/">Exchange</a></li>
<li><a href="docs/index.html">Documentation</a></li>
<li><a href="https://discord.gg/aitbc">Discord</a></li>
<li><a href="mailto:aitbc@bubuit.net">Contact</a></li>
<li><a href="mailto:andreas.fleckl@bubuit.net">Contact</a></li>
</ul>
<p>&copy; 2025 AITBC. All rights reserved.</p>
</div>
</footer>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "AITBC",
"description": "Production-ready AI blockchain platform with 7 live components and 30+ GPU services",
"url": "https://aitbc.bubuit.net",
"logo": "https://aitbc.bubuit.net/favicon.ico",
"sameAs": [
"https://github.com/aitbc"
],
"contactPoint": {
"@type": "ContactPoint",
"email": "andreas.fleckl@bubuit.net",
"contactType": "General Inquiries"
}
}
</script>
<script>
// Smooth scrolling for navigation links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
if (targetId && targetId !== '#') {
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth'
});
}
}
});
});
// Add animation on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
// Observe all feature cards
document.querySelectorAll('.feature-card, .arch-component').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
observer.observe(el);
});
// Dark mode functionality
function toggleDarkMode() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
// Update button display
const emoji = document.getElementById('darkModeEmoji');
const text = document.getElementById('darkModeText');
if (newTheme === 'dark') {
emoji.textContent = '🌙';
text.textContent = 'Dark';
} else {
emoji.textContent = '☀️';
text.textContent = 'Light';
}
// Register service worker for offline support
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/assets/js/sw.js')
.then(registration => console.log('SW registered'))
.catch(err => console.log('SW registration failed'));
}
// Check for saved theme preference - default to dark
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
// Set initial display
document.addEventListener('DOMContentLoaded', () => {
const emoji = document.getElementById('darkModeEmoji');
const text = document.getElementById('darkModeText');
if (savedTheme === 'dark') {
emoji.textContent = '🌙';
text.textContent = 'Dark';
} else {
emoji.textContent = '☀️';
text.textContent = 'Light';
}
});
</script>
<script src="/assets/js/main.js" async></script>
<script src="/assets/js/web-vitals.js" async></script>
<!-- Lightweight Analytics - privacy focused, no cookies -->
<script data-host="https://ackee.bubuit.net" data-ackee-server="https://ackee.bubuit.net" src="/assets/js/analytics.js" async data-auto-opts='{"website": "aitbc-main", "name": "AITBC Main Website"}'></script>
</body>
</html>