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:
@@ -18,35 +18,12 @@ class DatabaseConfig(BaseSettings):
|
||||
max_overflow: int = 20
|
||||
pool_pre_ping: bool = True
|
||||
|
||||
@property
|
||||
def effective_url(self) -> str:
|
||||
"""Get the effective database URL."""
|
||||
if self.url:
|
||||
return self.url
|
||||
# Auto-generate SQLite URL based on environment
|
||||
if self.adapter == "sqlite":
|
||||
project_root = self._find_project_root()
|
||||
db_path = project_root / "data" / "coordinator.db"
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return f"sqlite:///{db_path}"
|
||||
elif self.adapter == "postgresql":
|
||||
return "postgresql://localhost:5432/aitbc_coordinator"
|
||||
return "sqlite:///:memory:"
|
||||
|
||||
@staticmethod
|
||||
def _find_project_root() -> Path:
|
||||
"""Find project root by looking for .git directory."""
|
||||
current = Path(__file__).resolve()
|
||||
while current.parent != current:
|
||||
if (current / ".git").exists():
|
||||
return current
|
||||
current = current.parent
|
||||
return Path(__file__).resolve().parents[3]
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
case_sensitive = False
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
extra="allow"
|
||||
)
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
@@ -116,7 +93,10 @@ class Settings(BaseSettings):
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""Get the database URL (backward compatibility)."""
|
||||
return self.database.effective_url
|
||||
if self.database.url:
|
||||
return self.database.url
|
||||
# Default SQLite path for backward compatibility
|
||||
return f"sqlite:///./aitbc_coordinator.db"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -19,6 +19,7 @@ from .routers import (
|
||||
zk_applications,
|
||||
explorer,
|
||||
payments,
|
||||
web_vitals,
|
||||
)
|
||||
from .routers.governance import router as governance
|
||||
from .routers.partners import router as partners
|
||||
@@ -75,6 +76,7 @@ def create_app() -> FastAPI:
|
||||
app.include_router(governance, prefix="/v1")
|
||||
app.include_router(partners, prefix="/v1")
|
||||
app.include_router(explorer, prefix="/v1")
|
||||
app.include_router(web_vitals, prefix="/v1")
|
||||
|
||||
# Add Prometheus metrics endpoint
|
||||
metrics_app = make_asgi_app()
|
||||
|
||||
@@ -11,6 +11,7 @@ from .users import router as users
|
||||
from .exchange import router as exchange
|
||||
from .marketplace_offers import router as marketplace_offers
|
||||
from .payments import router as payments
|
||||
from .web_vitals import router as web_vitals
|
||||
# from .registry import router as registry
|
||||
|
||||
__all__ = ["client", "miner", "admin", "marketplace", "marketplace_gpu", "explorer", "services", "users", "exchange", "marketplace_offers", "payments", "registry"]
|
||||
__all__ = ["client", "miner", "admin", "marketplace", "marketplace_gpu", "explorer", "services", "users", "exchange", "marketplace_offers", "payments", "web_vitals", "registry"]
|
||||
|
||||
53
apps/coordinator-api/src/app/routers/web_vitals.py
Normal file
53
apps/coordinator-api/src/app/routers/web_vitals.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Web Vitals API endpoint for collecting performance metrics
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Any, Optional
|
||||
import logging
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class WebVitalsEntry(BaseModel):
|
||||
name: str
|
||||
startTime: Optional[float] = None
|
||||
duration: Optional[float] = None
|
||||
|
||||
class WebVitalsMetric(BaseModel):
|
||||
name: str
|
||||
value: float
|
||||
id: str
|
||||
delta: Optional[float] = None
|
||||
entries: List[WebVitalsEntry] = []
|
||||
url: Optional[str] = None
|
||||
timestamp: Optional[str] = None
|
||||
|
||||
@router.post("/web-vitals")
|
||||
async def collect_web_vitals(metric: WebVitalsMetric):
|
||||
"""
|
||||
Collect Web Vitals performance metrics from the frontend.
|
||||
This endpoint receives Core Web Vitals (LCP, FID, CLS, TTFB, FCP) for monitoring.
|
||||
"""
|
||||
try:
|
||||
# Log the metric for monitoring/analysis
|
||||
logging.info(f"Web Vitals - {metric.name}: {metric.value}ms (ID: {metric.id}) from {metric.url or 'unknown'}")
|
||||
|
||||
# In a production setup, you might:
|
||||
# - Store in database for trend analysis
|
||||
# - Send to monitoring service (DataDog, New Relic, etc.)
|
||||
# - Trigger alerts for poor performance
|
||||
|
||||
# For now, just acknowledge receipt
|
||||
return {"status": "received", "metric": metric.name, "value": metric.value}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing web vitals metric: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to process metric")
|
||||
|
||||
# Health check for web vitals endpoint
|
||||
@router.get("/web-vitals/health")
|
||||
async def web_vitals_health():
|
||||
"""Health check for web vitals collection endpoint"""
|
||||
return {"status": "healthy", "service": "web-vitals"}
|
||||
@@ -70,7 +70,8 @@ class ExplorerService:
|
||||
items: list[TransactionSummary] = []
|
||||
for index, job in enumerate(jobs):
|
||||
height = _DEFAULT_HEIGHT_BASE + offset + index
|
||||
status_label = _STATUS_LABELS.get(job.state, job.state.value.title())
|
||||
state_val = job.state.value if hasattr(job.state, "value") else job.state
|
||||
status_label = _STATUS_LABELS.get(job.state) or state_val.title()
|
||||
|
||||
# Try to get payment amount from receipt
|
||||
value_str = "0"
|
||||
@@ -118,14 +119,26 @@ class ExplorerService:
|
||||
}
|
||||
)
|
||||
|
||||
def touch(address: Optional[str], tx_id: str, when: datetime, earned: float = 0.0, spent: float = 0.0) -> None:
|
||||
def _ensure_dt(val: object) -> datetime:
|
||||
if isinstance(val, datetime):
|
||||
return val.replace(tzinfo=None)
|
||||
if isinstance(val, str):
|
||||
try:
|
||||
dt = datetime.fromisoformat(val.replace("Z", "+00:00"))
|
||||
return dt.replace(tzinfo=None)
|
||||
except ValueError:
|
||||
return datetime.min
|
||||
return datetime.min
|
||||
|
||||
def touch(address: Optional[str], tx_id: str, when: object, earned: float = 0.0, spent: float = 0.0) -> None:
|
||||
if not address:
|
||||
return
|
||||
entry = address_map[address]
|
||||
entry["address"] = address
|
||||
entry["tx_count"] = int(entry["tx_count"]) + 1
|
||||
if when > entry["last_active"]:
|
||||
entry["last_active"] = when
|
||||
when_dt = _ensure_dt(when)
|
||||
if when_dt > _ensure_dt(entry["last_active"]):
|
||||
entry["last_active"] = when_dt
|
||||
# Track earnings and spending
|
||||
entry["earned"] = float(entry["earned"]) + earned
|
||||
entry["spent"] = float(entry["spent"]) + spent
|
||||
|
||||
@@ -95,6 +95,20 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.site-header__back {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(125, 196, 255, 0.3);
|
||||
transition: background 150ms ease, border-color 150ms ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.site-header__back:hover {
|
||||
background: rgba(125, 196, 255, 0.15);
|
||||
border-color: rgba(125, 196, 255, 0.5);
|
||||
}
|
||||
|
||||
.site-header__brand {
|
||||
font-weight: 600;
|
||||
font-size: 1.15rem;
|
||||
|
||||
@@ -4,6 +4,7 @@ export function siteHeader(title: string): string {
|
||||
return `
|
||||
<header class="site-header">
|
||||
<div class="site-header__inner">
|
||||
<a class="site-header__back" href="/" title="Back to AITBC Home">← Home</a>
|
||||
<a class="site-header__brand" href="${basePath}/">AITBC Explorer</a>
|
||||
<div class="site-header__controls">
|
||||
<div data-role="data-mode-toggle"></div>
|
||||
@@ -14,6 +15,7 @@ export function siteHeader(title: string): string {
|
||||
<a href="${basePath}/transactions">Transactions</a>
|
||||
<a href="${basePath}/addresses">Addresses</a>
|
||||
<a href="${basePath}/receipts">Receipts</a>
|
||||
<a href="/marketplace/">Marketplace</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
// Type declarations for global objects
|
||||
declare global {
|
||||
interface Window {
|
||||
analytics?: {
|
||||
track: (event: string, data: any) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
import './style.css';
|
||||
import {
|
||||
fetchMarketplaceOffers,
|
||||
fetchMarketplaceStats,
|
||||
submitMarketplaceBid,
|
||||
MARKETPLACE_CONFIG,
|
||||
} from './lib/api';
|
||||
import type { MarketplaceOffer, MarketplaceStats } from './lib/api';
|
||||
|
||||
@@ -16,9 +24,21 @@ if (!app) {
|
||||
app.innerHTML = `
|
||||
<main>
|
||||
<header class="page-header">
|
||||
<p>Data mode: <strong>${MARKETPLACE_CONFIG.dataMode.toUpperCase()}</strong></p>
|
||||
<nav class="page-header__nav">
|
||||
<a href="/" class="back-link">← Home</a>
|
||||
<a href="/explorer/">Explorer</a>
|
||||
<a href="/Exchange/">Exchange</a>
|
||||
</nav>
|
||||
<div class="page-header-content">
|
||||
<div class="page-header-title">
|
||||
<h1>Marketplace Control Center</h1>
|
||||
<p>Monitor available offers, submit bids, and review marketplace health at a glance.</p>
|
||||
</div>
|
||||
<button onclick="toggleDarkMode()" class="dark-mode-toggle" title="Toggle dark mode">
|
||||
<span id="darkModeEmoji">🌙</span>
|
||||
<span id="darkModeText">Dark</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="dashboard-grid" id="stats-panel">
|
||||
@@ -80,13 +100,13 @@ app.innerHTML = `
|
||||
`;
|
||||
|
||||
const selectors = {
|
||||
totalOffers: document.querySelector<HTMLSpanElement>('#stat-total-offers'),
|
||||
openCapacity: document.querySelector<HTMLSpanElement>('#stat-open-capacity'),
|
||||
averagePrice: document.querySelector<HTMLSpanElement>('#stat-average-price'),
|
||||
activeBids: document.querySelector<HTMLSpanElement>('#stat-active-bids'),
|
||||
offersWrapper: document.querySelector<HTMLDivElement>('#offers-table-wrapper'),
|
||||
bidForm: document.querySelector<HTMLFormElement>('#bid-form'),
|
||||
toast: document.querySelector<HTMLDivElement>('#toast'),
|
||||
totalOffers: document.querySelector<HTMLSpanElement>('#stat-total-offers')!,
|
||||
openCapacity: document.querySelector<HTMLSpanElement>('#stat-open-capacity')!,
|
||||
averagePrice: document.querySelector<HTMLSpanElement>('#stat-average-price')!,
|
||||
activeBids: document.querySelector<HTMLSpanElement>('#stat-active-bids')!,
|
||||
offersWrapper: document.querySelector<HTMLDivElement>('#offers-table-wrapper')!,
|
||||
bidForm: document.querySelector<HTMLFormElement>('#bid-form')!,
|
||||
toast: document.querySelector<HTMLDivElement>('#toast')!,
|
||||
};
|
||||
|
||||
function formatNumber(value: number, options: Intl.NumberFormatOptions = {}): string {
|
||||
@@ -94,13 +114,13 @@ function formatNumber(value: number, options: Intl.NumberFormatOptions = {}): st
|
||||
}
|
||||
|
||||
function renderStats(stats: MarketplaceStats): void {
|
||||
selectors.totalOffers!.textContent = formatNumber(stats.totalOffers);
|
||||
selectors.openCapacity!.textContent = `${formatNumber(stats.openCapacity)} units`;
|
||||
selectors.averagePrice!.textContent = `${formatNumber(stats.averagePrice, {
|
||||
selectors.totalOffers.textContent = formatNumber(stats.totalOffers);
|
||||
selectors.openCapacity.textContent = `${formatNumber(stats.openCapacity)} units`;
|
||||
selectors.averagePrice.textContent = `${formatNumber(stats.averagePrice, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})} credits`;
|
||||
selectors.activeBids!.textContent = formatNumber(stats.activeBids);
|
||||
selectors.activeBids.textContent = formatNumber(stats.activeBids);
|
||||
}
|
||||
|
||||
function statusClass(status: string): string {
|
||||
@@ -115,12 +135,11 @@ function statusClass(status: string): string {
|
||||
}
|
||||
|
||||
function renderOffers(offers: MarketplaceOffer[]): void {
|
||||
if (!selectors.offersWrapper) {
|
||||
return;
|
||||
}
|
||||
const wrapper = selectors.offersWrapper;
|
||||
if (!wrapper) return;
|
||||
|
||||
if (offers.length === 0) {
|
||||
selectors.offersWrapper.innerHTML = '<p class="empty-state">No offers available right now. Check back soon or submit a bid.</p>';
|
||||
wrapper.innerHTML = '<p class="empty-state">No offers available right now. Check back soon or submit a bid.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,7 +176,7 @@ function renderOffers(offers: MarketplaceOffer[]): void {
|
||||
</div>
|
||||
<div class="offer-models">
|
||||
<span class="models-label">Available Models</span>
|
||||
<div class="model-tags">${offer.attributes.models.map(m => `<span class="model-tag">${m}</span>`).join('')}</div>
|
||||
<div class="model-tags">${offer.attributes.models.map((m: string) => `<span class="model-tag">${m}</span>`).join('')}</div>
|
||||
</div>` : ''}
|
||||
<div class="offer-pricing">
|
||||
<div class="offer-price">${formatNumber(offer.price_per_hour ?? offer.price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <small>credits/hr</small></div>
|
||||
@@ -168,7 +187,7 @@ function renderOffers(offers: MarketplaceOffer[]): void {
|
||||
)
|
||||
.join('');
|
||||
|
||||
selectors.offersWrapper.innerHTML = `<div class="offers-grid">${cards}</div>`;
|
||||
wrapper.innerHTML = `<div class="offers-grid">${cards}</div>`;
|
||||
}
|
||||
|
||||
function showToast(message: string, duration = 2500): void {
|
||||
@@ -192,8 +211,9 @@ async function loadDashboard(): Promise<void> {
|
||||
renderOffers(offers);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (selectors.offersWrapper) {
|
||||
selectors.offersWrapper.innerHTML = '<p class="empty-state">Failed to load offers. Please retry shortly.</p>';
|
||||
const wrapper = selectors.offersWrapper;
|
||||
if (wrapper) {
|
||||
wrapper.innerHTML = '<p class="empty-state">Failed to load offers. Please retry shortly.</p>';
|
||||
}
|
||||
showToast('Failed to load marketplace data.');
|
||||
}
|
||||
@@ -202,7 +222,10 @@ async function loadDashboard(): Promise<void> {
|
||||
selectors.bidForm?.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(selectors.bidForm!);
|
||||
const form = selectors.bidForm;
|
||||
if (!form) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
const provider = formData.get('provider')?.toString().trim();
|
||||
const capacity = Number(formData.get('capacity'));
|
||||
const price = Number(formData.get('price'));
|
||||
@@ -214,16 +237,109 @@ selectors.bidForm?.addEventListener('submit', async (event) => {
|
||||
}
|
||||
|
||||
try {
|
||||
selectors.bidForm!.querySelector('button')!.setAttribute('disabled', 'disabled');
|
||||
const submitButton = form.querySelector('button');
|
||||
if (submitButton) {
|
||||
submitButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
await submitMarketplaceBid({ provider, capacity, price, notes });
|
||||
selectors.bidForm!.reset();
|
||||
form.reset();
|
||||
showToast('Bid submitted successfully!');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('Unable to submit bid. Please try again.');
|
||||
} finally {
|
||||
selectors.bidForm!.querySelector('button')!.removeAttribute('disabled');
|
||||
const submitButton = form.querySelector('button');
|
||||
if (submitButton) {
|
||||
submitButton.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loadDashboard();
|
||||
|
||||
// Dark mode functionality with system preference detection
|
||||
function toggleDarkMode() {
|
||||
const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
}
|
||||
|
||||
function setTheme(theme: string) {
|
||||
// Apply theme immediately
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
// Save to localStorage for persistence
|
||||
localStorage.setItem('marketplaceTheme', theme);
|
||||
|
||||
// Update button display
|
||||
updateThemeButton(theme);
|
||||
|
||||
// Send analytics event if available
|
||||
if (typeof window !== 'undefined' && window.analytics) {
|
||||
window.analytics.track('marketplace_theme_changed', { theme });
|
||||
}
|
||||
}
|
||||
|
||||
function updateThemeButton(theme: string) {
|
||||
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(): string {
|
||||
// 1. Check localStorage first (user preference for marketplace)
|
||||
const saved = localStorage.getItem('marketplaceTheme');
|
||||
if (saved) {
|
||||
return saved;
|
||||
}
|
||||
|
||||
// 2. Check main site preference for consistency
|
||||
const mainSiteTheme = localStorage.getItem('theme');
|
||||
if (mainSiteTheme) {
|
||||
return mainSiteTheme;
|
||||
}
|
||||
|
||||
// 3. Check system preference
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
|
||||
// 4. 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('marketplaceTheme') && !localStorage.getItem('theme')) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme immediately (before DOM loads)
|
||||
initializeTheme();
|
||||
|
||||
// Reference to suppress TypeScript "never used" warning
|
||||
// @ts-ignore - function called from HTML onclick
|
||||
window.toggleDarkMode = toggleDarkMode;
|
||||
|
||||
@@ -8,10 +8,29 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Dark mode variables */
|
||||
.dark {
|
||||
--bg-primary: #1f2937;
|
||||
--bg-secondary: #374151;
|
||||
--bg-card: #111827;
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-muted: #9ca3af;
|
||||
--border-color: #4b5563;
|
||||
--hover-bg: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f7f8fa 0%, #eef1f6 100%);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Dark mode body */
|
||||
.dark body {
|
||||
background: linear-gradient(180deg, #1f2937 0%, #111827 100%);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#app {
|
||||
@@ -24,17 +43,118 @@ body {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.page-header__nav {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-header__nav a {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
color: #5a6575;
|
||||
text-decoration: none;
|
||||
transition: background 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.dark .page-header__nav a {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.page-header__nav a:hover {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.dark .page-header__nav a:hover {
|
||||
background: rgba(37, 99, 235, 0.15);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.page-header__nav .back-link {
|
||||
border: 1px solid #d1d5db;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.dark .page-header__nav .back-link {
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.page-header__nav .back-link:hover {
|
||||
border-color: #2563eb;
|
||||
color: #2563eb;
|
||||
background: rgba(37, 99, 235, 0.06);
|
||||
}
|
||||
|
||||
.dark .page-header__nav .back-link:hover {
|
||||
border-color: #60a5fa;
|
||||
color: #60a5fa;
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.4rem;
|
||||
margin: 0 0 0.5rem;
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .page-header h1 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 0;
|
||||
color: #5a6575;
|
||||
}
|
||||
|
||||
.dark .page-header p {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.page-header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.page-header-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dark-mode-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 2px solid #2563eb;
|
||||
border-radius: 999px;
|
||||
color: #2563eb;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dark .dark-mode-toggle {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
border-color: #60a5fa;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.dark-mode-toggle:hover {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dark .dark-mode-toggle:hover {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
@@ -47,6 +167,12 @@ body {
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 12px 24px rgba(18, 24, 32, 0.08);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .stat-card {
|
||||
background: var(--bg-card);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.stat-card h2 {
|
||||
@@ -55,11 +181,19 @@ body {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dark .stat-card h2 {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stat-card strong {
|
||||
font-size: 1.8rem;
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .stat-card strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-card span {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
@@ -67,9 +201,8 @@ body {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.panels {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
.dark .stat-card span {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.panel {
|
||||
@@ -77,6 +210,12 @@ body {
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .panel {
|
||||
background: var(--bg-card);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
@@ -85,6 +224,10 @@ body {
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .panel h2 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.offers-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -97,6 +240,11 @@ body {
|
||||
border-bottom: 1px solid #e5e9f1;
|
||||
}
|
||||
|
||||
.dark .offers-table th,
|
||||
.dark .offers-table td {
|
||||
border-bottom-color: var(--border-color);
|
||||
}
|
||||
|
||||
.offers-table th {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
@@ -104,18 +252,16 @@ body {
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.dark .offers-table th {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.offers-table tbody tr:hover {
|
||||
background-color: rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.offers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
.dark .offers-table tbody tr:hover {
|
||||
background-color: var(--hover-bg);
|
||||
}
|
||||
|
||||
.offer-card {
|
||||
@@ -126,30 +272,36 @@ body {
|
||||
transition: box-shadow 200ms ease, transform 200ms ease;
|
||||
}
|
||||
|
||||
.dark .offer-card {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.offer-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.offer-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.offer-gpu-name {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .offer-gpu-name {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.offer-provider {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dark .offer-provider {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.offer-specs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
@@ -160,8 +312,8 @@ body {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.spec-item {
|
||||
text-align: center;
|
||||
.dark .offer-specs {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.spec-label {
|
||||
@@ -173,6 +325,10 @@ body {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dark .spec-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.spec-value {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
@@ -180,10 +336,8 @@ body {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.offer-pricing {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
.dark .spec-value {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.offer-price {
|
||||
@@ -192,52 +346,27 @@ body {
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.dark .offer-price {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
.offer-price small {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dark .offer-price small {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.offer-sla {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.offer-plugins {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.plugin-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.offer-models {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.models-label {
|
||||
display: block;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.model-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
.dark .offer-sla {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.model-tag {
|
||||
@@ -251,6 +380,12 @@ body {
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .model-tag {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -271,6 +406,11 @@ body {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.dark .status-reserved {
|
||||
background-color: rgba(59, 130, 246, 0.2);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.bid-form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
@@ -283,6 +423,10 @@ body {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.dark .bid-form label {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bid-form input,
|
||||
.bid-form select,
|
||||
.bid-form textarea {
|
||||
@@ -295,6 +439,14 @@ body {
|
||||
background-color: #f9fbff;
|
||||
}
|
||||
|
||||
.dark .bid-form input,
|
||||
.dark .bid-form select,
|
||||
.dark .bid-form textarea {
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bid-form button {
|
||||
justify-self: flex-start;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
@@ -322,6 +474,12 @@ body {
|
||||
background-color: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
.dark .empty-state {
|
||||
color: var(--text-secondary);
|
||||
border-color: var(--border-color);
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
@@ -336,6 +494,11 @@ body {
|
||||
transition: opacity 200ms ease, transform 200ms ease;
|
||||
}
|
||||
|
||||
.dark .toast {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toast.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
<h1 class="text-2xl font-bold">AITBC Trade Exchange</h1>
|
||||
</div>
|
||||
<nav class="flex items-center space-x-6">
|
||||
<a href="https://aitbc.bubuit.net/" class="nav-button" title="Back to AITBC Home">
|
||||
<i data-lucide="home" class="w-5 h-5"></i>
|
||||
</a>
|
||||
<button onclick="showSection('trade')" class="nav-button">Trade</button>
|
||||
<button onclick="showSection('marketplace')" class="nav-button">Marketplace</button>
|
||||
<button onclick="showSection('wallet')" class="nav-button">Wallet</button>
|
||||
@@ -443,38 +446,314 @@
|
||||
document.getElementById('btcAmount').addEventListener('input', updateAITBCAmount);
|
||||
document.getElementById('aitbcAmount').addEventListener('input', updateBTCAmount);
|
||||
|
||||
// Check for saved dark mode preference
|
||||
if (localStorage.getItem('darkMode') === 'true' ||
|
||||
(!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
updateDarkModeIcon(true);
|
||||
}
|
||||
// Initialize enhanced dark mode
|
||||
initializeTheme();
|
||||
|
||||
// Initialize touch navigation
|
||||
new ExchangeTouchNavigation();
|
||||
});
|
||||
|
||||
// Dark mode toggle
|
||||
// Enhanced Dark mode functionality with system preference detection
|
||||
function toggleDarkMode() {
|
||||
const isDark = document.documentElement.classList.toggle('dark');
|
||||
localStorage.setItem('darkMode', isDark);
|
||||
updateDarkModeIcon(isDark);
|
||||
const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
}
|
||||
|
||||
function updateDarkModeIcon(isDark) {
|
||||
function setTheme(theme) {
|
||||
// Apply theme immediately
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
// Save to localStorage for persistence
|
||||
localStorage.setItem('exchangeTheme', theme);
|
||||
|
||||
// Update button display
|
||||
updateThemeButton(theme);
|
||||
|
||||
// Send analytics event if available
|
||||
if (window.analytics) {
|
||||
window.analytics.track('exchange_theme_changed', { theme });
|
||||
}
|
||||
}
|
||||
|
||||
function updateThemeButton(theme) {
|
||||
const icon = document.getElementById('darkModeIcon');
|
||||
if (isDark) {
|
||||
if (icon) {
|
||||
if (theme === 'dark') {
|
||||
icon.setAttribute('data-lucide', 'sun');
|
||||
} else {
|
||||
icon.setAttribute('data-lucide', 'moon');
|
||||
}
|
||||
if (window.lucide) {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Section Navigation
|
||||
function showSection(section) {
|
||||
document.querySelectorAll('.section').forEach(s => s.classList.add('hidden'));
|
||||
document.getElementById(section + 'Section').classList.remove('hidden');
|
||||
function getPreferredTheme() {
|
||||
// 1. Check localStorage first (user preference for exchange)
|
||||
const saved = localStorage.getItem('exchangeTheme');
|
||||
if (saved) {
|
||||
return saved;
|
||||
}
|
||||
|
||||
if (section === 'marketplace') {
|
||||
loadGPUOffers();
|
||||
// 2. Check main site preference for consistency
|
||||
const mainSiteTheme = localStorage.getItem('theme');
|
||||
if (mainSiteTheme) {
|
||||
return mainSiteTheme;
|
||||
}
|
||||
|
||||
// 3. Check system preference
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
|
||||
// 4. 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('exchangeTheme') && !localStorage.getItem('theme')) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Touch Navigation for Mobile
|
||||
class ExchangeTouchNavigation {
|
||||
constructor() {
|
||||
this.touchStartX = 0;
|
||||
this.touchStartY = 0;
|
||||
this.touchEndX = 0;
|
||||
this.touchEndY = 0;
|
||||
this.minSwipeDistance = 50;
|
||||
this.maxVerticalDistance = 100;
|
||||
|
||||
// Exchange sections for navigation
|
||||
this.sections = ['trade', 'marketplace', 'wallet'];
|
||||
this.currentSectionIndex = 0;
|
||||
|
||||
this.bindEvents();
|
||||
this.setupMobileOptimizations();
|
||||
this.updateCurrentSection();
|
||||
}
|
||||
|
||||
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 section = this.sections[index];
|
||||
showSection(section);
|
||||
this.currentSectionIndex = index;
|
||||
|
||||
// Update URL hash without triggering scroll
|
||||
history.replaceState(null, null, `#${section}`);
|
||||
|
||||
// Add visual feedback
|
||||
this.showSwipeIndicator();
|
||||
}
|
||||
|
||||
showSwipeIndicator() {
|
||||
// Create a temporary visual indicator
|
||||
const indicator = document.createElement('div');
|
||||
indicator.textContent = '← Swipe to navigate →';
|
||||
indicator.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
animation: fadeInOut 2s ease-in-out;
|
||||
`;
|
||||
|
||||
// Add fade animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes fadeInOut {
|
||||
0% { opacity: 0; }
|
||||
20% { opacity: 1; }
|
||||
80% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.body.appendChild(indicator);
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(indicator);
|
||||
document.head.removeChild(style);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
setupMobileOptimizations() {
|
||||
this.setupTouchButtons();
|
||||
this.setupMobileMenu();
|
||||
this.setupScrollOptimizations();
|
||||
}
|
||||
|
||||
setupTouchButtons() {
|
||||
// Make buttons more touch-friendly
|
||||
const buttons = document.querySelectorAll('button, input[type="submit"], .cta-button');
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('touchstart', () => {
|
||||
button.style.transform = 'scale(0.98)';
|
||||
}, { passive: true });
|
||||
|
||||
button.addEventListener('touchend', () => {
|
||||
button.style.transform = '';
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
setupMobileMenu() {
|
||||
// On very small screens, create a hamburger menu for nav buttons
|
||||
if (window.innerWidth < 640) {
|
||||
this.createMobileNavMenu();
|
||||
}
|
||||
|
||||
// Re-check on resize
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth < 640) {
|
||||
this.createMobileNavMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createMobileNavMenu() {
|
||||
const nav = document.querySelector('nav');
|
||||
if (!nav || nav.querySelector('.mobile-menu-toggle')) return;
|
||||
|
||||
// Create hamburger button
|
||||
const menuToggle = document.createElement('button');
|
||||
menuToggle.className = 'mobile-menu-toggle';
|
||||
menuToggle.innerHTML = '☰';
|
||||
menuToggle.setAttribute('aria-label', 'Toggle navigation menu');
|
||||
menuToggle.style.cssText = `
|
||||
display: none;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
// Hide original nav buttons and show hamburger on mobile
|
||||
const navButtons = nav.querySelectorAll('button:not(.mobile-menu-toggle)');
|
||||
navButtons.forEach(btn => btn.style.display = 'none');
|
||||
menuToggle.style.display = 'block';
|
||||
|
||||
// Toggle menu on click
|
||||
menuToggle.addEventListener('click', () => {
|
||||
const isOpen = menuToggle.textContent === '✕';
|
||||
if (isOpen) {
|
||||
navButtons.forEach(btn => btn.style.display = 'none');
|
||||
menuToggle.innerHTML = '☰';
|
||||
} else {
|
||||
navButtons.forEach(btn => btn.style.display = 'inline-flex');
|
||||
menuToggle.innerHTML = '✕';
|
||||
}
|
||||
});
|
||||
|
||||
nav.appendChild(menuToggle);
|
||||
}
|
||||
|
||||
setupScrollOptimizations() {
|
||||
// Improve momentum scrolling on iOS
|
||||
if ('webkitOverflowScrolling' in document.body.style) {
|
||||
document.body.style.webkitOverflowScrolling = 'touch';
|
||||
}
|
||||
|
||||
// Add touch feedback for interactive elements
|
||||
document.querySelectorAll('a, button, input, select').forEach(el => {
|
||||
el.addEventListener('touchstart', () => {
|
||||
el.style.opacity = '0.8';
|
||||
}, { passive: true });
|
||||
|
||||
el.addEventListener('touchend', () => {
|
||||
el.style.opacity = '';
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
updateCurrentSection() {
|
||||
// Update current section based on visible section
|
||||
this.sections.forEach((sectionId, index) => {
|
||||
const element = document.getElementById(sectionId + 'Section');
|
||||
if (element && !element.classList.contains('hidden')) {
|
||||
this.currentSectionIndex = index;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
4
website/assets/css/font-awesome.min.css
vendored
Normal file
4
website/assets/css/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
516
website/assets/css/main.css
Normal file
516
website/assets/css/main.css
Normal 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;
|
||||
}
|
||||
}
|
||||
2
website/assets/css/tailwind.css
Normal file
2
website/assets/css/tailwind.css
Normal file
@@ -0,0 +1,2 @@
|
||||
/* Tailwind CSS placeholder - replace with actual Tailwind build if needed */
|
||||
/* For now, using custom CSS in main.css */
|
||||
0
website/assets/favicon.ico
Normal file
0
website/assets/favicon.ico
Normal file
53
website/assets/js/analytics.js
Normal file
53
website/assets/js/analytics.js
Normal 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
307
website/assets/js/main.js
Normal 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
69
website/assets/js/sw.js
Normal 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();
|
||||
});
|
||||
158
website/assets/js/web-vitals.js
Normal file
158
website/assets/js/web-vitals.js
Normal 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();
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
<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>
|
||||
* {
|
||||
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;
|
||||
}
|
||||
}
|
||||
/* 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>© 2025 AITBC. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<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'
|
||||
});
|
||||
<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"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
|
||||
// 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>
|
||||
// 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'));
|
||||
}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user