383 lines
14 KiB
JavaScript
383 lines
14 KiB
JavaScript
// File: ./src/js/search.js
|
|
// Search and filtering functionality
|
|
(function() {
|
|
'use strict';
|
|
|
|
// State management
|
|
let currentFilters = {
|
|
search: '',
|
|
domain: '',
|
|
phase: '',
|
|
mode: 'selector'
|
|
};
|
|
|
|
let allTools = [];
|
|
let domains = [];
|
|
let phases = [];
|
|
|
|
// DOM elements
|
|
let searchInput;
|
|
let domainSelect;
|
|
let phaseSelect;
|
|
let resetButton;
|
|
let resetEmptyButton;
|
|
let toolResults;
|
|
let toolsGrid;
|
|
let defaultState;
|
|
let noResults;
|
|
let resultsTitle;
|
|
let resultsCount;
|
|
let resultsDescription;
|
|
let viewModes;
|
|
let modeButtons;
|
|
|
|
// Initialize when DOM is loaded
|
|
function init() {
|
|
// Get data from global variables (set by template)
|
|
if (typeof window.toolsData !== 'undefined') {
|
|
allTools = window.toolsData;
|
|
domains = window.domains || [];
|
|
phases = window.phases || [];
|
|
}
|
|
|
|
// Get DOM elements
|
|
searchInput = document.getElementById('search-input');
|
|
domainSelect = document.getElementById('domain-select');
|
|
phaseSelect = document.getElementById('phase-select');
|
|
resetButton = document.getElementById('reset-filters');
|
|
resetEmptyButton = document.getElementById('reset-filters-empty');
|
|
toolResults = document.getElementById('tool-results');
|
|
toolsGrid = document.getElementById('tools-grid');
|
|
defaultState = document.getElementById('default-state');
|
|
noResults = document.getElementById('no-results');
|
|
resultsTitle = document.getElementById('results-title');
|
|
resultsCount = document.getElementById('results-count');
|
|
resultsDescription = document.getElementById('results-description');
|
|
|
|
// View mode elements
|
|
viewModes = {
|
|
selector: document.getElementById('view-selector'),
|
|
matrix: document.getElementById('view-matrix'),
|
|
saas: document.getElementById('view-saas')
|
|
};
|
|
|
|
modeButtons = {
|
|
selector: document.getElementById('mode-selector'),
|
|
matrix: document.getElementById('mode-matrix'),
|
|
saas: document.getElementById('mode-saas')
|
|
};
|
|
|
|
// Add event listeners
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', handleSearchChange);
|
|
}
|
|
|
|
if (domainSelect) {
|
|
domainSelect.addEventListener('change', handleDomainChange);
|
|
}
|
|
|
|
if (phaseSelect) {
|
|
phaseSelect.addEventListener('change', handlePhaseChange);
|
|
}
|
|
|
|
if (resetButton) {
|
|
resetButton.addEventListener('click', resetFilters);
|
|
}
|
|
|
|
if (resetEmptyButton) {
|
|
resetEmptyButton.addEventListener('click', resetFilters);
|
|
}
|
|
|
|
// Mode switching
|
|
Object.keys(modeButtons).forEach(mode => {
|
|
if (modeButtons[mode]) {
|
|
modeButtons[mode].addEventListener('click', () => switchMode(mode));
|
|
}
|
|
});
|
|
|
|
// Initialize views
|
|
updateDisplay();
|
|
populateMatrix();
|
|
populateSaasGrid();
|
|
}
|
|
|
|
// Event handlers
|
|
function handleSearchChange(event) {
|
|
currentFilters.search = event.target.value;
|
|
updateDisplay();
|
|
}
|
|
|
|
function handleDomainChange(event) {
|
|
currentFilters.domain = event.target.value;
|
|
updateDisplay();
|
|
}
|
|
|
|
function handlePhaseChange(event) {
|
|
currentFilters.phase = event.target.value;
|
|
updateDisplay();
|
|
}
|
|
|
|
function resetFilters() {
|
|
currentFilters.search = '';
|
|
currentFilters.domain = '';
|
|
currentFilters.phase = '';
|
|
|
|
if (searchInput) searchInput.value = '';
|
|
if (domainSelect) domainSelect.value = '';
|
|
if (phaseSelect) phaseSelect.value = '';
|
|
|
|
updateDisplay();
|
|
}
|
|
|
|
function switchMode(mode) {
|
|
currentFilters.mode = mode;
|
|
|
|
// Update button states
|
|
Object.keys(modeButtons).forEach(m => {
|
|
if (modeButtons[m]) {
|
|
modeButtons[m].classList.toggle('view-mode-active', m === mode);
|
|
modeButtons[m].classList.toggle('view-mode-btn', m !== mode);
|
|
}
|
|
});
|
|
|
|
// Update view visibility
|
|
Object.keys(viewModes).forEach(m => {
|
|
if (viewModes[m]) {
|
|
viewModes[m].classList.toggle('hidden', m !== mode);
|
|
viewModes[m].classList.toggle('view-mode-active', m === mode);
|
|
}
|
|
});
|
|
|
|
// Update displays based on mode
|
|
if (mode === 'matrix') {
|
|
populateMatrix();
|
|
} else if (mode === 'saas') {
|
|
populateSaasGrid();
|
|
} else {
|
|
updateDisplay();
|
|
}
|
|
}
|
|
|
|
// Filter tools based on current filters
|
|
function filterTools() {
|
|
let filtered = allTools;
|
|
|
|
// Filter by type based on mode
|
|
if (currentFilters.mode === 'saas') {
|
|
filtered = filtered.filter(tool => tool.type === 'SaaS');
|
|
} else if (currentFilters.mode === 'selector' || currentFilters.mode === 'matrix') {
|
|
filtered = filtered.filter(tool => tool.type === 'FOSS');
|
|
}
|
|
|
|
// Apply search filter
|
|
if (currentFilters.search) {
|
|
const searchTerm = currentFilters.search.toLowerCase();
|
|
filtered = filtered.filter(tool =>
|
|
tool.name.toLowerCase().includes(searchTerm) ||
|
|
tool.description.toLowerCase().includes(searchTerm) ||
|
|
(tool.tags && tool.tags.some(tag => tag.toLowerCase().includes(searchTerm)))
|
|
);
|
|
}
|
|
|
|
// Apply domain filter (only for selector mode)
|
|
if (currentFilters.mode === 'selector' && currentFilters.domain) {
|
|
filtered = filtered.filter(tool =>
|
|
tool.domains && tool.domains.includes(currentFilters.domain)
|
|
);
|
|
}
|
|
|
|
// Apply phase filter (only for selector mode)
|
|
if (currentFilters.mode === 'selector' && currentFilters.phase) {
|
|
filtered = filtered.filter(tool =>
|
|
tool.phases && tool.phases.includes(currentFilters.phase)
|
|
);
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
// Update the display based on current filters
|
|
function updateDisplay() {
|
|
if (currentFilters.mode !== 'selector') return;
|
|
|
|
const hasFilters = currentFilters.search || currentFilters.domain || currentFilters.phase;
|
|
|
|
if (!hasFilters) {
|
|
// Show default state
|
|
if (toolResults) toolResults.classList.add('hidden');
|
|
if (defaultState) defaultState.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
const filtered = filterTools();
|
|
|
|
// Hide default state, show results
|
|
if (defaultState) defaultState.classList.add('hidden');
|
|
if (toolResults) toolResults.classList.remove('hidden');
|
|
|
|
// Update results metadata
|
|
if (resultsCount) resultsCount.textContent = filtered.length;
|
|
updateResultsDescription();
|
|
|
|
// Show/hide no results message
|
|
if (filtered.length === 0) {
|
|
if (toolsGrid) toolsGrid.classList.add('hidden');
|
|
if (noResults) noResults.classList.remove('hidden');
|
|
} else {
|
|
if (noResults) noResults.classList.add('hidden');
|
|
if (toolsGrid) toolsGrid.classList.remove('hidden');
|
|
renderTools(filtered);
|
|
}
|
|
}
|
|
|
|
// Update results description text
|
|
function updateResultsDescription() {
|
|
if (!resultsDescription) return;
|
|
|
|
const parts = [];
|
|
if (currentFilters.domain) parts.push(`Bereich: ${currentFilters.domain}`);
|
|
if (currentFilters.phase) parts.push(`Phase: ${currentFilters.phase}`);
|
|
if (currentFilters.search) parts.push(`Suche: "${currentFilters.search}"`);
|
|
|
|
resultsDescription.textContent = parts.join(' • ');
|
|
}
|
|
|
|
// Render tools in the grid
|
|
function renderTools(tools) {
|
|
if (!toolsGrid) return;
|
|
|
|
toolsGrid.innerHTML = tools.map(tool => createToolCard(tool)).join('');
|
|
|
|
// Add click handlers for tool cards
|
|
const toolCards = toolsGrid.querySelectorAll('.tool-card');
|
|
toolCards.forEach((card, index) => {
|
|
card.addEventListener('click', () => showToolModal(tools[index]));
|
|
});
|
|
}
|
|
|
|
// Create HTML for a tool card
|
|
function createToolCard(tool) {
|
|
const typeLabel = tool.type === 'SaaS' ?
|
|
'<span class="tag tag-purple">SaaS</span>' : '';
|
|
|
|
const tags = (tool.tags || []).slice(0, 3).map(tag =>
|
|
`<span class="tag tag-blue">${tag}</span>`
|
|
).join('');
|
|
|
|
const platforms = (tool.platforms || []).join(', ');
|
|
|
|
return `
|
|
<div class="tool-card" data-tool-id="${tool.id}">
|
|
<div class="flex items-start justify-between mb-2">
|
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100 text-lg">
|
|
${tool.name}
|
|
</h3>
|
|
<div class="flex items-center gap-2">
|
|
${typeLabel}
|
|
<svg class="w-4 h-4 text-gray-400 hover:text-blue-600 dark:text-gray-500 dark:hover:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<p class="text-gray-600 dark:text-gray-300 text-sm mb-3">${tool.description}</p>
|
|
<div class="flex flex-wrap gap-1 mb-2">
|
|
${tags}
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
${tool.skillLevel} • ${tool.accessType} • ${platforms}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Populate matrix view
|
|
function populateMatrix() {
|
|
if (currentFilters.mode !== 'matrix') return;
|
|
|
|
domains.forEach(domain => {
|
|
phases.forEach(phase => {
|
|
const cell = document.querySelector(`[data-domain="${domain}"][data-phase="${phase}"]`);
|
|
if (!cell) return;
|
|
|
|
const cellTools = allTools.filter(tool =>
|
|
tool.type === 'FOSS' &&
|
|
tool.domains && tool.domains.includes(domain) &&
|
|
tool.phases && tool.phases.includes(phase) &&
|
|
(!currentFilters.search ||
|
|
tool.name.toLowerCase().includes(currentFilters.search.toLowerCase()) ||
|
|
tool.description.toLowerCase().includes(currentFilters.search.toLowerCase()) ||
|
|
(tool.tags && tool.tags.some(tag => tag.toLowerCase().includes(currentFilters.search.toLowerCase()))))
|
|
);
|
|
|
|
if (cellTools.length === 0) {
|
|
cell.innerHTML = '<div class="text-gray-400 dark:text-gray-500 text-sm text-center py-2">-</div>';
|
|
} else {
|
|
cell.innerHTML = cellTools.map(tool =>
|
|
`<div class="matrix-cell-tool" data-tool-id="${tool.id}">${tool.name}</div>`
|
|
).join('');
|
|
|
|
// Add click handlers
|
|
const toolElements = cell.querySelectorAll('.matrix-cell-tool');
|
|
toolElements.forEach((element, index) => {
|
|
element.addEventListener('click', () => showToolModal(cellTools[index]));
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Populate SaaS grid
|
|
function populateSaasGrid() {
|
|
if (currentFilters.mode !== 'saas') return;
|
|
|
|
const saasGrid = document.getElementById('saas-grid');
|
|
const saasCount = document.getElementById('saas-count');
|
|
const noSaasResults = document.getElementById('no-saas-results');
|
|
|
|
if (!saasGrid) return;
|
|
|
|
const saasTools = filterTools();
|
|
|
|
if (saasCount) saasCount.textContent = saasTools.length;
|
|
|
|
if (saasTools.length === 0) {
|
|
saasGrid.classList.add('hidden');
|
|
if (noSaasResults) noSaasResults.classList.remove('hidden');
|
|
} else {
|
|
if (noSaasResults) noSaasResults.classList.add('hidden');
|
|
saasGrid.classList.remove('hidden');
|
|
|
|
saasGrid.innerHTML = saasTools.map(tool => createToolCard(tool)).join('');
|
|
|
|
// Add click handlers
|
|
const toolCards = saasGrid.querySelectorAll('.tool-card');
|
|
toolCards.forEach((card, index) => {
|
|
card.addEventListener('click', () => showToolModal(saasTools[index]));
|
|
});
|
|
}
|
|
}
|
|
|
|
// Show tool modal (defined in modal.js)
|
|
function showToolModal(tool) {
|
|
if (typeof window.showToolModal === 'function') {
|
|
window.showToolModal(tool);
|
|
}
|
|
}
|
|
|
|
// Export for use by other scripts
|
|
window.searchModule = {
|
|
init,
|
|
filterTools,
|
|
updateDisplay,
|
|
populateMatrix,
|
|
populateSaasGrid,
|
|
resetFilters
|
|
};
|
|
|
|
// Auto-initialize when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})(); |