main #11

Merged
mstoeck3 merged 66 commits from main into forensic-ai 2025-08-11 12:02:56 +00:00
7 changed files with 1147 additions and 244 deletions
Showing only changes of commit 4cc3e2c830 - Show all commits

View File

@ -5,6 +5,7 @@ import '../styles/global.css';
import '../styles/auditTrail.css';
import '../styles/knowledgebase.css';
import '../styles/palette.css';
import '../styles/autocomplete.css';
export interface Props {
title: string;

View File

@ -1,4 +1,4 @@
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses)
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses + related_software)
import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
@ -27,6 +27,7 @@ const ContributionToolSchema = z.object({
knowledgebase: z.boolean().optional().nullable(),
'domain-agnostic-software': z.array(z.string()).optional().nullable(),
related_concepts: z.array(z.string()).optional().nullable(),
related_software: z.array(z.string()).optional().nullable(),
tags: z.array(z.string()).default([]),
statusUrl: z.string().url('Must be a valid URL').optional().nullable()
});
@ -80,6 +81,38 @@ function sanitizeInput(obj: any): any {
return obj;
}
function preprocessFormData(body: any): any {
// Handle comma-separated strings from autocomplete inputs
if (body.tool) {
// Handle tags
if (typeof body.tool.tags === 'string') {
body.tool.tags = body.tool.tags.split(',').map((t: string) => t.trim()).filter(Boolean);
}
// Handle related concepts
if (body.tool.relatedConcepts) {
if (typeof body.tool.relatedConcepts === 'string') {
body.tool.related_concepts = body.tool.relatedConcepts.split(',').map((t: string) => t.trim()).filter(Boolean);
} else {
body.tool.related_concepts = body.tool.relatedConcepts;
}
delete body.tool.relatedConcepts; // Remove the original key
}
// Handle related software
if (body.tool.relatedSoftware) {
if (typeof body.tool.relatedSoftware === 'string') {
body.tool.related_software = body.tool.relatedSoftware.split(',').map((t: string) => t.trim()).filter(Boolean);
} else {
body.tool.related_software = body.tool.relatedSoftware;
}
delete body.tool.relatedSoftware; // Remove the original key
}
}
return body;
}
async function validateToolData(tool: any, action: string): Promise<{ valid: boolean; errors: string[] }> {
const errors: string[] = [];
@ -109,6 +142,17 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
}
}
// Validate related items exist (optional validation - could be enhanced)
if (tool.related_concepts && tool.related_concepts.length > 0) {
// Could validate that referenced concepts actually exist
console.log('[VALIDATION] Related concepts provided:', tool.related_concepts);
}
if (tool.related_software && tool.related_software.length > 0) {
// Could validate that referenced software actually exists
console.log('[VALIDATION] Related software provided:', tool.related_software);
}
return { valid: errors.length === 0, errors };
} catch (error) {
@ -143,6 +187,9 @@ export const POST: APIRoute = async ({ request }) => {
return apiSpecial.invalidJSON();
}
// Preprocess form data to handle autocomplete inputs
body = preprocessFormData(body);
const sanitizedBody = sanitizeInput(body);
let validatedData;
@ -153,6 +200,7 @@ export const POST: APIRoute = async ({ request }) => {
const errorMessages = error.errors.map(err =>
`${err.path.join('.')}: ${err.message}`
);
console.log('[VALIDATION] Zod validation errors:', errorMessages);
return apiError.validation('Validation failed', errorMessages);
}
@ -174,6 +222,16 @@ export const POST: APIRoute = async ({ request }) => {
}
};
console.log('[CONTRIBUTION] Processing contribution:', {
type: contributionData.type,
toolName: contributionData.tool.name,
toolType: contributionData.tool.type,
submitter: userEmail,
hasRelatedConcepts: !!(contributionData.tool.related_concepts?.length),
hasRelatedSoftware: !!(contributionData.tool.related_software?.length),
tagsCount: contributionData.tool.tags?.length || 0
});
try {
const gitManager = new GitContributionManager();
const result = await gitManager.submitContribution(contributionData);

View File

@ -22,6 +22,17 @@ const existingTools = data.tools;
const editToolName = Astro.url.searchParams.get('edit');
const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null;
const isEdit = !!editTool;
// Extract data for autocomplete
const allTags = [...new Set(existingTools.flatMap(tool => tool.tags || []))].sort();
const allSoftwareAndMethods = existingTools
.filter(tool => tool.type === 'software' || tool.type === 'method')
.map(tool => tool.name)
.sort();
const allConcepts = existingTools
.filter(tool => tool.type === 'concept')
.map(tool => tool.name)
.sort();
---
<BaseLayout title={isEdit ? `Edit ${editTool?.name}` : 'Contribute Tool'}>
@ -194,16 +205,27 @@ const isEdit = !!editTool;
</div>
</div>
<div id="concepts-fields" style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem; display: none;">
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Konzepte im Zusammenhang</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem;">
{existingTools.filter(tool => tool.type === 'concept').map(concept => (
<label class="checkbox-wrapper">
<input type="checkbox" name="relatedConcepts" value={concept.name}
checked={editTool?.related_concepts?.includes(concept.name)} />
<span>{concept.name}</span>
</label>
))}
<div id="relations-fields" style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem; display: none;">
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Verwandte Tools & Konzepte</h3>
<div style="display: grid; gap: 1.5rem;">
<div id="related-concepts-section">
<label for="related-concepts-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Verwandte Konzepte</label>
<input type="text" id="related-concepts-input" placeholder="Beginne zu tippen, um Konzepte zu finden..." />
<input type="hidden" id="related-concepts-hidden" name="relatedConcepts" value={editTool?.related_concepts?.join(', ') || ''} />
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
Konzepte, die mit diesem Tool/Methode in Verbindung stehen
</small>
</div>
<div id="related-software-section">
<label for="related-software-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Verwandte Software & Methoden</label>
<input type="text" id="related-software-input" placeholder="Beginne zu tippen, um Software/Methoden zu finden..." />
<input type="hidden" id="related-software-hidden" name="relatedSoftware" value={editTool?.related_software?.join(', ') || ''} />
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
Software oder Methoden, die oft zusammen mit diesem Tool verwendet werden
</small>
</div>
</div>
</div>
@ -211,9 +233,12 @@ const isEdit = !!editTool;
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Zusatzinfos</h3>
<div style="margin-bottom: 1.5rem;">
<label for="tags" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Tags</label>
<input type="text" id="tags" name="tags" value={editTool?.tags?.join(', ') || ''}
placeholder="Komma-getrennt: Passende Begriffe, nach denen ihr suchen würdet." />
<label for="tags-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Tags</label>
<input type="text" id="tags-input" placeholder="Beginne zu tippen, um Tags hinzuzufügen..." />
<input type="hidden" id="tags-hidden" name="tags" value={editTool?.tags?.join(', ') || ''} />
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
Passende Begriffe, nach denen ihr suchen würdet
</small>
</div>
<div style="margin-bottom: 1.5rem;">
@ -274,7 +299,275 @@ const isEdit = !!editTool;
</div>
</BaseLayout>
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware }}>
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware, allTags, allSoftwareAndMethods, allConcepts }}>
// Consolidated Autocomplete Functionality - inlined to avoid module loading issues
class AutocompleteManager {
constructor(inputElement, dataSource, options = {}) {
this.input = inputElement;
this.dataSource = dataSource;
this.options = {
minLength: 1,
maxResults: 10,
placeholder: 'Type to search...',
allowMultiple: false,
separator: ', ',
filterFunction: this.defaultFilter.bind(this),
renderFunction: this.defaultRender.bind(this),
...options
};
this.isOpen = false;
this.selectedIndex = -1;
this.filteredData = [];
this.selectedItems = new Set();
this.init();
}
init() {
this.createDropdown();
this.bindEvents();
if (this.options.allowMultiple) {
this.initMultipleMode();
}
}
createDropdown() {
this.dropdown = document.createElement('div');
this.dropdown.className = 'autocomplete-dropdown';
// Insert dropdown after input
this.input.parentNode.style.position = 'relative';
this.input.parentNode.insertBefore(this.dropdown, this.input.nextSibling);
}
bindEvents() {
this.input.addEventListener('input', (e) => {
this.handleInput(e.target.value);
});
this.input.addEventListener('keydown', (e) => {
this.handleKeydown(e);
});
this.input.addEventListener('focus', () => {
if (this.input.value.length >= this.options.minLength) {
this.showDropdown();
}
});
this.input.addEventListener('blur', (e) => {
// Delay to allow click events on dropdown items
setTimeout(() => {
if (!this.dropdown.contains(document.activeElement)) {
this.hideDropdown();
}
}, 150);
});
document.addEventListener('click', (e) => {
if (!this.input.contains(e.target) && !this.dropdown.contains(e.target)) {
this.hideDropdown();
}
});
}
initMultipleMode() {
this.selectedContainer = document.createElement('div');
this.selectedContainer.className = 'autocomplete-selected';
this.input.parentNode.insertBefore(this.selectedContainer, this.input);
this.updateSelectedDisplay();
}
handleInput(value) {
if (value.length >= this.options.minLength) {
this.filteredData = this.options.filterFunction(value);
this.selectedIndex = -1;
this.renderDropdown();
this.showDropdown();
} else {
this.hideDropdown();
}
}
handleKeydown(e) {
if (!this.isOpen) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredData.length - 1);
this.updateHighlight();
break;
case 'ArrowUp':
e.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
this.updateHighlight();
break;
case 'Enter':
e.preventDefault();
if (this.selectedIndex >= 0) {
this.selectItem(this.filteredData[this.selectedIndex]);
}
break;
case 'Escape':
this.hideDropdown();
break;
}
}
defaultFilter(query) {
const searchTerm = query.toLowerCase();
return this.dataSource
.filter(item => {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return text.toLowerCase().includes(searchTerm) &&
(!this.options.allowMultiple || !this.selectedItems.has(text));
})
.slice(0, this.options.maxResults);
}
defaultRender(item) {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return `<div class="autocomplete-item">${this.escapeHtml(text)}</div>`;
}
renderDropdown() {
if (this.filteredData.length === 0) {
this.dropdown.innerHTML = '<div class="autocomplete-no-results">No results found</div>';
return;
}
this.dropdown.innerHTML = this.filteredData
.map((item, index) => {
const content = this.options.renderFunction(item);
return `<div class="autocomplete-option" data-index="${index}">${content}</div>`;
})
.join('');
// Bind click events
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
option.addEventListener('click', () => {
this.selectItem(this.filteredData[index]);
});
option.addEventListener('mouseenter', () => {
this.selectedIndex = index;
this.updateHighlight();
});
});
}
updateHighlight() {
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
option.style.backgroundColor = index === this.selectedIndex
? 'var(--color-bg-secondary)'
: 'transparent';
});
}
selectItem(item) {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
if (this.options.allowMultiple) {
this.selectedItems.add(text);
this.updateSelectedDisplay();
this.updateInputValue();
this.input.value = '';
} else {
this.input.value = text;
this.hideDropdown();
}
// Trigger change event
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
}));
}
removeItem(text) {
if (this.options.allowMultiple) {
this.selectedItems.delete(text);
this.updateSelectedDisplay();
this.updateInputValue();
}
}
updateSelectedDisplay() {
if (!this.options.allowMultiple || !this.selectedContainer) return;
this.selectedContainer.innerHTML = Array.from(this.selectedItems)
.map(item => `
<span class="autocomplete-tag">
${this.escapeHtml(item)}
<button type="button" class="autocomplete-remove" data-item="${this.escapeHtml(item)}">×</button>
</span>
`)
.join('');
// Bind remove events
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.removeItem(btn.getAttribute('data-item'));
});
});
}
updateInputValue() {
if (this.options.allowMultiple && this.options.hiddenInput) {
this.options.hiddenInput.value = Array.from(this.selectedItems).join(this.options.separator);
}
}
showDropdown() {
this.dropdown.style.display = 'block';
this.isOpen = true;
}
hideDropdown() {
this.dropdown.style.display = 'none';
this.isOpen = false;
this.selectedIndex = -1;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
setDataSource(newDataSource) {
this.dataSource = newDataSource;
}
getSelectedItems() {
return Array.from(this.selectedItems);
}
setSelectedItems(items) {
this.selectedItems = new Set(items);
if (this.options.allowMultiple) {
this.updateSelectedDisplay();
this.updateInputValue();
}
}
destroy() {
if (this.dropdown && this.dropdown.parentNode) {
this.dropdown.parentNode.removeChild(this.dropdown);
}
if (this.selectedContainer && this.selectedContainer.parentNode) {
this.selectedContainer.parentNode.removeChild(this.selectedContainer);
}
}
}
console.log('[FORM] Script loaded, initializing...');
class ContributionForm {
@ -282,7 +575,8 @@ class ContributionForm {
this.isEdit = isEdit;
this.editTool = editTool;
this.elements = {};
this.isSubmitting = false;
this.isSubmitting = false;
this.autocompleteManagers = new Map();
this.init();
}
@ -303,14 +597,20 @@ class ContributionForm {
yamlPreview: document.getElementById('yaml-preview'),
successModal: document.getElementById('success-modal'),
softwareFields: document.getElementById('software-fields'),
conceptsFields: document.getElementById('concepts-fields'),
relationsFields: document.getElementById('relations-fields'),
descriptionCount: document.getElementById('description-count'),
reasonCount: document.getElementById('reason-count'),
validationErrors: document.getElementById('validation-errors'),
errorList: document.getElementById('error-list'),
platformsRequired: document.getElementById('platforms-required'),
licenseRequired: document.getElementById('license-required'),
licenseInput: document.getElementById('license')
licenseInput: document.getElementById('license'),
tagsInput: document.getElementById('tags-input'),
tagsHidden: document.getElementById('tags-hidden'),
relatedConceptsInput: document.getElementById('related-concepts-input'),
relatedConceptsHidden: document.getElementById('related-concepts-hidden'),
relatedSoftwareInput: document.getElementById('related-software-input'),
relatedSoftwareHidden: document.getElementById('related-software-hidden')
};
if (!this.elements.form || !this.elements.submitBtn) {
@ -327,6 +627,7 @@ class ContributionForm {
console.log('[FORM] Setting up handlers...');
this.setupEventListeners();
this.setupAutocomplete();
this.updateFieldVisibility();
this.setupCharacterCounters();
this.updateYAMLPreview();
@ -334,6 +635,65 @@ class ContributionForm {
console.log('[FORM] Initialization complete!');
}
setupAutocomplete() {
// Tags autocomplete
if (this.elements.tagsInput && this.elements.tagsHidden) {
const tagsManager = new AutocompleteManager(this.elements.tagsInput, allTags, {
allowMultiple: true,
hiddenInput: this.elements.tagsHidden,
placeholder: 'Beginne zu tippen, um Tags hinzuzufügen...'
});
// Set initial values if editing
if (this.editTool?.tags) {
tagsManager.setSelectedItems(this.editTool.tags);
}
this.autocompleteManagers.set('tags', tagsManager);
}
// Related concepts autocomplete
if (this.elements.relatedConceptsInput && this.elements.relatedConceptsHidden) {
const conceptsManager = new AutocompleteManager(this.elements.relatedConceptsInput, allConcepts, {
allowMultiple: true,
hiddenInput: this.elements.relatedConceptsHidden,
placeholder: 'Beginne zu tippen, um Konzepte zu finden...'
});
// Set initial values if editing
if (this.editTool?.related_concepts) {
conceptsManager.setSelectedItems(this.editTool.related_concepts);
}
this.autocompleteManagers.set('relatedConcepts', conceptsManager);
}
// Related software autocomplete
if (this.elements.relatedSoftwareInput && this.elements.relatedSoftwareHidden) {
const softwareManager = new AutocompleteManager(this.elements.relatedSoftwareInput, allSoftwareAndMethods, {
allowMultiple: true,
hiddenInput: this.elements.relatedSoftwareHidden,
placeholder: 'Beginne zu tippen, um Software/Methoden zu finden...'
});
// Set initial values if editing
if (this.editTool?.related_software) {
softwareManager.setSelectedItems(this.editTool.related_software);
}
this.autocompleteManagers.set('relatedSoftware', softwareManager);
}
// Listen for autocomplete changes to update YAML preview
Object.values(this.autocompleteManagers).forEach(manager => {
if (manager.input) {
manager.input.addEventListener('autocomplete:select', () => {
this.updateYAMLPreview();
});
}
});
}
setupEventListeners() {
this.elements.typeSelect.addEventListener('change', () => {
this.updateFieldVisibility();
@ -363,203 +723,231 @@ class ContributionForm {
console.log('[FORM] Event listeners attached');
}
updateFieldVisibility() {
const type = this.elements.typeSelect.value;
this.elements.softwareFields.style.display = 'none';
this.elements.conceptsFields.style.display = 'none';
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'none';
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'none';
if (type === 'software') {
this.elements.softwareFields.style.display = 'block';
this.elements.conceptsFields.style.display = 'block';
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'inline';
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'inline';
} else if (type === 'method') {
this.elements.conceptsFields.style.display = 'block';
}
console.log('[FORM] Field visibility updated for type:', type);
}
setupCharacterCounters() {
const counters = [
{ element: this.elements.descriptionTextarea, counter: this.elements.descriptionCount, max: 1000 },
{ element: this.elements.reasonTextarea, counter: this.elements.reasonCount, max: 500 }
];
counters.forEach(({ element, counter, max }) => {
if (element && counter) {
const updateCounter = () => {
const count = element.value.length;
counter.textContent = count;
counter.style.color = count > max * 0.9 ? 'var(--color-warning)' : 'var(--color-text-secondary)';
};
element.addEventListener('input', updateCounter);
updateCounter();
}
});
}
updateYAMLPreview() {
if (!this.elements.yamlPreview) return;
try {
const formData = new FormData(this.elements.form);
updateFieldVisibility() {
const type = this.elements.typeSelect.value;
const tool = {
name: formData.get('name') || 'Tool Name',
type: formData.get('type') || 'software',
description: formData.get('description') || 'Tool description',
domains: formData.getAll('domains'),
phases: formData.getAll('phases'),
skillLevel: formData.get('skillLevel') || 'intermediate',
url: formData.get('url') || 'https://example.com'
};
this.elements.softwareFields.style.display = 'none';
this.elements.relationsFields.style.display = 'none';
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'none';
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'none';
if (formData.get('icon')) {
tool.icon = formData.get('icon');
if (type === 'software') {
this.elements.softwareFields.style.display = 'block';
this.elements.relationsFields.style.display = 'block';
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'inline';
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'inline';
} else if (type === 'method') {
this.elements.relationsFields.style.display = 'block';
} else if (type === 'concept') {
// Concepts can only relate to software/methods, not other concepts
this.elements.relationsFields.style.display = 'block';
// Hide concepts section for concept types
const conceptsSection = document.getElementById('related-concepts-section');
const softwareSection = document.getElementById('related-software-section');
if (conceptsSection) conceptsSection.style.display = 'none';
if (softwareSection) softwareSection.style.display = 'block';
}
if (tool.type === 'software') {
tool.platforms = formData.getAll('platforms');
tool.license = formData.get('license') || 'Unknown';
if (formData.get('accessType')) {
tool.accessType = formData.get('accessType');
// Show appropriate relation sections
if (type !== 'concept') {
const conceptsSection = document.getElementById('related-concepts-section');
const softwareSection = document.getElementById('related-software-section');
if (conceptsSection) conceptsSection.style.display = 'block';
if (softwareSection) softwareSection.style.display = 'block';
}
console.log('[FORM] Field visibility updated for type:', type);
}
setupCharacterCounters() {
const counters = [
{ element: this.elements.descriptionTextarea, counter: this.elements.descriptionCount, max: 1000 },
{ element: this.elements.reasonTextarea, counter: this.elements.reasonCount, max: 500 }
];
counters.forEach(({ element, counter, max }) => {
if (element && counter) {
const updateCounter = () => {
const count = element.value.length;
counter.textContent = count;
counter.style.color = count > max * 0.9 ? 'var(--color-warning)' : 'var(--color-text-secondary)';
};
element.addEventListener('input', updateCounter);
updateCounter();
}
const domainAgnostic = formData.getAll('domainAgnostic');
if (domainAgnostic.length > 0) {
tool['domain-agnostic-software'] = domainAgnostic;
}
}
if (formData.has('knowledgebase')) {
tool.knowledgebase = true;
}
const tags = formData.get('tags');
if (tags) {
tool.tags = tags.split(',').map(t => t.trim()).filter(Boolean);
}
const relatedConcepts = formData.getAll('relatedConcepts');
if (relatedConcepts.length > 0) {
tool.related_concepts = relatedConcepts;
}
const yaml = this.generateYAML(tool);
this.elements.yamlPreview.textContent = yaml;
} catch (error) {
console.error('[FORM] YAML preview error:', error);
this.elements.yamlPreview.textContent = '# Error generating preview';
}
}
generateYAML(tool) {
const lines = [];
lines.push(`name: "${tool.name}"`);
if (tool.icon) lines.push(`icon: "${tool.icon}"`);
lines.push(`type: ${tool.type}`);
lines.push(`description: "${tool.description}"`);
lines.push(`domains: [${tool.domains.map(d => `"${d}"`).join(', ')}]`);
lines.push(`phases: [${tool.phases.map(p => `"${p}"`).join(', ')}]`);
lines.push(`skillLevel: ${tool.skillLevel}`);
lines.push(`url: "${tool.url}"`);
if (tool.platforms && tool.platforms.length > 0) {
lines.push(`platforms: [${tool.platforms.map(p => `"${p}"`).join(', ')}]`);
}
if (tool.license) lines.push(`license: "${tool.license}"`);
if (tool.accessType) lines.push(`accessType: ${tool.accessType}`);
if (tool['domain-agnostic-software']) {
lines.push(`domain-agnostic-software: [${tool['domain-agnostic-software'].map(c => `"${c}"`).join(', ')}]`);
}
if (tool.knowledgebase) lines.push(`knowledgebase: true`);
if (tool.tags && tool.tags.length > 0) {
lines.push(`tags: [${tool.tags.map(t => `"${t}"`).join(', ')}]`);
}
if (tool.related_concepts && tool.related_concepts.length > 0) {
lines.push(`related_concepts: [${tool.related_concepts.map(c => `"${c}"`).join(', ')}]`);
});
}
return lines.join('\n');
}
updateYAMLPreview() {
if (!this.elements.yamlPreview) return;
validateForm() {
const errors = [];
const formData = new FormData(this.elements.form);
const name = formData.get('name')?.trim();
if (!name) {
errors.push('Tool name is required');
}
const description = formData.get('description')?.trim();
if (!description) {
errors.push('Description is required');
} else if (description.length < 10) {
errors.push('Description must be at least 10 characters long');
}
const skillLevel = formData.get('skillLevel');
if (!skillLevel) {
errors.push('Skill level is required');
}
const type = formData.get('type');
if (!type) {
errors.push('Type is required');
}
const url = formData.get('url')?.trim();
if (!url) {
errors.push('Primary URL is required');
} else {
try {
new URL(url);
} catch {
errors.push('Primary URL must be a valid URL');
const formData = new FormData(this.elements.form);
const tool = {
name: formData.get('name') || 'Tool Name',
type: formData.get('type') || 'software',
description: formData.get('description') || 'Tool description',
domains: formData.getAll('domains'),
phases: formData.getAll('phases'),
skillLevel: formData.get('skillLevel') || 'intermediate',
url: formData.get('url') || 'https://example.com'
};
if (formData.get('icon')) {
tool.icon = formData.get('icon');
}
if (tool.type === 'software') {
tool.platforms = formData.getAll('platforms');
tool.license = formData.get('license') || 'Unknown';
if (formData.get('accessType')) {
tool.accessType = formData.get('accessType');
}
const domainAgnostic = formData.getAll('domainAgnostic');
if (domainAgnostic.length > 0) {
tool['domain-agnostic-software'] = domainAgnostic;
}
}
if (formData.has('knowledgebase')) {
tool.knowledgebase = true;
}
// Handle tags from autocomplete
const tagsValue = this.elements.tagsHidden?.value || '';
if (tagsValue) {
tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
}
// Handle related concepts from autocomplete
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
if (relatedConceptsValue) {
tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
}
// Handle related software from autocomplete
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
if (relatedSoftwareValue) {
tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
}
const yaml = this.generateYAML(tool);
this.elements.yamlPreview.textContent = yaml;
} catch (error) {
console.error('[FORM] YAML preview error:', error);
this.elements.yamlPreview.textContent = '# Error generating preview';
}
}
if (type === 'software') {
const platforms = formData.getAll('platforms');
if (platforms.length === 0) {
errors.push('At least one platform is required for software');
generateYAML(tool) {
const lines = [];
lines.push(`name: "${tool.name}"`);
if (tool.icon) lines.push(`icon: "${tool.icon}"`);
lines.push(`type: ${tool.type}`);
lines.push(`description: "${tool.description}"`);
lines.push(`domains: [${tool.domains.map(d => `"${d}"`).join(', ')}]`);
lines.push(`phases: [${tool.phases.map(p => `"${p}"`).join(', ')}]`);
lines.push(`skillLevel: ${tool.skillLevel}`);
lines.push(`url: "${tool.url}"`);
if (tool.platforms && tool.platforms.length > 0) {
lines.push(`platforms: [${tool.platforms.map(p => `"${p}"`).join(', ')}]`);
}
if (tool.license) lines.push(`license: "${tool.license}"`);
if (tool.accessType) lines.push(`accessType: ${tool.accessType}`);
if (tool['domain-agnostic-software']) {
lines.push(`domain-agnostic-software: [${tool['domain-agnostic-software'].map(c => `"${c}"`).join(', ')}]`);
}
if (tool.knowledgebase) lines.push(`knowledgebase: true`);
if (tool.tags && tool.tags.length > 0) {
lines.push(`tags: [${tool.tags.map(t => `"${t}"`).join(', ')}]`);
}
if (tool.related_concepts && tool.related_concepts.length > 0) {
lines.push(`related_concepts: [${tool.related_concepts.map(c => `"${c}"`).join(', ')}]`);
}
if (tool.related_software && tool.related_software.length > 0) {
lines.push(`related_software: [${tool.related_software.map(s => `"${s}"`).join(', ')}]`);
}
const license = formData.get('license')?.trim();
if (!license) {
errors.push('License is required for software');
return lines.join('\n');
}
validateForm() {
const errors = [];
const formData = new FormData(this.elements.form);
const name = formData.get('name')?.trim();
if (!name) {
errors.push('Tool name is required');
}
const description = formData.get('description')?.trim();
if (!description) {
errors.push('Description is required');
} else if (description.length < 10) {
errors.push('Description must be at least 10 characters long');
}
const skillLevel = formData.get('skillLevel');
if (!skillLevel) {
errors.push('Skill level is required');
}
const type = formData.get('type');
if (!type) {
errors.push('Type is required');
}
const url = formData.get('url')?.trim();
if (!url) {
errors.push('Primary URL is required');
} else {
try {
new URL(url);
} catch {
errors.push('Primary URL must be a valid URL');
}
}
if (type === 'software') {
const platforms = formData.getAll('platforms');
if (platforms.length === 0) {
errors.push('At least one platform is required for software');
}
const license = formData.get('license')?.trim();
if (!license) {
errors.push('License is required for software');
}
}
return errors;
}
return errors;
}
showValidationErrors(errors) {
if (errors.length === 0) {
this.elements.validationErrors.style.display = 'none';
return;
}
showValidationErrors(errors) {
if (errors.length === 0) {
this.elements.validationErrors.style.display = 'none';
return;
this.elements.errorList.innerHTML = '';
errors.forEach(error => {
const li = document.createElement('li');
li.textContent = error;
this.elements.errorList.appendChild(li);
});
this.elements.validationErrors.style.display = 'block';
this.elements.validationErrors.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
this.elements.errorList.innerHTML = '';
errors.forEach(error => {
const li = document.createElement('li');
li.textContent = error;
this.elements.errorList.appendChild(li);
});
this.elements.validationErrors.style.display = 'block';
this.elements.validationErrors.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
async handleSubmit() {
console.log('[FORM] Submit handler called!');
@ -597,14 +985,32 @@ showValidationErrors(errors) {
phases: formData.getAll('phases'),
skillLevel: formData.get('skillLevel'),
url: formData.get('url'),
tags: formData.get('tags') ?
formData.get('tags').split(',').map(t => t.trim()).filter(Boolean) : []
tags: []
},
metadata: {
reason: formData.get('reason') || ''
reason: formData.get('reason') || '',
contact: formData.get('contact') || ''
}
};
// Handle tags from autocomplete
const tagsValue = this.elements.tagsHidden?.value || '';
if (tagsValue) {
submission.tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
}
// Handle related concepts from autocomplete
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
if (relatedConceptsValue) {
submission.tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
}
// Handle related software from autocomplete
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
if (relatedSoftwareValue) {
submission.tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
}
if (formData.get('icon')) submission.tool.icon = formData.get('icon');
if (formData.has('knowledgebase')) submission.tool.knowledgebase = true;
@ -620,13 +1026,6 @@ showValidationErrors(errors) {
}
}
if (submission.tool.type !== 'concept') {
const related = formData.getAll('relatedConcepts');
if (related.length > 0) {
submission.tool.related_concepts = related;
}
}
console.log('[FORM] Sending submission:', submission);
const response = await fetch('/api/contribute/tool', {
@ -681,6 +1080,14 @@ showValidationErrors(errors) {
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
destroy() {
// Clean up autocomplete managers
this.autocompleteManagers.forEach(manager => {
manager.destroy();
});
this.autocompleteManagers.clear();
}
}
function initializeForm() {
@ -706,4 +1113,5 @@ if (document.readyState === 'loading') {
}
console.log('[FORM] Script loaded successfully');
</script>
</script>
</BaseLayout>

121
src/styles/autocomplete.css Normal file
View File

@ -0,0 +1,121 @@
/* ============================================================================
AUTOCOMPLETE COMPONENT STYLES
============================================================================ */
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 0.375rem;
box-shadow: var(--shadow-lg);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.autocomplete-option {
padding: 0.5rem;
cursor: pointer;
border-bottom: 1px solid var(--color-border-light);
transition: background-color 0.15s ease;
}
.autocomplete-option:last-child {
border-bottom: none;
}
.autocomplete-option:hover {
background-color: var(--color-bg-secondary);
}
.autocomplete-item {
font-size: 0.875rem;
color: var(--color-text);
}
.autocomplete-no-results {
padding: 0.75rem;
text-align: center;
color: var(--color-text-secondary);
font-size: 0.875rem;
font-style: italic;
}
.autocomplete-selected {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.5rem;
min-height: 1.5rem;
}
.autocomplete-tag {
background-color: var(--color-primary);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.25rem;
animation: fadeInScale 0.2s ease-out;
}
.autocomplete-remove {
background: none;
border: none;
color: white;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.15s ease;
}
.autocomplete-remove:hover {
background-color: rgba(255, 255, 255, 0.2);
}
/* Animation for tag appearance */
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Ensure autocomplete container has relative positioning */
.autocomplete-container {
position: relative;
}
/* Custom scrollbar for autocomplete dropdown */
.autocomplete-dropdown::-webkit-scrollbar {
width: 6px;
}
.autocomplete-dropdown::-webkit-scrollbar-track {
background: var(--color-bg-secondary);
}
.autocomplete-dropdown::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
}
.autocomplete-dropdown::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}

View File

@ -1,7 +1,7 @@
// src/utils/clientUtils.js
// src/utils/clientUtils.ts
// Client-side utilities that mirror server-side toolHelpers.ts
export function createToolSlug(toolName) {
export function createToolSlug(toolName: string): string {
if (!toolName || typeof toolName !== 'string') {
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
return '';
@ -14,18 +14,353 @@ export function createToolSlug(toolName) {
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
}
export function findToolByIdentifier(tools, identifier) {
export function findToolByIdentifier(tools: any[], identifier: string): any | undefined {
if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find(tool =>
return tools.find((tool: any) =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
);
}
export function isToolHosted(tool) {
export function isToolHosted(tool: any): boolean {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
// Consolidated Autocomplete Functionality
interface AutocompleteOptions {
minLength?: number;
maxResults?: number;
placeholder?: string;
allowMultiple?: boolean;
separator?: string;
filterFunction?: (query: string) => any[];
renderFunction?: (item: any) => string;
hiddenInput?: HTMLInputElement;
}
export class AutocompleteManager {
public input: HTMLInputElement;
public dataSource: any[];
public options: AutocompleteOptions;
public isOpen: boolean = false;
public selectedIndex: number = -1;
public filteredData: any[] = [];
public selectedItems: Set<string> = new Set();
public dropdown!: HTMLElement;
public selectedContainer!: HTMLElement;
constructor(inputElement: HTMLInputElement, dataSource: any[], options: AutocompleteOptions = {}) {
this.input = inputElement;
this.dataSource = dataSource;
this.options = {
minLength: 1,
maxResults: 10,
placeholder: 'Type to search...',
allowMultiple: false,
separator: ', ',
filterFunction: this.defaultFilter.bind(this),
renderFunction: this.defaultRender.bind(this),
...options
};
this.init();
}
init(): void {
this.createDropdown();
this.bindEvents();
if (this.options.allowMultiple) {
this.initMultipleMode();
}
}
createDropdown(): void {
this.dropdown = document.createElement('div');
this.dropdown.className = 'autocomplete-dropdown';
this.dropdown.style.cssText = `
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 0.375rem;
box-shadow: var(--shadow-lg);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
`;
// Insert dropdown after input
const parentElement = this.input.parentNode as HTMLElement;
parentElement.style.position = 'relative';
parentElement.insertBefore(this.dropdown, this.input.nextSibling);
}
bindEvents(): void {
this.input.addEventListener('input', (e) => {
this.handleInput((e.target as HTMLInputElement).value);
});
this.input.addEventListener('keydown', (e) => {
this.handleKeydown(e);
});
this.input.addEventListener('focus', () => {
if (this.input.value.length >= (this.options.minLength || 1)) {
this.showDropdown();
}
});
this.input.addEventListener('blur', () => {
// Delay to allow click events on dropdown items
setTimeout(() => {
const activeElement = document.activeElement;
if (!activeElement || !this.dropdown.contains(activeElement)) {
this.hideDropdown();
}
}, 150);
});
document.addEventListener('click', (e) => {
const target = e.target as Node;
if (!this.input.contains(target) && !this.dropdown.contains(target)) {
this.hideDropdown();
}
});
}
initMultipleMode(): void {
this.selectedContainer = document.createElement('div');
this.selectedContainer.className = 'autocomplete-selected';
this.selectedContainer.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.5rem;
min-height: 1.5rem;
`;
const parentElement = this.input.parentNode as HTMLElement;
parentElement.insertBefore(this.selectedContainer, this.input);
this.updateSelectedDisplay();
}
handleInput(value: string): void {
if (value.length >= (this.options.minLength || 1)) {
this.filteredData = this.options.filterFunction!(value);
this.selectedIndex = -1;
this.renderDropdown();
this.showDropdown();
} else {
this.hideDropdown();
}
}
handleKeydown(e: KeyboardEvent): void {
if (!this.isOpen) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredData.length - 1);
this.updateHighlight();
break;
case 'ArrowUp':
e.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
this.updateHighlight();
break;
case 'Enter':
e.preventDefault();
if (this.selectedIndex >= 0) {
this.selectItem(this.filteredData[this.selectedIndex]);
}
break;
case 'Escape':
this.hideDropdown();
break;
}
}
defaultFilter(query: string): any[] {
const searchTerm = query.toLowerCase();
return this.dataSource
.filter(item => {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return text.toLowerCase().includes(searchTerm) &&
(!this.options.allowMultiple || !this.selectedItems.has(text));
})
.slice(0, this.options.maxResults || 10);
}
defaultRender(item: any): string {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return `<div class="autocomplete-item">${this.escapeHtml(text)}</div>`;
}
renderDropdown(): void {
if (this.filteredData.length === 0) {
this.dropdown.innerHTML = '<div class="autocomplete-no-results">No results found</div>';
return;
}
this.dropdown.innerHTML = this.filteredData
.map((item, index) => {
const content = this.options.renderFunction!(item);
return `<div class="autocomplete-option" data-index="${index}" style="
padding: 0.5rem;
cursor: pointer;
border-bottom: 1px solid var(--color-border-light);
transition: background-color 0.15s ease;
">${content}</div>`;
})
.join('');
// Bind click events
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
option.addEventListener('click', () => {
this.selectItem(this.filteredData[index]);
});
option.addEventListener('mouseenter', () => {
this.selectedIndex = index;
this.updateHighlight();
});
});
}
updateHighlight(): void {
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
(option as HTMLElement).style.backgroundColor = index === this.selectedIndex
? 'var(--color-bg-secondary)'
: 'transparent';
});
}
selectItem(item: any): void {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
if (this.options.allowMultiple) {
this.selectedItems.add(text);
this.updateSelectedDisplay();
this.updateInputValue();
this.input.value = '';
} else {
this.input.value = text;
this.hideDropdown();
}
// Trigger change event
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
}));
}
removeItem(text: string): void {
if (this.options.allowMultiple) {
this.selectedItems.delete(text);
this.updateSelectedDisplay();
this.updateInputValue();
}
}
updateSelectedDisplay(): void {
if (!this.options.allowMultiple || !this.selectedContainer) return;
this.selectedContainer.innerHTML = Array.from(this.selectedItems)
.map(item => `
<span class="autocomplete-tag" style="
background-color: var(--color-primary);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.25rem;
">
${this.escapeHtml(item)}
<button type="button" class="autocomplete-remove" data-item="${this.escapeHtml(item)}" style="
background: none;
border: none;
color: white;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: center;
">×</button>
</span>
`)
.join('');
// Bind remove events
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.removeItem((btn as HTMLElement).getAttribute('data-item')!);
});
});
}
updateInputValue(): void {
if (this.options.allowMultiple && this.options.hiddenInput) {
this.options.hiddenInput.value = Array.from(this.selectedItems).join(this.options.separator || ', ');
}
}
showDropdown(): void {
this.dropdown.style.display = 'block';
this.isOpen = true;
}
hideDropdown(): void {
this.dropdown.style.display = 'none';
this.isOpen = false;
this.selectedIndex = -1;
}
escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
setDataSource(newDataSource: any[]): void {
this.dataSource = newDataSource;
}
getSelectedItems(): string[] {
return Array.from(this.selectedItems);
}
setSelectedItems(items: string[]): void {
this.selectedItems = new Set(items);
if (this.options.allowMultiple) {
this.updateSelectedDisplay();
this.updateInputValue();
}
}
destroy(): void {
if (this.dropdown && this.dropdown.parentNode) {
this.dropdown.parentNode.removeChild(this.dropdown);
}
if (this.selectedContainer && this.selectedContainer.parentNode) {
this.selectedContainer.parentNode.removeChild(this.selectedContainer);
}
}
}

View File

@ -5,22 +5,23 @@ export interface ContributionData {
type: 'add' | 'edit';
tool: {
name: string;
icon?: string;
icon?: string | null;
type: 'software' | 'method' | 'concept';
description: string;
domains: string[];
phases: string[];
platforms: string[];
skillLevel: string;
accessType?: string;
accessType?: string | null;
url: string;
projectUrl?: string;
license?: string;
knowledgebase?: boolean;
'domain-agnostic-software'?: string[];
related_concepts?: string[];
projectUrl?: string | null;
license?: string | null;
knowledgebase?: boolean | null;
'domain-agnostic-software'?: string[] | null;
related_concepts?: string[] | null;
related_software?: string[] | null;
tags: string[];
statusUrl?: string;
statusUrl?: string | null;
};
metadata: {
submitter: string;
@ -134,6 +135,7 @@ export class GitContributionManager {
if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl;
if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase;
if (tool.related_concepts?.length) cleanTool.related_concepts = tool.related_concepts;
if (tool.related_software?.length) cleanTool.related_software = tool.related_software;
if (tool.tags?.length) cleanTool.tags = tool.tags;
if (tool['domain-agnostic-software']?.length) {
cleanTool['domain-agnostic-software'] = tool['domain-agnostic-software'];
@ -272,6 +274,8 @@ ${data.tool.platforms?.length ? `- **Platforms:** ${data.tool.platforms.join(',
${data.tool.license ? `- **License:** ${data.tool.license}` : ''}
${data.tool.domains?.length ? `- **Domains:** ${data.tool.domains.join(', ')}` : ''}
${data.tool.phases?.length ? `- **Phases:** ${data.tool.phases.join(', ')}` : ''}
${data.tool.related_concepts?.length ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''}
${data.tool.related_software?.length ? `- **Related Software:** ${data.tool.related_software.join(', ')}` : ''}
${data.metadata.reason ? `### Reason
${data.metadata.reason}
@ -299,9 +303,6 @@ ${data.metadata.contact}
private generateKnowledgebaseIssueBody(data: KnowledgebaseContribution): string {
const sections: string[] = [];
/* ------------------------------------------------------------------ */
/* Header */
/* ------------------------------------------------------------------ */
sections.push(`## Knowledge Base Article: ${data.title ?? 'Untitled'}`);
sections.push('');
sections.push(`**Submitted by:** ${data.submitter}`);
@ -310,18 +311,12 @@ ${data.metadata.contact}
if (data.difficulty) sections.push(`**Difficulty:** ${data.difficulty}`);
sections.push('');
/* ------------------------------------------------------------------ */
/* Description */
/* ------------------------------------------------------------------ */
if (data.description) {
sections.push('### Description');
sections.push(data.description);
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Content */
/* ------------------------------------------------------------------ */
if (data.content) {
sections.push('### Article Content');
sections.push('```markdown');
@ -330,18 +325,12 @@ ${data.metadata.contact}
sections.push('');
}
/* ------------------------------------------------------------------ */
/* External resources */
/* ------------------------------------------------------------------ */
if (data.externalLink) {
sections.push('### External Resource');
sections.push(`- [External Documentation](${data.externalLink})`);
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Uploaded files */
/* ------------------------------------------------------------------ */
if (Array.isArray(data.uploadedFiles) && data.uploadedFiles.length) {
sections.push('### Uploaded Files');
data.uploadedFiles.forEach((file) => {
@ -359,9 +348,6 @@ ${data.metadata.contact}
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Categories & Tags */
/* ------------------------------------------------------------------ */
const hasCategories = Array.isArray(data.categories) && data.categories.length > 0;
const hasTags = Array.isArray(data.tags) && data.tags.length > 0;
@ -372,18 +358,12 @@ ${data.metadata.contact}
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Reason */
/* ------------------------------------------------------------------ */
if (data.reason) {
sections.push('### Reason for Contribution');
sections.push(data.reason);
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Footer */
/* ------------------------------------------------------------------ */
sections.push('### For Maintainers');
sections.push('1. Review the content for quality and accuracy');
sections.push('2. Create the appropriate markdown file in `src/content/knowledgebase/`');
@ -395,4 +375,4 @@ ${data.metadata.contact}
return sections.join('\n');
}
}
}