fix(marketplace): improve app detail page content rendering and UX

Fixed multiple issues with app detail page content display and formatting
This commit is contained in:
ntohidi
2025-10-23 16:12:30 +02:00
parent 97c92c4f62
commit 13e116610d
4 changed files with 112 additions and 126 deletions

View File

@@ -529,29 +529,19 @@ class AdminDashboard {
</label> </label>
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label>Long Description (Markdown - shown in Overview tab)</label> <label>Long Description (Markdown - Overview tab)</label>
<textarea id="form-long-description" rows="8" placeholder="Detailed description with markdown formatting...">${app?.long_description || ''}</textarea> <textarea id="form-long-description" rows="10" placeholder="Enter detailed description with markdown formatting...">${app?.long_description || ''}</textarea>
<small>Supports markdown: **bold**, *italic*, [links](url), # headers, etc.</small> <small>Markdown support: **bold**, *italic*, [links](url), # headers, code blocks, lists</small>
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label>Installation Command (shown in Integration tab)</label> <label>Integration Guide (Markdown - Integration tab)</label>
<textarea id="form-installation" rows="5" placeholder="pip install package-name\n# or installation steps...">${app?.installation_command || ''}</textarea> <textarea id="form-integration" rows="20" placeholder="Enter integration guide with installation, examples, and code snippets using markdown...">${app?.integration_guide || ''}</textarea>
<small>Single markdown field with installation, examples, and complete guide. Code blocks get auto copy buttons.</small>
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label>Examples (Code examples - shown in Integration tab)</label> <label>Documentation (Markdown - Documentation tab)</label>
<textarea id="form-examples" rows="10" placeholder="from package import module\n\n# Example usage\nresult = module.run()">${app?.examples || ''}</textarea> <textarea id="form-documentation" rows="20" placeholder="Enter documentation with API reference, examples, and best practices using markdown...">${app?.documentation || ''}</textarea>
</div> <small>Full documentation with API reference, examples, best practices, etc.</small>
<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>
`; `;
@@ -734,11 +724,8 @@ class AdminDashboard {
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.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.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

@@ -510,6 +510,31 @@
line-height: 1.5; line-height: 1.5;
} }
/* Markdown rendered code blocks */
.integration-content pre,
.docs-content pre {
background: var(--bg-dark);
border: 1px solid var(--border-color);
margin: 1rem 0;
padding: 1rem;
padding-top: 2.5rem; /* Space for copy button */
overflow-x: auto;
position: relative;
max-height: none; /* Remove any height restrictions */
height: auto; /* Allow content to expand */
}
.integration-content pre code,
.docs-content pre code {
background: transparent;
padding: 0;
color: var(--text-secondary);
font-size: 0.875rem;
line-height: 1.5;
white-space: pre; /* Preserve whitespace and line breaks */
display: block;
}
/* Feature Grid */ /* Feature Grid */
.feature-grid { .feature-grid {
display: grid; display: grid;

View File

@@ -80,20 +80,7 @@
<section id="overview-tab" class="tab-content active"> <section id="overview-tab" class="tab-content active">
<div class="overview-columns"> <div class="overview-columns">
<div class="overview-main"> <div class="overview-main">
<h2>Overview</h2>
<div id="app-overview">Overview content goes here.</div> <div id="app-overview">Overview content goes here.</div>
<h3>Key Features</h3>
<ul id="app-features" class="features-list">
<li>Feature 1</li>
<li>Feature 2</li>
<li>Feature 3</li>
</ul>
<h3>Use Cases</h3>
<div id="app-use-cases" class="use-cases">
<p>Describe how this app can help your workflow.</p>
</div>
</div> </div>
<aside class="sidebar"> <aside class="sidebar">
@@ -142,33 +129,14 @@
</section> </section>
<section id="integration-tab" class="tab-content"> <section id="integration-tab" class="tab-content">
<div class="integration-content"> <div class="integration-content" id="app-integration">
<h2>Integration Guide</h2> <!-- Integration guide markdown content will be rendered here -->
<h3>Installation</h3>
<div class="code-block">
<pre><code id="install-code"># Installation instructions will appear here</code></pre>
</div>
<h3>Basic Usage</h3>
<div class="code-block">
<pre><code id="usage-code"># Usage example will appear here</code></pre>
</div>
<h3>Complete Integration Example</h3>
<div class="code-block">
<button class="copy-btn" id="copy-integration">Copy</button>
<pre><code id="integration-code"># Complete integration guide will appear here</code></pre>
</div>
</div> </div>
</section> </section>
<section id="docs-tab" class="tab-content"> <section id="docs-tab" class="tab-content">
<div class="docs-content"> <div class="docs-content" id="app-docs">
<h2>Documentation</h2> <!-- Documentation markdown content will be rendered here -->
<div id="app-docs" class="doc-sections">
<p>Documentation coming soon.</p>
</div>
</div> </div>
</section> </section>

View File

@@ -138,61 +138,80 @@ class AppDetailPage {
} }
} }
// Integration tab - use integration_guide field from database
const integrationDiv = document.getElementById('app-integration');
if (integrationDiv) {
if (this.appData.integration_guide) {
integrationDiv.innerHTML = this.renderMarkdown(this.appData.integration_guide);
// Add copy buttons to all code blocks
this.addCopyButtonsToCodeBlocks(integrationDiv);
} else {
integrationDiv.innerHTML = '<p>Integration guide not yet available. Please check the official website for details.</p>';
}
}
// Documentation tab - use documentation field from database // Documentation tab - use documentation field from database
const docsDiv = document.getElementById('app-docs'); const docsDiv = document.getElementById('app-docs');
if (docsDiv) { if (docsDiv) {
if (this.appData.documentation) { if (this.appData.documentation) {
docsDiv.innerHTML = this.renderMarkdown(this.appData.documentation); docsDiv.innerHTML = this.renderMarkdown(this.appData.documentation);
// Add copy buttons to all code blocks
this.addCopyButtonsToCodeBlocks(docsDiv);
} else { } else {
docsDiv.innerHTML = '<p>Documentation coming soon.</p>'; docsDiv.innerHTML = '<p>Documentation coming soon.</p>';
} }
} }
// Integration tab - use integration_guide, installation_command, examples from database
this.renderIntegrationTab();
} }
renderIntegrationTab() { addCopyButtonsToCodeBlocks(container) {
// Installation code - use installation_command from database // Find all code blocks and add copy buttons
const installCode = document.getElementById('install-code'); const codeBlocks = container.querySelectorAll('pre code');
if (installCode) { codeBlocks.forEach(codeBlock => {
if (this.appData.installation_command) { const pre = codeBlock.parentElement;
installCode.textContent = this.appData.installation_command;
} else {
// Fallback to generic installation
installCode.textContent = `# Installation instructions not yet available\n# Please check ${this.appData.website_url || 'the official website'} for details`;
}
}
// Usage code - use examples field from database // Skip if already has a copy button
const usageCode = document.getElementById('usage-code'); if (pre.querySelector('.copy-btn')) return;
if (usageCode && this.appData.examples) {
// Extract first code block from examples if it contains multiple
const codeMatch = this.appData.examples.match(/```[\s\S]*?```/);
if (codeMatch) {
usageCode.textContent = codeMatch[0].replace(/```(\w+)?\n?/g, '').trim();
} else {
usageCode.textContent = this.appData.examples;
}
}
// Complete integration - use integration_guide field from database // Create copy button
const integrationCode = document.getElementById('integration-code'); const copyBtn = document.createElement('button');
if (integrationCode) { copyBtn.className = 'copy-btn';
if (this.appData.integration_guide) { copyBtn.textContent = 'Copy';
integrationCode.textContent = this.appData.integration_guide; copyBtn.onclick = () => {
} else { navigator.clipboard.writeText(codeBlock.textContent).then(() => {
// Fallback message copyBtn.textContent = '✓ Copied!';
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`; setTimeout(() => {
} copyBtn.textContent = 'Copy';
} }, 2000);
});
};
// Add button to pre element
pre.style.position = 'relative';
pre.insertBefore(copyBtn, codeBlock);
});
} }
renderMarkdown(text) { renderMarkdown(text) {
if (!text) return ''; if (!text) return '';
// Simple markdown rendering (convert to HTML) // Store code blocks temporarily to protect them from processing
return text const codeBlocks = [];
let processed = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
const placeholder = `___CODE_BLOCK_${codeBlocks.length}___`;
codeBlocks.push(`<pre><code class="language-${lang || ''}">${this.escapeHtml(code)}</code></pre>`);
return placeholder;
});
// Store inline code temporarily
const inlineCodes = [];
processed = processed.replace(/`([^`]+)`/g, (match, code) => {
const placeholder = `___INLINE_CODE_${inlineCodes.length}___`;
inlineCodes.push(`<code>${this.escapeHtml(code)}</code>`);
return placeholder;
});
// Now process the rest of the markdown
processed = processed
// Headers // Headers
.replace(/^### (.*$)/gim, '<h3>$1</h3>') .replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>') .replace(/^## (.*$)/gim, '<h2>$1</h2>')
@@ -203,10 +222,6 @@ class AppDetailPage {
.replace(/\*(.*?)\*/g, '<em>$1</em>') .replace(/\*(.*?)\*/g, '<em>$1</em>')
// Links // Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>') .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
// Code blocks
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
// Inline code
.replace(/`([^`]+)`/g, '<code>$1</code>')
// Line breaks // Line breaks
.replace(/\n\n/g, '</p><p>') .replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>') .replace(/\n/g, '<br>')
@@ -216,6 +231,24 @@ class AppDetailPage {
// Wrap in paragraphs // Wrap in paragraphs
.replace(/^(?!<[h|p|pre|ul|ol|li])/gim, '<p>') .replace(/^(?!<[h|p|pre|ul|ol|li])/gim, '<p>')
.replace(/(?<![>])$/gim, '</p>'); .replace(/(?<![>])$/gim, '</p>');
// Restore inline code
inlineCodes.forEach((code, i) => {
processed = processed.replace(`___INLINE_CODE_${i}___`, code);
});
// Restore code blocks
codeBlocks.forEach((block, i) => {
processed = processed.replace(`___CODE_BLOCK_${i}___`, block);
});
return processed;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
} }
formatNumber(num) { formatNumber(num) {
@@ -244,33 +277,6 @@ class AppDetailPage {
document.getElementById(`${tabName}-tab`).classList.add('active'); document.getElementById(`${tabName}-tab`).classList.add('active');
}); });
}); });
// Copy integration code
document.getElementById('copy-integration').addEventListener('click', () => {
const code = document.getElementById('integration-code').textContent;
navigator.clipboard.writeText(code).then(() => {
const btn = document.getElementById('copy-integration');
const originalText = btn.innerHTML;
btn.innerHTML = '<span>✓</span> Copied!';
setTimeout(() => {
btn.innerHTML = originalText;
}, 2000);
});
});
// Copy code buttons
document.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const codeBlock = e.target.closest('.code-block');
const code = codeBlock.querySelector('code').textContent;
navigator.clipboard.writeText(code).then(() => {
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = 'Copy';
}, 2000);
});
});
});
} }
async loadRelatedApps() { async loadRelatedApps() {