change framework
This commit is contained in:
24
src/components/Footer.astro
Normal file
24
src/components/Footer.astro
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
// Footer component
|
||||
---
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div>
|
||||
<p class="text-muted" style="margin: 0;">
|
||||
© 2025 DFIR Tools Hub - Academic Research Project
|
||||
</p>
|
||||
</div>
|
||||
<div style="display: flex; gap: 2rem; align-items: center;">
|
||||
<a href="https://github.com/your-org/dfir-tools-hub" target="_blank" rel="noopener noreferrer">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
GitHub Repository
|
||||
</a>
|
||||
<a href="/impressum">Impressum</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
37
src/components/Navigation.astro
Normal file
37
src/components/Navigation.astro
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
import ThemeToggle from './ThemeToggle.astro';
|
||||
|
||||
const currentPath = Astro.url.pathname;
|
||||
---
|
||||
|
||||
<nav>
|
||||
<div class="container">
|
||||
<div class="nav-wrapper">
|
||||
<div class="nav-brand">
|
||||
<img src="/logo-placeholder.svg" alt="DFIR Tools Hub" class="nav-logo" />
|
||||
<span style="font-weight: 600; font-size: 1.125rem;">DFIR Tools Hub</span>
|
||||
</div>
|
||||
|
||||
<ul class="nav-links">
|
||||
<li>
|
||||
<a href="/" class={`nav-link ${currentPath === '/' ? 'active' : ''}`}>
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/status" class={`nav-link ${currentPath === '/status' ? 'active' : ''}`}>
|
||||
Status
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about" class={`nav-link ${currentPath === '/about' ? 'active' : ''}`}>
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<ThemeToggle />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
51
src/components/ThemeToggle.astro
Normal file
51
src/components/ThemeToggle.astro
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
// Theme toggle component
|
||||
---
|
||||
|
||||
<button
|
||||
class="btn-icon"
|
||||
data-theme-toggle
|
||||
onclick="window.themeUtils.toggleTheme()"
|
||||
title="Toggle theme"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<!-- Sun icon -->
|
||||
<g class="theme-icon-light">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</g>
|
||||
<!-- Moon icon -->
|
||||
<path class="theme-icon-dark" d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
<!-- Auto icon -->
|
||||
<g class="theme-icon-auto">
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<path d="M12 3v18"></path>
|
||||
<path d="M12 3a9 9 0 0 1 0 18"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.theme-icon-light,
|
||||
.theme-icon-dark,
|
||||
.theme-icon-auto {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-current-theme="light"] .theme-icon-light,
|
||||
[data-current-theme="dark"] .theme-icon-dark,
|
||||
[data-current-theme="auto"] .theme-icon-auto {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-current-theme="auto"] path {
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
80
src/components/ToolCard.astro
Normal file
80
src/components/ToolCard.astro
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
export interface Props {
|
||||
tool: {
|
||||
name: string;
|
||||
description: string;
|
||||
domains: string[];
|
||||
phases: string[];
|
||||
platforms: string[];
|
||||
skillLevel: string;
|
||||
accessType: string;
|
||||
url: string;
|
||||
license: string;
|
||||
tags: string[];
|
||||
isHosted: boolean;
|
||||
statusUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const { tool } = Astro.props;
|
||||
|
||||
// Determine card styling
|
||||
const cardClass = tool.isHosted ? 'card card-hosted' : (tool.license !== 'Proprietary' ? 'card card-oss' : 'card');
|
||||
---
|
||||
|
||||
<div class={cardClass}>
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem;">
|
||||
<h3 style="margin: 0;">{tool.name}</h3>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
{tool.isHosted && <span class="badge badge-primary">Self-Hosted</span>}
|
||||
{tool.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-muted" style="font-size: 0.875rem; margin-bottom: 1rem;">
|
||||
{tool.description}
|
||||
</p>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="9" y1="9" x2="15" y2="9"></line>
|
||||
<line x1="9" y1="15" x2="15" y2="15"></line>
|
||||
</svg>
|
||||
<span class="text-muted" style="font-size: 0.75rem;">
|
||||
{tool.platforms.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 6v6l4 2"></path>
|
||||
</svg>
|
||||
<span class="text-muted" style="font-size: 0.75rem;">
|
||||
{tool.skillLevel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" 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"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
<span class="text-muted" style="font-size: 0.75rem;">
|
||||
{tool.license}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 1rem;">
|
||||
{tool.tags.map(tag => (
|
||||
<span class="tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="width: 100%;">
|
||||
{tool.isHosted ? 'Access Service' : 'Visit Website'}
|
||||
</a>
|
||||
</div>
|
||||
177
src/components/ToolFilters.astro
Normal file
177
src/components/ToolFilters.astro
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
import { promises as fs } from 'fs';
|
||||
import { load } from 'js-yaml';
|
||||
import path from 'path';
|
||||
|
||||
// Load tools data
|
||||
const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
|
||||
const yamlContent = await fs.readFile(yamlPath, 'utf8');
|
||||
const data = load(yamlContent) as any;
|
||||
|
||||
const domains = data.domains;
|
||||
const phases = data.phases;
|
||||
|
||||
// Get unique tags from all tools
|
||||
const allTags = [...new Set(data.tools.flatMap((tool: any) => tool.tags))].sort();
|
||||
---
|
||||
|
||||
<div class="filters-container">
|
||||
<!-- Search Bar -->
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<input
|
||||
type="text"
|
||||
id="search-input"
|
||||
placeholder="Search tools by name, description, or tags..."
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Domain and Phase Dropdowns -->
|
||||
<div class="grid grid-cols-2 gap-4" style="margin-bottom: 1.5rem;">
|
||||
<div>
|
||||
<label for="domain-select" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">
|
||||
Forensic Domain
|
||||
</label>
|
||||
<select id="domain-select">
|
||||
<option value="">All Domains</option>
|
||||
{domains.map((domain: any) => (
|
||||
<option value={domain.id}>{domain.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="phase-select" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">
|
||||
Investigation Phase
|
||||
</label>
|
||||
<select id="phase-select">
|
||||
<option value="">All Phases</option>
|
||||
{phases.map((phase: any) => (
|
||||
<option value={phase.id}>{phase.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Filters -->
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<div class="checkbox-wrapper" style="margin-bottom: 1rem;">
|
||||
<input type="checkbox" id="include-proprietary" />
|
||||
<label for="include-proprietary">Include Proprietary Software</label>
|
||||
</div>
|
||||
|
||||
<!-- Tag Filters -->
|
||||
<details>
|
||||
<summary style="cursor: pointer; font-weight: 500; margin-bottom: 0.5rem;">
|
||||
Filter by Tags
|
||||
</summary>
|
||||
<div class="grid grid-cols-3 gap-2" style="margin-top: 0.5rem;">
|
||||
{allTags.map(tag => (
|
||||
<div class="checkbox-wrapper">
|
||||
<input type="checkbox" id={`tag-${tag}`} data-tag={tag} class="tag-filter" />
|
||||
<label for={`tag-${tag}`} style="font-size: 0.875rem;">{tag}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- View Toggle -->
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1.5rem;">
|
||||
<button class="btn btn-secondary view-toggle active" data-view="grid">Grid View</button>
|
||||
<button class="btn btn-secondary view-toggle" data-view="matrix">Matrix View</button>
|
||||
<button class="btn btn-secondary view-toggle" data-view="hosted">Self-Hosted Only</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script define:vars={{ toolsData: data.tools }}>
|
||||
// Store tools data globally for filtering
|
||||
window.toolsData = toolsData;
|
||||
|
||||
// Initialize filters
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const domainSelect = document.getElementById('domain-select');
|
||||
const phaseSelect = document.getElementById('phase-select');
|
||||
const proprietaryCheckbox = document.getElementById('include-proprietary');
|
||||
const tagFilters = document.querySelectorAll('.tag-filter');
|
||||
const viewToggles = document.querySelectorAll('.view-toggle');
|
||||
|
||||
// Filter function
|
||||
function filterTools() {
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const selectedDomain = domainSelect.value;
|
||||
const selectedPhase = phaseSelect.value;
|
||||
const includeProprietary = proprietaryCheckbox.checked;
|
||||
const selectedTags = Array.from(tagFilters)
|
||||
.filter(cb => cb.checked)
|
||||
.map(cb => cb.getAttribute('data-tag'));
|
||||
|
||||
const filtered = window.toolsData.filter(tool => {
|
||||
// Search filter
|
||||
if (searchTerm && !(
|
||||
tool.name.toLowerCase().includes(searchTerm) ||
|
||||
tool.description.toLowerCase().includes(searchTerm) ||
|
||||
tool.tags.some(tag => tag.toLowerCase().includes(searchTerm))
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Domain filter
|
||||
if (selectedDomain && !tool.domains.includes(selectedDomain)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phase filter
|
||||
if (selectedPhase && !tool.phases.includes(selectedPhase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Proprietary filter
|
||||
if (!includeProprietary && tool.license === 'Proprietary') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tag filter
|
||||
if (selectedTags.length > 0 && !selectedTags.some(tag => tool.tags.includes(tag))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Emit custom event with filtered results
|
||||
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: filtered }));
|
||||
}
|
||||
|
||||
// View toggle handler
|
||||
function handleViewToggle(view) {
|
||||
viewToggles.forEach(btn => {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
|
||||
});
|
||||
|
||||
window.dispatchEvent(new CustomEvent('viewChanged', { detail: view }));
|
||||
|
||||
// Apply view-specific filters
|
||||
if (view === 'hosted') {
|
||||
const hosted = window.toolsData.filter(tool => tool.isHosted);
|
||||
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: hosted }));
|
||||
} else {
|
||||
filterTools();
|
||||
}
|
||||
}
|
||||
|
||||
// Attach event listeners
|
||||
searchInput.addEventListener('input', filterTools);
|
||||
domainSelect.addEventListener('change', filterTools);
|
||||
phaseSelect.addEventListener('change', filterTools);
|
||||
proprietaryCheckbox.addEventListener('change', filterTools);
|
||||
tagFilters.forEach(cb => cb.addEventListener('change', filterTools));
|
||||
viewToggles.forEach(btn => {
|
||||
btn.addEventListener('click', () => handleViewToggle(btn.getAttribute('data-view')));
|
||||
});
|
||||
|
||||
// Initial filter
|
||||
filterTools();
|
||||
});
|
||||
</script>
|
||||
170
src/components/ToolMatrix.astro
Normal file
170
src/components/ToolMatrix.astro
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
import { promises as fs } from 'fs';
|
||||
import { load } from 'js-yaml';
|
||||
import path from 'path';
|
||||
|
||||
// Load tools data
|
||||
const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
|
||||
const yamlContent = await fs.readFile(yamlPath, 'utf8');
|
||||
const data = load(yamlContent) as any;
|
||||
|
||||
const domains = data.domains;
|
||||
const phases = data.phases;
|
||||
const tools = data.tools;
|
||||
|
||||
// Create matrix structure
|
||||
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.domains.includes(domain.id) && tool.phases.includes(phase.id)
|
||||
);
|
||||
});
|
||||
});
|
||||
---
|
||||
|
||||
<div id="matrix-container" class="matrix-wrapper" style="display: none;">
|
||||
<table class="matrix-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 200px;">Domain / Phase</th>
|
||||
{phases.map((phase: any) => (
|
||||
<th>{phase.name}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{domains.map((domain: any) => (
|
||||
<tr>
|
||||
<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) => (
|
||||
<span
|
||||
class={`tool-chip ${tool.isHosted ? 'tool-chip-hosted' : tool.license !== 'Proprietary' ? 'tool-chip-oss' : ''}`}
|
||||
data-tool-name={tool.name}
|
||||
onclick={`window.showToolDetails('${tool.name}')`}
|
||||
>
|
||||
{tool.name}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Tool Details Modal -->
|
||||
<div class="modal-overlay" id="modal-overlay" onclick="window.hideToolDetails()"></div>
|
||||
<div class="tool-details" id="tool-details">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
|
||||
<h2 id="tool-name" style="margin: 0;">Tool Name</h2>
|
||||
<button class="btn-icon" onclick="window.hideToolDetails()">
|
||||
<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>
|
||||
|
||||
<p id="tool-description" class="text-muted"></p>
|
||||
|
||||
<div id="tool-badges" style="display: flex; gap: 0.5rem; margin-bottom: 1rem;"></div>
|
||||
|
||||
<div id="tool-metadata" style="margin-bottom: 1rem;"></div>
|
||||
|
||||
<div id="tool-tags" style="margin-bottom: 1rem;"></div>
|
||||
|
||||
<a id="tool-link" href="#" target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="width: 100%;">
|
||||
Visit Website
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script define:vars={{ toolsData: tools }}>
|
||||
// Tool details functions
|
||||
window.showToolDetails = function(toolName) {
|
||||
const tool = toolsData.find(t => t.name === toolName);
|
||||
if (!tool) return;
|
||||
|
||||
// Update modal content
|
||||
document.getElementById('tool-name').textContent = tool.name;
|
||||
document.getElementById('tool-description').textContent = tool.description;
|
||||
|
||||
// Badges
|
||||
const badgesContainer = document.getElementById('tool-badges');
|
||||
badgesContainer.innerHTML = '';
|
||||
if (tool.isHosted) {
|
||||
badgesContainer.innerHTML += '<span class="badge badge-primary">Self-Hosted</span>';
|
||||
}
|
||||
if (tool.license !== 'Proprietary') {
|
||||
badgesContainer.innerHTML += '<span class="badge badge-success">Open Source</span>';
|
||||
}
|
||||
|
||||
// Metadata
|
||||
const metadataContainer = document.getElementById('tool-metadata');
|
||||
metadataContainer.innerHTML = `
|
||||
<div style="display: grid; gap: 0.5rem;">
|
||||
<div><strong>Platforms:</strong> ${tool.platforms.join(', ')}</div>
|
||||
<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>
|
||||
<div><strong>License:</strong> ${tool.license}</div>
|
||||
<div><strong>Access Type:</strong> ${tool.accessType}</div>
|
||||
<div><strong>Domains:</strong> ${tool.domains.join(', ')}</div>
|
||||
<div><strong>Phases:</strong> ${tool.phases.join(', ')}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Tags
|
||||
const tagsContainer = document.getElementById('tool-tags');
|
||||
tagsContainer.innerHTML = `
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
|
||||
${tool.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Link
|
||||
const linkElement = document.getElementById('tool-link');
|
||||
linkElement.href = tool.url;
|
||||
linkElement.textContent = tool.isHosted ? 'Access Service' : 'Visit Website';
|
||||
|
||||
// Show modal
|
||||
document.getElementById('modal-overlay').classList.add('active');
|
||||
document.getElementById('tool-details').classList.add('active');
|
||||
};
|
||||
|
||||
window.hideToolDetails = function() {
|
||||
document.getElementById('modal-overlay').classList.remove('active');
|
||||
document.getElementById('tool-details').classList.remove('active');
|
||||
};
|
||||
|
||||
// Update matrix on filter change
|
||||
window.addEventListener('toolsFiltered', (event) => {
|
||||
const filtered = event.detail;
|
||||
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
|
||||
|
||||
if (currentView === 'matrix') {
|
||||
// Update matrix cells
|
||||
document.querySelectorAll('.matrix-cell').forEach(cell => {
|
||||
cell.innerHTML = '';
|
||||
});
|
||||
|
||||
// Re-populate with filtered tools
|
||||
filtered.forEach(tool => {
|
||||
tool.domains.forEach(domain => {
|
||||
tool.phases.forEach(phase => {
|
||||
const cell = document.querySelector(`[data-domain="${domain}"][data-phase="${phase}"]`);
|
||||
if (cell) {
|
||||
const chip = document.createElement('span');
|
||||
chip.className = `tool-chip ${tool.isHosted ? 'tool-chip-hosted' : tool.license !== 'Proprietary' ? 'tool-chip-oss' : ''}`;
|
||||
chip.textContent = tool.name;
|
||||
chip.onclick = () => window.showToolDetails(tool.name);
|
||||
cell.appendChild(chip);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user