first draft contributions
This commit is contained in:
732
src/pages/contribute/tool.astro
Normal file
732
src/pages/contribute/tool.astro
Normal 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>
|
||||
Reference in New Issue
Block a user