diff --git a/.gitignore b/.gitignore
index 710e1340..a5389a3e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,13 @@
# Scripts folder (private tools)
.scripts/
+# Database files
+*.db
+
+# Environment files
+.env
+.env.local
+
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
diff --git a/docs/md_v2/assets/images/logo.png b/docs/md_v2/assets/images/logo.png
new file mode 100644
index 00000000..ed82a3cc
Binary files /dev/null and b/docs/md_v2/assets/images/logo.png differ
diff --git a/docs/md_v2/marketplace/README.md b/docs/md_v2/marketplace/README.md
new file mode 100644
index 00000000..75e1b5c6
--- /dev/null
+++ b/docs/md_v2/marketplace/README.md
@@ -0,0 +1,66 @@
+# Crawl4AI Marketplace
+
+A terminal-themed marketplace for tools, integrations, and resources related to Crawl4AI.
+
+## Setup
+
+### Backend
+
+1. Install dependencies:
+```bash
+cd backend
+pip install -r requirements.txt
+```
+
+2. Generate dummy data:
+```bash
+python dummy_data.py
+```
+
+3. Run the server:
+```bash
+python server.py
+```
+
+The API will be available at http://localhost:8100
+
+### Frontend
+
+1. Open `frontend/index.html` in your browser
+2. Or serve via MkDocs as part of the documentation site
+
+## Database Schema
+
+The marketplace uses SQLite with automatic migration from `schema.yaml`. Tables include:
+- **apps**: Tools and integrations
+- **articles**: Reviews, tutorials, and news
+- **categories**: App categories
+- **sponsors**: Sponsored content
+
+## API Endpoints
+
+- `GET /api/apps` - List apps with filters
+- `GET /api/articles` - List articles
+- `GET /api/categories` - Get all categories
+- `GET /api/sponsors` - Get active sponsors
+- `GET /api/search?q=query` - Search across content
+- `GET /api/stats` - Marketplace statistics
+
+## Features
+
+- **Smart caching**: LocalStorage with TTL (1 hour)
+- **Terminal theme**: Consistent with Crawl4AI branding
+- **Responsive design**: Works on all devices
+- **Fast search**: Debounced with 300ms delay
+- **CORS protected**: Only crawl4ai.com and localhost
+
+## Admin Panel
+
+Coming soon - for now, edit the database directly or modify `dummy_data.py`
+
+## Deployment
+
+For production deployment on EC2:
+1. Update `API_BASE` in `marketplace.js` to production URL
+2. Run FastAPI with proper production settings (use gunicorn/uvicorn)
+3. Set up nginx proxy if needed
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/admin/admin.css b/docs/md_v2/marketplace/admin/admin.css
new file mode 100644
index 00000000..7296a801
--- /dev/null
+++ b/docs/md_v2/marketplace/admin/admin.css
@@ -0,0 +1,650 @@
+/* Admin Dashboard - C4AI Terminal Style */
+
+/* Utility Classes */
+.hidden {
+ display: none !important;
+}
+
+/* Brand Colors */
+:root {
+ --c4ai-cyan: #50ffff;
+ --c4ai-green: #50ff50;
+ --c4ai-yellow: #ffff50;
+ --c4ai-pink: #ff50ff;
+ --c4ai-blue: #5050ff;
+}
+
+.admin-container {
+ min-height: 100vh;
+ background: var(--bg-dark);
+}
+
+/* Login Screen */
+.login-screen {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, #070708 0%, #1a1a2e 100%);
+}
+
+.login-box {
+ background: var(--bg-secondary);
+ border: 2px solid var(--primary-cyan);
+ padding: 3rem;
+ width: 400px;
+ box-shadow: 0 0 40px rgba(80, 255, 255, 0.2);
+ text-align: center;
+}
+
+.login-logo {
+ height: 60px;
+ margin-bottom: 2rem;
+ filter: brightness(1.2);
+}
+
+.login-box h1 {
+ color: var(--primary-cyan);
+ font-size: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+#login-form input {
+ width: 100%;
+ padding: 0.75rem;
+ background: var(--bg-dark);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: inherit;
+ margin-bottom: 1rem;
+}
+
+#login-form input:focus {
+ outline: none;
+ border-color: var(--primary-cyan);
+}
+
+#login-form button {
+ width: 100%;
+ padding: 0.75rem;
+ background: linear-gradient(135deg, var(--primary-cyan), var(--primary-teal));
+ border: none;
+ color: var(--bg-dark);
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+#login-form button:hover {
+ box-shadow: 0 4px 15px rgba(80, 255, 255, 0.3);
+ transform: translateY(-2px);
+}
+
+.error-msg {
+ color: var(--error);
+ font-size: 0.875rem;
+ margin-top: 1rem;
+}
+
+/* Admin Dashboard */
+.admin-dashboard.hidden {
+ display: none;
+}
+
+.admin-header {
+ background: var(--bg-secondary);
+ border-bottom: 2px solid var(--primary-cyan);
+ padding: 1rem 0;
+}
+
+.header-content {
+ max-width: 1800px;
+ margin: 0 auto;
+ padding: 0 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.header-left {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.header-logo {
+ height: 35px;
+}
+
+.admin-header h1 {
+ font-size: 1.25rem;
+ color: var(--primary-cyan);
+}
+
+.header-right {
+ display: flex;
+ align-items: center;
+ gap: 2rem;
+}
+
+.admin-user {
+ color: var(--text-secondary);
+}
+
+.logout-btn {
+ padding: 0.5rem 1rem;
+ background: transparent;
+ border: 1px solid var(--error);
+ color: var(--error);
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.logout-btn:hover {
+ background: rgba(255, 60, 116, 0.1);
+}
+
+/* Layout */
+.admin-layout {
+ display: flex;
+ max-width: 1800px;
+ margin: 0 auto;
+ min-height: calc(100vh - 60px);
+}
+
+/* Sidebar */
+.admin-sidebar {
+ width: 250px;
+ background: var(--bg-secondary);
+ border-right: 1px solid var(--border-color);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+.sidebar-nav {
+ padding: 1rem 0;
+}
+
+.nav-btn {
+ width: 100%;
+ padding: 1rem 1.5rem;
+ background: transparent;
+ border: none;
+ border-left: 3px solid transparent;
+ color: var(--text-secondary);
+ text-align: left;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.nav-btn:hover {
+ background: rgba(80, 255, 255, 0.05);
+ color: var(--primary-cyan);
+}
+
+.nav-btn.active {
+ border-left-color: var(--primary-cyan);
+ background: rgba(80, 255, 255, 0.1);
+ color: var(--primary-cyan);
+}
+
+.nav-icon {
+ font-size: 1.25rem;
+ margin-right: 0.25rem;
+ display: inline-block;
+ width: 1.5rem;
+ text-align: center;
+}
+
+.nav-btn[data-section="stats"] .nav-icon {
+ color: var(--c4ai-cyan);
+}
+
+.nav-btn[data-section="apps"] .nav-icon {
+ color: var(--c4ai-green);
+}
+
+.nav-btn[data-section="articles"] .nav-icon {
+ color: var(--c4ai-yellow);
+}
+
+.nav-btn[data-section="categories"] .nav-icon {
+ color: var(--c4ai-pink);
+}
+
+.nav-btn[data-section="sponsors"] .nav-icon {
+ color: var(--c4ai-blue);
+}
+
+.sidebar-actions {
+ padding: 1rem;
+ border-top: 1px solid var(--border-color);
+}
+
+.action-btn {
+ width: 100%;
+ padding: 0.75rem;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-secondary);
+ cursor: pointer;
+ margin-bottom: 0.5rem;
+ transition: all 0.2s;
+}
+
+.action-btn:hover {
+ border-color: var(--primary-cyan);
+ color: var(--primary-cyan);
+}
+
+/* Main Content */
+.admin-main {
+ flex: 1;
+ padding: 2rem;
+ overflow-y: auto;
+}
+
+.content-section {
+ display: none;
+}
+
+.content-section.active {
+ display: block;
+}
+
+/* Stats Grid */
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 3rem;
+}
+
+.stat-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);
+ padding: 1.5rem;
+ display: flex;
+ gap: 1.5rem;
+}
+
+.stat-icon {
+ font-size: 2rem;
+ width: 3rem;
+ height: 3rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 2px solid;
+ border-radius: 4px;
+}
+
+.stat-card:nth-child(1) .stat-icon {
+ color: var(--c4ai-cyan);
+ border-color: var(--c4ai-cyan);
+}
+
+.stat-card:nth-child(2) .stat-icon {
+ color: var(--c4ai-green);
+ border-color: var(--c4ai-green);
+}
+
+.stat-card:nth-child(3) .stat-icon {
+ color: var(--c4ai-yellow);
+ border-color: var(--c4ai-yellow);
+}
+
+.stat-card:nth-child(4) .stat-icon {
+ color: var(--c4ai-pink);
+ border-color: var(--c4ai-pink);
+}
+
+.stat-number {
+ font-size: 2rem;
+ color: var(--primary-cyan);
+ font-weight: 600;
+}
+
+.stat-label {
+ color: var(--text-secondary);
+}
+
+.stat-detail {
+ font-size: 0.875rem;
+ color: var(--text-tertiary);
+ margin-top: 0.5rem;
+}
+
+/* Quick Actions */
+.quick-actions {
+ display: flex;
+ gap: 1rem;
+}
+
+.quick-btn {
+ padding: 0.75rem 1.5rem;
+ background: transparent;
+ border: 1px solid var(--primary-cyan);
+ color: var(--primary-cyan);
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.quick-btn:hover {
+ background: rgba(80, 255, 255, 0.1);
+ transform: translateY(-2px);
+}
+
+/* Section Headers */
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+}
+
+.section-header h2 {
+ font-size: 1.5rem;
+ color: var(--text-primary);
+}
+
+.header-actions {
+ display: flex;
+ gap: 1rem;
+}
+
+.search-input {
+ padding: 0.5rem 1rem;
+ background: var(--bg-dark);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ width: 250px;
+}
+
+.search-input:focus {
+ outline: none;
+ border-color: var(--primary-cyan);
+}
+
+.filter-select {
+ padding: 0.5rem;
+ background: var(--bg-dark);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+}
+
+.add-btn {
+ padding: 0.5rem 1rem;
+ background: linear-gradient(135deg, var(--primary-cyan), var(--primary-teal));
+ border: none;
+ color: var(--bg-dark);
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.add-btn:hover {
+ box-shadow: 0 4px 15px rgba(80, 255, 255, 0.3);
+ transform: translateY(-2px);
+}
+
+/* Data Tables */
+.data-table {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ overflow-x: auto;
+}
+
+.data-table table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.data-table th {
+ background: var(--bg-tertiary);
+ padding: 1rem;
+ text-align: left;
+ color: var(--primary-cyan);
+ font-weight: 600;
+ border-bottom: 2px solid var(--border-color);
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+.data-table td {
+ padding: 1rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.data-table tr:hover {
+ background: rgba(80, 255, 255, 0.03);
+}
+
+/* Table Actions */
+.table-actions {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.btn-edit, .btn-delete, .btn-duplicate {
+ padding: 0.25rem 0.5rem;
+ background: transparent;
+ border: 1px solid var(--border-color);
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 0.875rem;
+}
+
+.btn-edit:hover {
+ border-color: var(--primary-cyan);
+ color: var(--primary-cyan);
+}
+
+.btn-delete:hover {
+ border-color: var(--error);
+ color: var(--error);
+}
+
+.btn-duplicate:hover {
+ border-color: var(--accent-pink);
+ color: var(--accent-pink);
+}
+
+/* Badges in Tables */
+.badge {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+}
+
+.badge.featured {
+ background: var(--primary-cyan);
+ color: var(--bg-dark);
+}
+
+.badge.sponsored {
+ background: var(--warning);
+ color: var(--bg-dark);
+}
+
+.badge.active {
+ background: var(--success);
+ color: var(--bg-dark);
+}
+
+/* Modal Enhancements */
+.modal-content.large {
+ max-width: 1000px;
+ width: 90%;
+ max-height: 90vh;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.5rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.modal-body {
+ padding: 1.5rem;
+ overflow-y: auto;
+ max-height: calc(90vh - 140px);
+}
+
+.modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 1rem;
+ padding: 1rem 1.5rem;
+ border-top: 1px solid var(--border-color);
+}
+
+.btn-cancel, .btn-save {
+ padding: 0.5rem 1.5rem;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-cancel {
+ background: transparent;
+ border: 1px solid var(--border-color);
+ color: var(--text-secondary);
+}
+
+.btn-cancel:hover {
+ border-color: var(--error);
+ color: var(--error);
+}
+
+.btn-save {
+ background: linear-gradient(135deg, var(--primary-cyan), var(--primary-teal));
+ border: none;
+ color: var(--bg-dark);
+ font-weight: 600;
+}
+
+.btn-save:hover {
+ box-shadow: 0 4px 15px rgba(80, 255, 255, 0.3);
+}
+
+/* Form Styles */
+.form-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1.5rem;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.form-group label {
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+}
+
+.form-group input,
+.form-group select,
+.form-group textarea {
+ padding: 0.5rem;
+ background: var(--bg-dark);
+ border: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-family: inherit;
+}
+
+.form-group input:focus,
+.form-group select:focus,
+.form-group textarea:focus {
+ outline: none;
+ border-color: var(--primary-cyan);
+}
+
+.form-group.full-width {
+ grid-column: 1 / -1;
+}
+
+.checkbox-group {
+ display: flex;
+ gap: 2rem;
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
+}
+
+/* Rich Text Editor */
+.editor-toolbar {
+ display: flex;
+ gap: 0.5rem;
+ padding: 0.5rem;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ border-bottom: none;
+}
+
+.editor-btn {
+ padding: 0.25rem 0.5rem;
+ background: transparent;
+ border: 1px solid var(--border-color);
+ color: var(--text-secondary);
+ cursor: pointer;
+}
+
+.editor-btn:hover {
+ background: rgba(80, 255, 255, 0.1);
+ border-color: var(--primary-cyan);
+}
+
+.editor-content {
+ min-height: 300px;
+ padding: 1rem;
+ background: var(--bg-dark);
+ border: 1px solid var(--border-color);
+ font-family: 'Dank Mono', Monaco, monospace;
+}
+
+/* Responsive */
+@media (max-width: 1024px) {
+ .admin-layout {
+ flex-direction: column;
+ }
+
+ .admin-sidebar {
+ width: 100%;
+ border-right: none;
+ border-bottom: 1px solid var(--border-color);
+ }
+
+ .sidebar-nav {
+ display: flex;
+ overflow-x: auto;
+ padding: 0;
+ }
+
+ .nav-btn {
+ border-left: none;
+ border-bottom: 3px solid transparent;
+ white-space: nowrap;
+ }
+
+ .nav-btn.active {
+ border-bottom-color: var(--primary-cyan);
+ }
+
+ .sidebar-actions {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/admin/admin.js b/docs/md_v2/marketplace/admin/admin.js
new file mode 100644
index 00000000..861d3ba7
--- /dev/null
+++ b/docs/md_v2/marketplace/admin/admin.js
@@ -0,0 +1,757 @@
+// Admin Dashboard - Smart & Powerful
+const API_BASE = 'http://localhost:8100/api';
+
+class AdminDashboard {
+ constructor() {
+ this.token = localStorage.getItem('admin_token');
+ this.currentSection = 'stats';
+ this.data = {
+ apps: [],
+ articles: [],
+ categories: [],
+ sponsors: []
+ };
+ this.editingItem = null;
+ this.init();
+ }
+
+ async init() {
+ // Check auth
+ if (!this.token) {
+ this.showLogin();
+ return;
+ }
+
+ // Try to load stats to verify token
+ try {
+ await this.loadStats();
+ this.showDashboard();
+ this.setupEventListeners();
+ await this.loadAllData();
+ } catch (error) {
+ if (error.status === 401) {
+ this.showLogin();
+ }
+ }
+ }
+
+ showLogin() {
+ document.getElementById('login-screen').classList.remove('hidden');
+ document.getElementById('admin-dashboard').classList.add('hidden');
+
+ // Set up login button click handler
+ const loginBtn = document.getElementById('login-btn');
+ if (loginBtn) {
+ loginBtn.onclick = async () => {
+ const password = document.getElementById('password').value;
+ await this.login(password);
+ };
+ }
+ }
+
+ async login(password) {
+ try {
+ const response = await fetch(`${API_BASE}/admin/login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ password })
+ });
+
+ if (!response.ok) throw new Error('Invalid password');
+
+ const data = await response.json();
+ this.token = data.token;
+ localStorage.setItem('admin_token', this.token);
+
+ document.getElementById('login-screen').classList.add('hidden');
+ this.showDashboard();
+ this.setupEventListeners();
+ await this.loadAllData();
+ } catch (error) {
+ document.getElementById('login-error').textContent = 'Invalid password';
+ document.getElementById('password').value = '';
+ }
+ }
+
+ showDashboard() {
+ document.getElementById('login-screen').classList.add('hidden');
+ document.getElementById('admin-dashboard').classList.remove('hidden');
+ }
+
+ setupEventListeners() {
+ // Navigation
+ document.querySelectorAll('.nav-btn').forEach(btn => {
+ btn.onclick = () => this.switchSection(btn.dataset.section);
+ });
+
+ // Logout
+ document.getElementById('logout-btn').onclick = () => this.logout();
+
+ // Export/Backup
+ document.getElementById('export-btn').onclick = () => this.exportData();
+ document.getElementById('backup-btn').onclick = () => this.backupDatabase();
+
+ // Search
+ ['apps', 'articles'].forEach(type => {
+ const searchInput = document.getElementById(`${type}-search`);
+ if (searchInput) {
+ searchInput.oninput = (e) => this.filterTable(type, e.target.value);
+ }
+ });
+
+ // Category filter
+ const categoryFilter = document.getElementById('apps-filter');
+ if (categoryFilter) {
+ categoryFilter.onchange = (e) => this.filterByCategory(e.target.value);
+ }
+
+ // Save button in modal
+ document.getElementById('save-btn').onclick = () => this.saveItem();
+ }
+
+ async loadAllData() {
+ try {
+ await this.loadStats();
+ } catch (e) {
+ console.error('Failed to load stats:', e);
+ }
+
+ try {
+ await this.loadApps();
+ } catch (e) {
+ console.error('Failed to load apps:', e);
+ }
+
+ try {
+ await this.loadArticles();
+ } catch (e) {
+ console.error('Failed to load articles:', e);
+ }
+
+ try {
+ await this.loadCategories();
+ } catch (e) {
+ console.error('Failed to load categories:', e);
+ }
+
+ try {
+ await this.loadSponsors();
+ } catch (e) {
+ console.error('Failed to load sponsors:', e);
+ }
+
+ this.populateCategoryFilter();
+ }
+
+ async apiCall(endpoint, options = {}) {
+ const response = await fetch(`${API_BASE}${endpoint}`, {
+ ...options,
+ headers: {
+ 'Authorization': `Bearer ${this.token}`,
+ 'Content-Type': 'application/json',
+ ...options.headers
+ }
+ });
+
+ if (response.status === 401) {
+ this.logout();
+ throw { status: 401 };
+ }
+
+ if (!response.ok) throw new Error(`API Error: ${response.status}`);
+ return response.json();
+ }
+
+ async loadStats() {
+ const stats = await this.apiCall('/admin/stats');
+
+ document.getElementById('stat-apps').textContent = stats.apps.total;
+ document.getElementById('stat-featured').textContent = stats.apps.featured;
+ document.getElementById('stat-sponsored').textContent = stats.apps.sponsored;
+ document.getElementById('stat-articles').textContent = stats.articles;
+ document.getElementById('stat-sponsors').textContent = stats.sponsors.active;
+ document.getElementById('stat-views').textContent = this.formatNumber(stats.total_views);
+ }
+
+ async loadApps() {
+ this.data.apps = await this.apiCall('/apps?limit=100');
+ this.renderAppsTable(this.data.apps);
+ }
+
+ async loadArticles() {
+ this.data.articles = await this.apiCall('/articles?limit=100');
+ this.renderArticlesTable(this.data.articles);
+ }
+
+ async loadCategories() {
+ this.data.categories = await this.apiCall('/categories');
+ this.renderCategoriesTable(this.data.categories);
+ }
+
+ async loadSponsors() {
+ this.data.sponsors = await this.apiCall('/sponsors');
+ this.renderSponsorsTable(this.data.sponsors);
+ }
+
+ renderAppsTable(apps) {
+ const table = document.getElementById('apps-table');
+ table.innerHTML = `
+
+
+
+ | ID |
+ Name |
+ Category |
+ Type |
+ Rating |
+ Downloads |
+ Status |
+ Actions |
+
+
+
+ ${apps.map(app => `
+
+ | ${app.id} |
+ ${app.name} |
+ ${app.category} |
+ ${app.type} |
+ โ ${app.rating}/5 |
+ ${this.formatNumber(app.downloads)} |
+
+ ${app.featured ? 'Featured' : ''}
+ ${app.sponsored ? '' : ''}
+ |
+
+
+
+
+
+
+ |
+
+ `).join('')}
+
+
+ `;
+ }
+
+ renderArticlesTable(articles) {
+ const table = document.getElementById('articles-table');
+ table.innerHTML = `
+
+
+
+ | ID |
+ Title |
+ Category |
+ Author |
+ Published |
+ Views |
+ Actions |
+
+
+
+ ${articles.map(article => `
+
+ | ${article.id} |
+ ${article.title} |
+ ${article.category} |
+ ${article.author} |
+ ${new Date(article.published_date).toLocaleDateString()} |
+ ${this.formatNumber(article.views)} |
+
+
+
+
+
+
+ |
+
+ `).join('')}
+
+
+ `;
+ }
+
+ renderCategoriesTable(categories) {
+ const table = document.getElementById('categories-table');
+ table.innerHTML = `
+
+
+
+ | Order |
+ Icon |
+ Name |
+ Description |
+ Actions |
+
+
+
+ ${categories.map(cat => `
+
+ | ${cat.order_index} |
+ ${cat.icon} |
+ ${cat.name} |
+ ${cat.description} |
+
+
+
+
+
+ |
+
+ `).join('')}
+
+
+ `;
+ }
+
+ renderSponsorsTable(sponsors) {
+ const table = document.getElementById('sponsors-table');
+ table.innerHTML = `
+
+
+
+ | ID |
+ Company |
+ Tier |
+ Start |
+ End |
+ Status |
+ Actions |
+
+
+
+ ${sponsors.map(sponsor => `
+
+ | ${sponsor.id} |
+ ${sponsor.company_name} |
+ ${sponsor.tier} |
+ ${new Date(sponsor.start_date).toLocaleDateString()} |
+ ${new Date(sponsor.end_date).toLocaleDateString()} |
+ ${sponsor.active ? 'Active' : 'Inactive'} |
+
+
+
+
+
+ |
+
+ `).join('')}
+
+
+ `;
+ }
+
+ showAddForm(type) {
+ this.editingItem = null;
+ this.showModal(type, null);
+ }
+
+ async editItem(type, id) {
+ const item = this.data[type].find(i => i.id === id);
+ if (item) {
+ this.editingItem = item;
+ this.showModal(type, item);
+ }
+ }
+
+ async duplicateItem(type, id) {
+ const item = this.data[type].find(i => i.id === id);
+ if (item) {
+ const newItem = { ...item };
+ delete newItem.id;
+ newItem.name = `${newItem.name || newItem.title} (Copy)`;
+ if (newItem.slug) newItem.slug = `${newItem.slug}-copy-${Date.now()}`;
+
+ this.editingItem = null;
+ this.showModal(type, newItem);
+ }
+ }
+
+ showModal(type, item) {
+ const modal = document.getElementById('form-modal');
+ const title = document.getElementById('modal-title');
+ const body = document.getElementById('modal-body');
+
+ title.textContent = item ? `Edit ${type.slice(0, -1)}` : `Add New ${type.slice(0, -1)}`;
+
+ if (type === 'apps') {
+ body.innerHTML = this.getAppForm(item);
+ } else if (type === 'articles') {
+ body.innerHTML = this.getArticleForm(item);
+ } else if (type === 'categories') {
+ body.innerHTML = this.getCategoryForm(item);
+ } else if (type === 'sponsors') {
+ body.innerHTML = this.getSponsorForm(item);
+ }
+
+ modal.classList.remove('hidden');
+ modal.dataset.type = type;
+ }
+
+ getAppForm(app) {
+ return `
+
+ `;
+ }
+
+ getArticleForm(article) {
+ return `
+
+ `;
+ }
+
+ getCategoryForm(category) {
+ return `
+
+ `;
+ }
+
+ getSponsorForm(sponsor) {
+ return `
+
+ `;
+ }
+
+ async saveItem() {
+ const modal = document.getElementById('form-modal');
+ const type = modal.dataset.type;
+ const data = this.collectFormData(type);
+
+ try {
+ if (this.editingItem) {
+ await this.apiCall(`/admin/${type}/${this.editingItem.id}`, {
+ method: 'PUT',
+ body: JSON.stringify(data)
+ });
+ } else {
+ await this.apiCall(`/admin/${type}`, {
+ method: 'POST',
+ body: JSON.stringify(data)
+ });
+ }
+
+ this.closeModal();
+ await this[`load${type.charAt(0).toUpperCase() + type.slice(1)}`]();
+ await this.loadStats();
+ } catch (error) {
+ alert('Error saving item: ' + error.message);
+ }
+ }
+
+ collectFormData(type) {
+ const data = {};
+
+ if (type === 'apps') {
+ data.name = document.getElementById('form-name').value;
+ data.slug = document.getElementById('form-slug').value || this.generateSlug(data.name);
+ data.description = document.getElementById('form-description').value;
+ data.category = document.getElementById('form-category').value;
+ data.type = document.getElementById('form-type').value;
+ data.rating = parseFloat(document.getElementById('form-rating').value);
+ data.downloads = parseInt(document.getElementById('form-downloads').value);
+ data.image = document.getElementById('form-image').value;
+ data.website_url = document.getElementById('form-website').value;
+ data.github_url = document.getElementById('form-github').value;
+ data.pricing = document.getElementById('form-pricing').value;
+ data.contact_email = document.getElementById('form-email').value;
+ data.featured = document.getElementById('form-featured').checked ? 1 : 0;
+ data.sponsored = document.getElementById('form-sponsored').checked ? 1 : 0;
+ data.integration_guide = document.getElementById('form-integration').value;
+ } else if (type === 'articles') {
+ data.title = document.getElementById('form-title').value;
+ data.slug = this.generateSlug(data.title);
+ data.author = document.getElementById('form-author').value;
+ data.category = document.getElementById('form-category').value;
+ data.featured_image = document.getElementById('form-image').value;
+ data.content = document.getElementById('form-content').value;
+ } else if (type === 'categories') {
+ data.name = document.getElementById('form-name').value;
+ data.slug = this.generateSlug(data.name);
+ data.icon = document.getElementById('form-icon').value;
+ data.description = document.getElementById('form-description').value;
+ data.order_index = parseInt(document.getElementById('form-order').value);
+ } else if (type === 'sponsors') {
+ data.company_name = document.getElementById('form-name').value;
+ data.tier = document.getElementById('form-tier').value;
+ data.landing_url = document.getElementById('form-landing').value;
+ data.banner_url = document.getElementById('form-banner').value;
+ data.start_date = document.getElementById('form-start').value;
+ data.end_date = document.getElementById('form-end').value;
+ data.active = document.getElementById('form-active').checked ? 1 : 0;
+ }
+
+ return data;
+ }
+
+ async deleteItem(type, id) {
+ if (!confirm(`Are you sure you want to delete this ${type.slice(0, -1)}?`)) return;
+
+ try {
+ await this.apiCall(`/admin/${type}/${id}`, { method: 'DELETE' });
+ await this[`load${type.charAt(0).toUpperCase() + type.slice(1)}`]();
+ await this.loadStats();
+ } catch (error) {
+ alert('Error deleting item: ' + error.message);
+ }
+ }
+
+ async deleteCategory(id) {
+ const hasApps = this.data.apps.some(app =>
+ app.category === this.data.categories.find(c => c.id === id)?.name
+ );
+
+ if (hasApps) {
+ alert('Cannot delete category with existing apps');
+ return;
+ }
+
+ await this.deleteItem('categories', id);
+ }
+
+ closeModal() {
+ document.getElementById('form-modal').classList.add('hidden');
+ this.editingItem = null;
+ }
+
+ switchSection(section) {
+ // Update navigation
+ document.querySelectorAll('.nav-btn').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.section === section);
+ });
+
+ // Show section
+ document.querySelectorAll('.content-section').forEach(sec => {
+ sec.classList.remove('active');
+ });
+ document.getElementById(`${section}-section`).classList.add('active');
+
+ this.currentSection = section;
+ }
+
+ filterTable(type, query) {
+ const items = this.data[type].filter(item => {
+ const searchText = Object.values(item).join(' ').toLowerCase();
+ return searchText.includes(query.toLowerCase());
+ });
+
+ if (type === 'apps') {
+ this.renderAppsTable(items);
+ } else if (type === 'articles') {
+ this.renderArticlesTable(items);
+ }
+ }
+
+ filterByCategory(category) {
+ const apps = category
+ ? this.data.apps.filter(app => app.category === category)
+ : this.data.apps;
+ this.renderAppsTable(apps);
+ }
+
+ populateCategoryFilter() {
+ const filter = document.getElementById('apps-filter');
+ if (!filter) return;
+
+ filter.innerHTML = '';
+ this.data.categories.forEach(cat => {
+ filter.innerHTML += ``;
+ });
+ }
+
+ async exportData() {
+ const data = {
+ apps: this.data.apps,
+ articles: this.data.articles,
+ categories: this.data.categories,
+ sponsors: this.data.sponsors,
+ exported: new Date().toISOString()
+ };
+
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `marketplace-export-${Date.now()}.json`;
+ a.click();
+ }
+
+ async backupDatabase() {
+ // In production, this would download the SQLite file
+ alert('Database backup would be implemented on the server side');
+ }
+
+ generateSlug(text) {
+ return text.toLowerCase()
+ .replace(/[^\w\s-]/g, '')
+ .replace(/\s+/g, '-')
+ .replace(/-+/g, '-')
+ .trim();
+ }
+
+ formatNumber(num) {
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
+ return num.toString();
+ }
+
+ logout() {
+ localStorage.removeItem('admin_token');
+ this.token = null;
+ this.showLogin();
+ }
+}
+
+// Initialize
+const admin = new AdminDashboard();
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/admin/index.html b/docs/md_v2/marketplace/admin/index.html
new file mode 100644
index 00000000..3691c1eb
--- /dev/null
+++ b/docs/md_v2/marketplace/admin/index.html
@@ -0,0 +1,215 @@
+
+
+
+
+
+ Admin Dashboard - Crawl4AI Marketplace
+
+
+
+
+
+
+
+
+

+
[ Admin Access ]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dashboard Overview
+
+
+
โ
+
+
--
+
Total Apps
+
+ -- featured,
+ sponsored
+
+
+
+
+
+
+
+
+ Quick Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/backend/.env.example b/docs/md_v2/marketplace/backend/.env.example
new file mode 100644
index 00000000..7d46f19c
--- /dev/null
+++ b/docs/md_v2/marketplace/backend/.env.example
@@ -0,0 +1,14 @@
+# Marketplace Configuration
+# Copy this to .env and update with your values
+
+# Admin password (required)
+MARKETPLACE_ADMIN_PASSWORD=change_this_password
+
+# JWT secret key (required) - generate with: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
+MARKETPLACE_JWT_SECRET=change_this_to_a_secure_random_key
+
+# Database path (optional, defaults to ./marketplace.db)
+MARKETPLACE_DB_PATH=./marketplace.db
+
+# Token expiry in hours (optional, defaults to 4)
+MARKETPLACE_TOKEN_EXPIRY=4
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/backend/config.py b/docs/md_v2/marketplace/backend/config.py
new file mode 100644
index 00000000..29bb55d6
--- /dev/null
+++ b/docs/md_v2/marketplace/backend/config.py
@@ -0,0 +1,59 @@
+"""
+Marketplace Configuration - Loads from .env file
+"""
+import os
+import sys
+import hashlib
+from pathlib import Path
+from dotenv import load_dotenv
+
+# Load .env file
+env_path = Path(__file__).parent / '.env'
+if not env_path.exists():
+ print("\nโ ERROR: No .env file found!")
+ print("Please copy .env.example to .env and update with your values:")
+ print(f" cp {Path(__file__).parent}/.env.example {Path(__file__).parent}/.env")
+ print("\nThen edit .env with your secure values.")
+ sys.exit(1)
+
+load_dotenv(env_path)
+
+# Required environment variables
+required_vars = ['MARKETPLACE_ADMIN_PASSWORD', 'MARKETPLACE_JWT_SECRET']
+missing_vars = [var for var in required_vars if not os.getenv(var)]
+
+if missing_vars:
+ print(f"\nโ ERROR: Missing required environment variables: {', '.join(missing_vars)}")
+ print("Please check your .env file and ensure all required variables are set.")
+ sys.exit(1)
+
+class Config:
+ """Configuration loaded from environment variables"""
+
+ # Admin authentication - hashed from password in .env
+ ADMIN_PASSWORD_HASH = hashlib.sha256(
+ os.getenv('MARKETPLACE_ADMIN_PASSWORD').encode()
+ ).hexdigest()
+
+ # JWT secret for token generation
+ JWT_SECRET_KEY = os.getenv('MARKETPLACE_JWT_SECRET')
+
+ # Database path
+ DATABASE_PATH = os.getenv('MARKETPLACE_DB_PATH', './marketplace.db')
+
+ # Token expiry in hours
+ TOKEN_EXPIRY_HOURS = int(os.getenv('MARKETPLACE_TOKEN_EXPIRY', '4'))
+
+ # CORS origins - hardcoded as they don't contain secrets
+ ALLOWED_ORIGINS = [
+ "http://localhost:8000",
+ "http://localhost:8080",
+ "http://localhost:8100",
+ "http://127.0.0.1:8000",
+ "http://127.0.0.1:8080",
+ "http://127.0.0.1:8100",
+ "https://crawl4ai.com",
+ "https://www.crawl4ai.com",
+ "https://docs.crawl4ai.com",
+ "https://market.crawl4ai.com"
+ ]
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/backend/database.py b/docs/md_v2/marketplace/backend/database.py
new file mode 100644
index 00000000..8ccfbaf4
--- /dev/null
+++ b/docs/md_v2/marketplace/backend/database.py
@@ -0,0 +1,117 @@
+import sqlite3
+import yaml
+import json
+from pathlib import Path
+from typing import Dict, List, Any
+
+class DatabaseManager:
+ def __init__(self, db_path=None, schema_path='schema.yaml'):
+ self.schema = self._load_schema(schema_path)
+ # Use provided path or fallback to schema default
+ self.db_path = db_path or self.schema['database']['name']
+ self.conn = None
+ self._init_database()
+
+ def _load_schema(self, path: str) -> Dict:
+ with open(path, 'r') as f:
+ return yaml.safe_load(f)
+
+ def _init_database(self):
+ """Auto-create/migrate database from schema"""
+ self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
+ self.conn.row_factory = sqlite3.Row
+
+ for table_name, table_def in self.schema['tables'].items():
+ self._create_or_update_table(table_name, table_def['columns'])
+
+ def _create_or_update_table(self, table_name: str, columns: Dict):
+ cursor = self.conn.cursor()
+
+ # Check if table exists
+ cursor.execute(f"SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
+ table_exists = cursor.fetchone() is not None
+
+ if not table_exists:
+ # Create table
+ col_defs = []
+ for col_name, col_spec in columns.items():
+ col_def = f"{col_name} {col_spec['type']}"
+ if col_spec.get('primary'):
+ col_def += " PRIMARY KEY"
+ if col_spec.get('autoincrement'):
+ col_def += " AUTOINCREMENT"
+ if col_spec.get('unique'):
+ col_def += " UNIQUE"
+ if col_spec.get('required'):
+ col_def += " NOT NULL"
+ if 'default' in col_spec:
+ default = col_spec['default']
+ if default == 'CURRENT_TIMESTAMP':
+ col_def += f" DEFAULT {default}"
+ elif isinstance(default, str):
+ col_def += f" DEFAULT '{default}'"
+ else:
+ col_def += f" DEFAULT {default}"
+ col_defs.append(col_def)
+
+ create_sql = f"CREATE TABLE {table_name} ({', '.join(col_defs)})"
+ cursor.execute(create_sql)
+ else:
+ # Check for new columns and add them
+ cursor.execute(f"PRAGMA table_info({table_name})")
+ existing_columns = {row[1] for row in cursor.fetchall()}
+
+ for col_name, col_spec in columns.items():
+ if col_name not in existing_columns:
+ col_def = f"{col_spec['type']}"
+ if 'default' in col_spec:
+ default = col_spec['default']
+ if default == 'CURRENT_TIMESTAMP':
+ col_def += f" DEFAULT {default}"
+ elif isinstance(default, str):
+ col_def += f" DEFAULT '{default}'"
+ else:
+ col_def += f" DEFAULT {default}"
+
+ cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {col_name} {col_def}")
+
+ self.conn.commit()
+
+ def get_all(self, table: str, limit: int = 100, offset: int = 0, where: str = None) -> List[Dict]:
+ cursor = self.conn.cursor()
+ query = f"SELECT * FROM {table}"
+ if where:
+ query += f" WHERE {where}"
+ query += f" LIMIT {limit} OFFSET {offset}"
+
+ cursor.execute(query)
+ rows = cursor.fetchall()
+ return [dict(row) for row in rows]
+
+ def search(self, query: str, tables: List[str] = None) -> Dict[str, List[Dict]]:
+ if not tables:
+ tables = list(self.schema['tables'].keys())
+
+ results = {}
+ cursor = self.conn.cursor()
+
+ for table in tables:
+ # Search in text columns
+ columns = self.schema['tables'][table]['columns']
+ text_cols = [col for col, spec in columns.items()
+ if spec['type'] == 'TEXT' and col != 'id']
+
+ if text_cols:
+ where_clause = ' OR '.join([f"{col} LIKE ?" for col in text_cols])
+ params = [f'%{query}%'] * len(text_cols)
+
+ cursor.execute(f"SELECT * FROM {table} WHERE {where_clause} LIMIT 10", params)
+ rows = cursor.fetchall()
+ if rows:
+ results[table] = [dict(row) for row in rows]
+
+ return results
+
+ def close(self):
+ if self.conn:
+ self.conn.close()
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/backend/dummy_data.py b/docs/md_v2/marketplace/backend/dummy_data.py
new file mode 100644
index 00000000..3e7f46f9
--- /dev/null
+++ b/docs/md_v2/marketplace/backend/dummy_data.py
@@ -0,0 +1,267 @@
+import sqlite3
+import json
+import random
+from datetime import datetime, timedelta
+from database import DatabaseManager
+
+def generate_slug(text):
+ return text.lower().replace(' ', '-').replace('&', 'and')
+
+def generate_dummy_data():
+ db = DatabaseManager()
+ conn = db.conn
+ cursor = conn.cursor()
+
+ # Clear existing data
+ for table in ['apps', 'articles', 'categories', 'sponsors']:
+ cursor.execute(f"DELETE FROM {table}")
+
+ # Categories
+ categories = [
+ ("Browser Automation", "โ", "Tools for browser automation and control"),
+ ("Proxy Services", "๐", "Proxy providers and rotation services"),
+ ("LLM Integration", "๐ค", "AI/LLM tools and integrations"),
+ ("Data Processing", "๐", "Data extraction and processing tools"),
+ ("Cloud Infrastructure", "โ", "Cloud browser and computing services"),
+ ("Developer Tools", "๐ ", "Development and testing utilities")
+ ]
+
+ for i, (name, icon, desc) in enumerate(categories):
+ cursor.execute("""
+ INSERT INTO categories (name, slug, icon, description, order_index)
+ VALUES (?, ?, ?, ?, ?)
+ """, (name, generate_slug(name), icon, desc, i))
+
+ # Apps with real Unsplash images
+ apps_data = [
+ # Browser Automation
+ ("Playwright Cloud", "Browser Automation", "Paid", True, True,
+ "Scalable browser automation in the cloud with Playwright", "https://playwright.cloud",
+ None, "$99/month starter", 4.8, 12500,
+ "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&h=400&fit=crop"),
+
+ ("Selenium Grid Hub", "Browser Automation", "Freemium", False, False,
+ "Distributed Selenium grid for parallel testing", "https://seleniumhub.io",
+ "https://github.com/seleniumhub/grid", "Free - $299/month", 4.2, 8400,
+ "https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=800&h=400&fit=crop"),
+
+ ("Puppeteer Extra", "Browser Automation", "Open Source", True, False,
+ "Enhanced Puppeteer with stealth plugins and more", "https://puppeteer-extra.dev",
+ "https://github.com/berstend/puppeteer-extra", "Free", 4.6, 15200,
+ "https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=800&h=400&fit=crop"),
+
+ # Proxy Services
+ ("BrightData", "Proxy Services", "Paid", True, True,
+ "Premium proxy network with 72M+ IPs worldwide", "https://brightdata.com",
+ None, "Starting $500/month", 4.7, 9800,
+ "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&h=400&fit=crop"),
+
+ ("SmartProxy", "Proxy Services", "Paid", False, True,
+ "Residential and datacenter proxies with rotation", "https://smartproxy.com",
+ None, "Starting $75/month", 4.3, 7600,
+ "https://images.unsplash.com/photo-1544197150-b99a580bb7a8?w=800&h=400&fit=crop"),
+
+ ("ProxyMesh", "Proxy Services", "Freemium", False, False,
+ "Rotating proxy servers with sticky sessions", "https://proxymesh.com",
+ None, "$10-$50/month", 4.0, 4200,
+ "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&h=400&fit=crop"),
+
+ # LLM Integration
+ ("LangChain Crawl", "LLM Integration", "Open Source", True, False,
+ "LangChain integration for Crawl4AI workflows", "https://langchain-crawl.dev",
+ "https://github.com/langchain/crawl", "Free", 4.5, 18900,
+ "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&h=400&fit=crop"),
+
+ ("GPT Scraper", "LLM Integration", "Freemium", False, False,
+ "Extract structured data using GPT models", "https://gptscraper.ai",
+ None, "Free - $99/month", 4.1, 5600,
+ "https://images.unsplash.com/photo-1655720828018-edd2daec9349?w=800&h=400&fit=crop"),
+
+ ("Claude Extract", "LLM Integration", "Paid", True, True,
+ "Professional extraction using Claude AI", "https://claude-extract.com",
+ None, "$199/month", 4.9, 3200,
+ "https://images.unsplash.com/photo-1686191128892-3b09ad503b4f?w=800&h=400&fit=crop"),
+
+ # Data Processing
+ ("DataMiner Pro", "Data Processing", "Paid", False, False,
+ "Advanced data extraction and transformation", "https://dataminer.pro",
+ None, "$149/month", 4.2, 6700,
+ "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=400&fit=crop"),
+
+ ("ScraperAPI", "Data Processing", "Freemium", True, True,
+ "Simple API for web scraping with proxy rotation", "https://scraperapi.com",
+ None, "Free - $299/month", 4.6, 22300,
+ "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=400&fit=crop"),
+
+ ("Apify", "Data Processing", "Freemium", False, False,
+ "Web scraping and automation platform", "https://apify.com",
+ None, "$49-$499/month", 4.4, 14500,
+ "https://images.unsplash.com/photo-1504639725590-34d0984388bd?w=800&h=400&fit=crop"),
+
+ # Cloud Infrastructure
+ ("BrowserCloud", "Cloud Infrastructure", "Paid", True, True,
+ "Managed headless browsers in the cloud", "https://browsercloud.io",
+ None, "$199/month", 4.5, 8900,
+ "https://images.unsplash.com/photo-1667372393119-3d4c48d07fc9?w=800&h=400&fit=crop"),
+
+ ("LambdaTest", "Cloud Infrastructure", "Freemium", False, False,
+ "Cross-browser testing on cloud", "https://lambdatest.com",
+ None, "Free - $99/month", 4.1, 11200,
+ "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&h=400&fit=crop"),
+
+ ("Browserless", "Cloud Infrastructure", "Freemium", True, False,
+ "Headless browser automation API", "https://browserless.io",
+ None, "$50-$500/month", 4.7, 19800,
+ "https://images.unsplash.com/photo-1639762681485-074b7f938ba0?w=800&h=400&fit=crop"),
+
+ # Developer Tools
+ ("Crawl4AI VSCode", "Developer Tools", "Open Source", True, False,
+ "VSCode extension for Crawl4AI development", "https://marketplace.visualstudio.com",
+ "https://github.com/crawl4ai/vscode", "Free", 4.8, 34500,
+ "https://images.unsplash.com/photo-1629654297299-c8506221ca97?w=800&h=400&fit=crop"),
+
+ ("Postman Collection", "Developer Tools", "Open Source", False, False,
+ "Postman collection for Crawl4AI API testing", "https://postman.com/crawl4ai",
+ "https://github.com/crawl4ai/postman", "Free", 4.3, 7800,
+ "https://images.unsplash.com/photo-1599507593499-a3f7d7d97667?w=800&h=400&fit=crop"),
+
+ ("Debug Toolkit", "Developer Tools", "Open Source", False, False,
+ "Debugging tools for crawler development", "https://debug.crawl4ai.com",
+ "https://github.com/crawl4ai/debug", "Free", 4.0, 4300,
+ "https://images.unsplash.com/photo-1515879218367-8466d910aaa4?w=800&h=400&fit=crop"),
+ ]
+
+ for name, category, type_, featured, sponsored, desc, url, github, pricing, rating, downloads, image in apps_data:
+ screenshots = json.dumps([
+ f"https://images.unsplash.com/photo-{random.randint(1500000000000, 1700000000000)}-{random.randint(1000000000000, 9999999999999)}?w=800&h=600&fit=crop",
+ f"https://images.unsplash.com/photo-{random.randint(1500000000000, 1700000000000)}-{random.randint(1000000000000, 9999999999999)}?w=800&h=600&fit=crop"
+ ])
+ cursor.execute("""
+ INSERT INTO apps (name, slug, description, category, type, featured, sponsored,
+ website_url, github_url, pricing, rating, downloads, image, screenshots, logo_url,
+ integration_guide, contact_email, views)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """, (name, generate_slug(name), desc, category, type_, featured, sponsored,
+ url, github, pricing, rating, downloads, image, screenshots,
+ f"https://ui-avatars.com/api/?name={name}&background=50ffff&color=070708&size=128",
+ f"# {name} Integration\n\n```python\nfrom crawl4ai import AsyncWebCrawler\n# Integration code coming soon...\n```",
+ f"contact@{generate_slug(name)}.com",
+ random.randint(100, 5000)))
+
+ # Articles with real images
+ articles_data = [
+ ("Browser Automation Showdown: Playwright vs Puppeteer vs Selenium",
+ "Review", "John Doe", ["Playwright Cloud", "Puppeteer Extra"],
+ ["browser-automation", "comparison", "2024"],
+ "https://images.unsplash.com/photo-1587620962725-abab7fe55159?w=1200&h=630&fit=crop"),
+
+ ("Top 5 Proxy Services for Web Scraping in 2024",
+ "Comparison", "Jane Smith", ["BrightData", "SmartProxy", "ProxyMesh"],
+ ["proxy", "web-scraping", "guide"],
+ "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=1200&h=630&fit=crop"),
+
+ ("Integrating LLMs with Crawl4AI: A Complete Guide",
+ "Tutorial", "Crawl4AI Team", ["LangChain Crawl", "GPT Scraper", "Claude Extract"],
+ ["llm", "integration", "tutorial"],
+ "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=1200&h=630&fit=crop"),
+
+ ("Building Scalable Crawlers with Cloud Infrastructure",
+ "Tutorial", "Mike Johnson", ["BrowserCloud", "Browserless"],
+ ["cloud", "scalability", "architecture"],
+ "https://images.unsplash.com/photo-1667372393119-3d4c48d07fc9?w=1200&h=630&fit=crop"),
+
+ ("What's New in Crawl4AI Marketplace",
+ "News", "Crawl4AI Team", [],
+ ["marketplace", "announcement", "news"],
+ "https://images.unsplash.com/photo-1556075798-4825dfaaf498?w=1200&h=630&fit=crop"),
+
+ ("Cost Analysis: Self-Hosted vs Cloud Browser Solutions",
+ "Comparison", "Sarah Chen", ["BrowserCloud", "LambdaTest", "Browserless"],
+ ["cost", "cloud", "comparison"],
+ "https://images.unsplash.com/photo-1554224155-8d04cb21cd6c?w=1200&h=630&fit=crop"),
+
+ ("Getting Started with Browser Automation",
+ "Tutorial", "Crawl4AI Team", ["Playwright Cloud", "Selenium Grid Hub"],
+ ["beginner", "tutorial", "automation"],
+ "https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=1200&h=630&fit=crop"),
+
+ ("The Future of Web Scraping: AI-Powered Extraction",
+ "News", "Dr. Alan Turing", ["Claude Extract", "GPT Scraper"],
+ ["ai", "future", "trends"],
+ "https://images.unsplash.com/photo-1593720213428-28a5b9e94613?w=1200&h=630&fit=crop")
+ ]
+
+ for title, category, author, related_apps, tags, image in articles_data:
+ # Get app IDs for related apps
+ related_ids = []
+ for app_name in related_apps:
+ cursor.execute("SELECT id FROM apps WHERE name = ?", (app_name,))
+ result = cursor.fetchone()
+ if result:
+ related_ids.append(result[0])
+
+ content = f"""# {title}
+
+By {author} | {datetime.now().strftime('%B %d, %Y')}
+
+## Introduction
+
+This is a comprehensive article about {title.lower()}. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+## Key Points
+
+- Important point about the topic
+- Another crucial insight
+- Technical details and specifications
+- Performance comparisons
+
+## Conclusion
+
+In summary, this article explored various aspects of the topic. Stay tuned for more updates!
+"""
+
+ cursor.execute("""
+ INSERT INTO articles (title, slug, content, author, category, related_apps,
+ featured_image, tags, views)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """, (title, generate_slug(title), content, author, category,
+ json.dumps(related_ids), image, json.dumps(tags),
+ random.randint(200, 10000)))
+
+ # Sponsors
+ sponsors_data = [
+ ("BrightData", "Gold", "https://brightdata.com",
+ "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=728&h=90&fit=crop"),
+ ("ScraperAPI", "Gold", "https://scraperapi.com",
+ "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=728&h=90&fit=crop"),
+ ("BrowserCloud", "Silver", "https://browsercloud.io",
+ "https://images.unsplash.com/photo-1667372393119-3d4c48d07fc9?w=728&h=90&fit=crop"),
+ ("Claude Extract", "Silver", "https://claude-extract.com",
+ "https://images.unsplash.com/photo-1686191128892-3b09ad503b4f?w=728&h=90&fit=crop"),
+ ("SmartProxy", "Bronze", "https://smartproxy.com",
+ "https://images.unsplash.com/photo-1544197150-b99a580bb7a8?w=728&h=90&fit=crop")
+ ]
+
+ for company, tier, landing_url, banner in sponsors_data:
+ start_date = datetime.now() - timedelta(days=random.randint(1, 30))
+ end_date = datetime.now() + timedelta(days=random.randint(30, 180))
+
+ cursor.execute("""
+ INSERT INTO sponsors (company_name, logo_url, tier, banner_url,
+ landing_url, active, start_date, end_date)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ """, (company,
+ f"https://ui-avatars.com/api/?name={company}&background=09b5a5&color=fff&size=200",
+ tier, banner, landing_url, 1,
+ start_date.isoformat(), end_date.isoformat()))
+
+ conn.commit()
+ print("โ Dummy data generated successfully!")
+ print(f" - {len(categories)} categories")
+ print(f" - {len(apps_data)} apps")
+ print(f" - {len(articles_data)} articles")
+ print(f" - {len(sponsors_data)} sponsors")
+
+if __name__ == "__main__":
+ generate_dummy_data()
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/backend/requirements.txt b/docs/md_v2/marketplace/backend/requirements.txt
new file mode 100644
index 00000000..1401b0e3
--- /dev/null
+++ b/docs/md_v2/marketplace/backend/requirements.txt
@@ -0,0 +1,5 @@
+fastapi
+uvicorn
+pyyaml
+python-multipart
+python-dotenv
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/backend/schema.yaml b/docs/md_v2/marketplace/backend/schema.yaml
new file mode 100644
index 00000000..c5f443d0
--- /dev/null
+++ b/docs/md_v2/marketplace/backend/schema.yaml
@@ -0,0 +1,75 @@
+database:
+ name: marketplace.db
+
+tables:
+ apps:
+ columns:
+ id: {type: INTEGER, primary: true, autoincrement: true}
+ name: {type: TEXT, required: true}
+ slug: {type: TEXT, unique: true}
+ description: {type: TEXT}
+ long_description: {type: TEXT}
+ logo_url: {type: TEXT}
+ image: {type: TEXT}
+ screenshots: {type: JSON, default: '[]'}
+ category: {type: TEXT}
+ type: {type: TEXT, default: 'Open Source'}
+ status: {type: TEXT, default: 'Active'}
+ website_url: {type: TEXT}
+ github_url: {type: TEXT}
+ demo_url: {type: TEXT}
+ video_url: {type: TEXT}
+ documentation_url: {type: TEXT}
+ support_url: {type: TEXT}
+ discord_url: {type: TEXT}
+ pricing: {type: TEXT}
+ rating: {type: REAL, default: 0.0}
+ downloads: {type: INTEGER, default: 0}
+ featured: {type: BOOLEAN, default: 0}
+ sponsored: {type: BOOLEAN, default: 0}
+ integration_guide: {type: TEXT}
+ documentation: {type: TEXT}
+ examples: {type: TEXT}
+ installation_command: {type: TEXT}
+ requirements: {type: TEXT}
+ changelog: {type: TEXT}
+ tags: {type: JSON, default: '[]'}
+ added_date: {type: DATETIME, default: CURRENT_TIMESTAMP}
+ updated_date: {type: DATETIME, default: CURRENT_TIMESTAMP}
+ contact_email: {type: TEXT}
+ views: {type: INTEGER, default: 0}
+
+ articles:
+ columns:
+ id: {type: INTEGER, primary: true, autoincrement: true}
+ title: {type: TEXT, required: true}
+ slug: {type: TEXT, unique: true}
+ content: {type: TEXT}
+ author: {type: TEXT, default: 'Crawl4AI Team'}
+ category: {type: TEXT}
+ related_apps: {type: JSON, default: '[]'}
+ featured_image: {type: TEXT}
+ published_date: {type: DATETIME, default: CURRENT_TIMESTAMP}
+ tags: {type: JSON, default: '[]'}
+ views: {type: INTEGER, default: 0}
+
+ categories:
+ columns:
+ id: {type: INTEGER, primary: true, autoincrement: true}
+ name: {type: TEXT, unique: true}
+ slug: {type: TEXT, unique: true}
+ icon: {type: TEXT}
+ description: {type: TEXT}
+ order_index: {type: INTEGER, default: 0}
+
+ sponsors:
+ columns:
+ id: {type: INTEGER, primary: true, autoincrement: true}
+ company_name: {type: TEXT, required: true}
+ logo_url: {type: TEXT}
+ tier: {type: TEXT, default: 'Bronze'}
+ banner_url: {type: TEXT}
+ landing_url: {type: TEXT}
+ active: {type: BOOLEAN, default: 1}
+ start_date: {type: DATETIME}
+ end_date: {type: DATETIME}
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/backend/server.py b/docs/md_v2/marketplace/backend/server.py
new file mode 100644
index 00000000..37df188a
--- /dev/null
+++ b/docs/md_v2/marketplace/backend/server.py
@@ -0,0 +1,390 @@
+from fastapi import FastAPI, HTTPException, Query, Depends, Body
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from typing import Optional, List, Dict, Any
+import json
+import hashlib
+import secrets
+from database import DatabaseManager
+from datetime import datetime, timedelta
+
+# Import configuration (will exit if .env not found or invalid)
+from config import Config
+
+app = FastAPI(title="Crawl4AI Marketplace API")
+
+# Security setup
+security = HTTPBearer()
+tokens = {} # In production, use Redis or database for token storage
+
+# CORS configuration
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=Config.ALLOWED_ORIGINS,
+ allow_credentials=True,
+ allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
+ allow_headers=["*"],
+ max_age=3600
+)
+
+# Initialize database with configurable path
+db = DatabaseManager(Config.DATABASE_PATH)
+
+def json_response(data, cache_time=3600):
+ """Helper to return JSON with cache headers"""
+ return JSONResponse(
+ content=data,
+ headers={
+ "Cache-Control": f"public, max-age={cache_time}",
+ "X-Content-Type-Options": "nosniff"
+ }
+ )
+
+# ============= PUBLIC ENDPOINTS =============
+
+@app.get("/api/apps")
+async def get_apps(
+ category: Optional[str] = None,
+ type: Optional[str] = None,
+ featured: Optional[bool] = None,
+ sponsored: Optional[bool] = None,
+ limit: int = Query(default=20, le=10000),
+ offset: int = Query(default=0)
+):
+ """Get apps with optional filters"""
+ where_clauses = []
+ if category:
+ where_clauses.append(f"category = '{category}'")
+ if type:
+ where_clauses.append(f"type = '{type}'")
+ if featured is not None:
+ where_clauses.append(f"featured = {1 if featured else 0}")
+ if sponsored is not None:
+ where_clauses.append(f"sponsored = {1 if sponsored else 0}")
+
+ where = " AND ".join(where_clauses) if where_clauses else None
+ apps = db.get_all('apps', limit=limit, offset=offset, where=where)
+
+ # Parse JSON fields
+ for app in apps:
+ if app.get('screenshots'):
+ app['screenshots'] = json.loads(app['screenshots'])
+
+ return json_response(apps)
+
+@app.get("/api/apps/{slug}")
+async def get_app(slug: str):
+ """Get single app by slug"""
+ apps = db.get_all('apps', where=f"slug = '{slug}'", limit=1)
+ if not apps:
+ raise HTTPException(status_code=404, detail="App not found")
+
+ app = apps[0]
+ if app.get('screenshots'):
+ app['screenshots'] = json.loads(app['screenshots'])
+
+ return json_response(app)
+
+@app.get("/api/articles")
+async def get_articles(
+ category: Optional[str] = None,
+ limit: int = Query(default=20, le=10000),
+ offset: int = Query(default=0)
+):
+ """Get articles with optional category filter"""
+ where = f"category = '{category}'" if category else None
+ articles = db.get_all('articles', limit=limit, offset=offset, where=where)
+
+ # Parse JSON fields
+ for article in articles:
+ if article.get('related_apps'):
+ article['related_apps'] = json.loads(article['related_apps'])
+ if article.get('tags'):
+ article['tags'] = json.loads(article['tags'])
+
+ return json_response(articles)
+
+@app.get("/api/articles/{slug}")
+async def get_article(slug: str):
+ """Get single article by slug"""
+ articles = db.get_all('articles', where=f"slug = '{slug}'", limit=1)
+ if not articles:
+ raise HTTPException(status_code=404, detail="Article not found")
+
+ article = articles[0]
+ if article.get('related_apps'):
+ article['related_apps'] = json.loads(article['related_apps'])
+ if article.get('tags'):
+ article['tags'] = json.loads(article['tags'])
+
+ return json_response(article)
+
+@app.get("/api/categories")
+async def get_categories():
+ """Get all categories ordered by index"""
+ categories = db.get_all('categories', limit=50)
+ categories.sort(key=lambda x: x.get('order_index', 0))
+ return json_response(categories, cache_time=7200)
+
+@app.get("/api/sponsors")
+async def get_sponsors(active: Optional[bool] = True):
+ """Get sponsors, default active only"""
+ where = f"active = {1 if active else 0}" if active is not None else None
+ sponsors = db.get_all('sponsors', where=where, limit=20)
+
+ # Filter by date if active
+ if active:
+ now = datetime.now().isoformat()
+ sponsors = [s for s in sponsors
+ if (not s.get('start_date') or s['start_date'] <= now) and
+ (not s.get('end_date') or s['end_date'] >= now)]
+
+ return json_response(sponsors)
+
+@app.get("/api/search")
+async def search(q: str = Query(min_length=2)):
+ """Search across apps and articles"""
+ if len(q) < 2:
+ return json_response({})
+
+ results = db.search(q, tables=['apps', 'articles'])
+
+ # Parse JSON fields in results
+ for table, items in results.items():
+ for item in items:
+ if table == 'apps' and item.get('screenshots'):
+ item['screenshots'] = json.loads(item['screenshots'])
+ elif table == 'articles':
+ if item.get('related_apps'):
+ item['related_apps'] = json.loads(item['related_apps'])
+ if item.get('tags'):
+ item['tags'] = json.loads(item['tags'])
+
+ return json_response(results, cache_time=1800)
+
+@app.get("/api/stats")
+async def get_stats():
+ """Get marketplace statistics"""
+ stats = {
+ "total_apps": len(db.get_all('apps', limit=10000)),
+ "total_articles": len(db.get_all('articles', limit=10000)),
+ "total_categories": len(db.get_all('categories', limit=1000)),
+ "active_sponsors": len(db.get_all('sponsors', where="active = 1", limit=1000))
+ }
+ return json_response(stats, cache_time=1800)
+
+# ============= ADMIN AUTHENTICATION =============
+
+def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
+ """Verify admin authentication token"""
+ token = credentials.credentials
+ if token not in tokens or tokens[token] < datetime.now():
+ raise HTTPException(status_code=401, detail="Invalid or expired token")
+ return token
+
+@app.post("/api/admin/login")
+async def admin_login(password: str = Body(..., embed=True)):
+ """Admin login with password"""
+ provided_hash = hashlib.sha256(password.encode()).hexdigest()
+
+ if provided_hash != Config.ADMIN_PASSWORD_HASH:
+ # Log failed attempt in production
+ print(f"Failed login attempt at {datetime.now()}")
+ raise HTTPException(status_code=401, detail="Invalid password")
+
+ # Generate secure token
+ token = secrets.token_urlsafe(32)
+ tokens[token] = datetime.now() + timedelta(hours=Config.TOKEN_EXPIRY_HOURS)
+
+ return {
+ "token": token,
+ "expires_in": Config.TOKEN_EXPIRY_HOURS * 3600
+ }
+
+# ============= ADMIN ENDPOINTS =============
+
+@app.get("/api/admin/stats", dependencies=[Depends(verify_token)])
+async def get_admin_stats():
+ """Get detailed admin statistics"""
+ stats = {
+ "apps": {
+ "total": len(db.get_all('apps', limit=10000)),
+ "featured": len(db.get_all('apps', where="featured = 1", limit=10000)),
+ "sponsored": len(db.get_all('apps', where="sponsored = 1", limit=10000))
+ },
+ "articles": len(db.get_all('articles', limit=10000)),
+ "categories": len(db.get_all('categories', limit=1000)),
+ "sponsors": {
+ "active": len(db.get_all('sponsors', where="active = 1", limit=1000)),
+ "total": len(db.get_all('sponsors', limit=10000))
+ },
+ "total_views": sum(app.get('views', 0) for app in db.get_all('apps', limit=10000))
+ }
+ return stats
+
+# Apps CRUD
+@app.post("/api/admin/apps", dependencies=[Depends(verify_token)])
+async def create_app(app_data: Dict[str, Any]):
+ """Create new app"""
+ try:
+ # Handle JSON fields
+ for field in ['screenshots', 'tags']:
+ if field in app_data and isinstance(app_data[field], list):
+ app_data[field] = json.dumps(app_data[field])
+
+ cursor = db.conn.cursor()
+ columns = ', '.join(app_data.keys())
+ placeholders = ', '.join(['?' for _ in app_data])
+ cursor.execute(f"INSERT INTO apps ({columns}) VALUES ({placeholders})",
+ list(app_data.values()))
+ db.conn.commit()
+ return {"id": cursor.lastrowid, "message": "App created"}
+ except Exception as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+@app.put("/api/admin/apps/{app_id}", dependencies=[Depends(verify_token)])
+async def update_app(app_id: int, app_data: Dict[str, Any]):
+ """Update app"""
+ try:
+ # Handle JSON fields
+ for field in ['screenshots', 'tags']:
+ if field in app_data and isinstance(app_data[field], list):
+ app_data[field] = json.dumps(app_data[field])
+
+ set_clause = ', '.join([f"{k} = ?" for k in app_data.keys()])
+ cursor = db.conn.cursor()
+ cursor.execute(f"UPDATE apps SET {set_clause} WHERE id = ?",
+ list(app_data.values()) + [app_id])
+ db.conn.commit()
+ return {"message": "App updated"}
+ except Exception as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+@app.delete("/api/admin/apps/{app_id}", dependencies=[Depends(verify_token)])
+async def delete_app(app_id: int):
+ """Delete app"""
+ cursor = db.conn.cursor()
+ cursor.execute("DELETE FROM apps WHERE id = ?", (app_id,))
+ db.conn.commit()
+ return {"message": "App deleted"}
+
+# Articles CRUD
+@app.post("/api/admin/articles", dependencies=[Depends(verify_token)])
+async def create_article(article_data: Dict[str, Any]):
+ """Create new article"""
+ try:
+ for field in ['related_apps', 'tags']:
+ if field in article_data and isinstance(article_data[field], list):
+ article_data[field] = json.dumps(article_data[field])
+
+ cursor = db.conn.cursor()
+ columns = ', '.join(article_data.keys())
+ placeholders = ', '.join(['?' for _ in article_data])
+ cursor.execute(f"INSERT INTO articles ({columns}) VALUES ({placeholders})",
+ list(article_data.values()))
+ db.conn.commit()
+ return {"id": cursor.lastrowid, "message": "Article created"}
+ except Exception as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+@app.put("/api/admin/articles/{article_id}", dependencies=[Depends(verify_token)])
+async def update_article(article_id: int, article_data: Dict[str, Any]):
+ """Update article"""
+ try:
+ for field in ['related_apps', 'tags']:
+ if field in article_data and isinstance(article_data[field], list):
+ article_data[field] = json.dumps(article_data[field])
+
+ set_clause = ', '.join([f"{k} = ?" for k in article_data.keys()])
+ cursor = db.conn.cursor()
+ cursor.execute(f"UPDATE articles SET {set_clause} WHERE id = ?",
+ list(article_data.values()) + [article_id])
+ db.conn.commit()
+ return {"message": "Article updated"}
+ except Exception as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+@app.delete("/api/admin/articles/{article_id}", dependencies=[Depends(verify_token)])
+async def delete_article(article_id: int):
+ """Delete article"""
+ cursor = db.conn.cursor()
+ cursor.execute("DELETE FROM articles WHERE id = ?", (article_id,))
+ db.conn.commit()
+ return {"message": "Article deleted"}
+
+# Categories CRUD
+@app.post("/api/admin/categories", dependencies=[Depends(verify_token)])
+async def create_category(category_data: Dict[str, Any]):
+ """Create new category"""
+ try:
+ cursor = db.conn.cursor()
+ columns = ', '.join(category_data.keys())
+ placeholders = ', '.join(['?' for _ in category_data])
+ cursor.execute(f"INSERT INTO categories ({columns}) VALUES ({placeholders})",
+ list(category_data.values()))
+ db.conn.commit()
+ return {"id": cursor.lastrowid, "message": "Category created"}
+ except Exception as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+@app.put("/api/admin/categories/{cat_id}", dependencies=[Depends(verify_token)])
+async def update_category(cat_id: int, category_data: Dict[str, Any]):
+ """Update category"""
+ try:
+ set_clause = ', '.join([f"{k} = ?" for k in category_data.keys()])
+ cursor = db.conn.cursor()
+ cursor.execute(f"UPDATE categories SET {set_clause} WHERE id = ?",
+ list(category_data.values()) + [cat_id])
+ db.conn.commit()
+ return {"message": "Category updated"}
+ except Exception as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+# Sponsors CRUD
+@app.post("/api/admin/sponsors", dependencies=[Depends(verify_token)])
+async def create_sponsor(sponsor_data: Dict[str, Any]):
+ """Create new sponsor"""
+ try:
+ cursor = db.conn.cursor()
+ columns = ', '.join(sponsor_data.keys())
+ placeholders = ', '.join(['?' for _ in sponsor_data])
+ cursor.execute(f"INSERT INTO sponsors ({columns}) VALUES ({placeholders})",
+ list(sponsor_data.values()))
+ db.conn.commit()
+ return {"id": cursor.lastrowid, "message": "Sponsor created"}
+ except Exception as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+@app.put("/api/admin/sponsors/{sponsor_id}", dependencies=[Depends(verify_token)])
+async def update_sponsor(sponsor_id: int, sponsor_data: Dict[str, Any]):
+ """Update sponsor"""
+ try:
+ set_clause = ', '.join([f"{k} = ?" for k in sponsor_data.keys()])
+ cursor = db.conn.cursor()
+ cursor.execute(f"UPDATE sponsors SET {set_clause} WHERE id = ?",
+ list(sponsor_data.values()) + [sponsor_id])
+ db.conn.commit()
+ return {"message": "Sponsor updated"}
+ except Exception as e:
+ raise HTTPException(status_code=400, detail=str(e))
+
+@app.get("/")
+async def root():
+ """API info"""
+ return {
+ "name": "Crawl4AI Marketplace API",
+ "version": "1.0.0",
+ "endpoints": [
+ "/api/apps",
+ "/api/articles",
+ "/api/categories",
+ "/api/sponsors",
+ "/api/search?q=query",
+ "/api/stats"
+ ]
+ }
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="127.0.0.1", port=8100)
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/frontend/app-detail.css b/docs/md_v2/marketplace/frontend/app-detail.css
new file mode 100644
index 00000000..9f04c13a
--- /dev/null
+++ b/docs/md_v2/marketplace/frontend/app-detail.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/frontend/app-detail.html b/docs/md_v2/marketplace/frontend/app-detail.html
new file mode 100644
index 00000000..92b5a6dd
--- /dev/null
+++ b/docs/md_v2/marketplace/frontend/app-detail.html
@@ -0,0 +1,234 @@
+
+
+
+
+
+ App Details - Crawl4AI Marketplace
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Open Source
+ FEATURED
+
+
+
App Name
+
App description goes here
+
+
+
+ โ
โ
โ
โ
โ
+ Rating
+
+
+ 0
+ Downloads
+
+
+ Category
+ Category
+
+
+
+
+
+
+ Pricing:
+ Free
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Quick Start
+
Get started with this integration in just a few steps.
+
+
Installation
+
+
+
pip install crawl4ai
+
+
+
Basic Usage
+
+
+
from crawl4ai import AsyncWebCrawler
+
+async def main():
+ async with AsyncWebCrawler() as crawler:
+ result = await crawler.arun(
+ url="https://example.com",
+ # Your configuration here
+ )
+ print(result.markdown)
+
+if __name__ == "__main__":
+ import asyncio
+ asyncio.run(main())
+
+
+
Advanced Configuration
+
Customize the crawler with these advanced options:
+
+
+
+
๐ Performance
+
Optimize crawling speed with parallel processing and caching strategies.
+
+
+
๐ Authentication
+
Handle login forms, cookies, and session management automatically.
+
+
+
๐ฏ Extraction
+
Use CSS selectors, XPath, or AI-powered content extraction.
+
+
+
๐ Proxy Support
+
Rotate proxies and bypass rate limiting with built-in proxy management.
+
+
+
+
Integration Example
+
+
+
from crawl4ai import AsyncWebCrawler
+from crawl4ai.extraction_strategy import LLMExtractionStrategy
+
+async def extract_with_llm():
+ async with AsyncWebCrawler() as crawler:
+ result = await crawler.arun(
+ url="https://example.com",
+ extraction_strategy=LLMExtractionStrategy(
+ provider="openai",
+ api_key="your-api-key",
+ instruction="Extract product information"
+ ),
+ bypass_cache=True
+ )
+ return result.extracted_content
+
+# Run the extraction
+data = await extract_with_llm()
+print(data)
+
+
+
+
๐ก Pro Tip
+
Use the bypass_cache=True parameter when you need fresh data, or set cache_mode="write" to update the cache with new content.
+
+
+
+
+
+
+
+
Documentation
+
Complete documentation and API reference.
+
+
+
+
+
+
+
+
Examples
+
Real-world examples and use cases.
+
+
+
+
+
+
+
+
Support
+
+
+
๐ง Contact
+
contact@example.com
+
+
+
๐ Report Issues
+
Found a bug? Report it on GitHub Issues.
+
+
+
๐ฌ Community
+
Join our Discord for help and discussions.
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/frontend/app-detail.js b/docs/md_v2/marketplace/frontend/app-detail.js
new file mode 100644
index 00000000..82422f14
--- /dev/null
+++ b/docs/md_v2/marketplace/frontend/app-detail.js
@@ -0,0 +1,324 @@
+// App Detail Page JavaScript
+const API_BASE = 'http://localhost:8100/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 = 'โ 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 => `
+
+ `).join('');
+ } catch (error) {
+ console.error('Error loading related apps:', error);
+ }
+ }
+}
+
+// Initialize when DOM is loaded
+document.addEventListener('DOMContentLoaded', () => {
+ new AppDetailPage();
+});
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/frontend/index.html b/docs/md_v2/marketplace/frontend/index.html
new file mode 100644
index 00000000..d034638d
--- /dev/null
+++ b/docs/md_v2/marketplace/frontend/index.html
@@ -0,0 +1,147 @@
+
+
+
+
+
+ Marketplace - Crawl4AI
+
+
+
+
+
+
+
+
+
+
+ >
+
+ /
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
> Latest Apps
+
+
+
+
+
+
+
+
+
+
+
> Latest Articles
+
+
+
+
+
+
+
+
+
+
# Trending
+
+
+
+
+
+
+
+ Submit Your Tool
+
Share your integration
+
Submit โ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/frontend/marketplace.css b/docs/md_v2/marketplace/frontend/marketplace.css
new file mode 100644
index 00000000..ad26c344
--- /dev/null
+++ b/docs/md_v2/marketplace/frontend/marketplace.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/frontend/marketplace.js b/docs/md_v2/marketplace/frontend/marketplace.js
new file mode 100644
index 00000000..cdc22114
--- /dev/null
+++ b/docs/md_v2/marketplace/frontend/marketplace.js
@@ -0,0 +1,395 @@
+// Marketplace JS - Magazine Layout
+const API_BASE = 'http://localhost:8100/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 = `
+
+ ${!imageUrl ? `[${hero.category || 'APP'}]` : ''}
+
+
+
${hero.type || 'PAID'}
+
${hero.name}
+
${hero.description}
+
+ โ
${hero.rating || 0}/5
+ ${hero.downloads || 0} downloads
+
+
+ `;
+ 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 = `
+
+ ${!imageUrl ? `[${app.category || 'APP'}]` : ''}
+
+
+
${app.name}
+
${(app.description || '').substring(0, 100)}...
+
+ ${app.type || 'Open Source'} ยท โ
${app.rating || 0}/5
+
+
+ `;
+ 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 = `
+
+ `;
+ return;
+ }
+
+ const container = document.getElementById('sponsored-content');
+ container.innerHTML = sponsors.slice(0, 5).map(sponsor => `
+
+ `).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 => `
+
+
+
${app.name}
+
${app.description}
+
+ `).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 => `
+
+
+ ${article.category} ยท ${new Date(article.published_at).toLocaleDateString()}
+
+
${article.title}
+
by ${article.author}
+
+ `).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) => `
+
+
${i + 1}
+
+
${app.name}
+
${app.downloads} downloads
+
+
+ `).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 => `
+
+ `).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 => `
+
+
+
${app.name}
+
${app.description}
+
+ `).join('');
+ }
+
+ // Update articles with search results
+ if (results.articles && results.articles.length) {
+ const articlesList = document.getElementById('articles-list');
+ articlesList.innerHTML = results.articles.map(article => `
+
+
+ ${article.category} ยท ${new Date(article.published_at).toLocaleDateString()}
+
+
${article.title}
+
by ${article.author}
+
+ `).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 = `
+
+ ${app.name}
+ `;
+ 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();
+});
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
index 50f19fce..d39172f6 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -14,6 +14,8 @@ nav:
- "Demo Apps": "apps/index.md"
- "C4A-Script Editor": "apps/c4a-script/index.html"
- "LLM Context Builder": "apps/llmtxt/index.html"
+ - "Marketplace": "marketplace/frontend/index.html"
+ - "Marketplace Admin": "marketplace/admin/index.html"
- Setup & Installation:
- "Installation": "core/installation.md"
- "Docker Deployment": "core/docker-deployment.md"