823 lines
35 KiB
Plaintext
823 lines
35 KiB
Plaintext
---
|
||
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>
|
||
<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>
|
||
<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';
|
||
}
|
||
|
||
// 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');
|
||
// Hide share button
|
||
const shareButtonPrimary = document.getElementById('share-button-primary');
|
||
if (shareButtonPrimary) shareButtonPrimary.style.display = 'none';
|
||
}
|
||
if (secondaryModal) {
|
||
secondaryModal.classList.remove('active');
|
||
// Hide share button
|
||
const shareButtonSecondary = document.getElementById('share-button-secondary');
|
||
if (shareButtonSecondary) shareButtonSecondary.style.display = 'none';
|
||
}
|
||
if (overlay) overlay.classList.remove('active');
|
||
document.body.classList.remove('modals-side-by-side');
|
||
} else if (modalType === 'primary' && primaryModal) {
|
||
primaryModal.classList.remove('active');
|
||
const shareButtonPrimary = document.getElementById('share-button-primary');
|
||
if (shareButtonPrimary) shareButtonPrimary.style.display = 'none';
|
||
} else if (modalType === 'secondary' && secondaryModal) {
|
||
secondaryModal.classList.remove('active');
|
||
const shareButtonSecondary = document.getElementById('share-button-secondary');
|
||
if (shareButtonSecondary) shareButtonSecondary.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);
|
||
}
|
||
}
|
||
});
|
||
</script> |