first draft contributions

This commit is contained in:
overcuriousity
2025-07-22 21:56:01 +02:00
parent 9798837806
commit 043a2d32ac
4 changed files with 1742 additions and 0 deletions

View File

@@ -0,0 +1,732 @@
---
// src/pages/contribute/tool.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getAuthContext, requireAuth } from '../../utils/serverAuth.js';
import { getToolsData } from '../../utils/dataService.js';
// Check authentication
const authContext = await getAuthContext(Astro);
const authRedirect = requireAuth(authContext, Astro.url.toString());
if (authRedirect) return authRedirect;
// Load existing data for validation and editing
const data = await getToolsData();
const domains = data.domains;
const phases = data.phases;
const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
const existingTools = data.tools;
// Check if this is an edit operation
const editToolName = Astro.url.searchParams.get('edit');
const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null;
const isEdit = !!editTool;
const title = isEdit ? `Edit ${editTool?.name}` : 'Contribute New Tool';
---
<BaseLayout title={title} description="Contribute tools, methods, and concepts to the CC24-Guide database">
<section style="padding: 2rem 0;">
<!-- Header -->
<div style="text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%); color: white; border-radius: 1rem; border: 1px solid var(--color-border);">
<h1 style="margin-bottom: 1rem; font-size: 2rem;">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.75rem; vertical-align: middle;">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
{isEdit ? `Edit Tool: ${editTool?.name}` : 'Contribute New Tool'}
</h1>
<p style="margin: 0; opacity: 0.9; line-height: 1.5;">
{isEdit
? 'Update the information for this tool, method, or concept. Your changes will be submitted as a pull request for review.'
: 'Submit a new tool, method, or concept to the CC24-Guide database. Your contribution will be reviewed before being added.'
}
</p>
</div>
<!-- Form Container -->
<div style="max-width: 800px; margin: 0 auto;">
<div class="card" style="padding: 2rem;">
<form id="contribution-form" style="display: flex; flex-direction: column; gap: 1.5rem;">
<!-- Tool Type Selection -->
<div>
<label for="tool-type" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Tool Type <span style="color: var(--color-error);">*</span>
</label>
<select id="tool-type" name="type" required style="max-width: 300px;">
<option value="">Select type...</option>
<option value="software" selected={editTool?.type === 'software'}>Software</option>
<option value="method" selected={editTool?.type === 'method'}>Method</option>
<option value="concept" selected={editTool?.type === 'concept'}>Concept</option>
</select>
<div class="field-help" style="font-size: 0.8125rem; color: var(--color-text-secondary); margin-top: 0.25rem;">
Software: Applications and tools • Method: Procedures and methodologies • Concept: Fundamental knowledge
</div>
</div>
<!-- Basic Information -->
<div style="display: grid; grid-template-columns: 1fr; gap: 1rem;">
<!-- Tool Name -->
<div>
<label for="tool-name" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Name <span style="color: var(--color-error);">*</span>
</label>
<input type="text" id="tool-name" name="name" required maxlength="100"
value={editTool?.name || ''}
placeholder="e.g., Autopsy, Live Response Methodology, Regular Expressions" />
<div id="name-error" class="field-error" style="display: none;"></div>
</div>
<!-- Icon (Emoji) -->
<div>
<label for="tool-icon" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Icon (Emoji)
</label>
<input type="text" id="tool-icon" name="icon" maxlength="10"
value={editTool?.icon || ''}
placeholder="📦 🔧 📋 (optional, single emoji recommended)" />
<div class="field-help">
Choose an emoji that represents your tool/method/concept. Leave blank if unsure.
</div>
</div>
</div>
<!-- Description -->
<div>
<label for="tool-description" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Description <span style="color: var(--color-error);">*</span>
</label>
<textarea id="tool-description" name="description" required
rows="4" minlength="10" maxlength="1000"
placeholder="Provide a clear, concise description of what this tool/method/concept is and what it does...">{editTool?.description || ''}</textarea>
<div style="display: flex; justify-content: space-between; margin-top: 0.25rem;">
<div class="field-help">Be specific about functionality, use cases, and key features.</div>
<div id="description-count" style="font-size: 0.75rem; color: var(--color-text-secondary);">0/1000</div>
</div>
<div id="description-error" class="field-error" style="display: none;"></div>
</div>
<!-- URLs -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<!-- Main URL -->
<div>
<label for="tool-url" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Main URL <span style="color: var(--color-error);">*</span>
</label>
<input type="url" id="tool-url" name="url" required
value={editTool?.url || ''}
placeholder="https://example.com" />
<div class="field-help">Homepage, documentation, or primary resource link</div>
<div id="url-error" class="field-error" style="display: none;"></div>
</div>
<!-- Project URL (CC24 Server) -->
<div id="project-url-field" style="display: none;">
<label for="project-url" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
CC24 Server URL
</label>
<input type="url" id="project-url" name="projectUrl"
value={editTool?.projectUrl || ''}
placeholder="https://tool.cc24.dev" />
<div class="field-help">Internal CC24 server URL (if hosted)</div>
</div>
</div>
<!-- Categories -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<!-- Domains -->
<div>
<label for="tool-domains" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Forensic Domains
</label>
<select id="tool-domains" name="domains" multiple size="4">
{domains.map(domain => (
<option value={domain.id}
selected={editTool?.domains?.includes(domain.id)}>
{domain.name}
</option>
))}
</select>
<div class="field-help">Hold Ctrl/Cmd to select multiple. Leave empty for domain-agnostic.</div>
</div>
<!-- Phases -->
<div>
<label for="tool-phases" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Investigation Phases
</label>
<select id="tool-phases" name="phases" multiple size="4">
{phases.map(phase => (
<option value={phase.id}
selected={editTool?.phases?.includes(phase.id)}>
{phase.name}
</option>
))}
{domainAgnosticSoftware.map(section => (
<option value={section.id}
selected={editTool?.phases?.includes(section.id)}>
{section.name}
</option>
))}
</select>
<div class="field-help">Select applicable investigation phases</div>
</div>
</div>
<!-- Software-Specific Fields -->
<div id="software-fields" style="display: none;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<!-- Platforms -->
<div>
<label for="tool-platforms" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Platforms <span id="platforms-required" style="color: var(--color-error);">*</span>
</label>
<div id="platforms-checkboxes" style="display: grid; gap: 0.25rem; font-size: 0.875rem;">
{['Windows', 'macOS', 'Linux', 'Web', 'Mobile', 'Cross-platform'].map(platform => (
<label class="checkbox-wrapper" style="margin-bottom: 0.25rem;">
<input type="checkbox" name="platforms" value={platform}
checked={editTool?.platforms?.includes(platform)} />
<span>{platform}</span>
</label>
))}
</div>
<div id="platforms-error" class="field-error" style="display: none;"></div>
</div>
<!-- License -->
<div>
<label for="tool-license" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
License <span id="license-required" style="color: var(--color-error);">*</span>
</label>
<input type="text" id="tool-license" name="license" list="license-options"
value={editTool?.license || ''}
placeholder="e.g., MIT, Apache 2.0, GPL v3, Proprietary" />
<datalist id="license-options">
<option value="MIT" />
<option value="Apache 2.0" />
<option value="GPL v3" />
<option value="GPL v2" />
<option value="LGPL-3.0" />
<option value="LGPL-2.1" />
<option value="BSD-3-Clause" />
<option value="BSD-2-Clause" />
<option value="ISC" />
<option value="Mozilla Public License 2.0" />
<option value="Open Source" />
<option value="Proprietary" />
<option value="Freeware" />
<option value="Commercial" />
<option value="Dual License" />
</datalist>
<div class="field-help">Type any license or select from suggestions</div>
</div>
<!-- Access Type -->
<div style="grid-column: 1 / -1;">
<label for="access-type" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Access Type
</label>
<select id="access-type" name="accessType">
<option value="">Select access type...</option>
<option value="download" selected={editTool?.accessType === 'download'}>Download</option>
<option value="web" selected={editTool?.accessType === 'web'}>Web Application</option>
<option value="api" selected={editTool?.accessType === 'api'}>API</option>
<option value="cli" selected={editTool?.accessType === 'cli'}>Command Line</option>
<option value="service" selected={editTool?.accessType === 'service'}>Service</option>
</select>
</div>
</div>
</div>
<!-- Skill Level and Additional Options -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div>
<label for="skill-level" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Skill Level <span style="color: var(--color-error);">*</span>
</label>
<select id="skill-level" name="skillLevel" required>
<option value="">Select skill level...</option>
<option value="novice" selected={editTool?.skillLevel === 'novice'}>Novice</option>
<option value="beginner" selected={editTool?.skillLevel === 'beginner'}>Beginner</option>
<option value="intermediate" selected={editTool?.skillLevel === 'intermediate'}>Intermediate</option>
<option value="advanced" selected={editTool?.skillLevel === 'advanced'}>Advanced</option>
<option value="expert" selected={editTool?.skillLevel === 'expert'}>Expert</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Additional Options
</label>
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<label class="checkbox-wrapper">
<input type="checkbox" id="has-knowledgebase" name="knowledgebase"
checked={editTool?.knowledgebase} />
<span>Has knowledgebase article</span>
</label>
</div>
</div>
</div>
<!-- Tags -->
<div>
<label for="tool-tags" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Tags
</label>
<input type="text" id="tool-tags" name="tags"
value={editTool?.tags?.join(', ') || ''}
placeholder="gui, forensics, network-analysis, mobile (comma-separated)" />
<div class="field-help">
Add relevant tags separated by commas. Use lowercase with hyphens for multi-word tags.
</div>
</div>
<!-- Related Concepts (for software/methods) -->
<div id="related-concepts-field" style="display: none;">
<label for="related-concepts" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Related Concepts
</label>
<select id="related-concepts" name="relatedConcepts" multiple size="3">
{existingTools.filter(tool => tool.type === 'concept').map(concept => (
<option value={concept.name}
selected={editTool?.related_concepts?.includes(concept.name)}>
{concept.name}
</option>
))}
</select>
<div class="field-help">
Select concepts that users should understand when using this tool/method
</div>
</div>
<!-- Contribution Reason -->
<div>
<label for="contribution-reason" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Reason for Contribution (Optional)
</label>
<textarea id="contribution-reason" name="reason"
rows="2" maxlength="500"
placeholder="Why are you adding/updating this tool? Any additional context for reviewers..."></textarea>
<div style="display: flex; justify-content: space-between; margin-top: 0.25rem;">
<div class="field-help">Help reviewers understand your contribution</div>
<div id="reason-count" style="font-size: 0.75rem; color: var(--color-text-secondary);">0/500</div>
</div>
</div>
<!-- YAML Preview -->
<div>
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 0.5rem;">
<label style="font-weight: 600;">YAML Preview</label>
<button type="button" id="refresh-preview" class="btn btn-secondary" style="padding: 0.25rem 0.75rem; font-size: 0.8125rem;">
Refresh Preview
</button>
</div>
<pre id="yaml-preview" style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 0.375rem; padding: 1rem; font-size: 0.8125rem; overflow-x: auto; max-height: 300px;">
# YAML preview will appear here
</pre>
</div>
<!-- Submit Buttons -->
<div style="display: flex; gap: 1rem; justify-content: flex-end; margin-top: 1rem;">
<a href="/" class="btn btn-secondary">Cancel</a>
<button type="submit" id="submit-btn" class="btn btn-primary">
<span id="submit-text">{isEdit ? 'Update Tool' : 'Submit Contribution'}</span>
<svg id="submit-spinner" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none; margin-left: 0.5rem; animation: pulse 2s ease-in-out infinite;">
<path d="M21 12a9 9 0 0 0-9-9 7 7 0 0 0-7 7"/>
</svg>
</button>
</div>
</form>
</div>
</div>
<!-- Success Modal -->
<div id="success-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
<div class="card" style="max-width: 500px; width: 90%; margin: 2rem;">
<div style="text-align: center;">
<div style="background-color: var(--color-accent); color: white; width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20,6 9,17 4,12"/>
</svg>
</div>
<h3 style="margin-bottom: 1rem;">Contribution Submitted!</h3>
<p id="success-message" style="margin-bottom: 1.5rem; line-height: 1.5;"></p>
<div style="display: flex; gap: 1rem; justify-content: center;">
<a id="pr-link" href="#" target="_blank" class="btn btn-primary" style="display: none;">View Pull Request</a>
<a href="/" class="btn btn-secondary">Back to Home</a>
</div>
</div>
</div>
</div>
</section>
</BaseLayout>
<style>
.field-error {
color: var(--color-error);
font-size: 0.8125rem;
margin-top: 0.25rem;
}
.field-help {
font-size: 0.8125rem;
color: var(--color-text-secondary);
}
input:invalid, textarea:invalid, select:invalid {
border-color: var(--color-error);
}
input:valid, textarea:valid, select:valid {
border-color: var(--color-accent);
}
#yaml-preview {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
line-height: 1.4;
white-space: pre-wrap;
word-break: break-all;
}
select[multiple] {
min-height: auto;
}
/* Responsive adjustments */
@media (width <= 768px) {
div[style*="grid-template-columns: 1fr 1fr"] {
grid-template-columns: 1fr !important;
}
}
</style>
<script define:vars={{
isEdit,
editTool: editTool || null,
domains,
phases,
domainAgnosticSoftware,
existingConcepts: existingTools.filter(t => t.type === 'concept')
}}>
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contribution-form');
const typeSelect = document.getElementById('tool-type');
const submitBtn = document.getElementById('submit-btn');
const submitText = document.getElementById('submit-text');
const submitSpinner = document.getElementById('submit-spinner');
const yamlPreview = document.getElementById('yaml-preview');
const refreshPreviewBtn = document.getElementById('refresh-preview');
const successModal = document.getElementById('success-modal');
// Form elements
const nameInput = document.getElementById('tool-name');
const descriptionTextarea = document.getElementById('tool-description');
const reasonTextarea = document.getElementById('contribution-reason');
// Field groups
const softwareFields = document.getElementById('software-fields');
const projectUrlField = document.getElementById('project-url-field');
const relatedConceptsField = document.getElementById('related-concepts-field');
// Required indicators
const platformsRequired = document.getElementById('platforms-required');
const licenseRequired = document.getElementById('license-required');
// Update character counters
function updateCharacterCounter(textarea, countElement, maxLength) {
const count = textarea.value.length;
countElement.textContent = `${count}/${maxLength}`;
countElement.style.color = count > maxLength * 0.9 ? 'var(--color-warning)' : 'var(--color-text-secondary)';
}
// Set up character counters
const descriptionCount = document.getElementById('description-count');
const reasonCount = document.getElementById('reason-count');
descriptionTextarea.addEventListener('input', () => updateCharacterCounter(descriptionTextarea, descriptionCount, 1000));
reasonTextarea.addEventListener('input', () => updateCharacterCounter(reasonTextarea, reasonCount, 500));
// Initial counter update
updateCharacterCounter(descriptionTextarea, descriptionCount, 1000);
updateCharacterCounter(reasonTextarea, reasonCount, 500);
// Handle type-specific field visibility
function updateFieldVisibility() {
const selectedType = typeSelect.value;
// Hide all type-specific fields
softwareFields.style.display = 'none';
relatedConceptsField.style.display = 'none';
// Show project URL for software only
projectUrlField.style.display = selectedType === 'software' ? 'block' : 'none';
// Handle required fields
const platformsCheckboxes = document.querySelectorAll('input[name="platforms"]');
const licenseSelect = document.getElementById('tool-license');
if (selectedType === 'software') {
// Show software-specific fields
softwareFields.style.display = 'block';
relatedConceptsField.style.display = 'block';
// Make platforms and license required
platformsRequired.style.display = 'inline';
licenseRequired.style.display = 'inline';
platformsCheckboxes.forEach(cb => cb.setAttribute('required', 'required'));
licenseSelect.setAttribute('required', 'required');
} else {
// Hide required indicators and remove requirements
platformsRequired.style.display = 'none';
licenseRequired.style.display = 'none';
platformsCheckboxes.forEach(cb => cb.removeAttribute('required'));
licenseSelect.removeAttribute('required');
// Show related concepts for methods
if (selectedType === 'method') {
relatedConceptsField.style.display = 'block';
}
}
// Update YAML preview
updateYAMLPreview();
}
// Generate YAML preview
function updateYAMLPreview() {
try {
const formData = new FormData(form);
const toolData = {
name: formData.get('name') || '',
icon: formData.get('icon') || null,
type: formData.get('type') || '',
description: formData.get('description') || '',
domains: formData.getAll('domains') || [],
phases: formData.getAll('phases') || [],
skillLevel: formData.get('skillLevel') || '',
url: formData.get('url') || ''
};
// Add type-specific fields
if (toolData.type === 'software') {
toolData.platforms = formData.getAll('platforms') || [];
toolData.license = formData.get('license')?.trim() || null;
toolData.accessType = formData.get('accessType') || null;
toolData.projectUrl = formData.get('projectUrl') || null;
} else {
toolData.platforms = [];
toolData.license = null;
toolData.accessType = null;
}
// Add optional fields
toolData.knowledgebase = formData.has('knowledgebase') || null;
// Handle tags
const tagsValue = formData.get('tags');
toolData.tags = tagsValue ? tagsValue.split(',').map(tag => tag.trim()).filter(Boolean) : [];
// Handle related concepts
if (toolData.type !== 'concept') {
toolData.related_concepts = formData.getAll('relatedConcepts') || null;
}
// Convert to YAML-like format for preview
let yamlContent = `- name: "${toolData.name}"\n`;
if (toolData.icon) yamlContent += ` icon: "${toolData.icon}"\n`;
yamlContent += ` type: ${toolData.type}\n`;
yamlContent += ` description: >\n ${toolData.description}\n`;
if (toolData.domains.length > 0) {
yamlContent += ` domains:\n${toolData.domains.map(d => ` - ${d}`).join('\n')}\n`;
}
if (toolData.phases.length > 0) {
yamlContent += ` phases:\n${toolData.phases.map(p => ` - ${p}`).join('\n')}\n`;
}
if (toolData.platforms.length > 0) {
yamlContent += ` platforms:\n${toolData.platforms.map(p => ` - ${p}`).join('\n')}\n`;
}
yamlContent += ` skillLevel: ${toolData.skillLevel}\n`;
if (toolData.accessType) yamlContent += ` accessType: ${toolData.accessType}\n`;
yamlContent += ` url: ${toolData.url}\n`;
if (toolData.projectUrl) yamlContent += ` projectUrl: ${toolData.projectUrl}\n`;
if (toolData.license) yamlContent += ` license: ${toolData.license}\n`;
if (toolData.knowledgebase) yamlContent += ` knowledgebase: ${toolData.knowledgebase}\n`;
if (toolData.related_concepts && toolData.related_concepts.length > 0) {
yamlContent += ` related_concepts:\n${toolData.related_concepts.map(c => ` - ${c}`).join('\n')}\n`;
}
if (toolData.tags.length > 0) {
yamlContent += ` tags:\n${toolData.tags.map(t => ` - ${t}`).join('\n')}\n`;
}
yamlPreview.textContent = yamlContent;
} catch (error) {
yamlPreview.textContent = `# Error generating preview: ${error.message}`;
}
}
// Form validation
function validateForm() {
const errors = [];
// Basic validation
if (!nameInput.value.trim()) errors.push('Name is required');
if (!descriptionTextarea.value.trim() || descriptionTextarea.value.length < 10) {
errors.push('Description must be at least 10 characters');
}
const selectedType = typeSelect.value;
// Type-specific validation
if (selectedType === 'software') {
const platforms = new FormData(form).getAll('platforms');
if (platforms.length === 0) {
errors.push('At least one platform is required for software');
}
if (!document.getElementById('tool-license').value.trim()) {
errors.push('License is required for software');
}
}
return errors;
}
// Event listeners
typeSelect.addEventListener('change', updateFieldVisibility);
refreshPreviewBtn.addEventListener('click', updateYAMLPreview);
// Update preview on form changes
form.addEventListener('input', debounce(updateYAMLPreview, 500));
form.addEventListener('change', updateYAMLPreview);
// Form submission
form.addEventListener('submit', async (e) => {
e.preventDefault();
const errors = validateForm();
if (errors.length > 0) {
alert('Please fix the following errors:\n' + errors.join('\n'));
return;
}
// Show loading state
submitBtn.disabled = true;
submitText.textContent = isEdit ? 'Updating...' : 'Submitting...';
submitSpinner.style.display = 'inline-block';
try {
const formData = new FormData(form);
// Prepare submission data
const submissionData = {
action: isEdit ? 'edit' : 'add',
tool: {
name: formData.get('name'),
icon: formData.get('icon') || null,
type: formData.get('type'),
description: formData.get('description'),
domains: formData.getAll('domains'),
phases: formData.getAll('phases'),
skillLevel: formData.get('skillLevel'),
url: formData.get('url'),
tags: formData.get('tags') ? formData.get('tags').split(',').map(tag => tag.trim()).filter(Boolean) : []
},
metadata: {
reason: formData.get('reason') || null
}
};
// Add type-specific fields
if (submissionData.tool.type === 'software') {
submissionData.tool.platforms = formData.getAll('platforms');
submissionData.tool.license = formData.get('license').trim();
submissionData.tool.accessType = formData.get('accessType');
submissionData.tool.projectUrl = formData.get('projectUrl') || null;
}
// Add optional fields
submissionData.tool.knowledgebase = formData.has('knowledgebase') || null;
if (submissionData.tool.type !== 'concept') {
const relatedConcepts = formData.getAll('relatedConcepts');
submissionData.tool.related_concepts = relatedConcepts.length > 0 ? relatedConcepts : null;
}
const response = await fetch('/api/contribute/tool', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(submissionData)
});
const result = await response.json();
if (result.success) {
// Show success modal
document.getElementById('success-message').textContent =
`Your ${isEdit ? 'update' : 'contribution'} has been submitted successfully and will be reviewed by the maintainers.`;
if (result.prUrl) {
const prLink = document.getElementById('pr-link');
prLink.href = result.prUrl;
prLink.style.display = 'inline-flex';
}
successModal.style.display = 'flex';
} else {
let errorMessage = result.error || 'Submission failed';
if (result.details && Array.isArray(result.details)) {
errorMessage += '\n\nDetails:\n' + result.details.join('\n');
}
alert(errorMessage);
}
} catch (error) {
console.error('Submission error:', error);
alert('An error occurred while submitting your contribution. Please try again.');
} finally {
// Reset loading state
submitBtn.disabled = false;
submitText.textContent = isEdit ? 'Update Tool' : 'Submit Contribution';
submitSpinner.style.display = 'none';
}
});
// Initialize form
if (isEdit && editTool) {
// Pre-fill edit form
typeSelect.value = editTool.type;
updateFieldVisibility();
// Set checkboxes for platforms
if (editTool.platforms) {
editTool.platforms.forEach(platform => {
const checkbox = document.querySelector(`input[value="${platform}"]`);
if (checkbox) checkbox.checked = true;
});
}
updateYAMLPreview();
} else {
updateFieldVisibility();
}
// Debounce utility
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
});
</script>