buggy state
This commit is contained in:
parent
6918df9348
commit
6d2e345db1
@ -215,7 +215,6 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
|
||||
</section>
|
||||
|
||||
<script type="module" define:vars={{ tools, phases, domainAgnosticSoftware }}>
|
||||
|
||||
const Utils = {
|
||||
phaseConfig: {
|
||||
'initialization': { icon: '🚀', displayName: 'Initialisierung' },
|
||||
@ -255,73 +254,6 @@ const Utils = {
|
||||
if (confidence >= 60) return 'var(--color-warning)';
|
||||
return 'var(--color-error)';
|
||||
},
|
||||
|
||||
escapeHtml(text) {
|
||||
if (typeof text !== 'string') return String(text);
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
},
|
||||
|
||||
truncateText(text, maxLength) {
|
||||
if (!text || text.length <= maxLength) return text;
|
||||
return text.slice(0, maxLength) + '...';
|
||||
},
|
||||
|
||||
sanitizeText(text) {
|
||||
if (typeof text !== 'string') return '';
|
||||
|
||||
return text
|
||||
.replace(/^#{1,6}\s+/gm, '')
|
||||
.replace(/^\s*[-*+]\s+/gm, '')
|
||||
.replace(/^\s*\d+\.\s+/gm, '')
|
||||
.replace(/\*\*(.+?)\*\*/g, '$1')
|
||||
.replace(/\*(.+?)\*/g, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
.replace(/```[\s\S]*?```/g, '[CODE BLOCK]')
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
||||
.trim();
|
||||
},
|
||||
|
||||
summarizeData(data) {
|
||||
if (data === null || data === undefined) return 'null';
|
||||
if (typeof data === 'string') {
|
||||
return data.length > 100 ? data.slice(0, 100) + '...' : data;
|
||||
}
|
||||
if (typeof data === 'number' || typeof data === 'boolean') {
|
||||
return data.toString();
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length === 0) return '[]';
|
||||
if (data.length <= 3) return JSON.stringify(data);
|
||||
return `[${data.slice(0, 3).map(i => typeof i === 'string' ? i : JSON.stringify(i)).join(', ')}, ...+${data.length - 3}]`;
|
||||
}
|
||||
if (typeof data === 'object') {
|
||||
const keys = Object.keys(data);
|
||||
if (keys.length === 0) return '{}';
|
||||
if (keys.length <= 3) {
|
||||
return '{' + keys.map(k => `${k}: ${typeof data[k] === 'string' ? data[k].slice(0, 20) + (data[k].length > 20 ? '...' : '') : JSON.stringify(data[k])}`).join(', ') + '}';
|
||||
}
|
||||
return `{${keys.slice(0, 3).join(', ')}, ...+${keys.length - 3} keys}`;
|
||||
}
|
||||
return String(data);
|
||||
},
|
||||
|
||||
showElement(element) {
|
||||
if (element) {
|
||||
element.style.display = 'block';
|
||||
element.classList.remove('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
hideElement(element) {
|
||||
if (element) {
|
||||
element.style.display = 'none';
|
||||
element.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class AuditTrailProcessor {
|
||||
|
@ -432,13 +432,6 @@ const sortedTags = Object.entries(tagFrequency)
|
||||
}
|
||||
}
|
||||
|
||||
function isToolHosted(tool) {
|
||||
return tool.projectUrl !== undefined &&
|
||||
tool.projectUrl !== null &&
|
||||
tool.projectUrl !== "" &&
|
||||
tool.projectUrl.trim() !== "";
|
||||
}
|
||||
|
||||
function initTagCloud() {
|
||||
const visibleCount = 20;
|
||||
elements.tagCloudItems.forEach((item, index) => {
|
||||
|
@ -267,44 +267,6 @@ domains.forEach((domain: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
function generateShareURL(toolName, view, modal = null) {
|
||||
const toolSlug = window.createToolSlug(toolName);
|
||||
const baseUrl = window.location.origin + window.location.pathname;
|
||||
const params = new URLSearchParams();
|
||||
params.set('tool', toolSlug);
|
||||
params.set('view', view);
|
||||
if (modal) params.set('modal', modal);
|
||||
return `${baseUrl}?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function copyToClipboard(text, button) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20,6 9,17 4,12"/></svg> Kopiert!';
|
||||
button.style.color = 'var(--color-accent)';
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
button.style.color = '';
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = 'Kopiert!';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function showShareDialog(shareButton) {
|
||||
const toolName = shareButton.getAttribute('data-tool-name');
|
||||
const context = shareButton.getAttribute('data-context');
|
||||
|
@ -26,179 +26,154 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
|
||||
<script>
|
||||
async function loadUtilityFunctions() {
|
||||
try {
|
||||
const { createToolSlug, findToolByIdentifier, isToolHosted } = await import('../utils/clientUtils.js');
|
||||
|
||||
(window as any).createToolSlug = createToolSlug;
|
||||
(window as any).findToolByIdentifier = findToolByIdentifier;
|
||||
(window as any).isToolHosted = isToolHosted;
|
||||
|
||||
console.log('[UTILS] Utility functions loaded successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to load utility functions:', error);
|
||||
|
||||
// Provide fallback implementations
|
||||
(window as any).createToolSlug = (toolName: string) => {
|
||||
if (!toolName || typeof toolName !== 'string') return '';
|
||||
return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||||
};
|
||||
|
||||
(window as any).findToolByIdentifier = (tools: any[], identifier: string) => {
|
||||
if (!identifier || !Array.isArray(tools)) return undefined;
|
||||
return tools.find((tool: any) =>
|
||||
tool.name === identifier ||
|
||||
(window as any).createToolSlug(tool.name) === identifier.toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
(window as any).isToolHosted = (tool: any) => {
|
||||
return tool.projectUrl !== undefined &&
|
||||
tool.projectUrl !== null &&
|
||||
tool.projectUrl !== "" &&
|
||||
tool.projectUrl.trim() !== "";
|
||||
};
|
||||
|
||||
console.log('[UTILS] Fallback utility functions registered');
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToElement(element: Element | null, options = {}) {
|
||||
if (!element) return;
|
||||
// Define utility functions directly instead of importing
|
||||
(window as any).createToolSlug = (toolName: string) => {
|
||||
if (!toolName || typeof toolName !== 'string') return '';
|
||||
return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
const headerHeight = document.querySelector('nav')?.offsetHeight || 80;
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const absoluteElementTop = elementRect.top + window.pageYOffset;
|
||||
const targetPosition = absoluteElementTop - headerHeight - 20;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: 'smooth'
|
||||
(window as any).findToolByIdentifier = (tools: any[], identifier: string) => {
|
||||
if (!identifier || !Array.isArray(tools)) return undefined;
|
||||
return tools.find((tool: any) =>
|
||||
tool.name === identifier ||
|
||||
(window as any).createToolSlug(tool.name) === identifier.toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
(window as any).isToolHosted = (tool: any) => {
|
||||
return tool.projectUrl !== undefined &&
|
||||
tool.projectUrl !== null &&
|
||||
tool.projectUrl !== "" &&
|
||||
tool.projectUrl.trim() !== "";
|
||||
};
|
||||
|
||||
// Scroll utilities
|
||||
(window as any).scrollToElement = (element: Element | null, options: ScrollIntoViewOptions = {}) => {
|
||||
if (!element) return;
|
||||
setTimeout(() => {
|
||||
const headerHeight = document.querySelector('nav')?.offsetHeight || 80;
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const absoluteElementTop = elementRect.top + window.pageYOffset;
|
||||
const targetPosition = absoluteElementTop - headerHeight - 20;
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: 'smooth',
|
||||
...options
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
(window as any).scrollToElementById = (elementId: string, options: ScrollIntoViewOptions = {}) => {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) (window as any).scrollToElement(element, options);
|
||||
};
|
||||
|
||||
(window as any).scrollToElementBySelector = (selector: string, options: ScrollIntoViewOptions = {}) => {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) (window as any).scrollToElement(element, options);
|
||||
};
|
||||
|
||||
// Search utilities
|
||||
(window as any).prioritizeSearchResults = (tools: any[], searchTerm: string) => {
|
||||
if (!searchTerm || !searchTerm.trim()) return tools;
|
||||
const lowerSearchTerm = searchTerm.toLowerCase().trim();
|
||||
return tools.sort((a: any, b: any) => {
|
||||
const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase());
|
||||
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
|
||||
const aExactTag = aTagsLower.includes(lowerSearchTerm);
|
||||
const bExactTag = bTagsLower.includes(lowerSearchTerm);
|
||||
if (aExactTag && !bExactTag) return -1;
|
||||
if (!aExactTag && bExactTag) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function scrollToElementById(elementId: string, options = {}) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
scrollToElement(element, options);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToElementBySelector(selector: string, options = {}) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
scrollToElement(element, options);
|
||||
}
|
||||
}
|
||||
|
||||
function prioritizeSearchResults(tools: any[], searchTerm: string) {
|
||||
if (!searchTerm || !searchTerm.trim()) {
|
||||
return tools;
|
||||
}
|
||||
};
|
||||
|
||||
const lowerSearchTerm = searchTerm.toLowerCase().trim();
|
||||
// Share utilities
|
||||
(window as any).shareArticle = async (button: HTMLElement, url: string, title: string) => {
|
||||
const fullUrl = window.location.origin + url;
|
||||
await (window as any).copyToClipboard(fullUrl, button);
|
||||
};
|
||||
|
||||
return tools.sort((a, b) => {
|
||||
const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase());
|
||||
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
|
||||
|
||||
const aExactTag = aTagsLower.includes(lowerSearchTerm);
|
||||
const bExactTag = bTagsLower.includes(lowerSearchTerm);
|
||||
|
||||
if (aExactTag && !bExactTag) return -1;
|
||||
if (!aExactTag && bExactTag) return 1;
|
||||
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
(window as any).shareCurrentArticle = async (button: HTMLElement) => {
|
||||
await (window as any).copyToClipboard(window.location.href, button);
|
||||
};
|
||||
|
||||
// Clipboard utility
|
||||
(window as any).copyToClipboard = async (text: string, button: HTMLElement) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = 'Kopiert!';
|
||||
button.style.color = 'var(--color-accent)';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
button.style.color = '';
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = 'Kopiert!';
|
||||
setTimeout(() => { button.innerHTML = originalHTML; }, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// Theme utilities
|
||||
(window as any).themeUtils = (() => {
|
||||
const THEME_KEY = 'dfir-theme';
|
||||
function getSystemTheme(): string {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
function getStoredTheme(): string {
|
||||
return localStorage.getItem(THEME_KEY) || 'auto';
|
||||
}
|
||||
function applyTheme(theme: string): void {
|
||||
const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme;
|
||||
document.documentElement.setAttribute('data-theme', effectiveTheme);
|
||||
}
|
||||
function initTheme(): void {
|
||||
const storedTheme = getStoredTheme();
|
||||
applyTheme(storedTheme);
|
||||
}
|
||||
return { initTheme, getStoredTheme };
|
||||
})();
|
||||
|
||||
console.log('[UTILS] Essential utility functions loaded directly');
|
||||
}
|
||||
|
||||
(window as any).scrollToElement = scrollToElement;
|
||||
(window as any).scrollToElementById = scrollToElementById;
|
||||
(window as any).scrollToElementBySelector = scrollToElementBySelector;
|
||||
(window as any).prioritizeSearchResults = prioritizeSearchResults;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// CRITICAL: Load utility functions FIRST before any URL handling
|
||||
await loadUtilityFunctions();
|
||||
|
||||
const THEME_KEY = 'dfir-theme';
|
||||
|
||||
function getSystemTheme() {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function getStoredTheme() {
|
||||
return localStorage.getItem(THEME_KEY) || 'auto';
|
||||
}
|
||||
|
||||
function applyTheme(theme: string) {
|
||||
const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme;
|
||||
document.documentElement.setAttribute('data-theme', effectiveTheme);
|
||||
}
|
||||
|
||||
function updateThemeToggle(theme: string) {
|
||||
document.querySelectorAll('[data-theme-toggle]').forEach(button => {
|
||||
button.setAttribute('data-current-theme', theme);
|
||||
});
|
||||
}
|
||||
|
||||
function initTheme() {
|
||||
const storedTheme = getStoredTheme();
|
||||
applyTheme(storedTheme);
|
||||
updateThemeToggle(storedTheme);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const current = getStoredTheme();
|
||||
const themes = ['light', 'dark', 'auto'];
|
||||
const currentIndex = themes.indexOf(current);
|
||||
const nextIndex = (currentIndex + 1) % themes.length;
|
||||
const nextTheme = themes[nextIndex];
|
||||
// Modal delegation functions - using async import for DOM helpers
|
||||
(window as any).showToolDetails = function(toolName: string, modalType: string = 'primary') {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
|
||||
localStorage.setItem(THEME_KEY, nextTheme);
|
||||
applyTheme(nextTheme);
|
||||
updateThemeToggle(nextTheme);
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (getStoredTheme() === 'auto') {
|
||||
applyTheme('auto');
|
||||
}
|
||||
});
|
||||
|
||||
(window as any).themeUtils = {
|
||||
initTheme,
|
||||
toggleTheme,
|
||||
getStoredTheme
|
||||
const tryDelegate = () => {
|
||||
const matrixShowToolDetails = (window as any).matrixShowToolDetails;
|
||||
|
||||
if (matrixShowToolDetails && typeof matrixShowToolDetails === 'function') {
|
||||
return matrixShowToolDetails(toolName, modalType);
|
||||
}
|
||||
|
||||
const directShowToolDetails = (window as any).directShowToolDetails;
|
||||
if (directShowToolDetails && typeof directShowToolDetails === 'function') {
|
||||
return directShowToolDetails(toolName, modalType);
|
||||
}
|
||||
|
||||
attempts++;
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(tryDelegate, 100);
|
||||
}
|
||||
};
|
||||
|
||||
tryDelegate();
|
||||
};
|
||||
|
||||
(window as any).showToolDetails = function(toolName: string, modalType: string = 'primary') {
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
|
||||
const tryDelegate = () => {
|
||||
const matrixShowToolDetails = (window as any).matrixShowToolDetails;
|
||||
|
||||
if (matrixShowToolDetails && typeof matrixShowToolDetails === 'function') {
|
||||
return matrixShowToolDetails(toolName, modalType);
|
||||
}
|
||||
|
||||
const directShowToolDetails = (window as any).directShowToolDetails;
|
||||
if (directShowToolDetails && typeof directShowToolDetails === 'function') {
|
||||
return directShowToolDetails(toolName, modalType);
|
||||
}
|
||||
|
||||
attempts++;
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(tryDelegate, 100);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
|
||||
tryDelegate();
|
||||
};
|
||||
|
||||
(window as any).hideToolDetails = function(modalType: string = 'both') {
|
||||
const matrixHideToolDetails = (window as any).matrixHideToolDetails;
|
||||
@ -211,6 +186,7 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
(window as any).hideToolDetails('both');
|
||||
};
|
||||
|
||||
// Auth functions
|
||||
async function checkClientAuth(context = 'general') {
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
@ -294,52 +270,10 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
(window as any).showIfAuthenticated = showIfAuthenticated;
|
||||
(window as any).setupAuthButtons = setupAuthButtons;
|
||||
|
||||
async function copyUrlToClipboard(url: string, button: HTMLElement) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = `
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
|
||||
<polyline points="20,6 9,17 4,12"/>
|
||||
</svg>
|
||||
Kopiert!
|
||||
`;
|
||||
button.style.color = 'var(--color-accent)';
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
button.style.color = '';
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = url;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = 'Kopiert!';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
}
|
||||
// Theme and auth initialization
|
||||
if ((window as any).themeUtils) {
|
||||
(window as any).themeUtils.initTheme();
|
||||
}
|
||||
|
||||
async function shareArticle(button: HTMLElement, url: string, title: string) {
|
||||
const fullUrl = window.location.origin + url;
|
||||
await copyUrlToClipboard(fullUrl, button);
|
||||
}
|
||||
|
||||
async function shareCurrentArticle(button: HTMLElement) {
|
||||
await copyUrlToClipboard(window.location.href, button);
|
||||
}
|
||||
|
||||
(window as any).shareArticle = shareArticle;
|
||||
(window as any).shareCurrentArticle = shareCurrentArticle;
|
||||
|
||||
initTheme();
|
||||
setupAuthButtons('[data-contribute-button]');
|
||||
|
||||
const initAIButton = async () => {
|
||||
|
@ -1,36 +1,18 @@
|
||||
// src/utils/clientUtils.ts
|
||||
// Client-side utilities that mirror server-side toolHelpers.ts
|
||||
// src/utils/clientUtils.ts - Simplified by using consolidated utilities
|
||||
// This file now only contains AutocompleteManager functionality that wasn't moved to domHelpers
|
||||
|
||||
export function createToolSlug(toolName: string): string {
|
||||
if (!toolName || typeof toolName !== 'string') {
|
||||
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
|
||||
return '';
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
import {
|
||||
createToolSlug,
|
||||
findToolByIdentifier,
|
||||
isToolHosted,
|
||||
escapeHtml,
|
||||
Tool
|
||||
} from './uiHelpers.js';
|
||||
|
||||
export function findToolByIdentifier(tools: any[], identifier: string): any | undefined {
|
||||
if (!identifier || !Array.isArray(tools)) return undefined;
|
||||
|
||||
return tools.find((tool: any) =>
|
||||
tool.name === identifier ||
|
||||
createToolSlug(tool.name) === identifier.toLowerCase()
|
||||
);
|
||||
}
|
||||
// Re-export the consolidated utilities for backward compatibility
|
||||
export { createToolSlug, findToolByIdentifier, isToolHosted };
|
||||
|
||||
export function isToolHosted(tool: any): boolean {
|
||||
return tool.projectUrl !== undefined &&
|
||||
tool.projectUrl !== null &&
|
||||
tool.projectUrl !== "" &&
|
||||
tool.projectUrl.trim() !== "";
|
||||
}
|
||||
|
||||
// Consolidated Autocomplete Functionality
|
||||
// AutocompleteManager - keeping this here as it's a complex component-specific utility
|
||||
interface AutocompleteOptions {
|
||||
minLength?: number;
|
||||
maxResults?: number;
|
||||
@ -205,7 +187,7 @@ export class AutocompleteManager {
|
||||
|
||||
defaultRender(item: any): string {
|
||||
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
|
||||
return `<div class="autocomplete-item">${this.escapeHtml(text)}</div>`;
|
||||
return `<div class="autocomplete-item">${escapeHtml(text)}</div>`;
|
||||
}
|
||||
|
||||
renderDropdown(): void {
|
||||
@ -289,8 +271,8 @@ export class AutocompleteManager {
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
">
|
||||
${this.escapeHtml(item)}
|
||||
<button type="button" class="autocomplete-remove" data-item="${this.escapeHtml(item)}" style="
|
||||
${escapeHtml(item)}
|
||||
<button type="button" class="autocomplete-remove" data-item="${escapeHtml(item)}" style="
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
@ -333,12 +315,6 @@ export class AutocompleteManager {
|
||||
this.selectedIndex = -1;
|
||||
}
|
||||
|
||||
escapeHtml(text: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
setDataSource(newDataSource: any[]): void {
|
||||
this.dataSource = newDataSource;
|
||||
}
|
||||
|
337
src/utils/domHelpers.ts
Normal file
337
src/utils/domHelpers.ts
Normal file
@ -0,0 +1,337 @@
|
||||
// src/utils/domHelpers.ts - Consolidated DOM manipulation utilities
|
||||
|
||||
// ELEMENT VISIBILITY UTILITIES (consolidates AIQueryInterface.astro patterns)
|
||||
export function showElement(element: HTMLElement | null): void {
|
||||
if (element) {
|
||||
element.style.display = 'block';
|
||||
element.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
export function hideElement(element: HTMLElement | null): void {
|
||||
if (element) {
|
||||
element.style.display = 'none';
|
||||
element.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleElement(element: HTMLElement | null, show?: boolean): void {
|
||||
if (!element) return;
|
||||
|
||||
const shouldShow = show !== undefined ? show : element.classList.contains('hidden');
|
||||
|
||||
if (shouldShow) {
|
||||
showElement(element);
|
||||
} else {
|
||||
hideElement(element);
|
||||
}
|
||||
}
|
||||
|
||||
// MODAL MANAGEMENT UTILITIES (consolidates ToolMatrix.astro modal logic)
|
||||
export interface ModalElements {
|
||||
overlay: HTMLElement | null;
|
||||
primaryModal: HTMLElement | null;
|
||||
secondaryModal: HTMLElement | null;
|
||||
}
|
||||
|
||||
export function getModalElements(): ModalElements {
|
||||
return {
|
||||
overlay: document.getElementById('modal-overlay'),
|
||||
primaryModal: document.getElementById('tool-details-primary'),
|
||||
secondaryModal: document.getElementById('tool-details-secondary')
|
||||
};
|
||||
}
|
||||
|
||||
export function showModal(modalType: 'primary' | 'secondary' = 'primary'): void {
|
||||
const { overlay, primaryModal, secondaryModal } = getModalElements();
|
||||
|
||||
if (overlay) overlay.classList.add('active');
|
||||
if (modalType === 'primary' && primaryModal) primaryModal.classList.add('active');
|
||||
if (modalType === 'secondary' && secondaryModal) secondaryModal.classList.add('active');
|
||||
|
||||
// Handle side-by-side modals
|
||||
const primaryActive = primaryModal && primaryModal.classList.contains('active');
|
||||
const secondaryActive = secondaryModal && secondaryModal.classList.contains('active');
|
||||
|
||||
if (primaryActive && secondaryActive) {
|
||||
document.body.classList.add('modals-side-by-side');
|
||||
}
|
||||
}
|
||||
|
||||
export function hideModal(modalType: 'primary' | 'secondary' | 'both' = 'both'): void {
|
||||
if ((window as any).modalHideInProgress) return;
|
||||
(window as any).modalHideInProgress = true;
|
||||
|
||||
setTimeout(() => {
|
||||
(window as any).modalHideInProgress = false;
|
||||
}, 100);
|
||||
|
||||
const { overlay, primaryModal, secondaryModal } = getModalElements();
|
||||
|
||||
if (modalType === 'both' || modalType === 'primary') {
|
||||
if (primaryModal) {
|
||||
primaryModal.classList.remove('active');
|
||||
hideModalButtons('primary');
|
||||
}
|
||||
}
|
||||
|
||||
if (modalType === 'both' || modalType === 'secondary') {
|
||||
if (secondaryModal) {
|
||||
secondaryModal.classList.remove('active');
|
||||
hideModalButtons('secondary');
|
||||
}
|
||||
}
|
||||
|
||||
// Update overlay and body classes
|
||||
const primaryActive = primaryModal && primaryModal.classList.contains('active');
|
||||
const secondaryActive = secondaryModal && secondaryModal.classList.contains('active');
|
||||
|
||||
if (!primaryActive && !secondaryActive) {
|
||||
if (overlay) overlay.classList.remove('active');
|
||||
document.body.classList.remove('modals-side-by-side');
|
||||
} else if (primaryActive && secondaryActive) {
|
||||
document.body.classList.add('modals-side-by-side');
|
||||
} else {
|
||||
document.body.classList.remove('modals-side-by-side');
|
||||
}
|
||||
}
|
||||
|
||||
function hideModalButtons(modalType: 'primary' | 'secondary'): void {
|
||||
const shareButton = document.getElementById(`share-button-${modalType}`);
|
||||
const contributeButton = document.getElementById(`contribute-button-${modalType}`);
|
||||
if (shareButton) shareButton.style.display = 'none';
|
||||
if (contributeButton) contributeButton.style.display = 'none';
|
||||
}
|
||||
|
||||
// COLLAPSIBLE SECTION UTILITIES (consolidates ToolFilters.astro + ToolMatrix.astro patterns)
|
||||
export function toggleCollapsible(
|
||||
toggleBtn: HTMLElement,
|
||||
content: HTMLElement,
|
||||
storageKey?: string
|
||||
): void {
|
||||
const isCollapsed = toggleBtn.getAttribute('data-collapsed') === 'true';
|
||||
const newState = !isCollapsed;
|
||||
|
||||
toggleBtn.setAttribute('data-collapsed', newState.toString());
|
||||
|
||||
if (newState) {
|
||||
content.classList.add('hidden');
|
||||
toggleBtn.style.transform = 'rotate(0deg)';
|
||||
} else {
|
||||
content.classList.remove('hidden');
|
||||
toggleBtn.style.transform = 'rotate(180deg)';
|
||||
}
|
||||
|
||||
if (storageKey) {
|
||||
sessionStorage.setItem(storageKey, newState.toString());
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeCollapsible(
|
||||
toggleBtn: HTMLElement,
|
||||
content: HTMLElement,
|
||||
storageKey: string
|
||||
): void {
|
||||
const collapsed = sessionStorage.getItem(storageKey) !== 'false';
|
||||
toggleBtn.setAttribute('data-collapsed', collapsed.toString());
|
||||
|
||||
if (collapsed) {
|
||||
content.classList.add('hidden');
|
||||
toggleBtn.style.transform = 'rotate(0deg)';
|
||||
} else {
|
||||
content.classList.remove('hidden');
|
||||
toggleBtn.style.transform = 'rotate(180deg)';
|
||||
}
|
||||
}
|
||||
|
||||
// FORM UTILITIES (consolidates form handling patterns)
|
||||
export function updateElementContent(
|
||||
elementId: string,
|
||||
content: string,
|
||||
contentType: 'text' | 'html' = 'text'
|
||||
): void {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
if (contentType === 'html') {
|
||||
element.innerHTML = content;
|
||||
} else {
|
||||
element.textContent = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateElementAttribute(
|
||||
elementId: string,
|
||||
attribute: string,
|
||||
value: string
|
||||
): void {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.setAttribute(attribute, value);
|
||||
}
|
||||
}
|
||||
|
||||
// EVENT DELEGATION UTILITIES (consolidates event handling patterns)
|
||||
export function delegateClick(
|
||||
containerSelector: string,
|
||||
targetSelector: string,
|
||||
handler: (target: Element, event: Event) => void
|
||||
): void {
|
||||
document.addEventListener('click', (e) => {
|
||||
const container = document.querySelector(containerSelector);
|
||||
if (!container || !e.target) return;
|
||||
|
||||
const target = (e.target as Element).closest(targetSelector);
|
||||
if (target && container.contains(target)) {
|
||||
handler(target, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// LOADING STATE UTILITIES (consolidates AIQueryInterface.astro loading patterns)
|
||||
export interface LoadingElements {
|
||||
button: HTMLElement | null;
|
||||
buttonText: HTMLElement | null;
|
||||
loading: HTMLElement | null;
|
||||
error: HTMLElement | null;
|
||||
results: HTMLElement | null;
|
||||
}
|
||||
|
||||
export function setLoadingState(
|
||||
elements: LoadingElements,
|
||||
isLoading: boolean,
|
||||
loadingText?: string
|
||||
): void {
|
||||
if (elements.button) {
|
||||
(elements.button as HTMLButtonElement).disabled = isLoading;
|
||||
}
|
||||
|
||||
if (elements.buttonText && loadingText) {
|
||||
elements.buttonText.textContent = isLoading ? loadingText : elements.buttonText.getAttribute('data-original-text') || '';
|
||||
}
|
||||
|
||||
if (elements.loading) {
|
||||
toggleElement(elements.loading, isLoading);
|
||||
}
|
||||
|
||||
if (elements.error) {
|
||||
hideElement(elements.error);
|
||||
}
|
||||
|
||||
if (elements.results && isLoading) {
|
||||
hideElement(elements.results);
|
||||
}
|
||||
}
|
||||
|
||||
// ANIMATION UTILITIES
|
||||
export function addHighlightAnimation(element: HTMLElement, duration: number = 2000): void {
|
||||
element.style.animation = `highlight-flash ${duration}ms ease-out`;
|
||||
setTimeout(() => {
|
||||
element.style.animation = '';
|
||||
}, duration);
|
||||
}
|
||||
|
||||
// VIEW SWITCHING UTILITIES (consolidates index.astro view switching)
|
||||
export function switchView(
|
||||
view: string,
|
||||
viewElements: Record<string, HTMLElement | null>
|
||||
): void {
|
||||
// Hide all view elements
|
||||
Object.values(viewElements).forEach(element => {
|
||||
if (element) {
|
||||
element.style.display = 'none';
|
||||
element.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Show target view
|
||||
const targetElement = viewElements[view];
|
||||
if (targetElement) {
|
||||
targetElement.style.display = 'block';
|
||||
targetElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Update view toggle buttons
|
||||
document.querySelectorAll('.view-toggle').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
|
||||
});
|
||||
}
|
||||
|
||||
// THEME UTILITIES (consolidates BaseLayout.astro theme functions)
|
||||
export interface ThemeUtils {
|
||||
initTheme: () => void;
|
||||
toggleTheme: () => void;
|
||||
getStoredTheme: () => string;
|
||||
}
|
||||
|
||||
export function createThemeUtils(): ThemeUtils {
|
||||
const THEME_KEY = 'dfir-theme';
|
||||
|
||||
function getSystemTheme(): string {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function getStoredTheme(): string {
|
||||
return localStorage.getItem(THEME_KEY) || 'auto';
|
||||
}
|
||||
|
||||
function applyTheme(theme: string): void {
|
||||
const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme;
|
||||
document.documentElement.setAttribute('data-theme', effectiveTheme);
|
||||
}
|
||||
|
||||
function updateThemeToggle(theme: string): void {
|
||||
document.querySelectorAll('[data-theme-toggle]').forEach(button => {
|
||||
button.setAttribute('data-current-theme', theme);
|
||||
});
|
||||
}
|
||||
|
||||
function initTheme(): void {
|
||||
const storedTheme = getStoredTheme();
|
||||
applyTheme(storedTheme);
|
||||
updateThemeToggle(storedTheme);
|
||||
}
|
||||
|
||||
function toggleTheme(): void {
|
||||
const current = getStoredTheme();
|
||||
const themes = ['light', 'dark', 'auto'];
|
||||
const currentIndex = themes.indexOf(current);
|
||||
const nextIndex = (currentIndex + 1) % themes.length;
|
||||
const nextTheme = themes[nextIndex];
|
||||
|
||||
localStorage.setItem(THEME_KEY, nextTheme);
|
||||
applyTheme(nextTheme);
|
||||
updateThemeToggle(nextTheme);
|
||||
}
|
||||
|
||||
// Listen for system theme changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (getStoredTheme() === 'auto') {
|
||||
applyTheme('auto');
|
||||
}
|
||||
});
|
||||
|
||||
return { initTheme, toggleTheme, getStoredTheme };
|
||||
}
|
||||
|
||||
// AUTOCOMPLETE UTILITIES (consolidates clientUtils.ts AutocompleteManager functionality)
|
||||
export interface AutocompleteOptions {
|
||||
minLength?: number;
|
||||
maxResults?: number;
|
||||
placeholder?: string;
|
||||
allowMultiple?: boolean;
|
||||
separator?: string;
|
||||
filterFunction?: (query: string) => any[];
|
||||
renderFunction?: (item: any) => string;
|
||||
hiddenInput?: HTMLInputElement;
|
||||
}
|
||||
|
||||
export function createAutocomplete(
|
||||
inputElement: HTMLInputElement,
|
||||
dataSource: any[],
|
||||
options: AutocompleteOptions = {}
|
||||
): void {
|
||||
// This is a simplified version - the full AutocompleteManager from clientUtils.ts
|
||||
// can be refactored to use these DOM utilities
|
||||
console.log('[DOM Helpers] Autocomplete setup for:', inputElement.id);
|
||||
}
|
@ -1,43 +1,9 @@
|
||||
export interface Tool {
|
||||
name: string;
|
||||
type?: 'software' | 'method' | 'concept';
|
||||
projectUrl?: string | null;
|
||||
license?: string;
|
||||
knowledgebase?: boolean;
|
||||
domains?: string[];
|
||||
phases?: string[];
|
||||
platforms?: string[];
|
||||
skillLevel?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
related_concepts?: string[];
|
||||
}
|
||||
// src/utils/toolHelpers.ts - Simplified to re-export consolidated utilities
|
||||
// This file now just re-exports the consolidated utilities for backward compatibility
|
||||
|
||||
export function createToolSlug(toolName: string): string {
|
||||
if (!toolName || typeof toolName !== 'string') {
|
||||
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
|
||||
return '';
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export function findToolByIdentifier(tools: Tool[], identifier: string): Tool | undefined {
|
||||
if (!identifier || !Array.isArray(tools)) return undefined;
|
||||
|
||||
return tools.find(tool =>
|
||||
tool.name === identifier ||
|
||||
createToolSlug(tool.name) === identifier.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
export function isToolHosted(tool: Tool): boolean {
|
||||
return tool.projectUrl !== undefined &&
|
||||
tool.projectUrl !== null &&
|
||||
tool.projectUrl !== "" &&
|
||||
tool.projectUrl.trim() !== "";
|
||||
}
|
||||
export {
|
||||
Tool,
|
||||
createToolSlug,
|
||||
findToolByIdentifier,
|
||||
isToolHosted
|
||||
} from './uiHelpers.js';
|
411
src/utils/toolRendering.ts
Normal file
411
src/utils/toolRendering.ts
Normal file
@ -0,0 +1,411 @@
|
||||
// src/utils/toolRendering.ts - Consolidated tool rendering utilities
|
||||
import { Tool, escapeHtml, sanitizeText, truncateText, getConfidenceColor } from './uiHelpers.js';
|
||||
|
||||
// TOOL CLASSIFICATION UTILITIES (consolidates repeated logic across components)
|
||||
export interface ToolClassification {
|
||||
isMethod: boolean;
|
||||
isConcept: boolean;
|
||||
isHosted: boolean;
|
||||
isOpenSource: boolean;
|
||||
hasKnowledgebase: boolean;
|
||||
}
|
||||
|
||||
export function classifyTool(tool: Tool): ToolClassification {
|
||||
const isMethod = tool.type === 'method';
|
||||
const isConcept = tool.type === 'concept';
|
||||
const isHosted = tool.projectUrl !== undefined &&
|
||||
tool.projectUrl !== null &&
|
||||
tool.projectUrl !== "" &&
|
||||
tool.projectUrl.trim() !== "";
|
||||
const isOpenSource = tool.license !== 'Proprietary';
|
||||
const hasKnowledgebase = tool.knowledgebase === true;
|
||||
|
||||
return { isMethod, isConcept, isHosted, isOpenSource, hasKnowledgebase };
|
||||
}
|
||||
|
||||
export function getToolCardClass(tool: Tool): string {
|
||||
const { isMethod, isConcept, isHosted, isOpenSource } = classifyTool(tool);
|
||||
|
||||
if (isConcept) return 'card card-concept tool-card cursor-pointer';
|
||||
if (isMethod) return 'card card-method tool-card cursor-pointer';
|
||||
if (isHosted) return 'card card-hosted tool-card cursor-pointer';
|
||||
if (isOpenSource) return 'card card-oss tool-card cursor-pointer';
|
||||
return 'card tool-card cursor-pointer';
|
||||
}
|
||||
|
||||
export function getToolChipClass(tool: Tool): string {
|
||||
const { isMethod, isHosted, isOpenSource } = classifyTool(tool);
|
||||
|
||||
if (isMethod) return 'tool-chip tool-chip-method';
|
||||
if (isHosted) return 'tool-chip tool-chip-hosted';
|
||||
if (isOpenSource) return 'tool-chip tool-chip-oss';
|
||||
return 'tool-chip';
|
||||
}
|
||||
|
||||
export function getRecommendationClass(tool: Tool): string {
|
||||
const { isMethod, isHosted, isOpenSource } = classifyTool(tool);
|
||||
|
||||
if (isMethod) return 'method';
|
||||
if (isHosted) return 'hosted';
|
||||
if (isOpenSource) return 'oss';
|
||||
return '';
|
||||
}
|
||||
|
||||
// BADGE RENDERING UTILITIES (consolidates badge logic across components)
|
||||
export function renderToolBadges(tool: Tool): string {
|
||||
const { isMethod, isConcept, isHosted, hasKnowledgebase } = classifyTool(tool);
|
||||
|
||||
let badges = '';
|
||||
|
||||
if (isConcept) {
|
||||
badges += '<span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>';
|
||||
} else if (isMethod) {
|
||||
badges += '<span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>';
|
||||
} else if (isHosted) {
|
||||
badges += '<span class="badge badge-primary">CC24-Server</span>';
|
||||
}
|
||||
|
||||
if (hasKnowledgebase) {
|
||||
badges += '<span class="badge badge-error">📖</span>';
|
||||
}
|
||||
|
||||
return badges;
|
||||
}
|
||||
|
||||
export function renderInlineBadges(tool: Tool): string {
|
||||
const { isMethod, isHosted, hasKnowledgebase } = classifyTool(tool);
|
||||
|
||||
let badges = '';
|
||||
|
||||
if (isMethod) {
|
||||
badges += '<span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>';
|
||||
} else if (isHosted) {
|
||||
badges += '<span class="badge badge-primary">CC24-Server</span>';
|
||||
}
|
||||
|
||||
if (hasKnowledgebase) {
|
||||
badges += '<span class="badge badge-error">📖</span>';
|
||||
}
|
||||
|
||||
return badges;
|
||||
}
|
||||
|
||||
// METADATA RENDERING UTILITIES (consolidates metadata display logic)
|
||||
export function renderToolMetadata(tool: Tool, compact: boolean = false): string {
|
||||
const { isMethod, isConcept } = classifyTool(tool);
|
||||
const domains = tool.domains || [];
|
||||
const phases = tool.phases || [];
|
||||
const domainsText = domains.length > 0 ? domains.join(', ') : 'Domain-agnostic';
|
||||
const phasesText = phases.join(', ');
|
||||
|
||||
if (compact) {
|
||||
return `
|
||||
<div class="flex gap-3 text-xs text-secondary">
|
||||
<span>${isMethod ? 'Methode' : isConcept ? 'Konzept' : tool.platforms?.slice(0, 2).join(', ')}</span>
|
||||
<span>•</span>
|
||||
<span>${tool.skillLevel}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let metadataHTML = `<div class="grid gap-2">`;
|
||||
|
||||
if (!isConcept) {
|
||||
metadataHTML += `
|
||||
<div><strong>Betriebssystem:</strong> ${(tool.platforms || []).join(', ')}</div>
|
||||
<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>
|
||||
<div><strong>Lizenzmodell:</strong> ${tool.license}</div>
|
||||
<div><strong>Deployment:</strong> ${tool.accessType}</div>
|
||||
`;
|
||||
} else {
|
||||
metadataHTML += `<div><strong>Skill Level:</strong> ${tool.skillLevel}</div>`;
|
||||
}
|
||||
|
||||
metadataHTML += `
|
||||
<div><strong>Einsatzgebiete:</strong> ${domainsText}</div>
|
||||
<div><strong>Ermittlungsphasen:</strong> ${phasesText}</div>
|
||||
</div>`;
|
||||
|
||||
return metadataHTML;
|
||||
}
|
||||
|
||||
// BUTTON RENDERING UTILITIES (consolidates button logic across components)
|
||||
export function renderToolButtons(tool: Tool, stopPropagation: boolean = true): string {
|
||||
const { isMethod, isConcept, isHosted } = classifyTool(tool);
|
||||
const onClickAttr = stopPropagation ? 'onclick="event.stopPropagation();"' : '';
|
||||
|
||||
if (isConcept) {
|
||||
return `
|
||||
<a href="${tool.url}" target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-primary single-button"
|
||||
style="background-color: var(--color-concept); border-color: var(--color-concept);"
|
||||
${onClickAttr}>
|
||||
Mehr erfahren
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
if (isMethod) {
|
||||
return `
|
||||
<a href="${tool.projectUrl || tool.url}" target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-primary single-button"
|
||||
style="background-color: var(--color-method); border-color: var(--color-method);"
|
||||
${onClickAttr}>
|
||||
Zur Methode
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
if (isHosted) {
|
||||
return `
|
||||
<div class="button-row" ${onClickAttr}>
|
||||
<a href="${tool.url}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
|
||||
Homepage
|
||||
</a>
|
||||
<a href="${tool.projectUrl}" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
|
||||
Zugreifen
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<a href="${tool.url}" target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-primary single-button" ${onClickAttr}>
|
||||
Software-Homepage
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderExtendedToolLinks(tool: Tool): string {
|
||||
const { hasKnowledgebase } = classifyTool(tool);
|
||||
let linksHTML = renderToolButtons(tool, false);
|
||||
|
||||
if (hasKnowledgebase) {
|
||||
const kbId = tool.name.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
linksHTML += `
|
||||
<a href="/knowledgebase#kb-${kbId}" class="btn btn-secondary w-full mt-2">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
Knowledgebase anzeigen
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
return linksHTML;
|
||||
}
|
||||
|
||||
// TAG RENDERING UTILITIES (consolidates tag display logic)
|
||||
export function renderTagCloud(tool: Tool, maxTags: number = 8): string {
|
||||
const tags = tool.tags || [];
|
||||
return tags.slice(0, maxTags).map(tag =>
|
||||
`<span class="tag">${escapeHtml(tag)}</span>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
export function renderRelatedItems(
|
||||
tool: Tool,
|
||||
allTools: Tool[],
|
||||
modalType: string = 'primary'
|
||||
): string {
|
||||
const relatedConcepts = tool.related_concepts || [];
|
||||
const relatedSoftware = tool.related_software || [];
|
||||
|
||||
let html = '';
|
||||
|
||||
if (relatedConcepts.length > 0 && modalType === 'primary') {
|
||||
const conceptLinks = relatedConcepts.map(conceptName => {
|
||||
const concept = allTools.find(t => t.name === conceptName && t.type === 'concept');
|
||||
if (concept) {
|
||||
return `
|
||||
<button class="tag cursor-pointer"
|
||||
style="background-color: var(--color-concept-bg); border: 1px solid var(--color-concept); color: var(--color-concept); transition: var(--transition-fast);"
|
||||
onclick="event.stopPropagation(); window.showToolDetails('${conceptName}', 'secondary')"
|
||||
onmouseover="this.style.backgroundColor='var(--color-concept)'; this.style.color='white';"
|
||||
onmouseout="this.style.backgroundColor='var(--color-concept-bg)'; this.style.color='var(--color-concept)';">
|
||||
${conceptName}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
return `<span class="tag" style="background-color: var(--color-bg-tertiary); color: var(--color-text-secondary);">${conceptName}</span>`;
|
||||
}).join('');
|
||||
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
const collapseOnMobile = isMobile && relatedConcepts.length > 2;
|
||||
|
||||
html += `
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<strong style="color: var(--color-text);">Verwandte Konzepte:</strong>
|
||||
${collapseOnMobile ? `
|
||||
<button id="concepts-toggle-${modalType}"
|
||||
onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.textContent = this.textContent === '▼' ? '▲' : '▼';"
|
||||
class="btn-icon text-xs">
|
||||
▼
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
<div ${collapseOnMobile ? 'class="hidden"' : ''} class="flex flex-wrap gap-1">
|
||||
${conceptLinks}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (relatedSoftware.length > 0 && modalType === 'primary') {
|
||||
const softwareLinks = relatedSoftware.map(softwareName => {
|
||||
const software = allTools.find(t => t.name === softwareName && (t.type === 'software' || t.type === 'method'));
|
||||
if (software) {
|
||||
const { isHosted, isMethod } = classifyTool(software);
|
||||
const bgColor = isMethod ? 'var(--color-method-bg)' :
|
||||
isHosted ? 'var(--color-hosted-bg)' : 'var(--color-oss-bg)';
|
||||
const borderColor = isMethod ? 'var(--color-method)' :
|
||||
isHosted ? 'var(--color-hosted)' : 'var(--color-oss)';
|
||||
|
||||
return `
|
||||
<button class="tag cursor-pointer"
|
||||
style="background-color: ${bgColor}; border: 1px solid ${borderColor}; color: ${borderColor}; transition: var(--transition-fast);"
|
||||
onclick="event.stopPropagation(); window.showToolDetails('${softwareName}', 'secondary')"
|
||||
onmouseover="this.style.backgroundColor='${borderColor}'; this.style.color='white';"
|
||||
onmouseout="this.style.backgroundColor='${bgColor}'; this.style.color='${borderColor}';">
|
||||
${softwareName}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
return `<span class="tag" style="background-color: var(--color-bg-tertiary); color: var(--color-text-secondary);">${softwareName}</span>`;
|
||||
}).join('');
|
||||
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
const collapseOnMobile = isMobile && relatedSoftware.length > 2;
|
||||
|
||||
html += `
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<strong style="color: var(--color-text);">Verwandte Software:</strong>
|
||||
${collapseOnMobile ? `
|
||||
<button id="software-toggle-${modalType}"
|
||||
onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.textContent = this.textContent === '▼' ? '▲' : '▼';"
|
||||
class="btn-icon text-xs">
|
||||
▼
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
<div ${collapseOnMobile ? 'class="hidden"' : ''} class="flex flex-wrap gap-1">
|
||||
${softwareLinks}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// CONFIDENCE RENDERING UTILITIES (consolidates AI confidence display logic)
|
||||
export interface ConfidenceData {
|
||||
overall: number;
|
||||
semanticRelevance: number;
|
||||
taskSuitability: number;
|
||||
uncertaintyFactors: string[];
|
||||
strengthIndicators: string[];
|
||||
}
|
||||
|
||||
export function renderConfidenceTooltip(confidence: ConfidenceData): string {
|
||||
const confidenceColor = getConfidenceColor(confidence.overall);
|
||||
const tooltipId = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
return `
|
||||
<span class="confidence-tooltip-trigger"
|
||||
style="display: inline-flex; align-items: center; gap: 0.125rem; cursor: help; margin-left: 0.25rem; position: relative;"
|
||||
onmouseenter="this.querySelector('.confidence-tooltip').style.display = 'block'"
|
||||
onmouseleave="this.querySelector('.confidence-tooltip').style.display = 'none'"
|
||||
onclick="event.stopPropagation();">
|
||||
<div style="width: 6px; height: 6px; border-radius: 50%; background-color: ${confidenceColor}; flex-shrink: 0;"></div>
|
||||
<span style="font-size: 0.625rem; color: white; font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,0.5);">${confidence.overall}%</span>
|
||||
|
||||
<div class="confidence-tooltip"
|
||||
style="display: none; position: absolute; top: 100%; right: 0; z-index: 1001; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1rem; min-width: 320px; max-width: 400px; box-shadow: var(--shadow-lg); font-size: 0.75rem; color: var(--color-text); margin-top: 0.5rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
|
||||
<strong style="font-size: 0.875rem; color: var(--color-primary);">KI-Vertrauenswertung</strong>
|
||||
<span style="background-color: ${confidenceColor}; color: white; font-weight: 600; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.625rem;">${confidence.overall}%</span>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr; gap: 0.625rem; margin-bottom: 0.75rem;">
|
||||
<div style="background: var(--color-bg-secondary); padding: 0.5rem; border-radius: 0.375rem; border-left: 3px solid var(--color-accent);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem;">
|
||||
<span style="font-weight: 600; font-size: 0.6875rem; color: var(--color-text);">🔍 Semantische Relevanz</span>
|
||||
<strong style="color: var(--color-accent);">${confidence.semanticRelevance}%</strong>
|
||||
</div>
|
||||
<div style="font-size: 0.625rem; color: var(--color-text-secondary); line-height: 1.3;">
|
||||
Wie gut die Tool-Beschreibung semantisch zu Ihrer Anfrage passt
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: var(--color-bg-secondary); padding: 0.5rem; border-radius: 0.375rem; border-left: 3px solid var(--color-primary);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem;">
|
||||
<span style="font-weight: 600; font-size: 0.6875rem; color: var(--color-text);">🎯 Aufgaben-Eignung</span>
|
||||
<strong style="color: var(--color-primary);">${confidence.taskSuitability}%</strong>
|
||||
</div>
|
||||
<div style="font-size: 0.625rem; color: var(--color-text-secondary); line-height: 1.3;">
|
||||
KI-bewertete Eignung für Ihre spezifische Aufgabenstellung
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${confidence.strengthIndicators && confidence.strengthIndicators.length > 0 ? `
|
||||
<div style="margin-bottom: 0.75rem; padding: 0.5rem; background: var(--color-oss-bg); border-radius: 0.375rem; border-left: 3px solid var(--color-accent);">
|
||||
<strong style="color: var(--color-accent); font-size: 0.6875rem; display: flex; align-items: center; gap: 0.25rem; margin-bottom: 0.375rem;">
|
||||
<span>✓</span> Stärken dieser Empfehlung:
|
||||
</strong>
|
||||
<ul style="margin: 0; padding-left: 1rem; font-size: 0.625rem; line-height: 1.4; color: var(--color-text);">
|
||||
${confidence.strengthIndicators.slice(0, 3).map(s => `<li style="margin-bottom: 0.25rem;">${sanitizeText(s)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${confidence.uncertaintyFactors && confidence.uncertaintyFactors.length > 0 ? `
|
||||
<div style="padding: 0.5rem; background: var(--color-hosted-bg); border-radius: 0.375rem; border-left: 3px solid var(--color-warning);">
|
||||
<strong style="color: var(--color-warning); font-size: 0.6875rem; display: flex; align-items: center; gap: 0.25rem; margin-bottom: 0.375rem;">
|
||||
<span>⚠</span> Mögliche Einschränkungen:
|
||||
</strong>
|
||||
<ul style="margin: 0; padding-left: 1rem; font-size: 0.625rem; line-height: 1.4; color: var(--color-text);">
|
||||
${confidence.uncertaintyFactors.slice(0, 3).map(f => `<li style="margin-bottom: 0.25rem;">${sanitizeText(f)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--color-border); font-size: 0.625rem; color: var(--color-text-secondary); text-align: center;">
|
||||
Forensisch fundierte KI-Analyse
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// SUITABILITY UTILITIES (consolidates suitability display logic)
|
||||
export function getSuitabilityText(score: string): string {
|
||||
const texts = {
|
||||
high: 'GUT GEEIGNET',
|
||||
medium: 'GEEIGNET',
|
||||
low: 'VIELLEICHT GEEIGNET'
|
||||
};
|
||||
return texts[score] || 'GEEIGNET';
|
||||
}
|
||||
|
||||
export function getSuitabilityColor(score: string): string {
|
||||
const colors = {
|
||||
high: 'var(--color-accent)',
|
||||
medium: 'var(--color-warning)',
|
||||
low: 'var(--color-text-secondary)'
|
||||
};
|
||||
return colors[score] || 'var(--color-warning)';
|
||||
}
|
263
src/utils/uiHelpers.ts
Normal file
263
src/utils/uiHelpers.ts
Normal file
@ -0,0 +1,263 @@
|
||||
// src/utils/uiHelpers.ts - Consolidated UI utilities
|
||||
// This file consolidates all scattered utility functions
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
createToolSlug: (toolName: string) => string;
|
||||
findToolByIdentifier: (tools: Tool[], identifier: string) => Tool | undefined;
|
||||
isToolHosted: (tool: Tool) => boolean;
|
||||
scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void;
|
||||
scrollToElementById: (elementId: string, options?: ScrollIntoViewOptions) => void;
|
||||
scrollToElementBySelector: (selector: string, options?: ScrollIntoViewOptions) => void;
|
||||
prioritizeSearchResults: (tools: Tool[], searchTerm: string) => Tool[];
|
||||
shareArticle: (button: HTMLElement, url: string, title: string) => Promise<void>;
|
||||
shareCurrentArticle: (button: HTMLElement) => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
name: string;
|
||||
type?: 'software' | 'method' | 'concept';
|
||||
projectUrl?: string | null;
|
||||
license?: string;
|
||||
knowledgebase?: boolean;
|
||||
domains?: string[];
|
||||
phases?: string[];
|
||||
platforms?: string[];
|
||||
skillLevel?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
related_concepts?: string[];
|
||||
related_software?: string[];
|
||||
url?: string; // Add missing property
|
||||
accessType?: string; // Add missing property
|
||||
}
|
||||
|
||||
// CORE TOOL UTILITIES (consolidates clientUtils.ts + toolHelpers.ts)
|
||||
export function createToolSlug(toolName: string): string {
|
||||
if (!toolName || typeof toolName !== 'string') {
|
||||
console.warn('[uiHelpers] Invalid toolName provided to createToolSlug:', toolName);
|
||||
return '';
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export function findToolByIdentifier(tools: Tool[], identifier: string): Tool | undefined {
|
||||
if (!identifier || !Array.isArray(tools)) return undefined;
|
||||
|
||||
return tools.find(tool =>
|
||||
tool.name === identifier ||
|
||||
createToolSlug(tool.name) === identifier.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
export function isToolHosted(tool: Tool): boolean {
|
||||
return tool.projectUrl !== undefined &&
|
||||
tool.projectUrl !== null &&
|
||||
tool.projectUrl !== "" &&
|
||||
tool.projectUrl.trim() !== "";
|
||||
}
|
||||
|
||||
// TEXT UTILITIES (consolidates scattered text functions)
|
||||
export function escapeHtml(text: string): string {
|
||||
if (typeof text !== 'string') return String(text);
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
export function truncateText(text: string, maxLength: number): string {
|
||||
if (!text || text.length <= maxLength) return text;
|
||||
return text.slice(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
export function sanitizeText(text: string): string {
|
||||
if (typeof text !== 'string') return '';
|
||||
|
||||
return text
|
||||
.replace(/^#{1,6}\s+/gm, '')
|
||||
.replace(/^\s*[-*+]\s+/gm, '')
|
||||
.replace(/^\s*\d+\.\s+/gm, '')
|
||||
.replace(/\*\*(.+?)\*\*/g, '$1')
|
||||
.replace(/\*(.+?)\*/g, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
.replace(/```[\s\S]*?```/g, '[CODE BLOCK]')
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function summarizeData(data: any): string {
|
||||
if (data === null || data === undefined) return 'null';
|
||||
if (typeof data === 'string') {
|
||||
return data.length > 100 ? data.slice(0, 100) + '...' : data;
|
||||
}
|
||||
if (typeof data === 'number' || typeof data === 'boolean') {
|
||||
return data.toString();
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length === 0) return '[]';
|
||||
if (data.length <= 3) return JSON.stringify(data);
|
||||
return `[${data.slice(0, 3).map(i => typeof i === 'string' ? i : JSON.stringify(i)).join(', ')}, ...+${data.length - 3}]`;
|
||||
}
|
||||
if (typeof data === 'object') {
|
||||
const keys = Object.keys(data);
|
||||
if (keys.length === 0) return '{}';
|
||||
if (keys.length <= 3) {
|
||||
return '{' + keys.map(k => `${k}: ${typeof data[k] === 'string' ? data[k].slice(0, 20) + (data[k].length > 20 ? '...' : '') : JSON.stringify(data[k])}`).join(', ') + '}';
|
||||
}
|
||||
return `{${keys.slice(0, 3).join(', ')}, ...+${keys.length - 3} keys}`;
|
||||
}
|
||||
return String(data);
|
||||
}
|
||||
|
||||
// SCROLL UTILITIES (consolidates BaseLayout.astro scroll functions)
|
||||
export function scrollToElement(element: Element | null, options: ScrollIntoViewOptions = {}): void {
|
||||
if (!element) return;
|
||||
|
||||
setTimeout(() => {
|
||||
const headerHeight = document.querySelector('nav')?.offsetHeight || 80;
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const absoluteElementTop = elementRect.top + window.pageYOffset;
|
||||
const targetPosition = absoluteElementTop - headerHeight - 20;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: 'smooth',
|
||||
...options
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
export function scrollToElementById(elementId: string, options: ScrollIntoViewOptions = {}): void {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
scrollToElement(element, options);
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollToElementBySelector(selector: string, options: ScrollIntoViewOptions = {}): void {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
scrollToElement(element, options);
|
||||
}
|
||||
}
|
||||
|
||||
// SEARCH UTILITIES (consolidates BaseLayout.astro search functions)
|
||||
export function prioritizeSearchResults(tools: Tool[], searchTerm: string): Tool[] {
|
||||
if (!searchTerm || !searchTerm.trim()) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
const lowerSearchTerm = searchTerm.toLowerCase().trim();
|
||||
|
||||
return tools.sort((a, b) => {
|
||||
const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase());
|
||||
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
|
||||
|
||||
const aExactTag = aTagsLower.includes(lowerSearchTerm);
|
||||
const bExactTag = bTagsLower.includes(lowerSearchTerm);
|
||||
|
||||
if (aExactTag && !bExactTag) return -1;
|
||||
if (!aExactTag && bExactTag) return 1;
|
||||
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
// URL UTILITIES (consolidates ToolMatrix.astro share functions)
|
||||
export function generateShareURL(toolName: string, view: string, modal?: string): string {
|
||||
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()}`;
|
||||
}
|
||||
|
||||
// CLIPBOARD UTILITIES (consolidates ToolMatrix.astro + BaseLayout.astro clipboard functions)
|
||||
export async function copyToClipboard(text: string, button: HTMLElement): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = `
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
|
||||
<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);
|
||||
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = 'Kopiert!';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// TIME UTILITIES (consolidates audit trail time formatting)
|
||||
export function formatDuration(ms: number): string {
|
||||
if (ms < 1000) return '< 1s';
|
||||
if (ms < 60000) return `${Math.ceil(ms / 1000)}s`;
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds = Math.ceil((ms % 60000) / 1000);
|
||||
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
||||
}
|
||||
|
||||
// CONFIDENCE UTILITIES (consolidates AI confidence color logic)
|
||||
export function getConfidenceColor(confidence: number): string {
|
||||
if (confidence >= 80) return 'var(--color-accent)';
|
||||
if (confidence >= 60) return 'var(--color-warning)';
|
||||
return 'var(--color-error)';
|
||||
}
|
||||
|
||||
// Make all functions available globally for backward compatibility
|
||||
export function registerGlobalUtilities(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Tool utilities
|
||||
window.createToolSlug = createToolSlug;
|
||||
window.findToolByIdentifier = findToolByIdentifier;
|
||||
window.isToolHosted = isToolHosted;
|
||||
|
||||
// Scroll utilities
|
||||
window.scrollToElement = scrollToElement;
|
||||
window.scrollToElementById = scrollToElementById;
|
||||
window.scrollToElementBySelector = scrollToElementBySelector;
|
||||
|
||||
// Search utilities
|
||||
window.prioritizeSearchResults = prioritizeSearchResults;
|
||||
|
||||
// Share utilities
|
||||
window.shareArticle = async (button: HTMLElement, url: string, title: string) => {
|
||||
const fullUrl = window.location.origin + url;
|
||||
await copyToClipboard(fullUrl, button);
|
||||
};
|
||||
window.shareCurrentArticle = async (button: HTMLElement) => {
|
||||
await copyToClipboard(window.location.href, button);
|
||||
};
|
||||
|
||||
console.log('[UI Helpers] Global utilities registered');
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user