244 lines
8.7 KiB
JavaScript
244 lines
8.7 KiB
JavaScript
// File: ./src/js/modal.js
|
||
// Tool detail modal functionality
|
||
(function() {
|
||
'use strict';
|
||
|
||
let modal;
|
||
let modalContent;
|
||
let currentTool = null;
|
||
|
||
// Initialize modal system
|
||
function init() {
|
||
modal = document.getElementById('tool-modal');
|
||
modalContent = document.getElementById('tool-modal-content');
|
||
|
||
if (!modal || !modalContent) {
|
||
console.warn('Modal elements not found');
|
||
return;
|
||
}
|
||
|
||
// Add event listeners
|
||
setupEventListeners();
|
||
}
|
||
|
||
// Set up event listeners for modal
|
||
function setupEventListeners() {
|
||
// Click outside modal to close
|
||
modal.addEventListener('click', (event) => {
|
||
if (event.target === modal) {
|
||
closeModal();
|
||
}
|
||
});
|
||
|
||
// Escape key to close modal
|
||
document.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Escape' && !modal.classList.contains('hidden')) {
|
||
closeModal();
|
||
}
|
||
});
|
||
|
||
// Prevent body scroll when modal is open
|
||
modal.addEventListener('scroll', (event) => {
|
||
event.stopPropagation();
|
||
});
|
||
}
|
||
|
||
// Show tool detail modal
|
||
function showToolModal(tool) {
|
||
if (!tool || !modal || !modalContent) return;
|
||
|
||
currentTool = tool;
|
||
|
||
// Generate modal content
|
||
modalContent.innerHTML = generateModalContent(tool);
|
||
|
||
// Add close button handler
|
||
const closeButton = modalContent.querySelector('.modal-close');
|
||
if (closeButton) {
|
||
closeButton.addEventListener('click', closeModal);
|
||
}
|
||
|
||
// Add external link handler
|
||
const externalLink = modalContent.querySelector('.external-link');
|
||
if (externalLink) {
|
||
externalLink.addEventListener('click', (event) => {
|
||
// Let the link work normally, but track the click
|
||
trackToolClick(tool);
|
||
});
|
||
}
|
||
|
||
// Show modal
|
||
modal.classList.remove('hidden');
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
// Focus trap
|
||
trapFocus();
|
||
}
|
||
|
||
// Close modal
|
||
function closeModal() {
|
||
if (!modal) return;
|
||
|
||
modal.classList.add('hidden');
|
||
document.body.style.overflow = '';
|
||
currentTool = null;
|
||
|
||
// Return focus to the tool card that was clicked
|
||
const activeToolCard = document.querySelector('.tool-card:focus');
|
||
if (activeToolCard) {
|
||
activeToolCard.focus();
|
||
}
|
||
}
|
||
|
||
// Generate modal HTML content
|
||
function generateModalContent(tool) {
|
||
const typeLabel = tool.type === 'SaaS' ?
|
||
'<span class="tag tag-purple">SaaS</span>' :
|
||
'<span class="tag tag-green">FOSS</span>';
|
||
|
||
const domains = (tool.domains || []).map(domain =>
|
||
`<span class="block tag tag-green mb-2">${domain}</span>`
|
||
).join('');
|
||
|
||
const phases = (tool.phases || []).map(phase =>
|
||
`<span class="block tag tag-blue mb-2">${phase}</span>`
|
||
).join('');
|
||
|
||
const tags = (tool.tags || []).map(tag =>
|
||
`<span class="tag tag-gray mr-2 mb-2">${tag}</span>`
|
||
).join('');
|
||
|
||
const platforms = (tool.platforms || []).join(', ');
|
||
|
||
// Handle service URL for self-hosted services
|
||
const serviceInfo = tool.selfHosted && tool.serviceUrl ?
|
||
`<div class="mb-4 p-3 bg-blue-50 dark:bg-blue-900 rounded-lg">
|
||
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||
<strong>Lab Instance:</strong>
|
||
<a href="${tool.serviceUrl}" target="_blank" rel="noopener noreferrer"
|
||
class="underline hover:no-underline">${tool.serviceUrl}</a>
|
||
</p>
|
||
</div>` : '';
|
||
|
||
return `
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">${tool.name}</h2>
|
||
<div class="flex items-center gap-2 mt-1">
|
||
${typeLabel}
|
||
<span class="text-sm text-gray-500 dark:text-gray-400">${tool.skillLevel}</span>
|
||
</div>
|
||
</div>
|
||
<button
|
||
class="modal-close text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 text-2xl"
|
||
aria-label="Schließen"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<p class="text-gray-600 dark:text-gray-300 mb-6">${tool.description}</p>
|
||
|
||
${serviceInfo}
|
||
|
||
<div class="grid grid-cols-2 gap-6 mb-6">
|
||
<div>
|
||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3">Bereiche</h3>
|
||
<div class="space-y-2">
|
||
${domains || '<span class="text-gray-500 dark:text-gray-400 text-sm">Keine angegeben</span>'}
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3">Phasen</h3>
|
||
<div class="space-y-2">
|
||
${phases || '<span class="text-gray-500 dark:text-gray-400 text-sm">Keine angegeben</span>'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-6">
|
||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3">Tags</h3>
|
||
<div class="flex flex-wrap">
|
||
${tags || '<span class="text-gray-500 dark:text-gray-400 text-sm">Keine Tags</span>'}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex justify-between items-center pt-4 border-t border-gray-200 dark:border-gray-700">
|
||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||
<div>Zugriff: <span class="font-medium">${tool.accessType}</span></div>
|
||
<div>Plattformen: <span class="font-medium">${platforms}</span></div>
|
||
</div>
|
||
<a
|
||
href="${tool.url}"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
class="external-link flex items-center gap-2 px-6 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
||
>
|
||
Projekt öffnen
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Simple focus trap for modal
|
||
function trapFocus() {
|
||
const focusableElements = modal.querySelectorAll(
|
||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||
);
|
||
|
||
if (focusableElements.length === 0) return;
|
||
|
||
const firstElement = focusableElements[0];
|
||
const lastElement = focusableElements[focusableElements.length - 1];
|
||
|
||
// Focus first element
|
||
firstElement.focus();
|
||
|
||
// Handle tab key
|
||
modal.addEventListener('keydown', (event) => {
|
||
if (event.key !== 'Tab') return;
|
||
|
||
if (event.shiftKey) {
|
||
// Shift + Tab
|
||
if (document.activeElement === firstElement) {
|
||
event.preventDefault();
|
||
lastElement.focus();
|
||
}
|
||
} else {
|
||
// Tab
|
||
if (document.activeElement === lastElement) {
|
||
event.preventDefault();
|
||
firstElement.focus();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Track tool clicks for analytics (placeholder)
|
||
function trackToolClick(tool) {
|
||
// This is where you'd send analytics data
|
||
console.log('Tool clicked:', tool.name, tool.url);
|
||
|
||
// Could send to analytics service:
|
||
// analytics.track('tool_clicked', {
|
||
// tool_name: tool.name,
|
||
// tool_type: tool.type,
|
||
// tool_url: tool.url
|
||
// });
|
||
}
|
||
|
||
// Export functions for use by other scripts
|
||
window.showToolModal = showToolModal;
|
||
window.closeToolModal = closeModal;
|
||
|
||
// Initialize when DOM is ready
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})(); |