From 0d8d043109c58122300c27597a83d3c43f20e0ad Mon Sep 17 00:00:00 2001 From: unclecode Date: Tue, 30 Sep 2025 18:28:05 +0800 Subject: [PATCH 1/6] feat(docs): add brand book and page copy functionality - Add comprehensive brand book with color system, typography, components - Add page copy dropdown with markdown copy/view functionality - Update mkdocs.yml with new assets and branding navigation - Use terminal-style ASCII icons and condensed menu design --- docs/md_v2/assets/page_actions.css | 388 ++++++++ docs/md_v2/assets/page_actions.js | 346 +++++++ docs/md_v2/branding/index.md | 1371 ++++++++++++++++++++++++++++ mkdocs.yml | 9 +- 4 files changed, 2111 insertions(+), 3 deletions(-) create mode 100644 docs/md_v2/assets/page_actions.css create mode 100644 docs/md_v2/assets/page_actions.js create mode 100644 docs/md_v2/branding/index.md diff --git a/docs/md_v2/assets/page_actions.css b/docs/md_v2/assets/page_actions.css new file mode 100644 index 00000000..13fbffad --- /dev/null +++ b/docs/md_v2/assets/page_actions.css @@ -0,0 +1,388 @@ +/* ==== File: assets/page_actions.css ==== */ +/* Page Actions Dropdown - Terminal Style */ + +/* Wrapper - positioned in content area */ +.page-actions-wrapper { + position: absolute; + top: 1.3rem; + right: 1rem; + z-index: 1000; +} + +/* Floating Action Button */ +.page-actions-button { + position: relative; + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: #3f3f44; + border: 1px solid #50ffff; + color: #e8e9ed; + padding: 0.75rem 1rem; + border-radius: 6px; + font-family: 'Dank Mono', Monaco, monospace; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.page-actions-button:hover { + background: #50ffff; + color: #070708; + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(80, 255, 255, 0.3); +} + +.page-actions-button::before { + content: '▤'; + font-size: 1.2rem; + line-height: 1; +} + +.page-actions-button::after { + content: '▼'; + font-size: 0.6rem; + transition: transform 0.2s ease; +} + +.page-actions-button.active::after { + transform: rotate(180deg); +} + +/* Dropdown Menu */ +.page-actions-dropdown { + position: absolute; + top: 3.5rem; + right: 0; + z-index: 1001; + background: #1a1a1a; + border: 1px solid #3f3f44; + border-radius: 8px; + min-width: 280px; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all 0.2s ease; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + overflow: hidden; +} + +.page-actions-dropdown.active { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.page-actions-dropdown::before { + content: ''; + position: absolute; + top: -8px; + right: 1.5rem; + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid #3f3f44; +} + +/* Menu Header */ +.page-actions-header { + background: #3f3f44; + padding: 0.5rem 0.75rem; + border-bottom: 1px solid #50ffff; + font-family: 'Dank Mono', Monaco, monospace; + font-size: 0.7rem; + color: #a3abba; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.page-actions-header::before { + content: '┌─'; + margin-right: 0.5rem; + color: #50ffff; +} + +/* Menu Items */ +.page-actions-menu { + list-style: none; + margin: 0; + padding: 0.25rem 0; +} + +.page-action-item { + display: block; + padding: 0; +} + +ul>li.page-action-item::after{ + content: ''; +} +.page-action-link { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + color: #e8e9ed; + text-decoration: none !important; + font-family: 'Dank Mono', Monaco, monospace; + font-size: 0.8rem; + transition: all 0.15s ease; + cursor: pointer; + border-left: 3px solid transparent; +} + +.page-action-link:hover:not(.disabled) { + background: #3f3f44; + border-left-color: #50ffff; + color: #50ffff; + text-decoration: none; +} + +.page-action-link.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.page-action-link.disabled:hover { + background: transparent; + color: #e8e9ed; + text-decoration: none; +} + +/* Icons using ASCII/Terminal characters */ +.page-action-icon { + font-size: 1rem; + width: 1.5rem; + text-align: center; + font-weight: bold; + color: #50ffff; +} + +.page-action-link:hover:not(.disabled) .page-action-icon { + color: #50ffff; +} + +.page-action-link.disabled .page-action-icon { + color: #666; +} + +/* Specific icons */ +.icon-copy::before { + content: '⎘'; /* Copy/duplicate symbol */ +} + +.icon-view::before { + content: '⎙'; /* Document symbol */ +} + +.icon-ai::before { + content: '⚡'; /* Lightning/AI symbol */ +} + +/* Action Text */ +.page-action-text { + flex: 1; +} + +.page-action-label { + display: block; + font-weight: 600; + margin-bottom: 0.05rem; + line-height: 1.3; +} + +.page-action-description { + display: block; + font-size: 0.7rem; + color: #a3abba; + line-height: 1.2; +} + +/* Badge */ +.page-action-badge { + display: inline-block; + background: #f59e0b; + color: #070708; + padding: 0.125rem 0.5rem; + border-radius: 12px; + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* External link indicator */ +.page-action-external::after { + content: '→'; + margin-left: 0.25rem; + font-size: 0.75rem; +} + +/* Divider */ +.page-actions-divider { + height: 1px; + background: #3f3f44; + margin: 0.25rem 0; +} + +/* Success/Copy feedback */ +.page-action-copied { + background: #50ff50 !important; + color: #070708 !important; + border-left-color: #50ff50 !important; +} + +.page-action-copied .page-action-icon { + color: #070708 !important; +} + +.page-action-copied .page-action-icon::before { + content: '✓'; +} + +/* Mobile Responsive */ +@media (max-width: 768px) { + .page-actions-wrapper { + top: 0.5rem; + right: 0.5rem; + } + + .page-actions-button { + padding: 0.6rem 0.8rem; + font-size: 0.8rem; + } + + .page-actions-dropdown { + min-width: 260px; + max-width: calc(100vw - 2rem); + right: -0.5rem; + } + + .page-action-link { + padding: 0.6rem 0.8rem; + font-size: 0.8rem; + } + + .page-action-description { + font-size: 0.7rem; + } +} + +/* Animation for tooltip/notification */ +@keyframes slideInFromTop { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.page-actions-notification { + position: fixed; + top: calc(var(--header-height) + 0.5rem); + right: 50%; + transform: translateX(50%); + z-index: 1100; + background: #50ff50; + color: #070708; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-family: 'Dank Mono', Monaco, monospace; + font-size: 0.875rem; + font-weight: 600; + box-shadow: 0 4px 12px rgba(80, 255, 80, 0.4); + animation: slideInFromTop 0.3s ease; + pointer-events: none; +} + +.page-actions-notification::before { + content: '✓ '; + margin-right: 0.5rem; +} + +/* Hide on print */ +@media print { + .page-actions-button, + .page-actions-dropdown { + display: none !important; + } +} + +/* Overlay for mobile */ +.page-actions-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 998; + opacity: 0; + transition: opacity 0.2s ease; +} + +.page-actions-overlay.active { + display: block; + opacity: 1; +} + +@media (max-width: 768px) { + .page-actions-overlay { + display: block; + } +} + +/* Keyboard focus styles */ +.page-action-link:focus { + outline: 2px solid #50ffff; + outline-offset: -2px; +} + +.page-actions-button:focus { + outline: 2px solid #50ffff; + outline-offset: 2px; +} + +/* Loading state */ +.page-action-link.loading { + pointer-events: none; + opacity: 0.7; +} + +.page-action-link.loading .page-action-icon::before { + content: '⟳'; + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Terminal-style border effect on hover */ +.page-actions-dropdown:hover { + border-color: #50ffff; +} + +/* Footer info */ +.page-actions-footer { + background: #070708; + padding: 0.4rem 0.75rem; + border-top: 1px solid #3f3f44; + font-size: 0.65rem; + color: #666; + text-align: center; + font-family: 'Dank Mono', Monaco, monospace; +} + +.page-actions-footer::before { + content: '└─'; + margin-right: 0.5rem; + color: #3f3f44; +} \ No newline at end of file diff --git a/docs/md_v2/assets/page_actions.js b/docs/md_v2/assets/page_actions.js new file mode 100644 index 00000000..92893730 --- /dev/null +++ b/docs/md_v2/assets/page_actions.js @@ -0,0 +1,346 @@ +// ==== File: assets/page_actions.js ==== +// Page Actions - Copy/View Markdown functionality + +document.addEventListener('DOMContentLoaded', () => { + // Configuration + const config = { + githubRepo: 'unclecode/crawl4ai', + githubBranch: 'main', + docsPath: 'docs/md_v2', + excludePaths: ['/apps/c4a-script/', '/apps/llmtxt/', '/apps/crawl4ai-assistant/'], // Don't show on app pages + }; + + // Check if we should show the button on this page + function shouldShowButton() { + const currentPath = window.location.pathname; + + // Don't show on homepage + if (currentPath === '/' || currentPath === '/index.html') { + return false; + } + + // Don't show on excluded paths (apps) + for (const excludePath of config.excludePaths) { + if (currentPath.includes(excludePath)) { + return false; + } + } + + // Only show on documentation pages + return true; + } + + if (!shouldShowButton()) { + return; + } + + // Get current page markdown path + function getCurrentMarkdownPath() { + let path = window.location.pathname; + + // Remove leading/trailing slashes + path = path.replace(/^\/|\/$/g, ''); + + // Remove .html extension if present + path = path.replace(/\.html$/, ''); + + // Handle root/index + if (!path || path === 'index') { + return 'index.md'; + } + + // Add .md extension + return `${path}.md`; + } + + // Get GitHub raw URL for current page + function getGithubRawUrl() { + const mdPath = getCurrentMarkdownPath(); + return `https://raw.githubusercontent.com/${config.githubRepo}/${config.githubBranch}/${config.docsPath}/${mdPath}`; + } + + // Get GitHub file URL for current page (for viewing) + function getGithubFileUrl() { + const mdPath = getCurrentMarkdownPath(); + return `https://github.com/${config.githubRepo}/blob/${config.githubBranch}/${config.docsPath}/${mdPath}`; + } + + // Create the UI + function createPageActionsUI() { + // Find the main content area + const mainContent = document.getElementById('terminal-mkdocs-main-content'); + if (!mainContent) { + console.warn('Page Actions: Could not find #terminal-mkdocs-main-content'); + return null; + } + + // Create button + const button = document.createElement('button'); + button.className = 'page-actions-button'; + button.setAttribute('aria-label', 'Page copy'); + button.setAttribute('aria-expanded', 'false'); + button.innerHTML = 'Page Copy'; + + // Create overlay for mobile + const overlay = document.createElement('div'); + overlay.className = 'page-actions-overlay'; + + // Create dropdown + const dropdown = document.createElement('div'); + dropdown.className = 'page-actions-dropdown'; + dropdown.setAttribute('role', 'menu'); + dropdown.innerHTML = ` +
Page Copy
+ + + `; + + // Create a wrapper for button and dropdown + const wrapper = document.createElement('div'); + wrapper.className = 'page-actions-wrapper'; + wrapper.appendChild(button); + wrapper.appendChild(dropdown); + + // Inject into main content area + mainContent.appendChild(wrapper); + + // Append overlay to body + document.body.appendChild(overlay); + + return { button, dropdown, overlay, wrapper }; + } + + // Toggle dropdown + function toggleDropdown(button, dropdown, overlay) { + const isActive = dropdown.classList.contains('active'); + + if (isActive) { + closeDropdown(button, dropdown, overlay); + } else { + openDropdown(button, dropdown, overlay); + } + } + + function openDropdown(button, dropdown, overlay) { + dropdown.classList.add('active'); + // Don't activate overlay - not needed + button.classList.add('active'); + button.setAttribute('aria-expanded', 'true'); + } + + function closeDropdown(button, dropdown, overlay) { + dropdown.classList.remove('active'); + // Don't deactivate overlay - not needed + button.classList.remove('active'); + button.setAttribute('aria-expanded', 'false'); + } + + // Show notification + function showNotification(message, duration = 2000) { + const notification = document.createElement('div'); + notification.className = 'page-actions-notification'; + notification.textContent = message; + document.body.appendChild(notification); + + setTimeout(() => { + notification.remove(); + }, duration); + } + + // Copy markdown to clipboard + async function copyMarkdownToClipboard(link) { + const rawUrl = getGithubRawUrl(); + + // Add loading state + link.classList.add('loading'); + + try { + const response = await fetch(rawUrl); + + if (!response.ok) { + throw new Error(`Failed to fetch markdown: ${response.status}`); + } + + const markdown = await response.text(); + + // Copy to clipboard + await navigator.clipboard.writeText(markdown); + + // Visual feedback + link.classList.remove('loading'); + link.classList.add('page-action-copied'); + + showNotification('Markdown copied to clipboard!'); + + // Reset after delay + setTimeout(() => { + link.classList.remove('page-action-copied'); + }, 2000); + + } catch (error) { + console.error('Error copying markdown:', error); + link.classList.remove('loading'); + showNotification('Error: Could not copy markdown'); + } + } + + // View markdown in new tab + function viewMarkdown() { + const githubUrl = getGithubFileUrl(); + window.open(githubUrl, '_blank', 'noopener,noreferrer'); + } + + // Initialize + const { button, dropdown, overlay } = createPageActionsUI(); + + // Event listeners + button.addEventListener('click', (e) => { + e.stopPropagation(); + toggleDropdown(button, dropdown, overlay); + }); + + overlay.addEventListener('click', () => { + closeDropdown(button, dropdown, overlay); + }); + + // Copy markdown action + document.getElementById('action-copy-markdown').addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + await copyMarkdownToClipboard(e.currentTarget); + }); + + // View markdown action + document.getElementById('action-view-markdown').addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + viewMarkdown(); + closeDropdown(button, dropdown, overlay); + }); + + // Ask AI action (disabled for now) + document.getElementById('action-ask-ai').addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + // Future: Integrate with Ask AI feature + // For now, do nothing (disabled state) + }); + + // Close on ESC key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && dropdown.classList.contains('active')) { + closeDropdown(button, dropdown, overlay); + } + }); + + // Close when clicking outside + document.addEventListener('click', (e) => { + if (!dropdown.contains(e.target) && !button.contains(e.target)) { + closeDropdown(button, dropdown, overlay); + } + }); + + // Prevent dropdown from closing when clicking inside + dropdown.addEventListener('click', (e) => { + // Only stop propagation if not clicking on a link + if (!e.target.closest('.page-action-link')) { + e.stopPropagation(); + } + }); + + // Close dropdown on link click (except for copy which handles itself) + dropdown.querySelectorAll('.page-action-link:not(#action-copy-markdown)').forEach(link => { + link.addEventListener('click', () => { + if (!link.classList.contains('disabled')) { + setTimeout(() => { + closeDropdown(button, dropdown, overlay); + }, 100); + } + }); + }); + + // Handle window resize + let resizeTimer; + window.addEventListener('resize', () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + // Close dropdown on resize to prevent positioning issues + if (dropdown.classList.contains('active')) { + closeDropdown(button, dropdown, overlay); + } + }, 250); + }); + + // Accessibility: Focus management + button.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleDropdown(button, dropdown, overlay); + + // Focus first menu item when opening + if (dropdown.classList.contains('active')) { + const firstLink = dropdown.querySelector('.page-action-link:not(.disabled)'); + if (firstLink) { + setTimeout(() => firstLink.focus(), 100); + } + } + } + }); + + // Arrow key navigation within menu + dropdown.addEventListener('keydown', (e) => { + if (!dropdown.classList.contains('active')) return; + + const links = Array.from(dropdown.querySelectorAll('.page-action-link:not(.disabled)')); + const currentIndex = links.indexOf(document.activeElement); + + if (e.key === 'ArrowDown') { + e.preventDefault(); + const nextIndex = (currentIndex + 1) % links.length; + links[nextIndex].focus(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + const prevIndex = (currentIndex - 1 + links.length) % links.length; + links[prevIndex].focus(); + } else if (e.key === 'Home') { + e.preventDefault(); + links[0].focus(); + } else if (e.key === 'End') { + e.preventDefault(); + links[links.length - 1].focus(); + } + }); + + console.log('Page Actions initialized for:', getCurrentMarkdownPath()); +}); \ No newline at end of file diff --git a/docs/md_v2/branding/index.md b/docs/md_v2/branding/index.md new file mode 100644 index 00000000..5741344d --- /dev/null +++ b/docs/md_v2/branding/index.md @@ -0,0 +1,1371 @@ +# 🎨 Crawl4AI Brand Book + + + +
+

Crawl4AI Brand Guidelines

+

A comprehensive design system for building consistent, terminal-inspired experiences

+
+ +## 📖 About This Guide + +This brand book documents the complete visual language of Crawl4AI. Whether you're building documentation pages, interactive apps, or Chrome extensions, these guidelines ensure consistency while maintaining the unique terminal-aesthetic that defines our brand. + +--- + +
+ 🎨 +

Color System

+
+ +Our color palette is built around a terminal-dark aesthetic with vibrant cyan and pink accents. Every color serves a purpose and maintains accessibility standards. + +### Primary Colors + +
+
+
+
+

Primary Cyan

+ #50ffff +

Main brand color, links, highlights, CTAs

+
+
+ +
+
+
+

Primary Teal

+ #09b5a5 +

Hover states, dimmed accents, progress bars

+
+
+ +
+
+
+

Primary Green

+ #0fbbaa +

Alternative primary, buttons, nav links

+
+
+ +
+
+
+

Accent Pink

+ #f380f5 +

Secondary accents, keywords, highlights

+
+
+
+ +### Background Colors + +
+
+
+
+

Deep Black

+ #070708 +

Main background, code blocks, deep containers

+
+
+ +
+
+
+

Secondary Dark

+ #1a1a1a +

Headers, sidebars, secondary containers

+
+
+ +
+
+
+

Tertiary Gray

+ #3f3f44 +

Cards, borders, code backgrounds, modals

+
+
+ +
+
+
+

Block Background

+ #202020 +

Block elements, alternate rows

+
+
+
+ +### Text Colors + +
+
+
+
+

Primary Text

+ #e8e9ed +

Headings, body text, primary content

+
+
+ +
+
+
+

Secondary Text

+ #d5cec0 +

Body text, descriptions, warm tone

+
+
+ +
+
+
+

Tertiary Text

+ #a3abba +

Captions, labels, metadata, cool tone

+
+
+ +
+
+
+

Dimmed Text

+ #8b857a +

Disabled states, comments, subtle text

+
+
+
+ +### Semantic Colors + +
+
+
+
+

Success Green

+ #50ff50 +

Success messages, completed states, valid

+
+
+ +
+
+
+

Error Red

+ #ff3c74 +

Errors, warnings, destructive actions

+
+
+ +
+
+
+

Warning Orange

+ #f59e0b +

Warnings, beta status, caution

+
+
+ +
+
+
+

Info Blue

+ #4a9eff +

Info messages, external links

+
+
+
+ +--- + +
+ ✍️ +

Typography

+
+ +Our typography system is built around **Dank Mono**, a monospace font that reinforces the terminal aesthetic while maintaining excellent readability. + +### Font Family + +```css +--font-primary: 'Dank Mono', dm, Monaco, Courier New, monospace; +--font-code: 'Dank Mono', 'Monaco', 'Menlo', 'Consolas', monospace; +``` + +**Font Weights:** +- Regular: 400 +- Bold: 700 +- Italic: 400 (italic variant) + +### Type Scale + +
+ H1 / Hero +
+ Size: 2.5rem (40px) + Weight: 700 + Line-height: 1.2 +
+

The Quick Brown Fox Jumps Over

+
+ +
+ H2 / Section +
+ Size: 1.75rem (28px) + Weight: 700 + Line-height: 1.3 +
+

Advanced Web Scraping Features

+
+ +
+ H3 / Subsection +
+ Size: 1.3rem (20.8px) + Weight: 600 + Line-height: 1.4 +
+

Installation and Setup Guide

+
+ +
+ H4 / Component +
+ Size: 1.1rem (17.6px) + Weight: 600 + Line-height: 1.4 +
+

Quick Start Instructions

+
+ +
+ Body / Regular +
+ Size: 14px + Weight: 400 + Line-height: 1.6 +
+

Crawl4AI is the #1 trending GitHub repository, actively maintained by a vibrant community. It delivers blazing-fast, AI-ready web crawling tailored for large language models and data pipelines.

+
+ +
+ Code / Monospace +
+ Size: 13px + Weight: 400 + Line-height: 1.5 +
+ async with AsyncWebCrawler() as crawler: +
+ +
+ Small / Caption +
+ Size: 12px + Weight: 400 + Line-height: 1.5 +
+

Updated 2 hours ago • v0.7.2

+
+ +--- + +
+ 🧩 +

Components

+
+ +### Buttons + +
+
+

Primary Button

+
+ + +
+
+
+ HTML + CSS + +
+
<button class="brand-btn brand-btn-primary">
+  Launch Editor →
+</button>
+
+
+ +
+

Secondary Button

+
+ + +
+
+
+ HTML + CSS + +
+
<button class="brand-btn brand-btn-secondary">
+  View Documentation
+</button>
+
+
+ +
+

Accent Button

+
+ + +
+
+
+ HTML + CSS + +
+
<button class="brand-btn brand-btn-accent">
+  Try Beta Features
+</button>
+
+
+ +
+

Ghost Button

+
+ + +
+
+
+ HTML + CSS + +
+
<button class="brand-btn brand-btn-ghost">
+  Learn More
+</button>
+
+
+
+ +### Badges & Status Indicators + +
+

Status Badges

+
+ Available + Beta + Alpha + New! + Coming Soon +
+
+
+ HTML + CSS + +
+
<span class="brand-badge badge-available">Available</span>
+<span class="brand-badge badge-beta">Beta</span>
+<span class="brand-badge badge-alpha">Alpha</span>
+<span class="brand-badge badge-new">New!</span>
+
+
+ +### Cards + +
+
+

🎨 C4A-Script Editor

+

A visual, block-based programming environment for creating browser automation scripts. Perfect for beginners and experts alike!

+ +
+ +
+

🧠 LLM Context Builder

+

Generate optimized context files for your favorite LLM when working with Crawl4AI. Get focused, relevant documentation based on your needs.

+ +
+
+ +
+
+ HTML + CSS + +
+
<div class="brand-card">
+  <h3 class="brand-card-title">Card Title</h3>
+  <p class="brand-card-description">Card description...</p>
+</div>
+
+ +### Terminal Window + +
+
+
+ + + +
+ crawl4ai@terminal ~ % +
+
+

$ pip install crawl4ai

+

Successfully installed crawl4ai-0.7.2

+
+
+ +
+
+ HTML + CSS + +
+
<div class="terminal-window">
+  <div class="terminal-header">
+    <div class="terminal-dots">
+      <span class="terminal-dot red"></span>
+      <span class="terminal-dot yellow"></span>
+      <span class="terminal-dot green"></span>
+    </div>
+    <span class="terminal-title">Terminal Title</span>
+  </div>
+  <div class="terminal-content">
+    Your content here
+  </div>
+</div>
+
+ +--- + +
+ 📐 +

Spacing & Layout

+
+ +### Spacing System + +Our spacing system is based on multiples of **10px** for consistency and ease of calculation. + +
+
+
+ 10px - Extra Small (xs) +
+
+
+ 20px - Small (sm) +
+
+
+ 30px - Medium (md) +
+
+
+ 40px - Large (lg) +
+
+
+ 60px - Extra Large (xl) +
+
+
+ 80px - 2XL +
+
+ +### Layout Patterns + +#### Terminal Container +Full-height, flex-column layout with sticky header + +```css +.terminal-container { + min-height: 100vh; + display: flex; + flex-direction: column; +} +``` + +#### Content Grid +Auto-fit responsive grid for cards and components + +```css +.component-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; +} +``` + +#### Centered Content +Maximum width with auto margins for centered layouts + +```css +.content { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} +``` + +--- + +
+ +

Usage Guidelines

+
+ +### When to Use Each Style + +**Documentation Pages (`docs/md_v2/core`, `/advanced`, etc.)** +- Use main documentation styles from `styles.css` and `layout.css` +- Terminal theme with sidebar navigation +- Dense, informative content +- ToC on the right side +- Focus on readability and technical accuracy + +**Landing Pages (`docs/md_v2/apps/crawl4ai-assistant`, etc.)** +- Use `assistant.css` style approach +- Hero sections with gradients +- Feature cards with hover effects +- Video/demo sections +- Sticky header with navigation +- Marketing-focused, visually engaging + +**App Home (`docs/md_v2/apps/index.md`)** +- Grid-based card layouts +- Status badges +- Call-to-action buttons +- Feature highlights +- Mix of informational and promotional + +**Interactive Apps (`docs/md_v2/apps/llmtxt`, `/c4a-script`)** +- Full-screen application layouts +- Interactive controls +- Real-time feedback +- Tool-specific UI patterns +- Functional over decorative + +**Chrome Extension (`popup.css`)** +- Compact, fixed-width design (380px) +- Clear mode selection +- Session indicators +- Minimal but effective +- Fast loading, no heavy assets + +### Do's and Don'ts + +
+
+
✅ DO
+
+ +
+
+ Use primary cyan for main CTAs and important actions +
+
+ +
+
❌ DON'T
+
+ +
+
+ Don't use arbitrary colors not in the brand palette +
+
+
+ +
+
+
✅ DO
+
+
+ async with AsyncWebCrawler(): +
+
+
+ Use Dank Mono for all text to maintain terminal aesthetic +
+
+ +
+
❌ DON'T
+
+
+ async with AsyncWebCrawler(): +
+
+
+ Don't use non-monospace fonts (breaks terminal feel) +
+
+
+ +
+
+
✅ DO
+
+
+ Beta +

New Feature

+
+
+
+ Use status badges to indicate feature maturity +
+
+ +
+
❌ DON'T
+
+
+

New Feature (Beta)

+
+
+
+ Don't put status indicators in plain text +
+
+
+ +--- + +
+ 🎯 +

Accessibility

+
+ +### Color Contrast + +All color combinations meet WCAG AA standards: + +- **Primary Cyan (#50ffff) on Dark (#070708)**: 12.4:1 ✅ +- **Primary Text (#e8e9ed) on Dark (#070708)**: 11.8:1 ✅ +- **Secondary Text (#d5cec0) on Dark (#070708)**: 9.2:1 ✅ +- **Tertiary Text (#a3abba) on Dark (#070708)**: 6.8:1 ✅ + +### Focus States + +All interactive elements must have visible focus indicators: + +```css +button:focus, +a:focus { + outline: 2px solid #50ffff; + outline-offset: 2px; +} +``` + +### Motion + +Respect `prefers-reduced-motion` for users who need it: + +```css +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} +``` + +--- + +
+ 💾 +

CSS Variables

+
+ +Use these CSS variables for consistency across all styles: + +```css +:root { + /* Colors */ + --primary-color: #50ffff; + --primary-dimmed: #09b5a5; + --primary-green: #0fbbaa; + --accent-color: #f380f5; + + /* Backgrounds */ + --background-color: #070708; + --bg-secondary: #1a1a1a; + --code-bg-color: #3f3f44; + --border-color: #3f3f44; + + /* Text */ + --font-color: #e8e9ed; + --secondary-color: #d5cec0; + --tertiary-color: #a3abba; + + /* Semantic */ + --success-color: #50ff50; + --error-color: #ff3c74; + --warning-color: #f59e0b; + + /* Typography */ + --font-primary: 'Dank Mono', dm, Monaco, Courier New, monospace; + --global-font-size: 14px; + --global-line-height: 1.6; + + /* Spacing */ + --global-space: 10px; + + /* Layout */ + --header-height: 65px; + --sidebar-width: 280px; + --toc-width: 340px; + --content-max-width: 90em; +} +``` + +--- + +
+ 📚 +

Resources

+
+ +### Download Assets + +- [Dank Mono Font Files](/docs/md_v2/assets/) (Regular, Bold, Italic) +- [Brand CSS Template](/docs/md_v2/branding/assets/brand-examples.css) +- [Component Library](/docs/md_v2/apps/) + +### Reference Files + +- Main Documentation Styles: `docs/md_v2/assets/styles.css` +- Layout System: `docs/md_v2/assets/layout.css` +- Landing Page Style: `docs/md_v2/apps/crawl4ai-assistant/assistant.css` +- App Home Style: `docs/md_v2/apps/index.md` +- Extension Style: `docs/md_v2/apps/crawl4ai-assistant/popup/popup.css` + +### Questions? + +If you're unsure about which style to use or need help implementing these guidelines: + +- Check existing examples in the relevant section +- Review the "When to Use Each Style" guidelines above +- Ask in our [Discord community](https://discord.gg/crawl4ai) +- Open an issue on [GitHub](https://github.com/unclecode/crawl4ai) + +--- + +
+

🎨 Keep It Terminal

+

+ When in doubt, ask yourself: "Does this feel like a terminal?" If yes, you're on brand. +

+
+ + diff --git a/mkdocs.yml b/mkdocs.yml index ff148547..50f19fce 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,4 @@ site_name: Crawl4AI Documentation (v0.7.x) -site_favicon: docs/md_v2/favicon.ico site_description: 🚀🤖 Crawl4AI, Open-source LLM-Friendly Web Crawler & Scraper site_url: https://docs.crawl4ai.com repo_url: https://github.com/unclecode/crawl4ai @@ -66,10 +65,12 @@ nav: - "CrawlResult": "api/crawl-result.md" - "Strategies": "api/strategies.md" - "C4A-Script Reference": "api/c4a-script-reference.md" + - "Brand Book": "branding/index.md" theme: name: 'terminal' palette: 'dark' + favicon: favicon.ico custom_dir: docs/md_v2/overrides color_mode: 'dark' icon: @@ -98,6 +99,7 @@ extra_css: - assets/highlight.css - assets/dmvendor.css - assets/feedback-overrides.css + - assets/page_actions.css extra_javascript: - https://www.googletagmanager.com/gtag/js?id=G-58W0K2ZQ25 @@ -106,8 +108,9 @@ extra_javascript: - assets/highlight_init.js - https://buttons.github.io/buttons.js - assets/toc.js - - assets/github_stats.js + - assets/github_stats.js - assets/selection_ask_ai.js - assets/copy_code.js - assets/floating_ask_ai_button.js - - assets/mobile_menu.js \ No newline at end of file + - assets/mobile_menu.js + - assets/page_actions.js \ No newline at end of file From ef46df10da35093ba70a491e62646d7cbecb2572 Mon Sep 17 00:00:00 2001 From: unclecode Date: Tue, 30 Sep 2025 18:31:57 +0800 Subject: [PATCH 2/6] Update gitignore add local scripts folder --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 03c5b355..710e1340 100644 --- a/.gitignore +++ b/.gitignore @@ -270,4 +270,6 @@ docs/**/data .codecat/ docs/apps/linkdin/debug*/ -docs/apps/linkdin/samples/insights/* \ No newline at end of file +docs/apps/linkdin/samples/insights/* + +scripts/ From 8d3066264755f4c275a5fb1d52dbaf581188dd61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Sj=C3=B6borg?= Date: Thu, 2 Oct 2025 09:17:32 +0200 Subject: [PATCH 3/6] fix: remove this import as it causes python to treat "json" as a variable in the except block --- deploy/docker/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/deploy/docker/api.py b/deploy/docker/api.py index 78a36bf3..d0127e7b 100644 --- a/deploy/docker/api.py +++ b/deploy/docker/api.py @@ -563,7 +563,6 @@ async def handle_crawl_request( if isinstance(hook_manager, UserHookManager): try: # Ensure all hook data is JSON serializable - import json hook_data = { "status": hooks_status, "execution_log": hook_manager.execution_log, From 35dd206925e2e189e2338e654a55ed4a3cd2d918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Sj=C3=B6borg?= Date: Thu, 2 Oct 2025 09:20:59 +0200 Subject: [PATCH 4/6] fix: always return a list, even if we catch an exception --- crawl4ai/async_dispatcher.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crawl4ai/async_dispatcher.py b/crawl4ai/async_dispatcher.py index 5bb1a47c..bd44557c 100644 --- a/crawl4ai/async_dispatcher.py +++ b/crawl4ai/async_dispatcher.py @@ -455,8 +455,6 @@ class MemoryAdaptiveDispatcher(BaseDispatcher): # Update priorities for waiting tasks if needed await self._update_queue_priorities() - - return results except Exception as e: if self.monitor: @@ -467,6 +465,7 @@ class MemoryAdaptiveDispatcher(BaseDispatcher): memory_monitor.cancel() if self.monitor: self.monitor.stop() + return results async def _update_queue_priorities(self): """Periodically update priorities of items in the queue to prevent starvation""" From 408ad1b750b054c0a1b1a0801b008ee7d3406dd2 Mon Sep 17 00:00:00 2001 From: unclecode Date: Thu, 2 Oct 2025 16:41:11 +0800 Subject: [PATCH 5/6] feat(marketplace): Add Crawl4AI marketplace with secure configuration - Implement marketplace frontend and admin dashboard - Add FastAPI backend with environment-based configuration - Use .env file for secrets management - Include data generation scripts - Add proper CORS configuration - Remove hardcoded password from admin login - Update gitignore for security --- .gitignore | 7 + docs/md_v2/assets/images/logo.png | Bin 0 -> 1622 bytes docs/md_v2/marketplace/README.md | 66 ++ docs/md_v2/marketplace/admin/admin.css | 650 ++++++++++++ docs/md_v2/marketplace/admin/admin.js | 757 ++++++++++++++ docs/md_v2/marketplace/admin/index.html | 215 ++++ docs/md_v2/marketplace/backend/.env.example | 14 + docs/md_v2/marketplace/backend/config.py | 59 ++ docs/md_v2/marketplace/backend/database.py | 117 +++ docs/md_v2/marketplace/backend/dummy_data.py | 267 +++++ .../marketplace/backend/requirements.txt | 5 + docs/md_v2/marketplace/backend/schema.yaml | 75 ++ docs/md_v2/marketplace/backend/server.py | 390 +++++++ .../md_v2/marketplace/frontend/app-detail.css | 462 +++++++++ .../marketplace/frontend/app-detail.html | 234 +++++ docs/md_v2/marketplace/frontend/app-detail.js | 324 ++++++ docs/md_v2/marketplace/frontend/index.html | 147 +++ .../marketplace/frontend/marketplace.css | 957 ++++++++++++++++++ .../md_v2/marketplace/frontend/marketplace.js | 395 ++++++++ mkdocs.yml | 2 + 20 files changed, 5143 insertions(+) create mode 100644 docs/md_v2/assets/images/logo.png create mode 100644 docs/md_v2/marketplace/README.md create mode 100644 docs/md_v2/marketplace/admin/admin.css create mode 100644 docs/md_v2/marketplace/admin/admin.js create mode 100644 docs/md_v2/marketplace/admin/index.html create mode 100644 docs/md_v2/marketplace/backend/.env.example create mode 100644 docs/md_v2/marketplace/backend/config.py create mode 100644 docs/md_v2/marketplace/backend/database.py create mode 100644 docs/md_v2/marketplace/backend/dummy_data.py create mode 100644 docs/md_v2/marketplace/backend/requirements.txt create mode 100644 docs/md_v2/marketplace/backend/schema.yaml create mode 100644 docs/md_v2/marketplace/backend/server.py create mode 100644 docs/md_v2/marketplace/frontend/app-detail.css create mode 100644 docs/md_v2/marketplace/frontend/app-detail.html create mode 100644 docs/md_v2/marketplace/frontend/app-detail.js create mode 100644 docs/md_v2/marketplace/frontend/index.html create mode 100644 docs/md_v2/marketplace/frontend/marketplace.css create mode 100644 docs/md_v2/marketplace/frontend/marketplace.js 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 0000000000000000000000000000000000000000..ed82a3ccde17d44c7ed43c91687284894250eb42 GIT binary patch literal 1622 zcmV-c2C4apP)JU12>}FcPzs4AiaugUcoPT&g$FMBfPuuQTw96=z1kLO0f{Z; zl3wJryPprUr)STeJ*7;t+1=TH{_mS_{`u!$p|vJ5O(3|@^NnNJJ#Ab7h8z>rPb}k* z`R0L56nM@d@HEgMhvQXX94(i?xEzmTTF=nJh5dLtgR~z@j-WtcpsCF7%Gf(uKq)=o z{(Av%nITNqS8xj^AS4ip0*(Waz`&sC%bB%5yglGTm@x!kJ3u&M`Uv=C@DXqa52xX5 z-wyldv#?K`0@ths`ul-U2xD&pVQl*Q!FS$;ec~kSGiSk?8X%o^mCSA7+*i7s1mxbm zaOTbfQzg;eBfF2gtm0< zbc6&VVW58im^cy6p~K({C4gf?ZrciZ^eC820wW{7skq=PSHk)F8$e2kjt+!YzXkU6 z0P#35JnY7UM@4OEj$Uz#IwYw9Y9XjYA?lJ67M;65)peVy^CtE3<+K3mWRlvl%(!Hd z7Qonw6;z!!sk*vZbp8T$NlDfpKrJP8JnrI|u~`L#1fDzr=g$YKs(|iph*Kv`FqM_T zK6nV2Fab!X5&H0b;Ph!AnKVNmJpz|5h5hYbAQAz3{zYi*8lbruNF=~F-UOb14mfuX z;{17_NSOf7Q6L_tjz+0x&ZN5DNHs9T*v?(F!Xa7$)RmP~*RE6D>ZWRKqpq%|28
{Q2l+2>e`>wsg&uDgjw{}ZmNMHs>WvO@^XVQs)A;gfY#K-#YU>qQmVRos=kMe zZU2;(l*ZwlIaH0!hD38Kb!8=Wc{$aMCaPOq24wDBV=E0qf zyX>AA$gF3=7|xnyzE@7Tu*iBvUzT6r(V2x)GoGl8>78_ zRLw2abLJYK$wUsgOTc122@_O$;XUtw}b+sF`2_KHR!ugrZ(^Qd3OaS0`vfSgxZVp@{nCBZ!)Kyj4CD`&2Rc{|vODpxfd07>7 z-V%5^62-=_qQZdm-e>HS8d^fol7c#wqH6fVta(ctbyXGh?AcT;Z6;4#y+&P8;o3{a zcGNO@{{dCQRq7ct#tK41e|_(2~}&G z*&X#)Xic9^OA1=y5M!TzLDfHKlt`thqY;BuC=p@dLa_WLh`V(I~{7yO3*F1MM9UmIY_oGH}u)h`Kt6ix>Tg z$g>OL7PS;6!DLeL)Tvaz{Z7?=o2tE&vDcOrOvU>36;vHJsk&~n=$Buqr%a&^hpB}( z6jhG3jBzva*4bN5KEL;g)2|E$gCDQ`#|K0hfT^k;^HQ z#iT3%9z?)XlsXyL{)#yJBC~_McjTr}$6JDVfr6ehsoO(1FCBCADo~b>=Z7PxLJs=w zmp#{B7W`|>QDrPbegFpa6}Fd!K4c3=|N8oEgT5Df_uG#WC^W-o`4xT|nc$`RAG*CS UCSNaJPyhe`07*qoM6N<$f)VKA_5c6? literal 0 HcmV?d00001 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 = ` + + + + + + + + + + + + + + + ${apps.map(app => ` + + + + + + + + + + + `).join('')} + +
IDNameCategoryTypeRatingDownloadsStatusActions
${app.id}${app.name}${app.category}${app.type}◆ ${app.rating}/5${this.formatNumber(app.downloads)} + ${app.featured ? 'Featured' : ''} + ${app.sponsored ? '' : ''} + +
+ + + +
+
+ `; + } + + renderArticlesTable(articles) { + const table = document.getElementById('articles-table'); + table.innerHTML = ` + + + + + + + + + + + + + + ${articles.map(article => ` + + + + + + + + + + `).join('')} + +
IDTitleCategoryAuthorPublishedViewsActions
${article.id}${article.title}${article.category}${article.author}${new Date(article.published_date).toLocaleDateString()}${this.formatNumber(article.views)} +
+ + + +
+
+ `; + } + + renderCategoriesTable(categories) { + const table = document.getElementById('categories-table'); + table.innerHTML = ` + + + + + + + + + + + + ${categories.map(cat => ` + + + + + + + + `).join('')} + +
OrderIconNameDescriptionActions
${cat.order_index}${cat.icon}${cat.name}${cat.description} +
+ + +
+
+ `; + } + + renderSponsorsTable(sponsors) { + const table = document.getElementById('sponsors-table'); + table.innerHTML = ` + + + + + + + + + + + + + + ${sponsors.map(sponsor => ` + + + + + + + + + + `).join('')} + +
IDCompanyTierStartEndStatusActions
${sponsor.id}${sponsor.company_name}${sponsor.tier}${new Date(sponsor.start_date).toLocaleDateString()}${new Date(sponsor.end_date).toLocaleDateString()}${sponsor.active ? 'Active' : 'Inactive'} +
+ + +
+
+ `; + } + + 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 + + + + +
+ + + + + + + + +
+ + + + \ 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 + + + + +
+ +
+
+
+
+ +

+ [ + Marketplace + ] +

+
+
+ +
+
+ + +
+
+
+ +
+
+
+ Open Source + + +
+

App Name

+

App description goes here

+ +
+
+ ★★★★★ + Rating +
+
+ 0 + Downloads +
+
+ Category + Category +
+
+ +
+ + Visit Website + + + View on GitHub + + +
+ +
+ Pricing: + Free +
+
+
+
+ + + + + +
+ +
+
+

Quick Start

+

Get started with this integration in just a few steps.

+ +

Installation

+
+
+ bash + +
+
pip install crawl4ai
+
+ +

Basic Usage

+
+
+ python + +
+
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

+
+
+ python + +
+
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 + + + +
+ +
+
+
+
+ +

+ [ + Marketplace + ] +

+
+

Tools, Integrations & Resources for Web Crawling

+
+
+ Apps: -- + Articles: -- + Downloads: -- +
+
+
+ + +
+ +
+ + +
+
+ + +
+ + + + + + + + + + +
+ +
+
+

> Latest Apps

+ +
+
+ +
+
+ + +
+
+

> Latest Articles

+
+
+ +
+
+ + + +
+ + +
+
+

> More Apps

+ +
+
+ +
+
+
+ + +
+ + +
+
+ + + + \ 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.category} + ★ ${app.rating}/5 +
+
${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.title}
+ +
+ `).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) => ` + + `).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 => ` +
+
+ ${app.category} + ${app.type} +
+
${app.name}
+
+ `).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.category} + ★ ${app.rating}/5 +
+
${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.title}
+ +
+ `).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.category} + ${app.type} +
+
${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" From 749d200866b739595297f6a1aee0e39d67586806 Mon Sep 17 00:00:00 2001 From: unclecode Date: Thu, 2 Oct 2025 17:08:50 +0800 Subject: [PATCH 6/6] fix(marketplace): Update URLs to use /marketplace path and relative API endpoints - Change API_BASE to relative '/api' for production - Move marketplace to /marketplace instead of /marketplace/frontend - Update MkDocs navigation - Fix logo path in marketplace index --- docs/md_v2/marketplace/admin/admin.js | 2 +- docs/md_v2/marketplace/app-detail.css | 462 +++++++++ docs/md_v2/marketplace/app-detail.js | 324 ++++++ docs/md_v2/marketplace/frontend/app-detail.js | 2 +- .../md_v2/marketplace/frontend/marketplace.js | 2 +- docs/md_v2/marketplace/index.html | 147 +++ docs/md_v2/marketplace/marketplace.css | 957 ++++++++++++++++++ docs/md_v2/marketplace/marketplace.js | 395 ++++++++ mkdocs.yml | 2 +- 9 files changed, 2289 insertions(+), 4 deletions(-) create mode 100644 docs/md_v2/marketplace/app-detail.css create mode 100644 docs/md_v2/marketplace/app-detail.js create mode 100644 docs/md_v2/marketplace/index.html create mode 100644 docs/md_v2/marketplace/marketplace.css create mode 100644 docs/md_v2/marketplace/marketplace.js diff --git a/docs/md_v2/marketplace/admin/admin.js b/docs/md_v2/marketplace/admin/admin.js index 861d3ba7..258858da 100644 --- a/docs/md_v2/marketplace/admin/admin.js +++ b/docs/md_v2/marketplace/admin/admin.js @@ -1,5 +1,5 @@ // Admin Dashboard - Smart & Powerful -const API_BASE = 'http://localhost:8100/api'; +const API_BASE = '/api'; class AdminDashboard { constructor() { diff --git a/docs/md_v2/marketplace/app-detail.css b/docs/md_v2/marketplace/app-detail.css new file mode 100644 index 00000000..9f04c13a --- /dev/null +++ b/docs/md_v2/marketplace/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/app-detail.js b/docs/md_v2/marketplace/app-detail.js new file mode 100644 index 00000000..d1b3b559 --- /dev/null +++ b/docs/md_v2/marketplace/app-detail.js @@ -0,0 +1,324 @@ +// App Detail Page JavaScript +const API_BASE = '/api'; + +class AppDetailPage { + constructor() { + this.appSlug = this.getAppSlugFromURL(); + this.appData = null; + this.init(); + } + + getAppSlugFromURL() { + const params = new URLSearchParams(window.location.search); + return params.get('app') || ''; + } + + async init() { + if (!this.appSlug) { + window.location.href = 'index.html'; + return; + } + + await this.loadAppDetails(); + this.setupEventListeners(); + await this.loadRelatedApps(); + } + + async loadAppDetails() { + try { + const response = await fetch(`${API_BASE}/apps/${this.appSlug}`); + if (!response.ok) throw new Error('App not found'); + + this.appData = await response.json(); + this.renderAppDetails(); + } catch (error) { + console.error('Error loading app details:', error); + // Fallback to loading all apps and finding the right one + try { + const response = await fetch(`${API_BASE}/apps`); + const apps = await response.json(); + this.appData = apps.find(app => app.slug === this.appSlug || app.name.toLowerCase().replace(/\s+/g, '-') === this.appSlug); + if (this.appData) { + this.renderAppDetails(); + } else { + window.location.href = 'index.html'; + } + } catch (err) { + console.error('Error loading apps:', err); + window.location.href = 'index.html'; + } + } + } + + renderAppDetails() { + if (!this.appData) return; + + // Update title + document.title = `${this.appData.name} - Crawl4AI Marketplace`; + + // Hero image + const appImage = document.getElementById('app-image'); + if (this.appData.image) { + appImage.style.backgroundImage = `url('${this.appData.image}')`; + appImage.innerHTML = ''; + } else { + appImage.innerHTML = `[${this.appData.category || 'APP'}]`; + } + + // Basic info + document.getElementById('app-name').textContent = this.appData.name; + document.getElementById('app-description').textContent = this.appData.description; + document.getElementById('app-type').textContent = this.appData.type || 'Open Source'; + document.getElementById('app-category').textContent = this.appData.category; + document.getElementById('app-pricing').textContent = this.appData.pricing || 'Free'; + + // Badges + if (this.appData.featured) { + document.getElementById('app-featured').style.display = 'inline-block'; + } + if (this.appData.sponsored) { + document.getElementById('app-sponsored').style.display = 'inline-block'; + } + + // Stats + const rating = this.appData.rating || 0; + const stars = '★'.repeat(Math.floor(rating)) + '☆'.repeat(5 - Math.floor(rating)); + document.getElementById('app-rating').textContent = stars + ` ${rating}/5`; + document.getElementById('app-downloads').textContent = this.formatNumber(this.appData.downloads || 0); + + // Action buttons + const websiteBtn = document.getElementById('app-website'); + const githubBtn = document.getElementById('app-github'); + + if (this.appData.website_url) { + websiteBtn.href = this.appData.website_url; + } else { + websiteBtn.style.display = 'none'; + } + + if (this.appData.github_url) { + githubBtn.href = this.appData.github_url; + } else { + githubBtn.style.display = 'none'; + } + + // Contact + document.getElementById('app-contact').textContent = this.appData.contact_email || 'Not available'; + + // Integration guide + this.renderIntegrationGuide(); + } + + renderIntegrationGuide() { + // Installation code + const installCode = document.getElementById('install-code'); + if (this.appData.type === 'Open Source' && this.appData.github_url) { + installCode.textContent = `# Clone from GitHub +git clone ${this.appData.github_url} + +# Install dependencies +pip install -r requirements.txt`; + } else if (this.appData.name.toLowerCase().includes('api')) { + installCode.textContent = `# Install via pip +pip install ${this.appData.slug} + +# Or install from source +pip install git+${this.appData.github_url || 'https://github.com/example/repo'}`; + } + + // Usage code - customize based on category + const usageCode = document.getElementById('usage-code'); + if (this.appData.category === 'Browser Automation') { + usageCode.textContent = `from crawl4ai import AsyncWebCrawler +from ${this.appData.slug.replace(/-/g, '_')} import ${this.appData.name.replace(/\s+/g, '')} + +async def main(): + # Initialize ${this.appData.name} + automation = ${this.appData.name.replace(/\s+/g, '')}() + + async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + browser_config=automation.config, + wait_for="css:body" + ) + print(result.markdown)`; + } else if (this.appData.category === 'Proxy Services') { + usageCode.textContent = `from crawl4ai import AsyncWebCrawler +import ${this.appData.slug.replace(/-/g, '_')} + +# Configure proxy +proxy_config = { + "server": "${this.appData.website_url || 'https://proxy.example.com'}", + "username": "your_username", + "password": "your_password" +} + +async with AsyncWebCrawler(proxy=proxy_config) as crawler: + result = await crawler.arun( + url="https://example.com", + bypass_cache=True + ) + print(result.status_code)`; + } else if (this.appData.category === 'LLM Integration') { + usageCode.textContent = `from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +# Configure LLM extraction +strategy = LLMExtractionStrategy( + provider="${this.appData.name.toLowerCase().includes('gpt') ? 'openai' : 'anthropic'}", + api_key="your-api-key", + model="${this.appData.name.toLowerCase().includes('gpt') ? 'gpt-4' : 'claude-3'}", + instruction="Extract structured data" +) + +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + extraction_strategy=strategy + ) + print(result.extracted_content)`; + } + + // Integration example + const integrationCode = document.getElementById('integration-code'); + integrationCode.textContent = this.appData.integration_guide || +`# Complete ${this.appData.name} Integration Example + +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy +import json + +async def crawl_with_${this.appData.slug.replace(/-/g, '_')}(): + """ + Complete example showing how to use ${this.appData.name} + with Crawl4AI for production web scraping + """ + + # Define extraction schema + schema = { + "name": "ProductList", + "baseSelector": "div.product", + "fields": [ + {"name": "title", "selector": "h2", "type": "text"}, + {"name": "price", "selector": ".price", "type": "text"}, + {"name": "image", "selector": "img", "type": "attribute", "attribute": "src"}, + {"name": "link", "selector": "a", "type": "attribute", "attribute": "href"} + ] + } + + # Initialize crawler with ${this.appData.name} + async with AsyncWebCrawler( + browser_type="chromium", + headless=True, + verbose=True + ) as crawler: + + # Crawl with extraction + result = await crawler.arun( + url="https://example.com/products", + extraction_strategy=JsonCssExtractionStrategy(schema), + cache_mode="bypass", + wait_for="css:.product", + screenshot=True + ) + + # Process results + if result.success: + products = json.loads(result.extracted_content) + print(f"Found {len(products)} products") + + for product in products[:5]: + print(f"- {product['title']}: {product['price']}") + + return products + +# Run the crawler +if __name__ == "__main__": + import asyncio + asyncio.run(crawl_with_${this.appData.slug.replace(/-/g, '_')}())`; + } + + formatNumber(num) { + if (num >= 1000000) { + return (num / 1000000).toFixed(1) + 'M'; + } else if (num >= 1000) { + return (num / 1000).toFixed(1) + 'K'; + } + return num.toString(); + } + + setupEventListeners() { + // Tab switching + const tabs = document.querySelectorAll('.nav-tab'); + tabs.forEach(tab => { + tab.addEventListener('click', () => { + // Update active tab + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + // Show corresponding content + const tabName = tab.dataset.tab; + document.querySelectorAll('.tab-content').forEach(content => { + content.classList.remove('active'); + }); + document.getElementById(`${tabName}-tab`).classList.add('active'); + }); + }); + + // Copy integration code + document.getElementById('copy-integration').addEventListener('click', () => { + const code = document.getElementById('integration-code').textContent; + navigator.clipboard.writeText(code).then(() => { + const btn = document.getElementById('copy-integration'); + const originalText = btn.innerHTML; + btn.innerHTML = ' 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/app-detail.js b/docs/md_v2/marketplace/frontend/app-detail.js index 82422f14..d1b3b559 100644 --- a/docs/md_v2/marketplace/frontend/app-detail.js +++ b/docs/md_v2/marketplace/frontend/app-detail.js @@ -1,5 +1,5 @@ // App Detail Page JavaScript -const API_BASE = 'http://localhost:8100/api'; +const API_BASE = '/api'; class AppDetailPage { constructor() { diff --git a/docs/md_v2/marketplace/frontend/marketplace.js b/docs/md_v2/marketplace/frontend/marketplace.js index cdc22114..94a401bf 100644 --- a/docs/md_v2/marketplace/frontend/marketplace.js +++ b/docs/md_v2/marketplace/frontend/marketplace.js @@ -1,5 +1,5 @@ // Marketplace JS - Magazine Layout -const API_BASE = 'http://localhost:8100/api'; +const API_BASE = '/api'; const CACHE_TTL = 3600000; // 1 hour in ms class MarketplaceCache { diff --git a/docs/md_v2/marketplace/index.html b/docs/md_v2/marketplace/index.html new file mode 100644 index 00000000..c425420a --- /dev/null +++ b/docs/md_v2/marketplace/index.html @@ -0,0 +1,147 @@ + + + + + + Marketplace - Crawl4AI + + + +
+ +
+
+
+
+ +

