main #11
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
@ -282,7 +575,8 @@ class ContributionForm {
|
|||||||
this.isEdit = isEdit;
|
this.isEdit = isEdit;
|
||||||
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,203 +723,231 @@ 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.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);
|
|
||||||
|
|
||||||
const tool = {
|
this.elements.softwareFields.style.display = 'none';
|
||||||
name: formData.get('name') || 'Tool Name',
|
this.elements.relationsFields.style.display = 'none';
|
||||||
type: formData.get('type') || 'software',
|
|
||||||
description: formData.get('description') || 'Tool description',
|
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'none';
|
||||||
domains: formData.getAll('domains'),
|
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'none';
|
||||||
phases: formData.getAll('phases'),
|
|
||||||
skillLevel: formData.get('skillLevel') || 'intermediate',
|
|
||||||
url: formData.get('url') || 'https://example.com'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (formData.get('icon')) {
|
if (type === 'software') {
|
||||||
tool.icon = formData.get('icon');
|
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') {
|
// Show appropriate relation sections
|
||||||
tool.platforms = formData.getAll('platforms');
|
if (type !== 'concept') {
|
||||||
tool.license = formData.get('license') || 'Unknown';
|
const conceptsSection = document.getElementById('related-concepts-section');
|
||||||
if (formData.get('accessType')) {
|
const softwareSection = document.getElementById('related-software-section');
|
||||||
tool.accessType = formData.get('accessType');
|
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 {
|
try {
|
||||||
new URL(url);
|
const formData = new FormData(this.elements.form);
|
||||||
} catch {
|
|
||||||
errors.push('Primary URL must be a valid URL');
|
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') {
|
generateYAML(tool) {
|
||||||
const platforms = formData.getAll('platforms');
|
const lines = [];
|
||||||
if (platforms.length === 0) {
|
|
||||||
errors.push('At least one platform is required for software');
|
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();
|
return lines.join('\n');
|
||||||
if (!license) {
|
}
|
||||||
errors.push('License is required for software');
|
|
||||||
|
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) {
|
this.elements.errorList.innerHTML = '';
|
||||||
if (errors.length === 0) {
|
|
||||||
this.elements.validationErrors.style.display = 'none';
|
errors.forEach(error => {
|
||||||
return;
|
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() {
|
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() {
|
||||||
@ -706,4 +1113,5 @@ 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
121
src/styles/autocomplete.css
Normal 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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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/`');
|
||||||
@ -395,4 +375,4 @@ ${data.metadata.contact}
|
|||||||
|
|
||||||
return sections.join('\n');
|
return sections.join('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user