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

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

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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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"]

View 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"}

View File

@@ -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

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>
<h1>Marketplace Control Center</h1>
<p>Monitor available offers, submit bids, and review marketplace health at a glance.</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;

View File

@@ -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);

View File

@@ -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);
}
function updateDarkModeIcon(isDark) {
const icon = document.getElementById('darkModeIcon');
if (isDark) {
icon.setAttribute('data-lucide', 'sun');
} else {
icon.setAttribute('data-lucide', 'moon');
}
lucide.createIcons();
const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
}
// Section Navigation
function showSection(section) {
document.querySelectorAll('.section').forEach(s => s.classList.add('hidden'));
document.getElementById(section + 'Section').classList.remove('hidden');
function setTheme(theme) {
// Apply theme immediately
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
if (section === 'marketplace') {
loadGPUOffers();
// 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 (icon) {
if (theme === 'dark') {
icon.setAttribute('data-lucide', 'sun');
} else {
icon.setAttribute('data-lucide', 'moon');
}
if (window.lucide) {
lucide.createIcons();
}
}
}
function getPreferredTheme() {
// 1. Check localStorage first (user preference for exchange)
const saved = localStorage.getItem('exchangeTheme');
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('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;
}
});
}
}

View File

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

File diff suppressed because one or more lines are too long

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

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

View File

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

View File

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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