forensic-pathways/src/components/ToolMatrix.astro
overcuriousity f4acf39ca7 progress
2025-07-22 22:36:08 +02:00

883 lines
38 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import { getToolsData } from '../utils/dataService.js';
import ShareButton from './ShareButton.astro';
// Load tools data
const data = await getToolsData();
const domains = data.domains;
const phases = data.phases;
const tools = data.tools;
const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
// Get tools for each domain-agnostic section based on the tool's domain-agnostic-software field
const domainAgnosticTools = domainAgnosticSoftware.map((section: any) => ({
section,
tools: tools.filter((tool: any) =>
tool['domain-agnostic-software'] && tool['domain-agnostic-software'].includes(section.id)
)
}));
// Matrix shows ALL tools based on domains × phases (independent of domain-agnostic-software)
const matrix: Record<string, Record<string, any[]>> = {};
domains.forEach((domain: any) => {
matrix[domain.id] = {};
phases.forEach((phase: any) => {
matrix[domain.id][phase.id] = tools.filter((tool: any) =>
tool.type !== 'concept' && // Exclude concepts from matrix
tool.domains && tool.domains.includes(domain.id) &&
tool.phases && tool.phases.includes(phase.id)
);
});
});
---
<div id="matrix-container" class="matrix-wrapper" style="display: none;">
<!-- Domain-Agnostic Software Sections -->
{domainAgnosticTools.map((sectionData: any, index: number) => (
<div id={`domain-agnostic-section-${sectionData.section.id}`} class="card collaboration-section-collapsed" style="margin-bottom: 1.5rem; border-left: 4px solid var(--color-accent);">
<div class="collaboration-header" onclick={`toggleDomainAgnosticSection('${sectionData.section.id}')`} style="cursor: pointer; display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.1rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="8.5" cy="7" r="4"/>
<line x1="20" y1="8" x2="20" y2="14"/>
<line x1="23" y1="11" x2="17" y2="11"/>
</svg>
<h3 style="margin: 0; color: var(--color-accent); font-size: 1.125rem;">
{sectionData.section.name}
<span id={`count-${sectionData.section.id}`} class="badge" style="background-color: var(--color-text-secondary); color: var(--color-bg); margin-left: 0.5rem; font-size: 0.75rem;">
{sectionData.tools.length}
</span>
</h3>
<div class="collaboration-expand-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</div>
<div class="collaboration-content" style="display: none;">
<div class="collaboration-tools-compact" id={`domain-agnostic-tools-${sectionData.section.id}`}>
{sectionData.tools.map((tool: any) => {
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
return (
<div class={`collaboration-tool-compact ${hasValidProjectUrl ? 'hosted' : tool.license !== 'Proprietary' ? 'oss' : ''}`}
onclick={`window.showToolDetails('${tool.name}')`}>
<div class="tool-compact-header">
<h4 style="margin: 0; font-size: 0.875rem; font-weight: 600;">
{tool.icon && <span style="margin-right: 0.5rem;">{tool.icon}</span>}
{tool.name}
</h4>
<div style="display: flex; gap: 0.25rem;">
{hasValidProjectUrl && <span class="badge badge--mini badge-primary">CC24-Server</span>}
{tool.knowledgebase === true && <span class="badge badge--mini badge-error">📖</span>}
</div>
</div>
<p class="text-muted">
{tool.description}
</p>
<div style="display: flex; gap: 0.75rem; font-size: 0.6875rem; color: var(--color-text-secondary);">
<span>{tool.platforms.join(', ')}</span>
<span>•</span>
<span>{tool.skillLevel}</span>
</div>
</div>
);
})}
</div>
</div>
</div>
))}
<!-- DFIR Tools Matrix -->
<div id="dfir-matrix-section">
<h2 style="margin-bottom: 1rem; color: var(--color-text);">MATRIX</h2>
<table class="matrix-table">
<thead>
<tr>
<th style="width: 200px;">Domain / Phase</th>
{phases.map((phase: any) => (
<th data-phase={phase.id}>{phase.name}</th>
))}
</tr>
</thead>
<tbody>
{domains.map((domain: any) => (
<tr data-domain={domain.id}>
<th>{domain.name}</th>
{phases.map((phase: any) => (
<td class="matrix-cell" data-domain={domain.id} data-phase={phase.id}>
{matrix[domain.id][phase.id].map((tool: any) => {
const isMethod = tool.type === 'method';
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
const chipClass = isMethod ? 'tool-chip-method' :
hasValidProjectUrl ? 'tool-chip-hosted' :
tool.license !== 'Proprietary' ? 'tool-chip-oss' : '';
return (
<span
class={`tool-chip ${chipClass}`}
data-tool-name={tool.name}
onclick={`window.showToolDetails('${tool.name}')`}
title={`${tool.name}${tool.knowledgebase === true ? ' (KB verfügbar)' : ''}`}
>
{tool.name}
{tool.knowledgebase === true && <span style="margin-left: 0.25rem; font-size: 0.6875rem;">📖</span>}
</span>
);
})}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
<!-- Tool Details Modals - Dual Modal System -->
<div class="modal-overlay" id="modal-overlay" onclick="window.hideAllToolDetails()"></div>
<!-- Primary Modal -->
<div class="tool-details" id="tool-details-primary">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
<h2 id="tool-name-primary" style="margin: 0;">Tool Name</h2>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<div id="share-button-primary" style="display: none;">
<!-- Share button will be populated by JavaScript -->
</div>
<div id="contribute-button-primary" style="display: none;">
<!-- Contribution button will be populated by JavaScript -->
</div>
<button class="btn-icon" onclick="window.hideToolDetails('primary')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<p id="tool-description-primary" class="text-muted"></p>
<div id="tool-badges-primary" style="display: flex; gap: 0.5rem; margin-bottom: 1rem;"></div>
<div id="tool-metadata-primary" style="margin-bottom: 1rem;"></div>
<div id="tool-tags-primary" style="margin-bottom: 1rem;"></div>
<div id="tool-links-primary" style="display: flex; gap: 0.5rem; flex-direction: column;"></div>
</div>
<!-- Secondary Modal -->
<div class="tool-details" id="tool-details-secondary">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
<h2 id="tool-name-secondary" style="margin: 0;">Tool Name</h2>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<div id="share-button-secondary" style="display: none;">
<!-- Share button will be populated by JavaScript -->
</div>
<div id="contribute-button-secondary" style="display: none;">
<!-- Contribution button will be populated by JavaScript -->
</div>
<button class="btn-icon" onclick="window.hideToolDetails('secondary')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<p id="tool-description-secondary" class="text-muted"></p>
<div id="tool-badges-secondary" style="display: flex; gap: 0.5rem; margin-bottom: 1rem;"></div>
<div id="tool-metadata-secondary" style="margin-bottom: 1rem;"></div>
<div id="tool-tags-secondary" style="margin-bottom: 1rem;"></div>
<div id="tool-links-secondary" style="display: flex; gap: 0.5rem; flex-direction: column;"></div>
</div>
<script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}>
// Helper function to get selected phase from active button
function getSelectedPhase() {
const activePhaseButton = document.querySelector('.phase-button.active');
return activePhaseButton ? activePhaseButton.getAttribute('data-phase') : '';
}
// Helper function to get selected domain from dropdown
function getSelectedDomain() {
const domainSelect = document.getElementById('domain-select');
return domainSelect ? domainSelect.value : '';
}
// Update matrix highlighting based on current filters
function updateMatrixHighlighting() {
const matrixTable = document.querySelector('.matrix-table');
if (!matrixTable) return;
// Clear existing highlights
matrixTable.querySelectorAll('.highlight-row, .highlight-column').forEach(el => {
el.classList.remove('highlight-row', 'highlight-column');
});
const selectedDomain = getSelectedDomain();
const selectedPhase = getSelectedPhase();
// Highlight selected domain row
if (selectedDomain) {
const domainRow = matrixTable.querySelector(`tr[data-domain="${selectedDomain}"]`);
if (domainRow) {
domainRow.classList.add('highlight-row');
}
}
// Highlight selected phase column
if (selectedPhase) {
const phaseHeaders = matrixTable.querySelectorAll('thead th[data-phase]');
const phaseIndex = Array.from(phaseHeaders).findIndex(th =>
th.getAttribute('data-phase') === selectedPhase
);
if (phaseIndex >= 0) {
const columnIndex = phaseIndex + 1; // +1 because first column is domain names
matrixTable.querySelectorAll(`tr`).forEach(row => {
const cell = row.children[columnIndex];
if (cell) {
cell.classList.add('highlight-column');
}
});
}
}
}
// Toggle domain-agnostic section
function toggleDomainAgnosticSection(sectionId) {
const section = document.getElementById(`domain-agnostic-section-${sectionId}`);
const content = section?.querySelector('.collaboration-content');
const icon = section?.querySelector('.collaboration-expand-icon svg');
if (!section || !content || !icon) return;
const isExpanded = section.classList.contains('collaboration-section-expanded');
if (isExpanded) {
// Collapse
section.classList.remove('collaboration-section-expanded');
section.classList.add('collaboration-section-collapsed');
content.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
} else {
// Expand
section.classList.remove('collaboration-section-collapsed');
section.classList.add('collaboration-section-expanded');
content.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
}
}
// ===== SHARING FUNCTIONALITY =====
// Create tool slug from name (same logic as ShareButton.astro)
function createToolSlug(toolName) {
return toolName.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Remove duplicate hyphens
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
}
// Find tool by name or slug
function findTool(identifier) {
return toolsData.find(tool =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
);
}
// Generate share URLs
function generateShareURL(toolName, view, modal = null) {
const toolSlug = createToolSlug(toolName);
const baseUrl = window.location.origin + window.location.pathname;
const params = new URLSearchParams();
params.set('tool', toolSlug);
params.set('view', view);
if (modal) {
params.set('modal', modal);
}
return `${baseUrl}?${params.toString()}`;
}
// Copy to clipboard with feedback
async function copyToClipboard(text, button) {
try {
await navigator.clipboard.writeText(text);
// Show feedback
const originalHTML = button.innerHTML;
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20,6 9,17 4,12"/></svg> Kopiert!';
button.style.color = 'var(--color-accent)';
setTimeout(() => {
button.innerHTML = originalHTML;
button.style.color = '';
}, 2000);
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
// Show feedback
const originalHTML = button.innerHTML;
button.innerHTML = 'Kopiert!';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}
}
// Show share dialog
window.showShareDialog = function(shareButton) {
const toolName = shareButton.getAttribute('data-tool-name');
const context = shareButton.getAttribute('data-context');
// Create modal backdrop
let backdrop = document.getElementById('share-modal-backdrop');
if (!backdrop) {
backdrop = document.createElement('div');
backdrop.id = 'share-modal-backdrop';
backdrop.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5); z-index: 9999;
display: flex; align-items: center; justify-content: center;
opacity: 0; transition: opacity 0.2s ease;
`;
document.body.appendChild(backdrop);
}
// Create share dialog
const dialog = document.createElement('div');
dialog.style.cssText = `
background: var(--color-bg); border: 1px solid var(--color-border);
border-radius: 0.75rem; padding: 1.5rem; max-width: 400px; width: 90%;
box-shadow: var(--shadow-lg); transform: scale(0.9); transition: transform 0.2s ease;
`;
dialog.innerHTML = `
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
<h3 style="margin: 0; color: var(--color-primary);">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-align: middle;">
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
${toolName} teilen
</h3>
<button id="close-share-dialog" style="background: none; border: none; cursor: pointer; padding: 0.25rem;color: var(--color-text-secondary)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
<button class="share-option-btn" data-url="${generateShareURL(toolName, 'grid')}"
style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; background: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;">
<div style="width: 32px; height: 32px; background: var(--color-primary); border-radius: 0.25rem; display: flex; align-items: center; justify-content: center;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
</svg>
</div>
<div>
<div style="font-weight: 500; margin-bottom: 0.25rem;color: var(--color-text-secondary)">Kachelansicht</div>
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">Scrollt zur Karte in der Übersicht</div>
</div>
</button>
<button class="share-option-btn" data-url="${generateShareURL(toolName, 'matrix')}"
style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; background: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;">
<div style="width: 32px; height: 32px; background: var(--color-accent); border-radius: 0.25rem; display: flex; align-items: center; justify-content: center;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z"/>
</svg>
</div>
<div>
<div style="font-weight: 500; margin-bottom: 0.25rem;color: var(--color-text-secondary)">Matrix-Ansicht</div>
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">Zeigt Tool-Position in der Matrix</div>
</div>
</button>
<button class="share-option-btn" data-url="${generateShareURL(toolName, 'modal', 'primary')}"
style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border: 1px solid var(--color-border); border-radius: 0.5rem; background: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;">
<div style="width: 32px; height: 32px; background: var(--color-warning); border-radius: 0.25rem; display: flex; align-items: center; justify-content: center;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
</div>
<div>
<div style="font-weight: 500; margin-bottom: 0.25rem;color: var(--color-text-secondary)">Tool-Details</div>
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">Öffnet Detail-Fenster direkt</div>
</div>
</button>
</div>
`;
backdrop.appendChild(dialog);
// Show with animation
requestAnimationFrame(() => {
backdrop.style.opacity = '1';
dialog.style.transform = 'scale(1)';
});
// Event handlers
const closeDialog = () => {
backdrop.style.opacity = '0';
dialog.style.transform = 'scale(0.9)';
setTimeout(() => {
if (backdrop.parentNode) {
document.body.removeChild(backdrop);
}
}, 200);
};
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) closeDialog();
});
document.getElementById('close-share-dialog').addEventListener('click', closeDialog);
// Share option handlers
dialog.querySelectorAll('.share-option-btn').forEach(btn => {
btn.addEventListener('mouseover', () => {
btn.style.backgroundColor = 'var(--color-bg-secondary)';
btn.style.borderColor = 'var(--color-primary)';
});
btn.addEventListener('mouseout', () => {
btn.style.backgroundColor = 'var(--color-bg)';
btn.style.borderColor = 'var(--color-border)';
});
btn.addEventListener('click', () => {
const url = btn.getAttribute('data-url');
copyToClipboard(url, btn);
});
});
};
// Make functions globally available
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
// Enhanced modal system for side-by-side display
window.showToolDetails = function(toolName, modalType = 'primary') {
const tool = toolsData.find(t => t.name === toolName);
if (!tool) {
console.error('Tool not found:', toolName);
return;
}
const isMethod = tool.type === 'method';
const isConcept = tool.type === 'concept';
// Get modal-specific element IDs
const elements = {
name: document.getElementById(`tool-name-${modalType}`),
description: document.getElementById(`tool-description-${modalType}`),
badges: document.getElementById(`tool-badges-${modalType}`),
metadata: document.getElementById(`tool-metadata-${modalType}`),
tags: document.getElementById(`tool-tags-${modalType}`),
links: document.getElementById(`tool-links-${modalType}`)
};
// Check if all elements exist
for (const [key, element] of Object.entries(elements)) {
if (!element) {
console.error(`Element not found: tool-${key}-${modalType}`);
return;
}
}
// Update modal content
const iconHtml = tool.icon ? `<span style="margin-right: 0.75rem; font-size: 1.5rem;">${tool.icon}</span>` : '';
elements.name.innerHTML = `${iconHtml}${tool.name}`;
elements.description.textContent = tool.description;
// Badges
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
elements.badges.innerHTML = '';
if (isConcept) {
elements.badges.innerHTML += '<span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>';
} else if (isMethod) {
elements.badges.innerHTML += '<span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>';
} else if (hasValidProjectUrl) {
elements.badges.innerHTML += '<span class="badge badge-primary">CC24-Server</span>';
}
if (tool.knowledgebase === true) {
elements.badges.innerHTML += '<span class="badge badge-error">📖</span>';
}
// Metadata
const domains = tool.domains || [];
const phases = tool.phases || [];
const domainsText = domains.length > 0 ? domains.join(', ') : 'Domain-agnostic';
const phasesText = phases.join(', ');
let metadataHTML = `<div style="display: grid; gap: 0.5rem;">`;
if (!isConcept) {
metadataHTML += `
<div><strong>Betriebssystem:</strong> ${(tool.platforms || []).join(', ')}</div>
<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>
<div><strong>Lizenzmodell:</strong> ${tool.license}</div>
<div><strong>Deployment:</strong> ${tool.accessType}</div>
`;
} else {
metadataHTML += `<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>`;
}
metadataHTML += `
<div><strong>Einsatzgebiete:</strong> ${domainsText}</div>
<div><strong>Ermittlungsphasen:</strong> ${phasesText}</div>
</div>`;
elements.metadata.innerHTML = metadataHTML;
// Tags and Related Concepts
const tags = tool.tags || [];
let tagsHTML = `
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
${tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
`;
// Related Concepts section - only show in primary modal to avoid infinite loops
const relatedConcepts = tool.related_concepts || [];
if (relatedConcepts.length > 0 && modalType === 'primary') {
const conceptLinks = relatedConcepts.map(conceptName => {
const concept = toolsData.find(t => t.name === conceptName && t.type === 'concept');
if (concept) {
return `<button class="tag" style="cursor: pointer; background-color: var(--color-concept-bg); border: 1px solid var(--color-concept); color: var(--color-concept); transition: var(--transition-fast); margin: 0.125rem;"
onclick="event.stopPropagation(); window.showToolDetails('${conceptName}', 'secondary')"
onmouseover="this.style.backgroundColor='var(--color-concept)'; this.style.color='white';"
onmouseout="this.style.backgroundColor='var(--color-concept-bg)'; this.style.color='var(--color-concept)';">
${conceptName}
</button>`;
}
return `<span class="tag" style="background-color: var(--color-bg-tertiary); color: var(--color-text-secondary); margin: 0.125rem;">${conceptName}</span>`;
}).join('');
// Check if mobile device
const isMobile = window.innerWidth <= 768;
const collapseOnMobile = isMobile && relatedConcepts.length > 2;
tagsHTML += `
<div style="margin-top: 1rem;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<strong style="color: var(--color-text);">Verwandte Konzepte:</strong>
${collapseOnMobile ? `
<button id="concepts-toggle-${modalType}"
onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.textContent = this.textContent === '▼' ? '▲' : '▼';"
style="background: none; border: 1px solid var(--color-border); border-radius: 0.25rem; padding: 0.25rem 0.5rem; cursor: pointer; font-size: 0.75rem;">
</button>
` : ''}
</div>
<div ${collapseOnMobile ? 'style="display: none;"' : ''} style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
${conceptLinks}
</div>
</div>
`;
}
elements.tags.innerHTML = tagsHTML;
// Links
let linksHTML = '';
if (isConcept) {
linksHTML += `
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="width: 100%; background-color: var(--color-concept); border-color: var(--color-concept);">
Mehr erfahren
</a>
`;
} else if (isMethod) {
linksHTML += `
<a href="${tool.projectUrl || tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="width: 100%; background-color: var(--color-method); border-color: var(--color-method);">
Zur Methode
</a>
`;
} else if (hasValidProjectUrl) {
linksHTML += `
<div style="display: flex; gap: 0.5rem;">
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary" style="flex: 1;">
Homepage
</a>
<a href="${tool.projectUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="flex: 1;">
Zugreifen
</a>
</div>
`;
} else {
linksHTML += `
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="width: 100%;">
Software-Homepage
</a>
`;
}
if (tool.knowledgebase === true) {
const kbId = tool.name.toLowerCase().replace(/\s+/g, '-');
linksHTML += `
<a href="/knowledgebase#kb-${kbId}" class="btn btn-secondary" style="width: 100%; margin-top: 0.5rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
Knowledgebase anzeigen
</a>
`;
}
elements.links.innerHTML = linksHTML;
// ===== POPULATE SHARE BUTTON =====
const shareButtonContainer = document.getElementById(`share-button-${modalType}`);
if (shareButtonContainer) {
const toolSlug = createToolSlug(tool.name);
shareButtonContainer.innerHTML = `
<button class="share-btn share-btn--medium"
data-tool-name="${tool.name}"
data-tool-slug="${toolSlug}"
data-context="modal-${modalType}"
onclick="event.stopPropagation(); window.showShareDialog(this)"
title="${tool.name} teilen"
aria-label="${tool.name} teilen">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="18" cy="5" r="3"/>
<circle cx="6" cy="12" r="3"/>
<circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
</button>
`;
shareButtonContainer.style.display = 'block';
}
// ===== POPULATE CONTRIBUTION BUTTON =====
const contributeButtonContainer = document.getElementById(`contribute-button-${modalType}`);
if (contributeButtonContainer) {
contributeButtonContainer.innerHTML = `
<a href="/contribute/tool?edit=${encodeURIComponent(tool.name)}"
class="btn-icon"
data-contribute-button="edit"
data-tool-name="${tool.name}"
title="Edit ${tool.name}"
aria-label="Edit ${tool.name}"
onclick="event.stopPropagation();">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</a>
`;
contributeButtonContainer.style.display = 'block';
}
// Show modals and update layout
const overlay = document.getElementById('modal-overlay');
const primaryModal = document.getElementById('tool-details-primary');
const secondaryModal = document.getElementById('tool-details-secondary');
if (overlay) overlay.classList.add('active');
if (modalType === 'primary' && primaryModal) primaryModal.classList.add('active');
if (modalType === 'secondary' && secondaryModal) secondaryModal.classList.add('active');
// Check if both modals are now active
const primaryActive = primaryModal && primaryModal.classList.contains('active');
const secondaryActive = secondaryModal && secondaryModal.classList.contains('active');
if (primaryActive && secondaryActive) {
document.body.classList.add('modals-side-by-side');
}
};
window.hideToolDetails = function(modalType = 'both') {
const overlay = document.getElementById('modal-overlay');
const primaryModal = document.getElementById('tool-details-primary');
const secondaryModal = document.getElementById('tool-details-secondary');
if (modalType === 'both' || modalType === 'all') {
if (primaryModal) {
primaryModal.classList.remove('active');
const shareButtonPrimary = document.getElementById('share-button-primary');
const contributeButtonPrimary = document.getElementById('contribute-button-primary');
if (shareButtonPrimary) shareButtonPrimary.style.display = 'none';
if (contributeButtonPrimary) contributeButtonPrimary.style.display = 'none';
}
if (secondaryModal) {
secondaryModal.classList.remove('active');
const shareButtonSecondary = document.getElementById('share-button-secondary');
const contributeButtonSecondary = document.getElementById('contribute-button-secondary');
if (shareButtonSecondary) shareButtonSecondary.style.display = 'none';
if (contributeButtonSecondary) contributeButtonSecondary.style.display = 'none';
}
// ... rest of existing code
} else if (modalType === 'primary' && primaryModal) {
primaryModal.classList.remove('active');
const shareButtonPrimary = document.getElementById('share-button-primary');
const contributeButtonPrimary = document.getElementById('contribute-button-primary');
if (shareButtonPrimary) shareButtonPrimary.style.display = 'none';
if (contributeButtonPrimary) contributeButtonPrimary.style.display = 'none';
} else if (modalType === 'secondary' && secondaryModal) {
secondaryModal.classList.remove('active');
const shareButtonSecondary = document.getElementById('share-button-secondary');
const contributeButtonSecondary = document.getElementById('contribute-button-secondary');
if (shareButtonSecondary) shareButtonSecondary.style.display = 'none';
if (contributeButtonSecondary) contributeButtonSecondary.style.display = 'none';
}
// Check if any modal is still active
const primaryActive = primaryModal && primaryModal.classList.contains('active');
const secondaryActive = secondaryModal && secondaryModal.classList.contains('active');
if (!primaryActive && !secondaryActive) {
if (overlay) overlay.classList.remove('active');
document.body.classList.remove('modals-side-by-side');
} else if (primaryActive !== secondaryActive) {
// Only one modal left - remove side-by-side layout
document.body.classList.remove('modals-side-by-side');
}
};
window.hideAllToolDetails = function() {
window.hideToolDetails('both');
};
// Keep existing event listeners
window.addEventListener('viewChanged', (event) => {
const view = event.detail;
if (view === 'matrix') {
setTimeout(updateMatrixHighlighting, 100);
}
});
window.addEventListener('toolsFiltered', (event) => {
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView === 'matrix') {
setTimeout(updateMatrixHighlighting, 50);
}
});
window.addEventListener('toolsFiltered', (event) => {
const filtered = event.detail;
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView === 'matrix') {
const selectedPhase = getSelectedPhase();
const domainAgnosticPhaseIds = domainAgnosticSoftware.map(section => section.id);
const isDomainAgnosticPhase = domainAgnosticPhaseIds.includes(selectedPhase);
domainAgnosticSoftware.forEach(sectionData => {
const section = document.getElementById(`domain-agnostic-section-${sectionData.id}`);
const container = document.getElementById(`domain-agnostic-tools-${sectionData.id}`);
if (!section || !container) return;
});
if (!isDomainAgnosticPhase) {
document.getElementById('dfir-matrix-section').style.display = 'block';
document.querySelectorAll('.matrix-cell').forEach(cell => {
cell.innerHTML = '';
});
filtered.forEach(tool => {
if (tool.type === 'concept') {
return;
}
const isMethod = tool.type === 'method';
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
const domains = tool.domains || [];
const phases = tool.phases || [];
domains.forEach(domain => {
phases.forEach(phase => {
const cell = document.querySelector(`[data-domain="${domain}"][data-phase="${phase}"]`);
if (cell) {
const chip = document.createElement('span');
const chipClass = isMethod ? 'tool-chip-method' :
hasValidProjectUrl ? 'tool-chip-hosted' :
tool.license !== 'Proprietary' ? 'tool-chip-oss' : '';
chip.className = `tool-chip ${chipClass}`;
chip.setAttribute('title', `${tool.name}${tool.knowledgebase === true ? ' (KB verfügbar)' : ''}`);
chip.innerHTML = `${tool.name}${tool.knowledgebase === true ? '<span style="margin-left: 0.25rem; font-size: 0.6875rem;">📖</span>' : ''}`;
chip.onclick = () => window.showToolDetails(tool.name);
cell.appendChild(chip);
}
});
});
});
setTimeout(updateMatrixHighlighting, 50);
}
}
});
document.addEventListener('DOMContentLoaded', () => {
// Auth check for contribution buttons (similar to existing auth checking pattern)
function setupContributionButtonAuth() {
document.addEventListener('click', async (e) => {
const contributeButton = e.target.closest('[data-contribute-button]');
if (!contributeButton) return;
e.preventDefault();
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
if (data.authRequired && !data.authenticated) {
const returnUrl = contributeButton.href;
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
} else {
window.location.href = contributeButton.href;
}
} catch (error) {
console.error('Auth check failed:', error);
window.location.href = contributeButton.href;
}
});
}
setupContributionButtonAuth();
});
</script>