fix(marketplace): replace hardcoded app detail content with database-driven fields.

The app detail page was displaying hardcoded/templated content instead of
using actual data from the database. This prevented admins from controlling
the content shown in Overview, Integration, and Documentation tabs.
This commit is contained in:
ntohidi
2025-10-21 15:39:04 +02:00
parent 6d1a398419
commit 97c92c4f62
2 changed files with 105 additions and 124 deletions

View File

@@ -529,8 +529,29 @@ class AdminDashboard {
</label> </label>
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label>Integration Guide</label> <label>Long Description (Markdown - shown in Overview tab)</label>
<textarea id="form-integration" rows="10">${app?.integration_guide || ''}</textarea> <textarea id="form-long-description" rows="8" placeholder="Detailed description with markdown formatting...">${app?.long_description || ''}</textarea>
<small>Supports markdown: **bold**, *italic*, [links](url), # headers, etc.</small>
</div>
<div class="form-group full-width">
<label>Installation Command (shown in Integration tab)</label>
<textarea id="form-installation" rows="5" placeholder="pip install package-name\n# or installation steps...">${app?.installation_command || ''}</textarea>
</div>
<div class="form-group full-width">
<label>Examples (Code examples - shown in Integration tab)</label>
<textarea id="form-examples" rows="10" placeholder="from package import module\n\n# Example usage\nresult = module.run()">${app?.examples || ''}</textarea>
</div>
<div class="form-group full-width">
<label>Integration Guide (Complete guide - shown in Integration tab)</label>
<textarea id="form-integration" rows="15" placeholder="# Complete integration guide with Crawl4AI\n\nfrom crawl4ai import AsyncWebCrawler\n...">${app?.integration_guide || ''}</textarea>
</div>
<div class="form-group full-width">
<label>Documentation (Markdown - shown in Documentation tab)</label>
<textarea id="form-documentation" rows="15" placeholder="# Documentation\n\n## Getting Started\n...">${app?.documentation || ''}</textarea>
</div>
<div class="form-group full-width">
<label>Requirements</label>
<textarea id="form-requirements" rows="4" placeholder="Python >= 3.8\ncrawl4ai >= 0.4.0">${app?.requirements || ''}</textarea>
</div> </div>
</div> </div>
`; `;
@@ -712,7 +733,12 @@ class AdminDashboard {
data.contact_email = document.getElementById('form-email').value; data.contact_email = document.getElementById('form-email').value;
data.featured = document.getElementById('form-featured').checked ? 1 : 0; data.featured = document.getElementById('form-featured').checked ? 1 : 0;
data.sponsored = document.getElementById('form-sponsored').checked ? 1 : 0; data.sponsored = document.getElementById('form-sponsored').checked ? 1 : 0;
data.long_description = document.getElementById('form-long-description').value;
data.installation_command = document.getElementById('form-installation').value;
data.examples = document.getElementById('form-examples').value;
data.integration_guide = document.getElementById('form-integration').value; data.integration_guide = document.getElementById('form-integration').value;
data.documentation = document.getElementById('form-documentation').value;
data.requirements = document.getElementById('form-requirements').value;
} else if (type === 'articles') { } else if (type === 'articles') {
data.title = document.getElementById('form-title').value; data.title = document.getElementById('form-title').value;
data.slug = this.generateSlug(data.title); data.slug = this.generateSlug(data.title);

View File

@@ -123,144 +123,99 @@ class AppDetailPage {
document.getElementById('sidebar-pricing').textContent = this.appData.pricing || 'Free'; document.getElementById('sidebar-pricing').textContent = this.appData.pricing || 'Free';
document.getElementById('sidebar-contact').textContent = this.appData.contact_email || 'contact@example.com'; document.getElementById('sidebar-contact').textContent = this.appData.contact_email || 'contact@example.com';
// Integration guide // Render tab contents from database fields
this.renderIntegrationGuide(); this.renderTabContents();
} }
renderIntegrationGuide() { renderTabContents() {
// Installation code // Overview tab - use long_description from database
const overviewDiv = document.getElementById('app-overview');
if (overviewDiv) {
if (this.appData.long_description) {
overviewDiv.innerHTML = this.renderMarkdown(this.appData.long_description);
} else {
overviewDiv.innerHTML = `<p>${this.appData.description || 'No overview available.'}</p>`;
}
}
// Documentation tab - use documentation field from database
const docsDiv = document.getElementById('app-docs');
if (docsDiv) {
if (this.appData.documentation) {
docsDiv.innerHTML = this.renderMarkdown(this.appData.documentation);
} else {
docsDiv.innerHTML = '<p>Documentation coming soon.</p>';
}
}
// Integration tab - use integration_guide, installation_command, examples from database
this.renderIntegrationTab();
}
renderIntegrationTab() {
// Installation code - use installation_command from database
const installCode = document.getElementById('install-code'); const installCode = document.getElementById('install-code');
if (installCode) { if (installCode) {
if (this.appData.type === 'Open Source' && this.appData.github_url) { if (this.appData.installation_command) {
installCode.textContent = `# Clone from GitHub installCode.textContent = this.appData.installation_command;
git clone ${this.appData.github_url} } else {
// Fallback to generic installation
# Install dependencies installCode.textContent = `# Installation instructions not yet available\n# Please check ${this.appData.website_url || 'the official website'} for details`;
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 // Usage code - use examples field from database
const usageCode = document.getElementById('usage-code'); const usageCode = document.getElementById('usage-code');
if (usageCode) { if (usageCode && this.appData.examples) {
if (this.appData.category === 'Browser Automation') { // Extract first code block from examples if it contains multiple
usageCode.textContent = `from crawl4ai import AsyncWebCrawler const codeMatch = this.appData.examples.match(/```[\s\S]*?```/);
from ${this.appData.slug.replace(/-/g, '_')} import ${this.appData.name.replace(/\s+/g, '')} if (codeMatch) {
usageCode.textContent = codeMatch[0].replace(/```(\w+)?\n?/g, '').trim();
async def main(): } else {
# Initialize ${this.appData.name} usageCode.textContent = this.appData.examples;
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 // Complete integration - use integration_guide field from database
const integrationCode = document.getElementById('integration-code'); const integrationCode = document.getElementById('integration-code');
if (integrationCode) { if (integrationCode) {
integrationCode.textContent = this.appData.integration_guide || if (this.appData.integration_guide) {
`# Complete ${this.appData.name} Integration Example integrationCode.textContent = this.appData.integration_guide;
} else {
from crawl4ai import AsyncWebCrawler // Fallback message
from crawl4ai.extraction_strategy import JsonCssExtractionStrategy integrationCode.textContent = `# Integration guide not yet available for ${this.appData.name}\n\n# Please visit the admin panel to add integration instructions\n# Or check ${this.appData.website_url || 'the official website'} for integration details`;
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} renderMarkdown(text) {
async with AsyncWebCrawler( if (!text) return '';
browser_type="chromium",
headless=True,
verbose=True
) as crawler:
# Crawl with extraction // Simple markdown rendering (convert to HTML)
result = await crawler.arun( return text
url="https://example.com/products", // Headers
extraction_strategy=JsonCssExtractionStrategy(schema), .replace(/^### (.*$)/gim, '<h3>$1</h3>')
cache_mode="bypass", .replace(/^## (.*$)/gim, '<h2>$1</h2>')
wait_for="css:.product", .replace(/^# (.*$)/gim, '<h1>$1</h1>')
screenshot=True // Bold
) .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
// Italic
# Process results .replace(/\*(.*?)\*/g, '<em>$1</em>')
if result.success: // Links
products = json.loads(result.extracted_content) .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
print(f"Found {len(products)} products") // Code blocks
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
for product in products[:5]: // Inline code
print(f"- {product['title']}: {product['price']}") .replace(/`([^`]+)`/g, '<code>$1</code>')
// Line breaks
return products .replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
# Run the crawler // Lists
if __name__ == "__main__": .replace(/^\* (.*)$/gim, '<li>$1</li>')
import asyncio .replace(/^- (.*)$/gim, '<li>$1</li>')
asyncio.run(crawl_with_${this.appData.slug.replace(/-/g, '_')}())`; // Wrap in paragraphs
} .replace(/^(?!<[h|p|pre|ul|ol|li])/gim, '<p>')
.replace(/(?<![>])$/gim, '</p>');
} }
formatNumber(num) { formatNumber(num) {