sharing mechanic with eye-watering animation
This commit is contained in:
parent
6ae7f36660
commit
a6b51187b7
@ -21,7 +21,6 @@ export interface Props {
|
||||
|
||||
const { tool } = Astro.props;
|
||||
|
||||
|
||||
// Check types
|
||||
const isMethod = tool.type === 'method';
|
||||
const isConcept = tool.type === 'concept';
|
||||
|
@ -1,5 +1,7 @@
|
||||
---
|
||||
import { getToolsData } from '../utils/dataService.js';
|
||||
import ShareButton from './ShareButton.astro';
|
||||
|
||||
|
||||
|
||||
// Load tools data
|
||||
@ -146,6 +148,10 @@ domains.forEach((domain: any) => {
|
||||
<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>
|
||||
@ -153,6 +159,7 @@ domains.forEach((domain: any) => {
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p id="tool-description-primary" class="text-muted"></p>
|
||||
|
||||
@ -169,6 +176,10 @@ domains.forEach((domain: any) => {
|
||||
<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>
|
||||
@ -176,6 +187,7 @@ domains.forEach((domain: any) => {
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p id="tool-description-secondary" class="text-muted"></p>
|
||||
|
||||
@ -266,6 +278,201 @@ domains.forEach((domain: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 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;
|
||||
|
||||
@ -372,10 +579,23 @@ domains.forEach((domain: any) => {
|
||||
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;">
|
||||
<strong style="display: block; margin-bottom: 0.5rem; color: var(--color-text);">Verwandte Konzepte:</strong>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
|
||||
<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>
|
||||
@ -436,6 +656,30 @@ domains.forEach((domain: any) => {
|
||||
|
||||
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');
|
||||
@ -460,14 +704,28 @@ domains.forEach((domain: any) => {
|
||||
const secondaryModal = document.getElementById('tool-details-secondary');
|
||||
|
||||
if (modalType === 'both' || modalType === 'all') {
|
||||
if (primaryModal) primaryModal.classList.remove('active');
|
||||
if (secondaryModal) secondaryModal.classList.remove('active');
|
||||
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
|
||||
|
206
src/data/tools.yaml.example
Normal file
206
src/data/tools.yaml.example
Normal file
@ -0,0 +1,206 @@
|
||||
# This is a minimal example file of the real knowledgebase in ./src/data/tools.yaml
|
||||
- name: Rapid Incident Response Triage on macOS
|
||||
icon: 📋
|
||||
type: method
|
||||
description: >-
|
||||
Spezialisierte Methodik für die schnelle Incident Response auf
|
||||
macOS-Systemen mit Fokus auf die Sammlung kritischer forensischer
|
||||
Artefakte in unter einer Stunde. Adressiert die Lücke zwischen
|
||||
Windows-zentrierten IR-Prozessen und macOS-spezifischen
|
||||
Sicherheitsarchitekturen. Nutzt Tools wie Aftermath für effiziente
|
||||
Datensammlung ohne zeitaufwändige Full-Disk-Images. Besonders wertvoll für
|
||||
Unternehmensumgebungen mit gemischten Betriebssystem-Landschaften.
|
||||
domains:
|
||||
- incident-response
|
||||
- law-enforcement
|
||||
- malware-analysis
|
||||
phases:
|
||||
- data-collection
|
||||
- examination
|
||||
platforms: []
|
||||
related_concepts: null
|
||||
domain-agnostic-software: null
|
||||
skillLevel: intermediate
|
||||
accessType: null
|
||||
url: >-
|
||||
https://www.sans.org/white-papers/rapid-incident-response-on-macos-actionable-insights-under-hour/
|
||||
projectUrl: null
|
||||
license: null
|
||||
knowledgebase: null
|
||||
tags:
|
||||
- macos
|
||||
- rapid-response
|
||||
- triage
|
||||
- incident-response
|
||||
- aftermath
|
||||
- enterprise
|
||||
- methodology
|
||||
- apple
|
||||
- name: Aftermath
|
||||
icon: 📦
|
||||
type: software
|
||||
description: >-
|
||||
Jamf's Open-Source-Tool für die schnelle Sammlung forensischer Artefakte
|
||||
auf macOS-Systemen. Sammelt kritische Daten wie Prozessinformationen,
|
||||
Netzwerkverbindungen, Dateisystem-Metadaten und Systemkonfigurationen ohne
|
||||
Full-Disk-Imaging. Speziell entwickelt für die Rapid-Response-Triage in
|
||||
Enterprise-Umgebungen mit macOS-Geräten. Normalisiert Zeitstempel und
|
||||
erstellt durchsuchbare Ausgabeformate für effiziente Analyse.
|
||||
domains:
|
||||
- incident-response
|
||||
- law-enforcement
|
||||
- malware-analysis
|
||||
phases:
|
||||
- data-collection
|
||||
- examination
|
||||
platforms:
|
||||
- macOS
|
||||
related_concepts: null
|
||||
domain-agnostic-software: null
|
||||
skillLevel: intermediate
|
||||
accessType: download
|
||||
url: https://github.com/jamf/aftermath/
|
||||
projectUrl: ''
|
||||
license: Apache 2.0
|
||||
knowledgebase: false
|
||||
tags:
|
||||
- macos
|
||||
- incident-response
|
||||
- triage
|
||||
- artifact-collection
|
||||
- rapid-response
|
||||
- jamf
|
||||
- enterprise
|
||||
- commandline
|
||||
- name: Regular Expressions (Regex)
|
||||
icon: 🔤
|
||||
type: concept
|
||||
description: >-
|
||||
Pattern matching language for searching, extracting, and manipulating
|
||||
text. Essential for log analysis, malware signature creation, and data
|
||||
extraction from unstructured sources. Forms the backbone of many forensic
|
||||
tools and custom scripts.
|
||||
domains:
|
||||
- incident-response
|
||||
- malware-analysis
|
||||
- network-forensics
|
||||
- fraud-investigation
|
||||
phases:
|
||||
- examination
|
||||
- analysis
|
||||
platforms: []
|
||||
related_concepts: null
|
||||
domain-agnostic-software: null
|
||||
skillLevel: intermediate
|
||||
accessType: null
|
||||
url: https://regexr.com/
|
||||
projectUrl: null
|
||||
license: null
|
||||
knowledgebase: true
|
||||
tags:
|
||||
- pattern-matching
|
||||
- text-processing
|
||||
- log-analysis
|
||||
- string-manipulation
|
||||
- search-algorithms
|
||||
- name: SQL Query Fundamentals
|
||||
icon: 🗃️
|
||||
type: concept
|
||||
description: >-
|
||||
Structured Query Language for database interrogation and analysis.
|
||||
Critical for examining application databases, SQLite artifacts from
|
||||
mobile devices, and browser history databases. Enables complex
|
||||
correlation and filtering of large datasets.
|
||||
domains:
|
||||
- incident-response
|
||||
- mobile-forensics
|
||||
- fraud-investigation
|
||||
- cloud-forensics
|
||||
phases:
|
||||
- examination
|
||||
- analysis
|
||||
platforms: []
|
||||
related_concepts: null
|
||||
domain-agnostic-software: null
|
||||
skillLevel: intermediate
|
||||
accessType: null
|
||||
url: https://www.w3schools.com/sql/
|
||||
projectUrl: null
|
||||
license: null
|
||||
knowledgebase: false
|
||||
tags:
|
||||
- database-analysis
|
||||
- query-language
|
||||
- data-correlation
|
||||
- mobile-artifacts
|
||||
- browser-forensics
|
||||
- name: Hash Functions & Digital Signatures
|
||||
icon: 🔐
|
||||
type: concept
|
||||
description: >-
|
||||
Cryptographic principles for data integrity verification and
|
||||
authentication. Fundamental for evidence preservation, malware
|
||||
identification, and establishing chain of custody. Understanding of MD5,
|
||||
SHA, and digital signature validation.
|
||||
domains:
|
||||
- incident-response
|
||||
- law-enforcement
|
||||
- malware-analysis
|
||||
- cloud-forensics
|
||||
phases:
|
||||
- data-collection
|
||||
- examination
|
||||
platforms: []
|
||||
related_concepts: null
|
||||
domain-agnostic-software: null
|
||||
skillLevel: advanced
|
||||
accessType: null
|
||||
url: https://en.wikipedia.org/wiki/Cryptographic_hash_function
|
||||
projectUrl: null
|
||||
license: null
|
||||
knowledgebase: false
|
||||
tags:
|
||||
- cryptography
|
||||
- data-integrity
|
||||
- evidence-preservation
|
||||
- malware-identification
|
||||
- chain-of-custody
|
||||
domains:
|
||||
- id: incident-response
|
||||
name: Incident Response & Breach-Untersuchung
|
||||
- id: law-enforcement
|
||||
name: Strafverfolgung & Kriminalermittlung
|
||||
- id: malware-analysis
|
||||
name: Malware-Analyse & Reverse Engineering
|
||||
- id: fraud-investigation
|
||||
name: Betrugs- & Finanzkriminalität
|
||||
- id: network-forensics
|
||||
name: Netzwerk-Forensik & Traffic-Analyse
|
||||
- id: mobile-forensics
|
||||
name: Mobile Geräte & App-Forensik
|
||||
- id: cloud-forensics
|
||||
name: Cloud & Virtuelle Umgebungen
|
||||
- id: ics-forensics
|
||||
name: Industrielle Kontrollsysteme (ICS/SCADA)
|
||||
phases:
|
||||
- id: data-collection
|
||||
name: Datensammlung
|
||||
description: Imaging, Acquisition, Remote Collection Tools
|
||||
- id: examination
|
||||
name: Auswertung
|
||||
description: Parsing, Extraction, Initial Analysis Tools
|
||||
- id: analysis
|
||||
name: Analyse
|
||||
description: Deep Analysis, Correlation, Visualization Tools
|
||||
- id: reporting
|
||||
name: Bericht & Präsentation
|
||||
description: >-
|
||||
Documentation, Visualization, Presentation Tools (z.B. QGIS für Geodaten,
|
||||
Timeline-Tools)
|
||||
domain-agnostic-software:
|
||||
- id: collaboration-general
|
||||
name: Übergreifend & Kollaboration
|
||||
description: Cross-cutting tools and collaboration platforms
|
||||
- id: specific-os
|
||||
name: Betriebssysteme
|
||||
description: Operating Systems which focus on forensics
|
@ -93,15 +93,31 @@ const tools = data.tools;
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
// Extend Window interface for custom properties
|
||||
declare global {
|
||||
interface Window {
|
||||
toolsData: any[];
|
||||
showToolDetails: (toolName: string, modalType?: string) => void;
|
||||
hideToolDetails: (modalType?: string) => void;
|
||||
hideAllToolDetails: () => void;
|
||||
clearAllFilters?: () => void;
|
||||
restoreAIResults?: () => void;
|
||||
switchToAIView?: () => void;
|
||||
showShareDialog: (shareButton: HTMLElement) => void;
|
||||
navigateToGrid: (toolName: string) => void;
|
||||
navigateToMatrix: (toolName: string) => void;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle view changes and filtering
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toolsContainer = document.getElementById('tools-container');
|
||||
const toolsGrid = document.getElementById('tools-grid');
|
||||
const matrixContainer = document.getElementById('matrix-container');
|
||||
const aiInterface = document.getElementById('ai-interface');
|
||||
const filtersSection = document.getElementById('filters-section');
|
||||
const noResults = document.getElementById('no-results');
|
||||
const aiQueryBtn = document.getElementById('ai-query-btn');
|
||||
const toolsContainer = document.getElementById('tools-container') as HTMLElement;
|
||||
const toolsGrid = document.getElementById('tools-grid') as HTMLElement;
|
||||
const matrixContainer = document.getElementById('matrix-container') as HTMLElement;
|
||||
const aiInterface = document.getElementById('ai-interface') as HTMLElement;
|
||||
const filtersSection = document.getElementById('filters-section') as HTMLElement;
|
||||
const noResults = document.getElementById('no-results') as HTMLElement;
|
||||
const aiQueryBtn = document.getElementById('ai-query-btn') as HTMLButtonElement;
|
||||
|
||||
// Guard against null elements
|
||||
if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) {
|
||||
@ -109,12 +125,9 @@ const tools = data.tools;
|
||||
return;
|
||||
}
|
||||
|
||||
// Initial tools HTML
|
||||
const initialToolsHTML = toolsContainer.innerHTML;
|
||||
|
||||
// Simple sorting function - no external imports needed
|
||||
function sortTools(tools, sortBy = 'default') {
|
||||
const sorted = [...tools]; // Don't mutate original array
|
||||
// Simple sorting function
|
||||
function sortTools(tools: any[], sortBy = 'default') {
|
||||
const sorted = [...tools];
|
||||
|
||||
switch (sortBy) {
|
||||
case 'alphabetical':
|
||||
@ -122,16 +135,16 @@ const tools = data.tools;
|
||||
case 'difficulty':
|
||||
const difficultyOrder = { 'novice': 0, 'beginner': 1, 'intermediate': 2, 'advanced': 3, 'expert': 4 };
|
||||
return sorted.sort((a, b) =>
|
||||
(difficultyOrder[a.skillLevel] || 999) - (difficultyOrder[b.skillLevel] || 999)
|
||||
(difficultyOrder[a.skillLevel as keyof typeof difficultyOrder] || 999) - (difficultyOrder[b.skillLevel as keyof typeof difficultyOrder] || 999)
|
||||
);
|
||||
case 'type':
|
||||
const typeOrder = { 'concept': 0, 'method': 1, 'software': 2 };
|
||||
return sorted.sort((a, b) =>
|
||||
(typeOrder[a.type] || 999) - (typeOrder[b.type] || 999)
|
||||
(typeOrder[a.type as keyof typeof typeOrder] || 999) - (typeOrder[b.type as keyof typeof typeOrder] || 999)
|
||||
);
|
||||
case 'default':
|
||||
default:
|
||||
return sorted; // No sorting - embrace the entropy
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,31 +172,21 @@ const tools = data.tools;
|
||||
const authStatus = await checkAuthentication();
|
||||
|
||||
if (authStatus.authRequired && !authStatus.authenticated) {
|
||||
// Redirect to login, then back to AI view
|
||||
const returnUrl = `${window.location.pathname}?view=ai`;
|
||||
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
|
||||
} else {
|
||||
// Switch to AI view directly
|
||||
switchToView('ai');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check URL parameters on page load for view switching
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const viewParam = urlParams.get('view');
|
||||
if (viewParam === 'ai') {
|
||||
// User was redirected after authentication, switch to AI view
|
||||
switchToView('ai');
|
||||
}
|
||||
|
||||
// Function to switch between different views
|
||||
function switchToView(view) {
|
||||
// Hide all views first (using non-null assertions since we've already checked)
|
||||
toolsGrid!.style.display = 'none';
|
||||
matrixContainer!.style.display = 'none';
|
||||
aiInterface!.style.display = 'none';
|
||||
filtersSection!.style.display = 'none';
|
||||
function switchToView(view: string) {
|
||||
// Hide all views first
|
||||
toolsGrid.style.display = 'none';
|
||||
matrixContainer.style.display = 'none';
|
||||
aiInterface.style.display = 'none';
|
||||
filtersSection.style.display = 'none';
|
||||
|
||||
// Update view toggle buttons
|
||||
const viewToggles = document.querySelectorAll('.view-toggle');
|
||||
@ -191,128 +194,29 @@ const tools = data.tools;
|
||||
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
|
||||
});
|
||||
|
||||
// Show appropriate view
|
||||
// Show appropriate view and manage filter visibility
|
||||
switch (view) {
|
||||
case 'ai':
|
||||
aiInterface!.style.display = 'block';
|
||||
// Keep filters visible in AI mode for view switching
|
||||
filtersSection!.style.display = 'block';
|
||||
|
||||
// Hide filter controls in AI mode - AGGRESSIVE APPROACH
|
||||
const domainPhaseContainer = document.querySelector('.domain-phase-container') as HTMLElement;
|
||||
const searchInput = document.getElementById('search-input') as HTMLElement;
|
||||
const tagCloud = document.querySelector('.tag-cloud') as HTMLElement;
|
||||
// Hide all checkbox wrappers
|
||||
const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper');
|
||||
// Hide the "Nach Tags filtern" header and button
|
||||
const tagHeader = document.querySelector('.tag-header') as HTMLElement;
|
||||
// Hide any elements containing "Proprietäre Software" or "Nach Tags filtern"
|
||||
const filterLabels = document.querySelectorAll('label, .tag-header, h4, h3');
|
||||
// Hide ALL input elements in the filters section (more aggressive)
|
||||
const allInputs = filtersSection!.querySelectorAll('input, select, textarea');
|
||||
|
||||
|
||||
if (domainPhaseContainer) domainPhaseContainer.style.display = 'none';
|
||||
if (searchInput) searchInput.style.display = 'none';
|
||||
if (tagCloud) tagCloud.style.display = 'none';
|
||||
if (tagHeader) tagHeader.style.display = 'none';
|
||||
|
||||
// Hide ALL inputs in the filters section
|
||||
allInputs.forEach(input => {
|
||||
(input as HTMLElement).style.display = 'none';
|
||||
});
|
||||
|
||||
checkboxWrappers.forEach(wrapper => {
|
||||
(wrapper as HTMLElement).style.display = 'none';
|
||||
});
|
||||
|
||||
// Hide specific filter section elements by text content
|
||||
filterLabels.forEach(element => {
|
||||
const text = element.textContent?.toLowerCase() || '';
|
||||
if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
|
||||
(element as HTMLElement).style.display = 'none';
|
||||
aiInterface.style.display = 'block';
|
||||
filtersSection.style.display = 'block';
|
||||
hideFilterControls();
|
||||
if (window.restoreAIResults) {
|
||||
window.restoreAIResults();
|
||||
}
|
||||
});
|
||||
|
||||
// Restore previous AI results if they exist
|
||||
if ((window as any).restoreAIResults) {
|
||||
(window as any).restoreAIResults();
|
||||
}
|
||||
// Focus on the input
|
||||
const aiInput = document.getElementById('ai-query-input');
|
||||
const aiInput = document.getElementById('ai-query-input') as HTMLTextAreaElement;
|
||||
if (aiInput) {
|
||||
setTimeout(() => aiInput.focus(), 100);
|
||||
}
|
||||
break;
|
||||
case 'matrix':
|
||||
matrixContainer!.style.display = 'block';
|
||||
filtersSection!.style.display = 'block';
|
||||
|
||||
// Show filter controls in matrix mode
|
||||
const domainPhaseContainerMatrix = document.querySelector('.domain-phase-container') as HTMLElement;
|
||||
const searchInputMatrix = document.getElementById('search-input') as HTMLElement;
|
||||
const tagCloudMatrix = document.querySelector('.tag-cloud') as HTMLElement;
|
||||
const checkboxWrappersMatrix = document.querySelectorAll('.checkbox-wrapper');
|
||||
const tagHeaderMatrix = document.querySelector('.tag-header') as HTMLElement;
|
||||
const filterLabelsMatrix = document.querySelectorAll('label, .tag-header, h4, h3');
|
||||
const allInputsMatrix = filtersSection!.querySelectorAll('input, select, textarea');
|
||||
|
||||
if (domainPhaseContainerMatrix) domainPhaseContainerMatrix.style.display = 'grid';
|
||||
if (searchInputMatrix) searchInputMatrix.style.display = 'block';
|
||||
if (tagCloudMatrix) tagCloudMatrix.style.display = 'flex';
|
||||
if (tagHeaderMatrix) tagHeaderMatrix.style.display = 'flex';
|
||||
|
||||
// Restore ALL inputs in the filters section
|
||||
allInputsMatrix.forEach(input => {
|
||||
(input as HTMLElement).style.display = 'block';
|
||||
});
|
||||
|
||||
checkboxWrappersMatrix.forEach(wrapper => {
|
||||
(wrapper as HTMLElement).style.display = 'flex';
|
||||
});
|
||||
|
||||
// Restore filter section elements
|
||||
filterLabelsMatrix.forEach(element => {
|
||||
const text = element.textContent?.toLowerCase() || '';
|
||||
if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
|
||||
(element as HTMLElement).style.display = 'block';
|
||||
}
|
||||
});
|
||||
matrixContainer.style.display = 'block';
|
||||
filtersSection.style.display = 'block';
|
||||
showFilterControls();
|
||||
break;
|
||||
default: // grid
|
||||
toolsGrid!.style.display = 'block';
|
||||
filtersSection!.style.display = 'block';
|
||||
|
||||
// Show filter controls in grid mode
|
||||
const domainPhaseContainerGrid = document.querySelector('.domain-phase-container') as HTMLElement;
|
||||
const searchInputGrid = document.getElementById('search-input') as HTMLElement;
|
||||
const tagCloudGrid = document.querySelector('.tag-cloud') as HTMLElement;
|
||||
const checkboxWrappersGrid = document.querySelectorAll('.checkbox-wrapper');
|
||||
const tagHeaderGrid = document.querySelector('.tag-header') as HTMLElement;
|
||||
const filterLabelsGrid = document.querySelectorAll('label, .tag-header, h4, h3');
|
||||
const allInputsGrid = filtersSection!.querySelectorAll('input, select, textarea');
|
||||
|
||||
if (domainPhaseContainerGrid) domainPhaseContainerGrid.style.display = 'grid';
|
||||
if (searchInputGrid) searchInputGrid.style.display = 'block';
|
||||
if (tagCloudGrid) tagCloudGrid.style.display = 'flex';
|
||||
if (tagHeaderGrid) tagHeaderGrid.style.display = 'flex';
|
||||
|
||||
// Restore ALL inputs in the filters section
|
||||
allInputsGrid.forEach(input => {
|
||||
(input as HTMLElement).style.display = 'block';
|
||||
});
|
||||
|
||||
checkboxWrappersGrid.forEach(wrapper => {
|
||||
(wrapper as HTMLElement).style.display = 'flex';
|
||||
});
|
||||
|
||||
// Restore filter section elements
|
||||
filterLabelsGrid.forEach(element => {
|
||||
const text = element.textContent?.toLowerCase() || '';
|
||||
if (text.includes('proprietäre') || text.includes('tags filtern') || text.includes('nach tags') || text.includes('suchen') || text.includes('search')) {
|
||||
(element as HTMLElement).style.display = 'block';
|
||||
}
|
||||
});
|
||||
toolsGrid.style.display = 'block';
|
||||
filtersSection.style.display = 'block';
|
||||
showFilterControls();
|
||||
break;
|
||||
}
|
||||
|
||||
@ -322,14 +226,204 @@ const tools = data.tools;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for filter control visibility
|
||||
function hideFilterControls() {
|
||||
const elements = [
|
||||
'.domain-phase-container',
|
||||
'#search-input',
|
||||
'.tag-cloud',
|
||||
'.tag-header',
|
||||
'.checkbox-wrapper'
|
||||
];
|
||||
|
||||
elements.forEach(selector => {
|
||||
const element = document.querySelector(selector) as HTMLElement;
|
||||
if (element) element.style.display = 'none';
|
||||
});
|
||||
|
||||
const allInputs = filtersSection.querySelectorAll('input, select, textarea');
|
||||
allInputs.forEach(input => (input as HTMLElement).style.display = 'none');
|
||||
}
|
||||
|
||||
function showFilterControls() {
|
||||
const domainPhaseContainer = document.querySelector('.domain-phase-container') as HTMLElement;
|
||||
const searchInput = document.getElementById('search-input') as HTMLElement;
|
||||
const tagCloud = document.querySelector('.tag-cloud') as HTMLElement;
|
||||
const tagHeader = document.querySelector('.tag-header') as HTMLElement;
|
||||
const checkboxWrappers = document.querySelectorAll('.checkbox-wrapper');
|
||||
const allInputs = filtersSection.querySelectorAll('input, select, textarea');
|
||||
|
||||
if (domainPhaseContainer) domainPhaseContainer.style.display = 'grid';
|
||||
if (searchInput) searchInput.style.display = 'block';
|
||||
if (tagCloud) tagCloud.style.display = 'flex';
|
||||
if (tagHeader) tagHeader.style.display = 'flex';
|
||||
|
||||
allInputs.forEach(input => (input as HTMLElement).style.display = 'block');
|
||||
checkboxWrappers.forEach(wrapper => (wrapper as HTMLElement).style.display = 'flex');
|
||||
}
|
||||
|
||||
// Create tool slug from name
|
||||
function createToolSlug(toolName: string): string {
|
||||
return toolName.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
// Find tool by name or slug
|
||||
function findTool(identifier: string) {
|
||||
return window.toolsData.find(tool =>
|
||||
tool.name === identifier ||
|
||||
createToolSlug(tool.name) === identifier.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
// Navigation functions for sharing
|
||||
window.navigateToGrid = function(toolName: string) {
|
||||
console.log('Navigating to grid for tool:', toolName);
|
||||
|
||||
// Switch to grid view first
|
||||
switchToView('grid');
|
||||
|
||||
// Wait for view switch, then find and scroll to tool
|
||||
setTimeout(() => {
|
||||
// Clear any filters first
|
||||
if (window.clearAllFilters) {
|
||||
window.clearAllFilters();
|
||||
}
|
||||
|
||||
// Wait for filters to clear and re-render
|
||||
setTimeout(() => {
|
||||
const toolCards = document.querySelectorAll('.tool-card');
|
||||
let targetCard: Element | null = null;
|
||||
|
||||
toolCards.forEach(card => {
|
||||
const cardTitle = card.querySelector('h3');
|
||||
if (cardTitle) {
|
||||
// Clean title text (remove icons and extra spaces)
|
||||
const titleText = cardTitle.textContent?.replace(/[^\w\s\-\.]/g, '').trim();
|
||||
if (titleText === toolName) {
|
||||
targetCard = card;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (targetCard) {
|
||||
console.log('Found target card, scrolling...');
|
||||
// Cast to Element to fix TypeScript issue
|
||||
(targetCard as Element).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
(targetCard as HTMLElement).style.animation = 'highlight-flash 2s ease-out';
|
||||
|
||||
setTimeout(() => {
|
||||
if (targetCard) {
|
||||
(targetCard as HTMLElement).style.animation = '';
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
console.warn('Tool card not found in grid:', toolName);
|
||||
}
|
||||
}, 300);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
window.navigateToMatrix = function(toolName: string) {
|
||||
console.log('Navigating to matrix for tool:', toolName);
|
||||
|
||||
// Switch to matrix view
|
||||
switchToView('matrix');
|
||||
|
||||
// Wait for view switch and matrix to render
|
||||
setTimeout(() => {
|
||||
const toolChips = document.querySelectorAll('.tool-chip');
|
||||
let firstMatch: Element | null = null;
|
||||
let matchCount = 0;
|
||||
|
||||
toolChips.forEach(chip => {
|
||||
// Clean the chip text (remove emoji and extra spaces)
|
||||
const chipText = chip.textContent?.replace(/📖/g, '').replace(/[^\w\s\-\.]/g, '').trim();
|
||||
if (chipText === toolName) {
|
||||
// Highlight this occurrence
|
||||
(chip as HTMLElement).style.animation = 'highlight-flash 2s ease-out';
|
||||
matchCount++;
|
||||
|
||||
// Remember the first match for scrolling
|
||||
if (!firstMatch) {
|
||||
firstMatch = chip;
|
||||
}
|
||||
|
||||
// Clean up animation after it completes
|
||||
setTimeout(() => {
|
||||
(chip as HTMLElement).style.animation = '';
|
||||
}, 8000);
|
||||
}
|
||||
});
|
||||
|
||||
if (firstMatch) {
|
||||
console.log(`Found ${matchCount} occurrences of tool, highlighting all and scrolling to first`);
|
||||
// Cast to Element to fix TypeScript issue
|
||||
(firstMatch as Element).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
} else {
|
||||
console.warn('Tool chip not found in matrix:', toolName);
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// Handle URL parameters on page load
|
||||
function handleSharedURL() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const toolParam = urlParams.get('tool');
|
||||
const viewParam = urlParams.get('view');
|
||||
const modalParam = urlParams.get('modal');
|
||||
|
||||
if (!toolParam) {
|
||||
// Check for AI view parameter
|
||||
if (viewParam === 'ai') {
|
||||
switchToView('ai');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the tool by name or slug
|
||||
const tool = findTool(toolParam);
|
||||
if (!tool) {
|
||||
console.warn('Shared tool not found:', toolParam);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear URL parameters to avoid re-triggering
|
||||
const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
|
||||
window.history.replaceState({}, document.title, cleanUrl);
|
||||
|
||||
// Handle different view types
|
||||
setTimeout(() => {
|
||||
switch (viewParam) {
|
||||
case 'grid':
|
||||
window.navigateToGrid(tool.name);
|
||||
break;
|
||||
case 'matrix':
|
||||
window.navigateToMatrix(tool.name);
|
||||
break;
|
||||
case 'modal':
|
||||
if (modalParam === 'secondary') {
|
||||
window.showToolDetails(tool.name, 'secondary');
|
||||
} else {
|
||||
window.showToolDetails(tool.name, 'primary');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
window.navigateToGrid(tool.name);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Handle filtered results
|
||||
window.addEventListener('toolsFiltered', (event: Event) => {
|
||||
window.addEventListener('toolsFiltered', (event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
const filtered = customEvent.detail;
|
||||
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
|
||||
|
||||
if (currentView === 'matrix' || currentView === 'ai') {
|
||||
// Matrix and AI views handle their own rendering
|
||||
return;
|
||||
}
|
||||
|
||||
@ -341,11 +435,8 @@ const tools = data.tools;
|
||||
} else {
|
||||
noResults.style.display = 'none';
|
||||
|
||||
// Apply sorting here - single place for all sorting logic
|
||||
const currentSortOption = 'default'; // Will be dynamic later
|
||||
const sortedTools = sortTools(filtered, currentSortOption);
|
||||
const sortedTools = sortTools(filtered, 'default');
|
||||
|
||||
// Render sorted tools
|
||||
sortedTools.forEach((tool: any) => {
|
||||
const toolCard = createToolCard(tool);
|
||||
toolsContainer.appendChild(toolCard);
|
||||
@ -354,16 +445,17 @@ const tools = data.tools;
|
||||
});
|
||||
|
||||
// Handle view changes
|
||||
window.addEventListener('viewChanged', (event: Event) => {
|
||||
window.addEventListener('viewChanged', (event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
const view = customEvent.detail;
|
||||
switchToView(view);
|
||||
});
|
||||
|
||||
// Make switchToView available globally for the AI button
|
||||
(window as any).switchToAIView = () => switchToView('ai');
|
||||
// Make switchToView available globally
|
||||
window.switchToAIView = () => switchToView('ai');
|
||||
|
||||
function createToolCard(tool) {
|
||||
// Tool card creation function
|
||||
function createToolCard(tool: any): HTMLElement {
|
||||
const isMethod = tool.type === 'method';
|
||||
const isConcept = tool.type === 'concept';
|
||||
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||
@ -380,8 +472,10 @@ function createToolCard(tool) {
|
||||
(tool.license !== 'Proprietary' ? 'card card-oss tool-card' : 'card tool-card');
|
||||
cardDiv.className = cardClass;
|
||||
cardDiv.style.cursor = 'pointer';
|
||||
cardDiv.onclick = () => (window as any).showToolDetails(tool.name);
|
||||
cardDiv.onclick = () => window.showToolDetails(tool.name);
|
||||
|
||||
// Create tool slug for share button
|
||||
const toolSlug = createToolSlug(tool.name);
|
||||
|
||||
cardDiv.innerHTML = `
|
||||
<div class="tool-card-header">
|
||||
@ -389,6 +483,21 @@ function createToolCard(tool) {
|
||||
<div class="tool-card-badges">
|
||||
${!isMethod && !isConcept && hasValidProjectUrl ? '<span class="badge badge-primary">CC24-Server</span>' : ''}
|
||||
${hasKnowledgebase ? '<span class="badge badge-error">📖</span>' : ''}
|
||||
<button class="share-btn share-btn--small"
|
||||
data-tool-name="${tool.name}"
|
||||
data-tool-slug="${toolSlug}"
|
||||
data-context="card"
|
||||
onclick="event.stopPropagation(); window.showShareDialog(this)"
|
||||
title="${tool.name} teilen"
|
||||
aria-label="${tool.name} teilen">
|
||||
<svg width="14" height="14" 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -430,7 +539,7 @@ function createToolCard(tool) {
|
||||
</div>
|
||||
|
||||
<div class="tool-tags-container">
|
||||
${(tool.tags || []).slice(0, 8).map(tag => `<span class="tag">${tag}</span>`).join('')}
|
||||
${(tool.tags || []).slice(0, 8).map((tag: string) => `<span class="tag">${tag}</span>`).join('')}
|
||||
</div>
|
||||
|
||||
<div class="tool-card-buttons" onclick="event.stopPropagation();">
|
||||
@ -461,5 +570,8 @@ function createToolCard(tool) {
|
||||
|
||||
return cardDiv;
|
||||
}
|
||||
|
||||
// Initialize URL handling
|
||||
handleSharedURL();
|
||||
});
|
||||
</script>
|
@ -240,6 +240,25 @@ nav {
|
||||
background-color: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
/* Icon Button */
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: var(--transition-fast);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
background-color: var(--color-accent);
|
||||
color: white;
|
||||
@ -392,6 +411,8 @@ input[type="checkbox"] {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.metadata-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -647,7 +668,7 @@ input[type="checkbox"] {
|
||||
padding: 2rem;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 100vh;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
box-shadow: var(--shadow-lg);
|
||||
@ -1197,6 +1218,13 @@ Collaboration Section Collapse */
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Enhanced highlight flash for different contexts */
|
||||
.tool-card.highlight-flash,
|
||||
.tool-chip.highlight-flash,
|
||||
.tool-recommendation.highlight-flash {
|
||||
animation: highlight-flash 2s ease-out;
|
||||
}
|
||||
|
||||
.pros-cons-section {
|
||||
animation: fadeIn 0.4s ease-in;
|
||||
}
|
||||
@ -1309,11 +1337,76 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes highlight-flash {
|
||||
0% { background-color: rgb(37 99 235 / 10%); }
|
||||
100% { background-color: transparent; }
|
||||
}
|
||||
/*Perfect! Here's the absolutely brutal, eye-melting version:*/
|
||||
|
||||
@keyframes highlight-flash {
|
||||
0% {
|
||||
background-color: rgb(57 255 20 / 60%);
|
||||
box-shadow: 0 0 0 8px rgb(255 20 147 / 50%), 0 0 20px rgb(57 255 20 / 80%);
|
||||
transform: scale(1.12) rotate(2deg);
|
||||
border: 3px solid rgb(255 255 0);
|
||||
}
|
||||
12.5% {
|
||||
background-color: rgb(255 20 147 / 70%);
|
||||
box-shadow: 0 0 0 15px rgb(0 191 255 / 60%), 0 0 30px rgb(255 20 147 / 90%);
|
||||
transform: scale(1.18) rotate(-3deg);
|
||||
border: 3px solid rgb(57 255 20);
|
||||
}
|
||||
25% {
|
||||
background-color: rgb(0 191 255 / 65%);
|
||||
box-shadow: 0 0 0 12px rgb(191 0 255 / 55%), 0 0 25px rgb(0 191 255 / 85%);
|
||||
transform: scale(1.15) rotate(1deg);
|
||||
border: 3px solid rgb(255 20 147);
|
||||
}
|
||||
37.5% {
|
||||
background-color: rgb(191 0 255 / 75%);
|
||||
box-shadow: 0 0 0 18px rgb(255 255 0 / 65%), 0 0 35px rgb(191 0 255 / 95%);
|
||||
transform: scale(1.20) rotate(-2deg);
|
||||
border: 3px solid rgb(0 191 255);
|
||||
}
|
||||
50% {
|
||||
background-color: rgb(255 255 0 / 80%);
|
||||
box-shadow: 0 0 0 10px rgb(57 255 20 / 70%), 0 0 40px rgb(255 255 0 / 100%);
|
||||
transform: scale(1.16) rotate(3deg);
|
||||
border: 3px solid rgb(191 0 255);
|
||||
}
|
||||
62.5% {
|
||||
background-color: rgb(255 69 0 / 70%);
|
||||
box-shadow: 0 0 0 16px rgb(255 20 147 / 60%), 0 0 30px rgb(255 69 0 / 90%);
|
||||
transform: scale(1.22) rotate(-1deg);
|
||||
border: 3px solid rgb(255 255 0);
|
||||
}
|
||||
75% {
|
||||
background-color: rgb(255 20 147 / 65%);
|
||||
box-shadow: 0 0 0 14px rgb(0 191 255 / 50%), 0 0 45px rgb(255 20 147 / 85%);
|
||||
transform: scale(1.14) rotate(2deg);
|
||||
border: 3px solid rgb(57 255 20);
|
||||
}
|
||||
87.5% {
|
||||
background-color: rgb(57 255 20 / 75%);
|
||||
box-shadow: 0 0 0 20px rgb(191 0 255 / 65%), 0 0 35px rgb(57 255 20 / 95%);
|
||||
transform: scale(1.25) rotate(-3deg);
|
||||
border: 3px solid rgb(255 69 0);
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
transform: scale(1) rotate(0deg);
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
/*
|
||||
This monstrosity includes:
|
||||
|
||||
Neon rainbow cycling: Bright green → Hot pink → Electric blue → Neon purple → Nuclear yellow → Orange-red
|
||||
Double shadows: Inner colored shadow + outer glow effect
|
||||
Aggressive scaling: Up to 1.25x size
|
||||
Rotation wobble: Cards wiggle back and forth
|
||||
Strobing borders: Bright colored borders that change with each keyframe
|
||||
8 keyframes: More frequent color/effect changes
|
||||
Higher opacity: More saturated colors (up to 100% on yellow)
|
||||
|
||||
This will literally assault the user's retinas. They'll need sunglasses to look at their shared tools! 🌈💥👁️🗨️*/
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
@ -1404,6 +1497,12 @@ footer {
|
||||
width: 90vw;
|
||||
max-height: 35vh;
|
||||
}
|
||||
.tool-details {
|
||||
max-height: 80vh;
|
||||
padding: 1.5rem;
|
||||
width: 95%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
@ -1488,6 +1587,11 @@ footer {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.375rem;
|
||||
}
|
||||
.tool-details {
|
||||
max-height: 75vh;
|
||||
padding: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1578,3 +1682,34 @@ footer {
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Share Button Styles */
|
||||
.share-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: var(--transition-fast);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.share-btn:hover {
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.share-btn--small {
|
||||
padding: 0.125rem;
|
||||
}
|
||||
|
||||
.share-btn--medium {
|
||||
padding: 0.375rem;
|
||||
}
|
||||
|
||||
.share-btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user