844 lines
36 KiB
Plaintext
844 lines
36 KiB
Plaintext
---
|
|
import { getToolsData } from '../utils/dataService.js';
|
|
import ShareButton from './ShareButton.astro';
|
|
|
|
const data = await getToolsData();
|
|
|
|
const domains = data.domains;
|
|
const phases = data.phases;
|
|
const tools = data.tools;
|
|
const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
|
|
|
|
const domainAgnosticTools = domainAgnosticSoftware.map((section: any) => ({
|
|
section,
|
|
tools: tools.filter((tool: any) =>
|
|
tool['domain-agnostic-software'] && tool['domain-agnostic-software'].includes(section.id)
|
|
)
|
|
}));
|
|
|
|
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' &&
|
|
tool.domains && tool.domains.includes(domain.id) &&
|
|
tool.phases && tool.phases.includes(phase.id)
|
|
);
|
|
});
|
|
});
|
|
---
|
|
|
|
<div id="matrix-container" class="matrix-wrapper hidden">
|
|
{domainAgnosticTools.map((sectionData: any, index: number) => (
|
|
<div id={`domain-agnostic-section-${sectionData.section.id}`} class="card collaboration-section-collapsed mb-6 border-l-4" style="border-left-color: var(--color-accent);">
|
|
<div class="collaboration-header cursor-pointer flex items-center gap-3" onclick={`toggleDomainAgnosticSection('${sectionData.section.id}')`} style="margin-bottom: 0.1rem;">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2" class="mr-2">
|
|
<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 class="m-0 text-lg text-accent">
|
|
{sectionData.section.name}
|
|
<span id={`count-${sectionData.section.id}`} class="badge text-xs" style="background-color: var(--color-text-secondary); color: var(--color-bg); margin-left: 0.5rem;">
|
|
{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 hidden">
|
|
<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 cursor-pointer ${hasValidProjectUrl ? 'hosted' : tool.license !== 'Proprietary' ? 'oss' : ''}`}
|
|
onclick={`window.showToolDetails('${tool.name}')`}>
|
|
<div class="tool-compact-header">
|
|
<h4 class="m-0 text-sm font-semibold">
|
|
{tool.icon && <span class="mr-2">{tool.icon}</span>}
|
|
{tool.name}
|
|
</h4>
|
|
<div class="flex gap-1">
|
|
{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 class="flex gap-3 text-xs text-secondary">
|
|
<span>{tool.platforms.join(', ')}</span>
|
|
<span>•</span>
|
|
<span>{tool.skillLevel}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<div id="dfir-matrix-section">
|
|
<h2 class="mb-4" style="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 class="text-xs" style="margin-left: 0.25rem;">📖</span>}
|
|
</span>
|
|
);
|
|
})}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="modal-overlay" onclick="window.hideAllToolDetails()"></div>
|
|
|
|
<div class="tool-details" id="tool-details-primary">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<h2 id="tool-name-primary" class="m-0">Tool Name</h2>
|
|
<div class="flex items-center gap-2">
|
|
<div id="share-button-primary" class="hidden">
|
|
</div>
|
|
<div id="contribute-button-primary" class="hidden">
|
|
</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" class="flex gap-2 mb-4"></div>
|
|
|
|
<div id="tool-metadata-primary" class="mb-4"></div>
|
|
|
|
<div id="tool-tags-primary" class="mb-4"></div>
|
|
|
|
<div id="tool-links-primary" class="flex flex-col gap-2"></div>
|
|
</div>
|
|
|
|
<div class="tool-details" id="tool-details-secondary">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<h2 id="tool-name-secondary" class="m-0">Tool Name</h2>
|
|
<div class="flex items-center gap-2">
|
|
<div id="share-button-secondary" class="hidden">
|
|
</div>
|
|
<div id="contribute-button-secondary" class="hidden">
|
|
</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" class="flex gap-2 mb-4"></div>
|
|
|
|
<div id="tool-metadata-secondary" class="mb-4"></div>
|
|
|
|
<div id="tool-tags-secondary" class="mb-4"></div>
|
|
|
|
<div id="tool-links-secondary" class="flex flex-col gap-2"></div>
|
|
</div>
|
|
|
|
<script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}>
|
|
if (!window.isToolHosted) {
|
|
window.isToolHosted = function(tool) {
|
|
return tool.projectUrl !== undefined &&
|
|
tool.projectUrl !== null &&
|
|
tool.projectUrl !== "" &&
|
|
tool.projectUrl.trim() !== "";
|
|
};
|
|
}
|
|
|
|
function getSelectedPhase() {
|
|
const activePhaseChip = document.querySelector('.phase-chip.active');
|
|
return activePhaseChip ? activePhaseChip.getAttribute('data-phase') : '';
|
|
}
|
|
|
|
function getSelectedDomain() {
|
|
const domainSelect = document.getElementById('domain-select');
|
|
return domainSelect ? domainSelect.value : '';
|
|
}
|
|
|
|
function updateMatrixHighlighting() {
|
|
const matrixTable = document.querySelector('.matrix-table');
|
|
if (!matrixTable) return;
|
|
|
|
matrixTable.querySelectorAll('.highlight-row, .highlight-column').forEach(el => {
|
|
el.classList.remove('highlight-row', 'highlight-column');
|
|
});
|
|
|
|
const selectedDomain = getSelectedDomain();
|
|
const selectedPhase = getSelectedPhase();
|
|
|
|
if (selectedDomain) {
|
|
const domainRow = matrixTable.querySelector(`tr[data-domain="${selectedDomain}"]`);
|
|
if (domainRow) domainRow.classList.add('highlight-row');
|
|
}
|
|
|
|
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;
|
|
matrixTable.querySelectorAll(`tr`).forEach(row => {
|
|
const cell = row.children[columnIndex];
|
|
if (cell) cell.classList.add('highlight-column');
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
section.classList.remove('collaboration-section-expanded');
|
|
section.classList.add('collaboration-section-collapsed');
|
|
content.style.display = 'none';
|
|
icon.style.transform = 'rotate(0deg)';
|
|
} else {
|
|
section.classList.remove('collaboration-section-collapsed');
|
|
section.classList.add('collaboration-section-expanded');
|
|
content.style.display = 'block';
|
|
icon.style.transform = 'rotate(180deg)';
|
|
}
|
|
}
|
|
|
|
function generateShareURL(toolName, view, modal = null) {
|
|
const toolSlug = window.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()}`;
|
|
}
|
|
|
|
async function copyToClipboard(text, button) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
|
|
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) {
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = text;
|
|
document.body.appendChild(textArea);
|
|
textArea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textArea);
|
|
|
|
const originalHTML = button.innerHTML;
|
|
button.innerHTML = 'Kopiert!';
|
|
setTimeout(() => {
|
|
button.innerHTML = originalHTML;
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
function showShareDialog(shareButton) {
|
|
const toolName = shareButton.getAttribute('data-tool-name');
|
|
const context = shareButton.getAttribute('data-context');
|
|
|
|
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-color: rgba(0, 0, 0, 0.5);
|
|
z-index: 1000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease;
|
|
`;
|
|
document.body.appendChild(backdrop);
|
|
}
|
|
|
|
const dialog = document.createElement('div');
|
|
dialog.style.cssText = `
|
|
background-color: var(--color-bg);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
max-width: 28rem;
|
|
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); display: flex; align-items: center;">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
|
<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" class="btn-icon">
|
|
<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-color: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;" onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'" onmouseout="this.style.backgroundColor='var(--color-bg)'">
|
|
<div style="width: 2rem; height: 2rem; background-color: 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);">Kachelansicht</div>
|
|
<div style="font-size: 0.875rem; 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-color: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;" onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'" onmouseout="this.style.backgroundColor='var(--color-bg)'">
|
|
<div style="width: 2rem; height: 2rem; background-color: 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">
|
|
<rect x="3" y="3" width="4" height="4"/>
|
|
<rect x="10" y="3" width="4" height="4"/>
|
|
<rect x="17" y="3" width="4" height="4"/>
|
|
<rect x="3" y="10" width="4" height="4"/>
|
|
<rect x="10" y="10" width="4" height="4"/>
|
|
<rect x="17" y="10" width="4" height="4"/>
|
|
<rect x="3" y="17" width="4" height="4"/>
|
|
<rect x="10" y="17" width="4" height="4"/>
|
|
<rect x="17" y="17" width="4" height="4"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div style="font-weight: 500; margin-bottom: 0.25rem; color: var(--color-text);">Matrix-Ansicht</div>
|
|
<div style="font-size: 0.875rem; 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-color: var(--color-bg); cursor: pointer; transition: var(--transition-fast); text-align: left; width: 100%;" onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'" onmouseout="this.style.backgroundColor='var(--color-bg)'">
|
|
<div style="width: 2rem; height: 2rem; background-color: 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"/>
|
|
<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>
|
|
</div>
|
|
<div>
|
|
<div style="font-weight: 500; margin-bottom: 0.25rem; color: var(--color-text);">Tool-Details</div>
|
|
<div style="font-size: 0.875rem; color: var(--color-text-secondary);">Öffnet Detail-Fenster direkt</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
backdrop.appendChild(dialog);
|
|
|
|
requestAnimationFrame(() => {
|
|
backdrop.style.opacity = '1';
|
|
dialog.style.transform = 'scale(1)';
|
|
});
|
|
|
|
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);
|
|
|
|
dialog.querySelectorAll('.share-option-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const url = btn.getAttribute('data-url');
|
|
copyToClipboard(url, btn);
|
|
});
|
|
});
|
|
}
|
|
|
|
function showToolDetails(toolName, modalType = 'primary') {
|
|
const tool = toolsData.find(t => t.name === toolName);
|
|
if (!tool) return;
|
|
|
|
const isMethod = tool.type === 'method';
|
|
const isConcept = tool.type === 'concept';
|
|
|
|
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}`)
|
|
};
|
|
|
|
for (const [key, element] of Object.entries(elements)) {
|
|
if (!element) return;
|
|
}
|
|
|
|
const iconHtml = tool.icon ? `<span class="mr-3 text-xl">${tool.icon}</span>` : '';
|
|
elements.name.innerHTML = `${iconHtml}${tool.name}`;
|
|
elements.description.textContent = tool.description;
|
|
|
|
const hasValidProjectUrl = window.isToolHosted(tool);
|
|
|
|
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>';
|
|
}
|
|
|
|
const domains = tool.domains || [];
|
|
const phases = tool.phases || [];
|
|
const domainsText = domains.length > 0 ? domains.join(', ') : 'Domain-agnostic';
|
|
const phasesText = phases.join(', ');
|
|
|
|
let metadataHTML = `<div class="grid gap-2">`;
|
|
|
|
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;
|
|
|
|
const tags = tool.tags || [];
|
|
let tagsHTML = `
|
|
<div class="flex flex-wrap gap-1">
|
|
${tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
|
|
</div>
|
|
`;
|
|
|
|
const relatedConcepts = tool.related_concepts || [];
|
|
const relatedSoftware = tool.related_software || [];
|
|
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 cursor-pointer" style="background-color: var(--color-concept-bg); border: 1px solid var(--color-concept); color: var(--color-concept); transition: var(--transition-fast);"
|
|
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);">${conceptName}</span>`;
|
|
}).join('');
|
|
|
|
const isMobile = window.innerWidth <= 768;
|
|
const collapseOnMobile = isMobile && relatedConcepts.length > 2;
|
|
|
|
tagsHTML += `
|
|
<div class="mt-4">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<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 === '▼' ? '▲' : '▼';"
|
|
class="btn-icon text-xs">
|
|
▼
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
<div ${collapseOnMobile ? 'class="hidden"' : ''} class="flex flex-wrap gap-1">
|
|
${conceptLinks}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (relatedSoftware.length > 0 && modalType === 'primary') {
|
|
const softwareLinks = relatedSoftware.map(softwareName => {
|
|
const software = toolsData.find(t => t.name === softwareName && (t.type === 'software' || t.type === 'method'));
|
|
if (software) {
|
|
const isHosted = window.isToolHosted(software);
|
|
const isSoftwareMethod = software.type === 'method';
|
|
const bgColor = isSoftwareMethod ? 'var(--color-method-bg)' :
|
|
isHosted ? 'var(--color-hosted-bg)' : 'var(--color-oss-bg)';
|
|
const borderColor = isSoftwareMethod ? 'var(--color-method)' :
|
|
isHosted ? 'var(--color-hosted)' : 'var(--color-oss)';
|
|
|
|
return `<button class="tag cursor-pointer" style="background-color: ${bgColor}; border: 1px solid ${borderColor}; color: ${borderColor}; transition: var(--transition-fast);"
|
|
onclick="event.stopPropagation(); window.showToolDetails('${softwareName}', 'secondary')"
|
|
onmouseover="this.style.backgroundColor='${borderColor}'; this.style.color='white';"
|
|
onmouseout="this.style.backgroundColor='${bgColor}'; this.style.color='${borderColor}';">
|
|
${softwareName}
|
|
</button>`;
|
|
}
|
|
return `<span class="tag" style="background-color: var(--color-bg-tertiary); color: var(--color-text-secondary);">${softwareName}</span>`;
|
|
}).join('');
|
|
|
|
const isMobile = window.innerWidth <= 768;
|
|
const collapseOnMobile = isMobile && relatedSoftware.length > 2;
|
|
|
|
tagsHTML += `
|
|
<div class="mt-4">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<strong style="color: var(--color-text);">Verwandte Software:</strong>
|
|
${collapseOnMobile ? `
|
|
<button id="software-toggle-${modalType}"
|
|
onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.textContent = this.textContent === '▼' ? '▲' : '▼';"
|
|
class="btn-icon text-xs">
|
|
▼
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
<div ${collapseOnMobile ? 'class="hidden"' : ''} class="flex flex-wrap gap-1">
|
|
${softwareLinks}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
elements.tags.innerHTML = tagsHTML;
|
|
|
|
let linksHTML = '';
|
|
|
|
if (isConcept) {
|
|
linksHTML += `
|
|
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary w-full" style="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 w-full" style="background-color: var(--color-method); border-color: var(--color-method);">
|
|
Zur Methode
|
|
</a>
|
|
`;
|
|
} else if (hasValidProjectUrl) {
|
|
linksHTML += `
|
|
<div class="flex gap-2">
|
|
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary flex-1">
|
|
Homepage
|
|
</a>
|
|
<a href="${tool.projectUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-primary flex-1">
|
|
Zugreifen
|
|
</a>
|
|
</div>
|
|
`;
|
|
} else {
|
|
linksHTML += `
|
|
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-primary w-full">
|
|
Software-Homepage
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
if (tool.knowledgebase === true) {
|
|
const kbId = window.createToolSlug(tool.name);
|
|
linksHTML += `
|
|
<a href="/knowledgebase#kb-${kbId}" class="btn btn-secondary w-full mt-2">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-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"/>
|
|
<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;
|
|
|
|
const shareButtonContainer = document.getElementById(`share-button-${modalType}`);
|
|
if (shareButtonContainer) {
|
|
const toolSlug = window.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';
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
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');
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
function hideToolDetails(modalType = 'both') {
|
|
const overlay = document.getElementById('modal-overlay');
|
|
const primaryModal = document.getElementById('tool-details-primary');
|
|
const secondaryModal = document.getElementById('tool-details-secondary');
|
|
|
|
if (window.modalHideInProgress) return;
|
|
window.modalHideInProgress = true;
|
|
|
|
setTimeout(() => {
|
|
window.modalHideInProgress = false;
|
|
}, 100);
|
|
|
|
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';
|
|
}
|
|
} 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';
|
|
}
|
|
|
|
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) {
|
|
document.body.classList.add('modals-side-by-side');
|
|
} else {
|
|
document.body.classList.remove('modals-side-by-side');
|
|
}
|
|
}
|
|
|
|
function hideAllToolDetails() {
|
|
hideToolDetails('both');
|
|
}
|
|
|
|
window.showToolDetails = showToolDetails;
|
|
window.hideToolDetails = hideToolDetails;
|
|
window.hideAllToolDetails = hideAllToolDetails;
|
|
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
|
|
window.showShareDialog = showShareDialog;
|
|
|
|
window.matrixShowToolDetails = showToolDetails;
|
|
window.matrixHideToolDetails = hideToolDetails;
|
|
|
|
window.addEventListener('viewChanged', (event) => {
|
|
const view = event.detail;
|
|
if (view === 'matrix') {
|
|
setTimeout(() => {
|
|
if (window.filterTools && typeof window.filterTools === 'function') {
|
|
window.filterTools();
|
|
} else {
|
|
const allTools = window.toolsData || [];
|
|
window.dispatchEvent(new CustomEvent('toolsFiltered', {
|
|
detail: {
|
|
tools: allTools,
|
|
semanticSearch: false
|
|
}
|
|
}));
|
|
}
|
|
}, 100);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('toolsFiltered', (event) => {
|
|
const { tools: filtered, semanticSearch } = 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);
|
|
|
|
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 = window.isToolHosted(tool);
|
|
|
|
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('data-tool-name', tool.name);
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
</script> |