+ [ + Marketplace + ] +

+
+

Tools, Integrations & Resources for Web Crawling

+
+
+ Apps: -- + Articles: -- + Downloads: -- +
+
+
+ + +
+ +
+ + +
+
+ + +
+ + + + + + + + + + +
+ +
+
+

> Latest Apps

+ +
+
+ +
+
+ + +
+
+

> Latest Articles

+
+
+ +
+
+ + + +
+ + +
+
+

> More Apps

+ +
+
+ +
+
+
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/docs/md_v2/marketplace/marketplace.css b/docs/md_v2/marketplace/marketplace.css new file mode 100644 index 00000000..ad26c344 --- /dev/null +++ b/docs/md_v2/marketplace/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/marketplace.js b/docs/md_v2/marketplace/marketplace.js new file mode 100644 index 00000000..94a401bf --- /dev/null +++ b/docs/md_v2/marketplace/marketplace.js @@ -0,0 +1,395 @@ +// Marketplace JS - Magazine Layout +const API_BASE = '/api'; +const CACHE_TTL = 3600000; // 1 hour in ms + +class MarketplaceCache { + constructor() { + this.prefix = 'c4ai_market_'; + } + + get(key) { + const item = localStorage.getItem(this.prefix + key); + if (!item) return null; + + const data = JSON.parse(item); + if (Date.now() > data.expires) { + localStorage.removeItem(this.prefix + key); + return null; + } + return data.value; + } + + set(key, value, ttl = CACHE_TTL) { + const data = { + value: value, + expires: Date.now() + ttl + }; + localStorage.setItem(this.prefix + key, JSON.stringify(data)); + } + + clear() { + Object.keys(localStorage) + .filter(k => k.startsWith(this.prefix)) + .forEach(k => localStorage.removeItem(k)); + } +} + +class MarketplaceAPI { + constructor() { + this.cache = new MarketplaceCache(); + this.searchTimeout = null; + } + + async fetch(endpoint, useCache = true) { + const cacheKey = endpoint.replace(/[^\w]/g, '_'); + + if (useCache) { + const cached = this.cache.get(cacheKey); + if (cached) return cached; + } + + try { + const response = await fetch(`${API_BASE}${endpoint}`); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + + const data = await response.json(); + this.cache.set(cacheKey, data); + return data; + } catch (error) { + console.error('API Error:', error); + return null; + } + } + + async getStats() { + return this.fetch('/stats'); + } + + async getCategories() { + return this.fetch('/categories'); + } + + async getApps(params = {}) { + const query = new URLSearchParams(params).toString(); + return this.fetch(`/apps${query ? '?' + query : ''}`); + } + + async getArticles(params = {}) { + const query = new URLSearchParams(params).toString(); + return this.fetch(`/articles${query ? '?' + query : ''}`); + } + + async getSponsors() { + return this.fetch('/sponsors'); + } + + async search(query) { + if (query.length < 2) return {}; + return this.fetch(`/search?q=${encodeURIComponent(query)}`, false); + } +} + +class MarketplaceUI { + constructor() { + this.api = new MarketplaceAPI(); + this.currentCategory = 'all'; + this.currentType = ''; + this.searchTimeout = null; + this.loadedApps = 10; + this.init(); + } + + async init() { + await this.loadStats(); + await this.loadCategories(); + await this.loadFeaturedContent(); + await this.loadSponsors(); + await this.loadMainContent(); + this.setupEventListeners(); + } + + async loadStats() { + const stats = await this.api.getStats(); + if (stats) { + document.getElementById('total-apps').textContent = stats.total_apps || '0'; + document.getElementById('total-articles').textContent = stats.total_articles || '0'; + document.getElementById('total-downloads').textContent = stats.total_downloads || '0'; + document.getElementById('last-update').textContent = new Date().toLocaleDateString(); + } + } + + async loadCategories() { + const categories = await this.api.getCategories(); + if (!categories) return; + + const filter = document.getElementById('category-filter'); + categories.forEach(cat => { + const btn = document.createElement('button'); + btn.className = 'filter-btn'; + btn.dataset.category = cat.slug; + btn.textContent = cat.name; + btn.onclick = () => this.filterByCategory(cat.slug); + filter.appendChild(btn); + }); + } + + async loadFeaturedContent() { + // Load hero featured + const featured = await this.api.getApps({ featured: true, limit: 4 }); + if (!featured || !featured.length) return; + + // Hero card (first featured) + const hero = featured[0]; + const heroCard = document.getElementById('featured-hero'); + if (hero) { + const imageUrl = hero.image || ''; + heroCard.innerHTML = ` +
+ ${!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.category} + ★ ${app.rating}/5 +
+
${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.title}
+ +
+ `).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) => ` + + `).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 => ` +
+
+ ${app.category} + ${app.type} +
+
${app.name}
+
+ `).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.category} + ★ ${app.rating}/5 +
+
${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.title}
+ +
+ `).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.category} + ${app.type} +
+
${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 d39172f6..6406b028 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,7 +14,7 @@ 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": "marketplace/index.html" - "Marketplace Admin": "marketplace/admin/index.html" - Setup & Installation: - "Installation": "core/installation.md"