fix contrib mechanic

This commit is contained in:
overcuriousity 2025-08-10 23:00:01 +02:00
parent 2fcc84991a
commit 4cc3e2c830
7 changed files with 1147 additions and 244 deletions

View File

@ -5,6 +5,7 @@ import '../styles/global.css';
import '../styles/auditTrail.css'; import '../styles/auditTrail.css';
import '../styles/knowledgebase.css'; import '../styles/knowledgebase.css';
import '../styles/palette.css'; import '../styles/palette.css';
import '../styles/autocomplete.css';
export interface Props { export interface Props {
title: string; 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 type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js'; import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js'; import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
@ -27,6 +27,7 @@ const ContributionToolSchema = z.object({
knowledgebase: z.boolean().optional().nullable(), knowledgebase: z.boolean().optional().nullable(),
'domain-agnostic-software': z.array(z.string()).optional().nullable(), 'domain-agnostic-software': z.array(z.string()).optional().nullable(),
related_concepts: 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([]), tags: z.array(z.string()).default([]),
statusUrl: z.string().url('Must be a valid URL').optional().nullable() statusUrl: z.string().url('Must be a valid URL').optional().nullable()
}); });
@ -80,6 +81,38 @@ function sanitizeInput(obj: any): any {
return obj; 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[] }> { async function validateToolData(tool: any, action: string): Promise<{ valid: boolean; errors: string[] }> {
const 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 }; return { valid: errors.length === 0, errors };
} catch (error) { } catch (error) {
@ -143,6 +187,9 @@ export const POST: APIRoute = async ({ request }) => {
return apiSpecial.invalidJSON(); return apiSpecial.invalidJSON();
} }
// Preprocess form data to handle autocomplete inputs
body = preprocessFormData(body);
const sanitizedBody = sanitizeInput(body); const sanitizedBody = sanitizeInput(body);
let validatedData; let validatedData;
@ -153,6 +200,7 @@ export const POST: APIRoute = async ({ request }) => {
const errorMessages = error.errors.map(err => const errorMessages = error.errors.map(err =>
`${err.path.join('.')}: ${err.message}` `${err.path.join('.')}: ${err.message}`
); );
console.log('[VALIDATION] Zod validation errors:', errorMessages);
return apiError.validation('Validation failed', 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 { try {
const gitManager = new GitContributionManager(); const gitManager = new GitContributionManager();
const result = await gitManager.submitContribution(contributionData); const result = await gitManager.submitContribution(contributionData);

View File

@ -22,6 +22,17 @@ const existingTools = data.tools;
const editToolName = Astro.url.searchParams.get('edit'); const editToolName = Astro.url.searchParams.get('edit');
const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null; const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null;
const isEdit = !!editTool; 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'}> <BaseLayout title={isEdit ? `Edit ${editTool?.name}` : 'Contribute Tool'}>
@ -194,16 +205,27 @@ const isEdit = !!editTool;
</div> </div>
</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;"> <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;">Konzepte im Zusammenhang</h3> <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; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem;">
{existingTools.filter(tool => tool.type === 'concept').map(concept => ( <div style="display: grid; gap: 1.5rem;">
<label class="checkbox-wrapper"> <div id="related-concepts-section">
<input type="checkbox" name="relatedConcepts" value={concept.name} <label for="related-concepts-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Verwandte Konzepte</label>
checked={editTool?.related_concepts?.includes(concept.name)} /> <input type="text" id="related-concepts-input" placeholder="Beginne zu tippen, um Konzepte zu finden..." />
<span>{concept.name}</span> <input type="hidden" id="related-concepts-hidden" name="relatedConcepts" value={editTool?.related_concepts?.join(', ') || ''} />
</label> <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>
</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> <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;"> <div style="margin-bottom: 1.5rem;">
<label for="tags" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Tags</label> <label for="tags-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Tags</label>
<input type="text" id="tags" name="tags" value={editTool?.tags?.join(', ') || ''} <input type="text" id="tags-input" placeholder="Beginne zu tippen, um Tags hinzuzufügen..." />
placeholder="Komma-getrennt: Passende Begriffe, nach denen ihr suchen würdet." /> <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>
<div style="margin-bottom: 1.5rem;"> <div style="margin-bottom: 1.5rem;">
@ -274,7 +299,275 @@ const isEdit = !!editTool;
</div> </div>
</BaseLayout> </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...'); console.log('[FORM] Script loaded, initializing...');
class ContributionForm { class ContributionForm {
@ -283,6 +576,7 @@ class ContributionForm {
this.editTool = editTool; this.editTool = editTool;
this.elements = {}; this.elements = {};
this.isSubmitting = false; this.isSubmitting = false;
this.autocompleteManagers = new Map();
this.init(); this.init();
} }
@ -303,14 +597,20 @@ class ContributionForm {
yamlPreview: document.getElementById('yaml-preview'), yamlPreview: document.getElementById('yaml-preview'),
successModal: document.getElementById('success-modal'), successModal: document.getElementById('success-modal'),
softwareFields: document.getElementById('software-fields'), softwareFields: document.getElementById('software-fields'),
conceptsFields: document.getElementById('concepts-fields'), relationsFields: document.getElementById('relations-fields'),
descriptionCount: document.getElementById('description-count'), descriptionCount: document.getElementById('description-count'),
reasonCount: document.getElementById('reason-count'), reasonCount: document.getElementById('reason-count'),
validationErrors: document.getElementById('validation-errors'), validationErrors: document.getElementById('validation-errors'),
errorList: document.getElementById('error-list'), errorList: document.getElementById('error-list'),
platformsRequired: document.getElementById('platforms-required'), platformsRequired: document.getElementById('platforms-required'),
licenseRequired: document.getElementById('license-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) { if (!this.elements.form || !this.elements.submitBtn) {
@ -327,6 +627,7 @@ class ContributionForm {
console.log('[FORM] Setting up handlers...'); console.log('[FORM] Setting up handlers...');
this.setupEventListeners(); this.setupEventListeners();
this.setupAutocomplete();
this.updateFieldVisibility(); this.updateFieldVisibility();
this.setupCharacterCounters(); this.setupCharacterCounters();
this.updateYAMLPreview(); this.updateYAMLPreview();
@ -334,6 +635,65 @@ class ContributionForm {
console.log('[FORM] Initialization complete!'); 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() { setupEventListeners() {
this.elements.typeSelect.addEventListener('change', () => { this.elements.typeSelect.addEventListener('change', () => {
this.updateFieldVisibility(); this.updateFieldVisibility();
@ -363,28 +723,44 @@ class ContributionForm {
console.log('[FORM] Event listeners attached'); console.log('[FORM] Event listeners attached');
} }
updateFieldVisibility() { updateFieldVisibility() {
const type = this.elements.typeSelect.value; const type = this.elements.typeSelect.value;
this.elements.softwareFields.style.display = 'none'; this.elements.softwareFields.style.display = 'none';
this.elements.conceptsFields.style.display = 'none'; this.elements.relationsFields.style.display = 'none';
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'none'; if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'none';
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'none'; if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'none';
if (type === 'software') { if (type === 'software') {
this.elements.softwareFields.style.display = 'block'; this.elements.softwareFields.style.display = 'block';
this.elements.conceptsFields.style.display = 'block'; this.elements.relationsFields.style.display = 'block';
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'inline'; if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'inline';
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'inline'; if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'inline';
} else if (type === 'method') { } else if (type === 'method') {
this.elements.conceptsFields.style.display = 'block'; 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';
}
// 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); console.log('[FORM] Field visibility updated for type:', type);
} }
setupCharacterCounters() { setupCharacterCounters() {
const counters = [ const counters = [
{ element: this.elements.descriptionTextarea, counter: this.elements.descriptionCount, max: 1000 }, { element: this.elements.descriptionTextarea, counter: this.elements.descriptionCount, max: 1000 },
{ element: this.elements.reasonTextarea, counter: this.elements.reasonCount, max: 500 } { element: this.elements.reasonTextarea, counter: this.elements.reasonCount, max: 500 }
@ -402,9 +778,9 @@ setupCharacterCounters() {
updateCounter(); updateCounter();
} }
}); });
} }
updateYAMLPreview() { updateYAMLPreview() {
if (!this.elements.yamlPreview) return; if (!this.elements.yamlPreview) return;
try { try {
@ -440,14 +816,22 @@ updateYAMLPreview() {
tool.knowledgebase = true; tool.knowledgebase = true;
} }
const tags = formData.get('tags'); // Handle tags from autocomplete
if (tags) { const tagsValue = this.elements.tagsHidden?.value || '';
tool.tags = tags.split(',').map(t => t.trim()).filter(Boolean); if (tagsValue) {
tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
} }
const relatedConcepts = formData.getAll('relatedConcepts'); // Handle related concepts from autocomplete
if (relatedConcepts.length > 0) { const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
tool.related_concepts = relatedConcepts; 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); const yaml = this.generateYAML(tool);
@ -457,9 +841,9 @@ updateYAMLPreview() {
console.error('[FORM] YAML preview error:', error); console.error('[FORM] YAML preview error:', error);
this.elements.yamlPreview.textContent = '# Error generating preview'; this.elements.yamlPreview.textContent = '# Error generating preview';
} }
} }
generateYAML(tool) { generateYAML(tool) {
const lines = []; const lines = [];
lines.push(`name: "${tool.name}"`); lines.push(`name: "${tool.name}"`);
@ -486,11 +870,14 @@ generateYAML(tool) {
if (tool.related_concepts && tool.related_concepts.length > 0) { if (tool.related_concepts && tool.related_concepts.length > 0) {
lines.push(`related_concepts: [${tool.related_concepts.map(c => `"${c}"`).join(', ')}]`); 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(', ')}]`);
}
return lines.join('\n'); return lines.join('\n');
} }
validateForm() { validateForm() {
const errors = []; const errors = [];
const formData = new FormData(this.elements.form); const formData = new FormData(this.elements.form);
@ -540,9 +927,9 @@ validateForm() {
} }
return errors; return errors;
} }
showValidationErrors(errors) { showValidationErrors(errors) {
if (errors.length === 0) { if (errors.length === 0) {
this.elements.validationErrors.style.display = 'none'; this.elements.validationErrors.style.display = 'none';
return; return;
@ -559,7 +946,8 @@ showValidationErrors(errors) {
this.elements.validationErrors.style.display = 'block'; this.elements.validationErrors.style.display = 'block';
this.elements.validationErrors.scrollIntoView({ behavior: 'smooth', block: 'center' }); this.elements.validationErrors.scrollIntoView({ behavior: 'smooth', block: 'center' });
} }
async handleSubmit() { async handleSubmit() {
console.log('[FORM] Submit handler called!'); console.log('[FORM] Submit handler called!');
@ -597,14 +985,32 @@ showValidationErrors(errors) {
phases: formData.getAll('phases'), phases: formData.getAll('phases'),
skillLevel: formData.get('skillLevel'), skillLevel: formData.get('skillLevel'),
url: formData.get('url'), url: formData.get('url'),
tags: formData.get('tags') ? tags: []
formData.get('tags').split(',').map(t => t.trim()).filter(Boolean) : []
}, },
metadata: { 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.get('icon')) submission.tool.icon = formData.get('icon');
if (formData.has('knowledgebase')) submission.tool.knowledgebase = true; 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); console.log('[FORM] Sending submission:', submission);
const response = await fetch('/api/contribute/tool', { const response = await fetch('/api/contribute/tool', {
@ -681,6 +1080,14 @@ showValidationErrors(errors) {
timeout = setTimeout(() => func.apply(this, args), wait); timeout = setTimeout(() => func.apply(this, args), wait);
}; };
} }
destroy() {
// Clean up autocomplete managers
this.autocompleteManagers.forEach(manager => {
manager.destroy();
});
this.autocompleteManagers.clear();
}
} }
function initializeForm() { function initializeForm() {
@ -707,3 +1114,4 @@ if (document.readyState === 'loading') {
console.log('[FORM] Script loaded successfully'); 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 // Client-side utilities that mirror server-side toolHelpers.ts
export function createToolSlug(toolName) { export function createToolSlug(toolName: string): string {
if (!toolName || typeof toolName !== 'string') { if (!toolName || typeof toolName !== 'string') {
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName); console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
return ''; return '';
@ -14,18 +14,353 @@ export function createToolSlug(toolName) {
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens .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; if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find(tool => return tools.find((tool: any) =>
tool.name === identifier || tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase() createToolSlug(tool.name) === identifier.toLowerCase()
); );
} }
export function isToolHosted(tool) { export function isToolHosted(tool: any): boolean {
return tool.projectUrl !== undefined && return tool.projectUrl !== undefined &&
tool.projectUrl !== null && tool.projectUrl !== null &&
tool.projectUrl !== "" && tool.projectUrl !== "" &&
tool.projectUrl.trim() !== ""; 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'; type: 'add' | 'edit';
tool: { tool: {
name: string; name: string;
icon?: string; icon?: string | null;
type: 'software' | 'method' | 'concept'; type: 'software' | 'method' | 'concept';
description: string; description: string;
domains: string[]; domains: string[];
phases: string[]; phases: string[];
platforms: string[]; platforms: string[];
skillLevel: string; skillLevel: string;
accessType?: string; accessType?: string | null;
url: string; url: string;
projectUrl?: string; projectUrl?: string | null;
license?: string; license?: string | null;
knowledgebase?: boolean; knowledgebase?: boolean | null;
'domain-agnostic-software'?: string[]; 'domain-agnostic-software'?: string[] | null;
related_concepts?: string[]; related_concepts?: string[] | null;
related_software?: string[] | null;
tags: string[]; tags: string[];
statusUrl?: string; statusUrl?: string | null;
}; };
metadata: { metadata: {
submitter: string; submitter: string;
@ -134,6 +135,7 @@ export class GitContributionManager {
if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl; if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl;
if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase; if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase;
if (tool.related_concepts?.length) cleanTool.related_concepts = tool.related_concepts; 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.tags?.length) cleanTool.tags = tool.tags;
if (tool['domain-agnostic-software']?.length) { if (tool['domain-agnostic-software']?.length) {
cleanTool['domain-agnostic-software'] = tool['domain-agnostic-software']; 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.license ? `- **License:** ${data.tool.license}` : ''}
${data.tool.domains?.length ? `- **Domains:** ${data.tool.domains.join(', ')}` : ''} ${data.tool.domains?.length ? `- **Domains:** ${data.tool.domains.join(', ')}` : ''}
${data.tool.phases?.length ? `- **Phases:** ${data.tool.phases.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 ? `### Reason
${data.metadata.reason} ${data.metadata.reason}
@ -299,9 +303,6 @@ ${data.metadata.contact}
private generateKnowledgebaseIssueBody(data: KnowledgebaseContribution): string { private generateKnowledgebaseIssueBody(data: KnowledgebaseContribution): string {
const sections: string[] = []; const sections: string[] = [];
/* ------------------------------------------------------------------ */
/* Header */
/* ------------------------------------------------------------------ */
sections.push(`## Knowledge Base Article: ${data.title ?? 'Untitled'}`); sections.push(`## Knowledge Base Article: ${data.title ?? 'Untitled'}`);
sections.push(''); sections.push('');
sections.push(`**Submitted by:** ${data.submitter}`); sections.push(`**Submitted by:** ${data.submitter}`);
@ -310,18 +311,12 @@ ${data.metadata.contact}
if (data.difficulty) sections.push(`**Difficulty:** ${data.difficulty}`); if (data.difficulty) sections.push(`**Difficulty:** ${data.difficulty}`);
sections.push(''); sections.push('');
/* ------------------------------------------------------------------ */
/* Description */
/* ------------------------------------------------------------------ */
if (data.description) { if (data.description) {
sections.push('### Description'); sections.push('### Description');
sections.push(data.description); sections.push(data.description);
sections.push(''); sections.push('');
} }
/* ------------------------------------------------------------------ */
/* Content */
/* ------------------------------------------------------------------ */
if (data.content) { if (data.content) {
sections.push('### Article Content'); sections.push('### Article Content');
sections.push('```markdown'); sections.push('```markdown');
@ -330,18 +325,12 @@ ${data.metadata.contact}
sections.push(''); sections.push('');
} }
/* ------------------------------------------------------------------ */
/* External resources */
/* ------------------------------------------------------------------ */
if (data.externalLink) { if (data.externalLink) {
sections.push('### External Resource'); sections.push('### External Resource');
sections.push(`- [External Documentation](${data.externalLink})`); sections.push(`- [External Documentation](${data.externalLink})`);
sections.push(''); sections.push('');
} }
/* ------------------------------------------------------------------ */
/* Uploaded files */
/* ------------------------------------------------------------------ */
if (Array.isArray(data.uploadedFiles) && data.uploadedFiles.length) { if (Array.isArray(data.uploadedFiles) && data.uploadedFiles.length) {
sections.push('### Uploaded Files'); sections.push('### Uploaded Files');
data.uploadedFiles.forEach((file) => { data.uploadedFiles.forEach((file) => {
@ -359,9 +348,6 @@ ${data.metadata.contact}
sections.push(''); sections.push('');
} }
/* ------------------------------------------------------------------ */
/* Categories & Tags */
/* ------------------------------------------------------------------ */
const hasCategories = Array.isArray(data.categories) && data.categories.length > 0; const hasCategories = Array.isArray(data.categories) && data.categories.length > 0;
const hasTags = Array.isArray(data.tags) && data.tags.length > 0; const hasTags = Array.isArray(data.tags) && data.tags.length > 0;
@ -372,18 +358,12 @@ ${data.metadata.contact}
sections.push(''); sections.push('');
} }
/* ------------------------------------------------------------------ */
/* Reason */
/* ------------------------------------------------------------------ */
if (data.reason) { if (data.reason) {
sections.push('### Reason for Contribution'); sections.push('### Reason for Contribution');
sections.push(data.reason); sections.push(data.reason);
sections.push(''); sections.push('');
} }
/* ------------------------------------------------------------------ */
/* Footer */
/* ------------------------------------------------------------------ */
sections.push('### For Maintainers'); sections.push('### For Maintainers');
sections.push('1. Review the content for quality and accuracy'); sections.push('1. Review the content for quality and accuracy');
sections.push('2. Create the appropriate markdown file in `src/content/knowledgebase/`'); sections.push('2. Create the appropriate markdown file in `src/content/knowledgebase/`');