fix(marketplace): Update URLs to use /marketplace path and relative API endpoints

- Change API_BASE to relative '/api' for production
- Move marketplace to /marketplace instead of /marketplace/frontend
- Update MkDocs navigation
- Fix logo path in marketplace index
This commit is contained in:
unclecode
2025-10-02 17:08:50 +08:00
parent 408ad1b750
commit 749d200866
9 changed files with 2289 additions and 4 deletions

View File

@@ -1,5 +1,5 @@
// Admin Dashboard - Smart & Powerful
const API_BASE = 'http://localhost:8100/api';
const API_BASE = '/api';
class AdminDashboard {
constructor() {

View File

@@ -0,0 +1,462 @@
/* App Detail Page Styles */
.app-detail-container {
min-height: 100vh;
background: var(--bg-dark);
}
/* Back Button */
.header-nav {
display: flex;
align-items: center;
}
.back-btn {
padding: 0.5rem 1rem;
background: transparent;
border: 1px solid var(--border-color);
color: var(--primary-cyan);
text-decoration: none;
transition: all 0.2s;
font-size: 0.875rem;
}
.back-btn:hover {
border-color: var(--primary-cyan);
background: rgba(80, 255, 255, 0.1);
}
/* App Hero Section */
.app-hero {
max-width: 1800px;
margin: 2rem auto;
padding: 0 2rem;
}
.app-hero-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 3rem;
background: linear-gradient(135deg, #1a1a2e, #0f0f1e);
border: 2px solid var(--primary-cyan);
padding: 2rem;
box-shadow: 0 0 30px rgba(80, 255, 255, 0.15),
inset 0 0 20px rgba(80, 255, 255, 0.05);
}
.app-hero-image {
width: 100%;
height: 300px;
background: linear-gradient(135deg, rgba(80, 255, 255, 0.1), rgba(243, 128, 245, 0.05));
background-size: cover;
background-position: center;
border: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
color: var(--primary-cyan);
}
.app-badges {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.app-badge {
padding: 0.3rem 0.6rem;
background: var(--bg-tertiary);
color: var(--text-secondary);
font-size: 0.75rem;
text-transform: uppercase;
font-weight: 600;
}
.app-badge.featured {
background: linear-gradient(135deg, var(--primary-cyan), var(--primary-teal));
color: var(--bg-dark);
box-shadow: 0 2px 10px rgba(80, 255, 255, 0.3);
}
.app-badge.sponsored {
background: linear-gradient(135deg, var(--warning), #ff8c00);
color: var(--bg-dark);
box-shadow: 0 2px 10px rgba(245, 158, 11, 0.3);
}
.app-hero-info h1 {
font-size: 2.5rem;
color: var(--primary-cyan);
margin: 0.5rem 0;
text-shadow: 0 0 20px rgba(80, 255, 255, 0.5);
}
.app-tagline {
font-size: 1.1rem;
color: var(--text-secondary);
margin-bottom: 2rem;
}
/* Stats */
.app-stats {
display: flex;
gap: 2rem;
margin: 2rem 0;
padding: 1rem 0;
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
}
.stat {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat-value {
font-size: 1.5rem;
color: var(--primary-cyan);
font-weight: 600;
}
.stat-label {
font-size: 0.875rem;
color: var(--text-tertiary);
}
/* Action Buttons */
.app-actions {
display: flex;
gap: 1rem;
margin: 2rem 0;
}
.action-btn {
padding: 0.75rem 1.5rem;
border: 1px solid var(--border-color);
background: transparent;
color: var(--text-primary);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
cursor: pointer;
font-family: inherit;
font-size: 0.9rem;
}
.action-btn.primary {
background: linear-gradient(135deg, var(--primary-cyan), var(--primary-teal));
color: var(--bg-dark);
border-color: var(--primary-cyan);
font-weight: 600;
}
.action-btn.primary:hover {
box-shadow: 0 4px 15px rgba(80, 255, 255, 0.3);
transform: translateY(-2px);
}
.action-btn.secondary {
border-color: var(--accent-pink);
color: var(--accent-pink);
}
.action-btn.secondary:hover {
background: rgba(243, 128, 245, 0.1);
box-shadow: 0 4px 15px rgba(243, 128, 245, 0.2);
}
.action-btn.ghost {
border-color: var(--border-color);
color: var(--text-secondary);
}
.action-btn.ghost:hover {
border-color: var(--primary-cyan);
color: var(--primary-cyan);
}
/* Pricing */
.pricing-info {
display: flex;
align-items: center;
gap: 1rem;
font-size: 1.1rem;
}
.pricing-label {
color: var(--text-tertiary);
}
.pricing-value {
color: var(--warning);
font-weight: 600;
}
/* Navigation Tabs */
.app-nav {
max-width: 1800px;
margin: 2rem auto 0;
padding: 0 2rem;
display: flex;
gap: 1rem;
border-bottom: 2px solid var(--border-color);
}
.nav-tab {
padding: 1rem 1.5rem;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
font-size: 0.9rem;
margin-bottom: -2px;
}
.nav-tab:hover {
color: var(--primary-cyan);
}
.nav-tab.active {
color: var(--primary-cyan);
border-bottom-color: var(--primary-cyan);
}
/* Content Sections */
.app-content {
max-width: 1800px;
margin: 2rem auto;
padding: 0 2rem;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.docs-content {
max-width: 1200px;
padding: 2rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.docs-content h2 {
font-size: 1.8rem;
color: var(--primary-cyan);
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
}
.docs-content h3 {
font-size: 1.3rem;
color: var(--text-primary);
margin: 2rem 0 1rem;
}
.docs-content h4 {
font-size: 1.1rem;
color: var(--accent-pink);
margin: 1.5rem 0 0.5rem;
}
.docs-content p {
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 1rem;
}
.docs-content code {
background: var(--bg-tertiary);
padding: 0.2rem 0.4rem;
color: var(--primary-cyan);
font-family: 'Dank Mono', Monaco, monospace;
font-size: 0.9em;
}
/* Code Blocks */
.code-block {
background: var(--bg-dark);
border: 1px solid var(--border-color);
margin: 1rem 0;
overflow: hidden;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.code-lang {
color: var(--primary-cyan);
font-size: 0.875rem;
text-transform: uppercase;
}
.copy-btn {
padding: 0.25rem 0.5rem;
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
cursor: pointer;
font-size: 0.75rem;
transition: all 0.2s;
}
.copy-btn:hover {
border-color: var(--primary-cyan);
color: var(--primary-cyan);
}
.code-block pre {
margin: 0;
padding: 1rem;
overflow-x: auto;
}
.code-block code {
background: transparent;
padding: 0;
color: var(--text-secondary);
font-size: 0.875rem;
line-height: 1.5;
}
/* Feature Grid */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin: 2rem 0;
}
.feature-card {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
padding: 1.5rem;
transition: all 0.2s;
}
.feature-card:hover {
border-color: var(--primary-cyan);
background: rgba(80, 255, 255, 0.05);
}
.feature-card h4 {
margin-top: 0;
}
/* Info Box */
.info-box {
background: linear-gradient(135deg, rgba(80, 255, 255, 0.05), rgba(243, 128, 245, 0.03));
border: 1px solid var(--primary-cyan);
border-left: 4px solid var(--primary-cyan);
padding: 1.5rem;
margin: 2rem 0;
}
.info-box h4 {
margin-top: 0;
color: var(--primary-cyan);
}
/* Support Grid */
.support-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin: 2rem 0;
}
.support-card {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
padding: 1.5rem;
text-align: center;
}
.support-card h3 {
color: var(--primary-cyan);
margin-bottom: 0.5rem;
}
/* Related Apps */
.related-apps {
max-width: 1800px;
margin: 4rem auto;
padding: 0 2rem;
}
.related-apps h2 {
font-size: 1.5rem;
color: var(--text-primary);
margin-bottom: 1.5rem;
}
.related-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
.related-app-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.related-app-card:hover {
border-color: var(--primary-cyan);
transform: translateY(-2px);
}
/* Responsive */
@media (max-width: 1024px) {
.app-hero-content {
grid-template-columns: 1fr;
}
.app-stats {
justify-content: space-around;
}
}
@media (max-width: 768px) {
.app-hero-info h1 {
font-size: 2rem;
}
.app-actions {
flex-direction: column;
}
.app-nav {
overflow-x: auto;
gap: 0;
}
.nav-tab {
white-space: nowrap;
}
.feature-grid,
.support-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,324 @@
// App Detail Page JavaScript
const API_BASE = '/api';
class AppDetailPage {
constructor() {
this.appSlug = this.getAppSlugFromURL();
this.appData = null;
this.init();
}
getAppSlugFromURL() {
const params = new URLSearchParams(window.location.search);
return params.get('app') || '';
}
async init() {
if (!this.appSlug) {
window.location.href = 'index.html';
return;
}
await this.loadAppDetails();
this.setupEventListeners();
await this.loadRelatedApps();
}
async loadAppDetails() {
try {
const response = await fetch(`${API_BASE}/apps/${this.appSlug}`);
if (!response.ok) throw new Error('App not found');
this.appData = await response.json();
this.renderAppDetails();
} catch (error) {
console.error('Error loading app details:', error);
// Fallback to loading all apps and finding the right one
try {
const response = await fetch(`${API_BASE}/apps`);
const apps = await response.json();
this.appData = apps.find(app => app.slug === this.appSlug || app.name.toLowerCase().replace(/\s+/g, '-') === this.appSlug);
if (this.appData) {
this.renderAppDetails();
} else {
window.location.href = 'index.html';
}
} catch (err) {
console.error('Error loading apps:', err);
window.location.href = 'index.html';
}
}
}
renderAppDetails() {
if (!this.appData) return;
// Update title
document.title = `${this.appData.name} - Crawl4AI Marketplace`;
// Hero image
const appImage = document.getElementById('app-image');
if (this.appData.image) {
appImage.style.backgroundImage = `url('${this.appData.image}')`;
appImage.innerHTML = '';
} else {
appImage.innerHTML = `[${this.appData.category || 'APP'}]`;
}
// Basic info
document.getElementById('app-name').textContent = this.appData.name;
document.getElementById('app-description').textContent = this.appData.description;
document.getElementById('app-type').textContent = this.appData.type || 'Open Source';
document.getElementById('app-category').textContent = this.appData.category;
document.getElementById('app-pricing').textContent = this.appData.pricing || 'Free';
// Badges
if (this.appData.featured) {
document.getElementById('app-featured').style.display = 'inline-block';
}
if (this.appData.sponsored) {
document.getElementById('app-sponsored').style.display = 'inline-block';
}
// Stats
const rating = this.appData.rating || 0;
const stars = '★'.repeat(Math.floor(rating)) + '☆'.repeat(5 - Math.floor(rating));
document.getElementById('app-rating').textContent = stars + ` ${rating}/5`;
document.getElementById('app-downloads').textContent = this.formatNumber(this.appData.downloads || 0);
// Action buttons
const websiteBtn = document.getElementById('app-website');
const githubBtn = document.getElementById('app-github');
if (this.appData.website_url) {
websiteBtn.href = this.appData.website_url;
} else {
websiteBtn.style.display = 'none';
}
if (this.appData.github_url) {
githubBtn.href = this.appData.github_url;
} else {
githubBtn.style.display = 'none';
}
// Contact
document.getElementById('app-contact').textContent = this.appData.contact_email || 'Not available';
// Integration guide
this.renderIntegrationGuide();
}
renderIntegrationGuide() {
// Installation code
const installCode = document.getElementById('install-code');
if (this.appData.type === 'Open Source' && this.appData.github_url) {
installCode.textContent = `# Clone from GitHub
git clone ${this.appData.github_url}
# Install dependencies
pip install -r requirements.txt`;
} else if (this.appData.name.toLowerCase().includes('api')) {
installCode.textContent = `# Install via pip
pip install ${this.appData.slug}
# Or install from source
pip install git+${this.appData.github_url || 'https://github.com/example/repo'}`;
}
// Usage code - customize based on category
const usageCode = document.getElementById('usage-code');
if (this.appData.category === 'Browser Automation') {
usageCode.textContent = `from crawl4ai import AsyncWebCrawler
from ${this.appData.slug.replace(/-/g, '_')} import ${this.appData.name.replace(/\s+/g, '')}
async def main():
# Initialize ${this.appData.name}
automation = ${this.appData.name.replace(/\s+/g, '')}()
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
url="https://example.com",
browser_config=automation.config,
wait_for="css:body"
)
print(result.markdown)`;
} else if (this.appData.category === 'Proxy Services') {
usageCode.textContent = `from crawl4ai import AsyncWebCrawler
import ${this.appData.slug.replace(/-/g, '_')}
# Configure proxy
proxy_config = {
"server": "${this.appData.website_url || 'https://proxy.example.com'}",
"username": "your_username",
"password": "your_password"
}
async with AsyncWebCrawler(proxy=proxy_config) as crawler:
result = await crawler.arun(
url="https://example.com",
bypass_cache=True
)
print(result.status_code)`;
} else if (this.appData.category === 'LLM Integration') {
usageCode.textContent = `from crawl4ai import AsyncWebCrawler
from crawl4ai.extraction_strategy import LLMExtractionStrategy
# Configure LLM extraction
strategy = LLMExtractionStrategy(
provider="${this.appData.name.toLowerCase().includes('gpt') ? 'openai' : 'anthropic'}",
api_key="your-api-key",
model="${this.appData.name.toLowerCase().includes('gpt') ? 'gpt-4' : 'claude-3'}",
instruction="Extract structured data"
)
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
url="https://example.com",
extraction_strategy=strategy
)
print(result.extracted_content)`;
}
// Integration example
const integrationCode = document.getElementById('integration-code');
integrationCode.textContent = this.appData.integration_guide ||
`# Complete ${this.appData.name} Integration Example
from crawl4ai import AsyncWebCrawler
from crawl4ai.extraction_strategy import JsonCssExtractionStrategy
import json
async def crawl_with_${this.appData.slug.replace(/-/g, '_')}():
"""
Complete example showing how to use ${this.appData.name}
with Crawl4AI for production web scraping
"""
# Define extraction schema
schema = {
"name": "ProductList",
"baseSelector": "div.product",
"fields": [
{"name": "title", "selector": "h2", "type": "text"},
{"name": "price", "selector": ".price", "type": "text"},
{"name": "image", "selector": "img", "type": "attribute", "attribute": "src"},
{"name": "link", "selector": "a", "type": "attribute", "attribute": "href"}
]
}
# Initialize crawler with ${this.appData.name}
async with AsyncWebCrawler(
browser_type="chromium",
headless=True,
verbose=True
) as crawler:
# Crawl with extraction
result = await crawler.arun(
url="https://example.com/products",
extraction_strategy=JsonCssExtractionStrategy(schema),
cache_mode="bypass",
wait_for="css:.product",
screenshot=True
)
# Process results
if result.success:
products = json.loads(result.extracted_content)
print(f"Found {len(products)} products")
for product in products[:5]:
print(f"- {product['title']}: {product['price']}")
return products
# Run the crawler
if __name__ == "__main__":
import asyncio
asyncio.run(crawl_with_${this.appData.slug.replace(/-/g, '_')}())`;
}
formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
setupEventListeners() {
// Tab switching
const tabs = document.querySelectorAll('.nav-tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// Update active tab
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Show corresponding content
const tabName = tab.dataset.tab;
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabName}-tab`).classList.add('active');
});
});
// Copy integration code
document.getElementById('copy-integration').addEventListener('click', () => {
const code = document.getElementById('integration-code').textContent;
navigator.clipboard.writeText(code).then(() => {
const btn = document.getElementById('copy-integration');
const originalText = btn.innerHTML;
btn.innerHTML = '<span>✓</span> Copied!';
setTimeout(() => {
btn.innerHTML = originalText;
}, 2000);
});
});
// Copy code buttons
document.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const codeBlock = e.target.closest('.code-block');
const code = codeBlock.querySelector('code').textContent;
navigator.clipboard.writeText(code).then(() => {
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = 'Copy';
}, 2000);
});
});
});
}
async loadRelatedApps() {
try {
const response = await fetch(`${API_BASE}/apps?category=${encodeURIComponent(this.appData.category)}&limit=4`);
const apps = await response.json();
const relatedApps = apps.filter(app => app.slug !== this.appSlug).slice(0, 3);
const grid = document.getElementById('related-apps-grid');
grid.innerHTML = relatedApps.map(app => `
<div class="related-app-card" onclick="window.location.href='app-detail.html?app=${app.slug || app.name.toLowerCase().replace(/\s+/g, '-')}'">
<h4>${app.name}</h4>
<p>${app.description.substring(0, 100)}...</p>
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.75rem;">
<span style="color: var(--primary-cyan)">${app.type}</span>
<span style="color: var(--warning)">★ ${app.rating}/5</span>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading related apps:', error);
}
}
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new AppDetailPage();
});

View File

@@ -1,5 +1,5 @@
// App Detail Page JavaScript
const API_BASE = 'http://localhost:8100/api';
const API_BASE = '/api';
class AppDetailPage {
constructor() {

View File

@@ -1,5 +1,5 @@
// Marketplace JS - Magazine Layout
const API_BASE = 'http://localhost:8100/api';
const API_BASE = '/api';
const CACHE_TTL = 3600000; // 1 hour in ms
class MarketplaceCache {

View File

@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Marketplace - Crawl4AI</title>
<link rel="stylesheet" href="marketplace.css">
</head>
<body>
<div class="marketplace-container">
<!-- Header -->
<header class="marketplace-header">
<div class="header-content">
<div class="header-left">
<div class="logo-title">
<img src="../assets/images/logo.png" alt="Crawl4AI" class="header-logo">
<h1>
<span class="ascii-border">[</span>
Marketplace
<span class="ascii-border">]</span>
</h1>
</div>
<p class="tagline">Tools, Integrations & Resources for Web Crawling</p>
</div>
<div class="header-stats" id="stats">
<span class="stat-item">Apps: <span id="total-apps">--</span></span>
<span class="stat-item">Articles: <span id="total-articles">--</span></span>
<span class="stat-item">Downloads: <span id="total-downloads">--</span></span>
</div>
</div>
</header>
<!-- Search and Category Bar -->
<div class="search-filter-bar">
<div class="search-box">
<span class="search-icon">></span>
<input type="text" id="search-input" placeholder="Search apps, articles, tools..." />
<kbd>/</kbd>
</div>
<div class="category-filter" id="category-filter">
<button class="filter-btn active" data-category="all">All</button>
<!-- Categories will be loaded here -->
</div>
</div>
<!-- Magazine Grid Layout -->
<main class="magazine-layout">
<!-- Hero Featured Section -->
<section class="hero-featured">
<div id="featured-hero" class="featured-hero-card">
<!-- Large featured card with big image -->
</div>
</section>
<!-- Secondary Featured -->
<section class="secondary-featured">
<div id="featured-secondary" class="featured-secondary-cards">
<!-- 2-3 medium featured cards with images -->
</div>
</section>
<!-- Sponsored Section -->
<section class="sponsored-section">
<div class="section-label">SPONSORED</div>
<div id="sponsored-content" class="sponsored-cards">
<!-- Sponsored content cards -->
</div>
</section>
<!-- Main Content Grid -->
<section class="main-content">
<!-- Apps Column -->
<div class="apps-column">
<div class="column-header">
<h2><span class="ascii-icon">></span> Latest Apps</h2>
<select id="type-filter" class="mini-filter">
<option value="">All</option>
<option value="Open Source">Open Source</option>
<option value="Paid">Paid</option>
</select>
</div>
<div id="apps-grid" class="apps-compact-grid">
<!-- Compact app cards -->
</div>
</div>
<!-- Articles Column -->
<div class="articles-column">
<div class="column-header">
<h2><span class="ascii-icon">></span> Latest Articles</h2>
</div>
<div id="articles-list" class="articles-compact-list">
<!-- Article items -->
</div>
</div>
<!-- Trending/Tools Column -->
<div class="trending-column">
<div class="column-header">
<h2><span class="ascii-icon">#</span> Trending</h2>
</div>
<div id="trending-list" class="trending-items">
<!-- Trending items -->
</div>
<div class="submit-box">
<h3><span class="ascii-icon">+</span> Submit Your Tool</h3>
<p>Share your integration</p>
<a href="mailto:marketplace@crawl4ai.com" class="submit-btn">Submit →</a>
</div>
</div>
</section>
<!-- More Apps Grid -->
<section class="more-apps">
<div class="section-header">
<h2><span class="ascii-icon">></span> More Apps</h2>
<button id="load-more" class="load-more-btn">Load More ↓</button>
</div>
<div id="more-apps-grid" class="more-apps-grid">
<!-- Additional app cards -->
</div>
</section>
</main>
<!-- Footer -->
<footer class="marketplace-footer">
<div class="footer-content">
<div class="footer-section">
<h3>About Marketplace</h3>
<p>Discover tools and integrations built by the Crawl4AI community.</p>
</div>
<div class="footer-section">
<h3>Become a Sponsor</h3>
<p>Reach developers building with Crawl4AI</p>
<a href="mailto:sponsors@crawl4ai.com" class="sponsor-btn">Learn More →</a>
</div>
</div>
<div class="footer-bottom">
<p>[ Crawl4AI Marketplace · Updated <span id="last-update">--</span> ]</p>
</div>
</footer>
</div>
<script src="marketplace.js"></script>
</body>
</html>

View File

@@ -0,0 +1,957 @@
/* Marketplace CSS - Magazine Style Terminal Theme */
@import url('../../assets/styles.css');
:root {
--primary-cyan: #50ffff;
--primary-teal: #09b5a5;
--accent-pink: #f380f5;
--bg-dark: #070708;
--bg-secondary: #1a1a1a;
--bg-tertiary: #3f3f44;
--text-primary: #e8e9ed;
--text-secondary: #d5cec0;
--text-tertiary: #a3abba;
--border-color: #3f3f44;
--success: #50ff50;
--error: #ff3c74;
--warning: #f59e0b;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Dank Mono', Monaco, monospace;
background: var(--bg-dark);
color: var(--text-primary);
line-height: 1.6;
}
/* Global link styles */
a {
color: var(--primary-cyan);
text-decoration: none;
transition: color 0.2s;
}
a:hover {
color: var(--accent-pink);
}
.marketplace-container {
min-height: 100vh;
}
/* Header */
.marketplace-header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 1.5rem 0;
}
.header-content {
max-width: 1800px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo-title {
display: flex;
align-items: center;
gap: 1rem;
}
.header-logo {
height: 40px;
width: auto;
filter: brightness(1.2);
}
.marketplace-header h1 {
font-size: 1.5rem;
color: var(--primary-cyan);
margin: 0;
}
.ascii-border {
color: var(--border-color);
}
.tagline {
font-size: 0.875rem;
color: var(--text-tertiary);
margin-top: 0.25rem;
}
.header-stats {
display: flex;
gap: 2rem;
}
.stat-item {
font-size: 0.875rem;
color: var(--text-secondary);
}
.stat-item span {
color: var(--primary-cyan);
font-weight: 600;
}
/* Search and Filter Bar */
.search-filter-bar {
max-width: 1800px;
margin: 1.5rem auto;
padding: 0 2rem;
display: flex;
gap: 1rem;
align-items: center;
}
.search-box {
flex: 1;
max-width: 500px;
display: flex;
align-items: center;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
padding: 0.75rem 1rem;
transition: border-color 0.2s;
}
.search-box:focus-within {
border-color: var(--primary-cyan);
}
.search-icon {
color: var(--text-tertiary);
margin-right: 1rem;
}
#search-input {
flex: 1;
background: transparent;
border: none;
color: var(--text-primary);
font-family: inherit;
font-size: 0.9rem;
outline: none;
}
.search-box kbd {
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-tertiary);
}
.category-filter {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.filter-btn {
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
padding: 0.5rem 1rem;
font-family: inherit;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
}
.filter-btn:hover {
border-color: var(--primary-cyan);
color: var(--primary-cyan);
}
.filter-btn.active {
background: var(--primary-cyan);
color: var(--bg-dark);
border-color: var(--primary-cyan);
}
/* Magazine Layout */
.magazine-layout {
max-width: 1800px;
margin: 0 auto;
padding: 0 2rem 4rem;
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
/* Hero Featured Section */
.hero-featured {
grid-column: 1 / -1;
position: relative;
}
.hero-featured::before {
content: '';
position: absolute;
top: -20px;
left: -20px;
right: -20px;
bottom: -20px;
background: radial-gradient(ellipse at center, rgba(80, 255, 255, 0.05), transparent 70%);
pointer-events: none;
z-index: -1;
}
.featured-hero-card {
background: linear-gradient(135deg, #1a1a2e, #0f0f1e);
border: 2px solid var(--primary-cyan);
box-shadow: 0 0 30px rgba(80, 255, 255, 0.15),
inset 0 0 20px rgba(80, 255, 255, 0.05);
height: 380px;
position: relative;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}
.featured-hero-card:hover {
border-color: var(--accent-pink);
box-shadow: 0 0 40px rgba(243, 128, 245, 0.2),
inset 0 0 30px rgba(243, 128, 245, 0.05);
transform: translateY(-2px);
}
.hero-image {
width: 100%;
height: 240px;
background: linear-gradient(135deg, rgba(80, 255, 255, 0.1), rgba(243, 128, 245, 0.05));
background-size: cover;
background-position: center;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: var(--primary-cyan);
flex-shrink: 0;
position: relative;
filter: brightness(1.1) contrast(1.1);
}
.hero-image::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60%;
background: linear-gradient(to top, rgba(10, 10, 20, 0.95), transparent);
}
.hero-content {
padding: 1.5rem;
}
.hero-badge {
display: inline-block;
padding: 0.3rem 0.6rem;
background: linear-gradient(135deg, var(--primary-cyan), var(--primary-teal));
color: var(--bg-dark);
font-size: 0.7rem;
text-transform: uppercase;
margin-bottom: 0.5rem;
font-weight: 600;
box-shadow: 0 2px 10px rgba(80, 255, 255, 0.3);
}
.hero-title {
font-size: 1.6rem;
color: var(--primary-cyan);
margin: 0.5rem 0;
text-shadow: 0 0 20px rgba(80, 255, 255, 0.5);
}
.hero-description {
color: var(--text-secondary);
line-height: 1.5;
}
.hero-meta {
display: flex;
gap: 1.5rem;
margin-top: 1rem;
font-size: 0.875rem;
}
.hero-meta span {
color: var(--text-tertiary);
}
.hero-meta span:first-child {
color: var(--warning);
}
/* Secondary Featured */
.secondary-featured {
grid-column: 1 / -1;
height: 380px;
display: flex;
align-items: stretch;
}
.featured-secondary-cards {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.75rem;
justify-content: space-between;
}
.secondary-card {
background: linear-gradient(135deg, rgba(80, 255, 255, 0.03), rgba(243, 128, 245, 0.02));
border: 1px solid rgba(80, 255, 255, 0.3);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
overflow: hidden;
height: calc((380px - 1.5rem) / 3);
flex: 1;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.secondary-card:hover {
border-color: var(--accent-pink);
background: linear-gradient(135deg, rgba(243, 128, 245, 0.05), rgba(80, 255, 255, 0.03));
box-shadow: 0 4px 15px rgba(243, 128, 245, 0.2);
transform: translateX(-3px);
}
.secondary-image {
width: 120px;
background: linear-gradient(135deg, var(--bg-tertiary), var(--bg-secondary));
background-size: cover;
background-position: center;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: var(--primary-cyan);
flex-shrink: 0;
}
.secondary-content {
flex: 1;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.secondary-title {
font-size: 1rem;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.secondary-desc {
font-size: 0.75rem;
color: var(--text-secondary);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.secondary-meta {
font-size: 0.75rem;
color: var(--text-tertiary);
}
.secondary-meta span:last-child {
color: var(--warning);
}
/* Sponsored Section */
.sponsored-section {
grid-column: 1 / -1;
background: var(--bg-secondary);
border: 1px solid var(--warning);
padding: 1rem;
position: relative;
}
.section-label {
position: absolute;
top: -0.5rem;
left: 1rem;
background: var(--bg-secondary);
padding: 0 0.5rem;
color: var(--warning);
font-size: 0.65rem;
letter-spacing: 0.1em;
}
.sponsored-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.sponsor-card {
padding: 1rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
}
.sponsor-card h4 {
color: var(--accent-pink);
margin-bottom: 0.5rem;
}
.sponsor-card p {
color: var(--text-secondary);
font-size: 0.85rem;
margin-bottom: 0.75rem;
}
.sponsor-card a {
color: var(--primary-cyan);
text-decoration: none;
font-size: 0.85rem;
}
.sponsor-card a:hover {
color: var(--accent-pink);
}
/* Main Content Grid */
.main-content {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
/* Column Headers */
.column-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.5rem;
}
.column-header h2 {
font-size: 1.1rem;
color: var(--text-primary);
}
.mini-filter {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 0.25rem 0.5rem;
font-family: inherit;
font-size: 0.75rem;
}
.ascii-icon {
color: var(--primary-cyan);
}
/* Apps Column */
.apps-compact-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.app-compact {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-left: 3px solid var(--border-color);
padding: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.app-compact:hover {
border-color: var(--primary-cyan);
border-left-color: var(--accent-pink);
transform: translateX(2px);
}
.app-compact-header {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--text-tertiary);
margin-bottom: 0.25rem;
}
.app-compact-header span:first-child {
color: var(--primary-cyan);
}
.app-compact-header span:last-child {
color: var(--warning);
}
.app-compact-title {
font-size: 0.9rem;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.app-compact-desc {
font-size: 0.75rem;
color: var(--text-secondary);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Articles Column */
.articles-compact-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.article-compact {
border-left: 2px solid var(--border-color);
padding-left: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.article-compact:hover {
border-left-color: var(--primary-cyan);
}
.article-meta {
font-size: 0.7rem;
color: var(--text-tertiary);
margin-bottom: 0.25rem;
}
.article-meta span:first-child {
color: var(--accent-pink);
}
.article-title {
font-size: 0.9rem;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.article-author {
font-size: 0.75rem;
color: var(--text-secondary);
}
/* Trending Column */
.trending-items {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.trending-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.2s;
}
.trending-item:hover {
border-color: var(--primary-cyan);
}
.trending-rank {
font-size: 1.2rem;
color: var(--primary-cyan);
width: 2rem;
text-align: center;
}
.trending-info {
flex: 1;
}
.trending-name {
font-size: 0.85rem;
color: var(--text-primary);
}
.trending-stats {
font-size: 0.7rem;
color: var(--text-tertiary);
}
/* Submit Box */
.submit-box {
margin-top: 1.5rem;
background: var(--bg-secondary);
border: 1px solid var(--primary-cyan);
padding: 1rem;
text-align: center;
}
.submit-box h3 {
font-size: 1rem;
color: var(--primary-cyan);
margin-bottom: 0.5rem;
}
.submit-box p {
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: 0.75rem;
}
.submit-btn {
display: inline-block;
padding: 0.5rem 1rem;
background: transparent;
border: 1px solid var(--primary-cyan);
color: var(--primary-cyan);
text-decoration: none;
transition: all 0.2s;
}
.submit-btn:hover {
background: var(--primary-cyan);
color: var(--bg-dark);
}
/* More Apps Section */
.more-apps {
grid-column: 1 / -1;
margin-top: 2rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.more-apps-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.load-more-btn {
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
padding: 0.5rem 1.5rem;
font-family: inherit;
cursor: pointer;
transition: all 0.2s;
}
.load-more-btn:hover {
border-color: var(--primary-cyan);
color: var(--primary-cyan);
}
/* Footer */
.marketplace-footer {
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
margin-top: 4rem;
padding: 2rem 0;
}
.footer-content {
max-width: 1800px;
margin: 0 auto;
padding: 0 2rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.footer-section h3 {
font-size: 1rem;
margin-bottom: 0.5rem;
color: var(--primary-cyan);
}
.footer-section p {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 1rem;
}
.sponsor-btn {
display: inline-block;
padding: 0.5rem 1rem;
background: transparent;
border: 1px solid var(--primary-cyan);
color: var(--primary-cyan);
text-decoration: none;
transition: all 0.2s;
}
.sponsor-btn:hover {
background: var(--primary-cyan);
color: var(--bg-dark);
}
.footer-bottom {
max-width: 1800px;
margin: 2rem auto 0;
padding: 1rem 2rem 0;
border-top: 1px solid var(--border-color);
font-size: 0.75rem;
color: var(--text-tertiary);
}
/* Modal */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.hidden {
display: none;
}
.modal-content {
background: var(--bg-secondary);
border: 1px solid var(--primary-cyan);
max-width: 800px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
position: relative;
}
.modal-close {
position: absolute;
top: 1rem;
right: 1rem;
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 0.25rem 0.5rem;
cursor: pointer;
font-size: 1.2rem;
}
.modal-close:hover {
border-color: var(--error);
color: var(--error);
}
.app-detail {
padding: 2rem;
}
.app-detail h2 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--primary-cyan);
}
/* Loading */
.loading {
text-align: center;
padding: 2rem;
color: var(--text-tertiary);
}
.no-results {
text-align: center;
padding: 2rem;
color: var(--text-tertiary);
}
/* Responsive - Tablet */
@media (min-width: 768px) {
.magazine-layout {
grid-template-columns: repeat(2, 1fr);
}
.hero-featured {
grid-column: 1 / -1;
}
.secondary-featured {
grid-column: 1 / -1;
}
.sponsored-section {
grid-column: 1 / -1;
}
.main-content {
grid-column: 1 / -1;
grid-template-columns: repeat(2, 1fr);
}
}
/* Responsive - Desktop */
@media (min-width: 1024px) {
.magazine-layout {
grid-template-columns: repeat(3, 1fr);
}
.hero-featured {
grid-column: 1 / 3;
grid-row: 1;
}
.secondary-featured {
grid-column: 3 / 4;
grid-row: 1;
}
.featured-secondary-cards {
flex-direction: column;
}
.sponsored-section {
grid-column: 1 / -1;
}
.main-content {
grid-column: 1 / -1;
grid-template-columns: repeat(3, 1fr);
}
}
/* Responsive - Wide Desktop */
@media (min-width: 1400px) {
.magazine-layout {
grid-template-columns: repeat(4, 1fr);
}
.hero-featured {
grid-column: 1 / 3;
}
.secondary-featured {
grid-column: 3 / 5;
grid-row: 1;
}
.featured-secondary-cards {
grid-template-columns: repeat(2, 1fr);
}
.main-content {
grid-template-columns: repeat(4, 1fr);
}
.apps-column {
grid-column: span 2;
}
.more-apps-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
/* Responsive - Ultra Wide Desktop (for coders with wide monitors) */
@media (min-width: 1800px) {
.magazine-layout {
grid-template-columns: repeat(5, 1fr);
}
.hero-featured {
grid-column: 1 / 3;
}
.secondary-featured {
grid-column: 3 / 6;
}
.featured-secondary-cards {
grid-template-columns: repeat(3, 1fr);
}
.sponsored-section {
grid-column: 1 / -1;
}
.sponsored-cards {
grid-template-columns: repeat(5, 1fr);
}
.main-content {
grid-template-columns: repeat(5, 1fr);
}
.apps-column {
grid-column: span 2;
}
.articles-column {
grid-column: span 2;
}
.more-apps-grid {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
}
/* Responsive - Mobile */
@media (max-width: 767px) {
.header-content {
flex-direction: column;
gap: 1rem;
}
.search-filter-bar {
flex-direction: column;
align-items: stretch;
}
.search-box {
max-width: none;
}
.magazine-layout {
padding: 0 1rem 2rem;
}
.footer-content {
grid-template-columns: 1fr;
}
.secondary-card {
flex-direction: column;
}
.secondary-image {
width: 100%;
height: 150px;
}
}

View File

@@ -0,0 +1,395 @@
// Marketplace JS - Magazine Layout
const API_BASE = '/api';
const CACHE_TTL = 3600000; // 1 hour in ms
class MarketplaceCache {
constructor() {
this.prefix = 'c4ai_market_';
}
get(key) {
const item = localStorage.getItem(this.prefix + key);
if (!item) return null;
const data = JSON.parse(item);
if (Date.now() > data.expires) {
localStorage.removeItem(this.prefix + key);
return null;
}
return data.value;
}
set(key, value, ttl = CACHE_TTL) {
const data = {
value: value,
expires: Date.now() + ttl
};
localStorage.setItem(this.prefix + key, JSON.stringify(data));
}
clear() {
Object.keys(localStorage)
.filter(k => k.startsWith(this.prefix))
.forEach(k => localStorage.removeItem(k));
}
}
class MarketplaceAPI {
constructor() {
this.cache = new MarketplaceCache();
this.searchTimeout = null;
}
async fetch(endpoint, useCache = true) {
const cacheKey = endpoint.replace(/[^\w]/g, '_');
if (useCache) {
const cached = this.cache.get(cacheKey);
if (cached) return cached;
}
try {
const response = await fetch(`${API_BASE}${endpoint}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
this.cache.set(cacheKey, data);
return data;
} catch (error) {
console.error('API Error:', error);
return null;
}
}
async getStats() {
return this.fetch('/stats');
}
async getCategories() {
return this.fetch('/categories');
}
async getApps(params = {}) {
const query = new URLSearchParams(params).toString();
return this.fetch(`/apps${query ? '?' + query : ''}`);
}
async getArticles(params = {}) {
const query = new URLSearchParams(params).toString();
return this.fetch(`/articles${query ? '?' + query : ''}`);
}
async getSponsors() {
return this.fetch('/sponsors');
}
async search(query) {
if (query.length < 2) return {};
return this.fetch(`/search?q=${encodeURIComponent(query)}`, false);
}
}
class MarketplaceUI {
constructor() {
this.api = new MarketplaceAPI();
this.currentCategory = 'all';
this.currentType = '';
this.searchTimeout = null;
this.loadedApps = 10;
this.init();
}
async init() {
await this.loadStats();
await this.loadCategories();
await this.loadFeaturedContent();
await this.loadSponsors();
await this.loadMainContent();
this.setupEventListeners();
}
async loadStats() {
const stats = await this.api.getStats();
if (stats) {
document.getElementById('total-apps').textContent = stats.total_apps || '0';
document.getElementById('total-articles').textContent = stats.total_articles || '0';
document.getElementById('total-downloads').textContent = stats.total_downloads || '0';
document.getElementById('last-update').textContent = new Date().toLocaleDateString();
}
}
async loadCategories() {
const categories = await this.api.getCategories();
if (!categories) return;
const filter = document.getElementById('category-filter');
categories.forEach(cat => {
const btn = document.createElement('button');
btn.className = 'filter-btn';
btn.dataset.category = cat.slug;
btn.textContent = cat.name;
btn.onclick = () => this.filterByCategory(cat.slug);
filter.appendChild(btn);
});
}
async loadFeaturedContent() {
// Load hero featured
const featured = await this.api.getApps({ featured: true, limit: 4 });
if (!featured || !featured.length) return;
// Hero card (first featured)
const hero = featured[0];
const heroCard = document.getElementById('featured-hero');
if (hero) {
const imageUrl = hero.image || '';
heroCard.innerHTML = `
<div class="hero-image" ${imageUrl ? `style="background-image: url('${imageUrl}')"` : ''}>
${!imageUrl ? `[${hero.category || 'APP'}]` : ''}
</div>
<div class="hero-content">
<span class="hero-badge">${hero.type || 'PAID'}</span>
<h2 class="hero-title">${hero.name}</h2>
<p class="hero-description">${hero.description}</p>
<div class="hero-meta">
<span>★ ${hero.rating || 0}/5</span>
<span>${hero.downloads || 0} downloads</span>
</div>
</div>
`;
heroCard.onclick = () => this.showAppDetail(hero);
}
// Secondary featured cards
const secondary = document.getElementById('featured-secondary');
secondary.innerHTML = '';
if (featured.length > 1) {
featured.slice(1, 4).forEach(app => {
const card = document.createElement('div');
card.className = 'secondary-card';
const imageUrl = app.image || '';
card.innerHTML = `
<div class="secondary-image" ${imageUrl ? `style="background-image: url('${imageUrl}')"` : ''}>
${!imageUrl ? `[${app.category || 'APP'}]` : ''}
</div>
<div class="secondary-content">
<h3 class="secondary-title">${app.name}</h3>
<p class="secondary-desc">${(app.description || '').substring(0, 100)}...</p>
<div class="secondary-meta">
<span>${app.type || 'Open Source'}</span> · <span>★ ${app.rating || 0}/5</span>
</div>
</div>
`;
card.onclick = () => this.showAppDetail(app);
secondary.appendChild(card);
});
}
}
async loadSponsors() {
const sponsors = await this.api.getSponsors();
if (!sponsors || !sponsors.length) {
// Show placeholder if no sponsors
const container = document.getElementById('sponsored-content');
container.innerHTML = `
<div class="sponsor-card">
<h4>Become a Sponsor</h4>
<p>Reach thousands of developers using Crawl4AI</p>
<a href="mailto:sponsors@crawl4ai.com">Contact Us →</a>
</div>
`;
return;
}
const container = document.getElementById('sponsored-content');
container.innerHTML = sponsors.slice(0, 5).map(sponsor => `
<div class="sponsor-card">
<h4>${sponsor.company_name}</h4>
<p>${sponsor.tier} Sponsor - Premium Solutions</p>
<a href="${sponsor.landing_url}" target="_blank">Learn More →</a>
</div>
`).join('');
}
async loadMainContent() {
// Load apps column
const apps = await this.api.getApps({ limit: 8 });
if (apps && apps.length) {
const appsGrid = document.getElementById('apps-grid');
appsGrid.innerHTML = apps.map(app => `
<div class="app-compact" onclick="marketplace.showAppDetail(${JSON.stringify(app).replace(/"/g, '&quot;')})">
<div class="app-compact-header">
<span>${app.category}</span>
<span>★ ${app.rating}/5</span>
</div>
<div class="app-compact-title">${app.name}</div>
<div class="app-compact-desc">${app.description}</div>
</div>
`).join('');
}
// Load articles column
const articles = await this.api.getArticles({ limit: 6 });
if (articles && articles.length) {
const articlesList = document.getElementById('articles-list');
articlesList.innerHTML = articles.map(article => `
<div class="article-compact" onclick="marketplace.showArticle('${article.id}')">
<div class="article-meta">
<span>${article.category}</span> · <span>${new Date(article.published_at).toLocaleDateString()}</span>
</div>
<div class="article-title">${article.title}</div>
<div class="article-author">by ${article.author}</div>
</div>
`).join('');
}
// Load trending
if (apps && apps.length) {
const trending = apps.slice(0, 5);
const trendingList = document.getElementById('trending-list');
trendingList.innerHTML = trending.map((app, i) => `
<div class="trending-item" onclick="marketplace.showAppDetail(${JSON.stringify(app).replace(/"/g, '&quot;')})">
<div class="trending-rank">${i + 1}</div>
<div class="trending-info">
<div class="trending-name">${app.name}</div>
<div class="trending-stats">${app.downloads} downloads</div>
</div>
</div>
`).join('');
}
// Load more apps grid
const moreApps = await this.api.getApps({ offset: 8, limit: 12 });
if (moreApps && moreApps.length) {
const moreGrid = document.getElementById('more-apps-grid');
moreGrid.innerHTML = moreApps.map(app => `
<div class="app-compact" onclick="marketplace.showAppDetail(${JSON.stringify(app).replace(/"/g, '&quot;')})">
<div class="app-compact-header">
<span>${app.category}</span>
<span>${app.type}</span>
</div>
<div class="app-compact-title">${app.name}</div>
</div>
`).join('');
}
}
setupEventListeners() {
// Search
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', (e) => {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => this.search(e.target.value), 300);
});
// Keyboard shortcut
document.addEventListener('keydown', (e) => {
if (e.key === '/' && !searchInput.contains(document.activeElement)) {
e.preventDefault();
searchInput.focus();
}
if (e.key === 'Escape' && searchInput.contains(document.activeElement)) {
searchInput.blur();
searchInput.value = '';
}
});
// Type filter
const typeFilter = document.getElementById('type-filter');
typeFilter.addEventListener('change', (e) => {
this.currentType = e.target.value;
this.loadMainContent();
});
// Load more
const loadMore = document.getElementById('load-more');
loadMore.addEventListener('click', () => this.loadMoreApps());
}
async filterByCategory(category) {
// Update active state
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.category === category);
});
this.currentCategory = category;
await this.loadMainContent();
}
async search(query) {
if (!query) {
await this.loadMainContent();
return;
}
const results = await this.api.search(query);
if (!results) return;
// Update apps grid with search results
if (results.apps && results.apps.length) {
const appsGrid = document.getElementById('apps-grid');
appsGrid.innerHTML = results.apps.map(app => `
<div class="app-compact" onclick="marketplace.showAppDetail(${JSON.stringify(app).replace(/"/g, '&quot;')})">
<div class="app-compact-header">
<span>${app.category}</span>
<span>★ ${app.rating}/5</span>
</div>
<div class="app-compact-title">${app.name}</div>
<div class="app-compact-desc">${app.description}</div>
</div>
`).join('');
}
// Update articles with search results
if (results.articles && results.articles.length) {
const articlesList = document.getElementById('articles-list');
articlesList.innerHTML = results.articles.map(article => `
<div class="article-compact" onclick="marketplace.showArticle('${article.id}')">
<div class="article-meta">
<span>${article.category}</span> · <span>${new Date(article.published_at).toLocaleDateString()}</span>
</div>
<div class="article-title">${article.title}</div>
<div class="article-author">by ${article.author}</div>
</div>
`).join('');
}
}
async loadMoreApps() {
this.loadedApps += 12;
const moreApps = await this.api.getApps({ offset: this.loadedApps, limit: 12 });
if (moreApps && moreApps.length) {
const moreGrid = document.getElementById('more-apps-grid');
moreApps.forEach(app => {
const card = document.createElement('div');
card.className = 'app-compact';
card.innerHTML = `
<div class="app-compact-header">
<span>${app.category}</span>
<span>${app.type}</span>
</div>
<div class="app-compact-title">${app.name}</div>
`;
card.onclick = () => this.showAppDetail(app);
moreGrid.appendChild(card);
});
}
}
showAppDetail(app) {
// Navigate to detail page instead of showing modal
const slug = app.slug || app.name.toLowerCase().replace(/\s+/g, '-');
window.location.href = `app-detail.html?app=${slug}`;
}
showArticle(articleId) {
// Could create article detail page similarly
console.log('Show article:', articleId);
}
}
// Initialize marketplace
let marketplace;
document.addEventListener('DOMContentLoaded', () => {
marketplace = new MarketplaceUI();
});