sharing mechanic with eye-watering animation

This commit is contained in:
overcuriousity 2025-07-21 22:31:43 +02:00
parent 6ae7f36660
commit a6b51187b7
6 changed files with 983 additions and 273 deletions

View File

@ -21,7 +21,6 @@ export interface Props {
const { tool } = Astro.props;
// Check types
const isMethod = tool.type === 'method';
const isConcept = tool.type === 'concept';

View File

@ -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
View 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

View File

@ -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 to switch between different views
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();">
@ -460,6 +569,9 @@ function createToolCard(tool) {
`;
return cardDiv;
}
}
// Initialize URL handling
handleSharedURL();
});
</script>

View File

@ -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;
}