alt index page
· 1 year ago
2f0e93a2d00008cac8799c867216ff626be98e2f
Parent:
7798fd031
1 file changed +627 −0
- index2.php +627 −0
Diff
--- /dev/null +++ b/index2.php @@ -0,0 +1,627 @@ +<?php +// Set content type and encoding +header('Content-Type: text/html; charset=utf-8'); + +// --- Configuration --- +$excludedItems = [ + '.', + '..', + 'index.php', + 'browse.html', + 'images', + 'LICENSE', + 'README.md', + 'PROMPT.txt', + 'history_tree_style.css', + 'safety_data.js', +]; + +$cheatsheetDir = '.'; +$scheme = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; +$host = $_SERVER['HTTP_HOST']; +$scriptDir = dirname($_SERVER['SCRIPT_NAME']); +$baseUrl = rtrim($scheme . '://' . $host . $scriptDir, '/') . '/'; + +// --- !!! Action Needed: Define Categories !!! --- +// You need a way to map filenames or metadata to categories. +// Example: Simple mapping (replace with your logic) +function getCategoryForFile(string $filename): string { + $filenameLower = strtolower($filename); + if (str_contains($filenameLower, 'ai') || str_contains($filenameLower, 'safety')) return 'ai'; + if (str_contains($filenameLower, 'bitcoin') || str_contains($filenameLower, 'crypto')) return 'crypto'; + if (str_contains($filenameLower, 'leadership')) return 'leadership'; + if (str_contains($filenameLower, 'buddhism') || str_contains($filenameLower, 'judaism') || str_contains($filenameLower, 'philosophy') || str_contains($filenameLower, 'objectivism') || str_contains($filenameLower, 'capitalism')) return 'philosophy'; + if (str_contains($filenameLower, 'database') || str_contains($filenameLower, 'postgres') || str_contains($filenameLower, 'versioncontrol') || str_contains($filenameLower, 'sql')) return 'tech'; + return 'other'; // Default category +} + +// --- Helper Function: Extract Metadata --- +function extractMetadata(string $filepath): array { + global $baseUrl, $scheme, $host; + + $filename = basename($filepath); + $metadata = [ + 'id' => 'cs-' . pathinfo($filename, PATHINFO_FILENAME), // Unique ID for JS targeting + 'title' => pathinfo($filename, PATHINFO_FILENAME), + 'description' => 'Explore this cheatsheet to learn more.', + 'image' => null, + 'url' => $baseUrl . $filename, + 'category' => getCategoryForFile($filename), // Assign category + 'error' => null + ]; + + $content = @file_get_contents($filepath); + if ($content === false) { + $metadata['error'] = "Could not read file: " . $filename; + return $metadata; + } + + $dom = new DOMDocument(); + @$dom->loadHTML($content, LIBXML_NOERROR | LIBXML_NOWARNING); + $xpath = new DOMXPath($dom); + + $titleNode = $xpath->query('//title')->item(0); + if ($titleNode) $metadata['title'] = trim($titleNode->textContent); + + $descNode = $xpath->query('//meta[@name="description"]/@content')->item(0); + if ($descNode) { + $metadata['description'] = trim($descNode->nodeValue); + } else { + $ogDescNode = $xpath->query('//meta[@property="og:description"]/@content')->item(0); + if ($ogDescNode) $metadata['description'] = trim($ogDescNode->nodeValue); + } + + $imgNode = $xpath->query('//meta[@property="og:image"]/@content')->item(0); + if ($imgNode) { + $imageUrl = trim($imgNode->nodeValue); + if (!preg_match('/^https?:\/\//i', $imageUrl)) { + if (str_starts_with($imageUrl, '/')) { + $metadata['image'] = $scheme . '://' . $host . $imageUrl; + } else { + $absoluteImagePath = realpath(dirname(__FILE__)) . '/' . $imageUrl; + if (file_exists($absoluteImagePath)) { + $metadata['image'] = $baseUrl . $imageUrl; + } else { + error_log("Relative image path not found: " . $imageUrl . " in " . $filename); + } + } + } else { + $metadata['image'] = $imageUrl; + } + } + + if (mb_strlen($metadata['description']) > 150) { + $metadata['description'] = mb_substr($metadata['description'], 0, 147) . '...'; + } + + return $metadata; +} + +// --- Main Logic: Scan Directory and Build Cheatsheet List --- +$cheatsheets = []; +$errors = []; +$categories = ['all']; // Start with 'all' + +try { + $files = scandir($cheatsheetDir); + if ($files === false) throw new Exception("Could not scan directory: " . $cheatsheetDir); + + foreach ($files as $file) { + $filePath = $cheatsheetDir . '/' . $file; + if (in_array($file, $excludedItems, true) || !is_file($filePath) || !is_readable($filePath)) continue; + + if (str_ends_with(strtolower($file), '.html')) { + $meta = extractMetadata($filePath); + if ($meta['error']) { + $errors[] = $meta['error']; + } else { + $cheatsheets[] = $meta; + if (!in_array($meta['category'], $categories)) { + $categories[] = $meta['category']; + } + } + } + } + sort($categories); // Sort categories alphabetically (optional) + // Sort cheatsheets alphabetically by title + usort($cheatsheets, fn($a, $b) => strcasecmp($a['title'], $b['title'])); + +} catch (Exception $e) { + $errors[] = "An error occurred: " . $e->getMessage(); +} + +?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + + <!-- Metadata --> + <title>David Veksler's Interactive Cheatsheet Gallery</title> + <meta name="description" content="Explore an interactive gallery of expertly crafted cheatsheets by David Veksler. Filter by topic and view details dynamically. Custom design services available."> + <meta name="keywords" content="interactive cheatsheets, portfolio, gallery, filter, javascript gallery, cheatsheet design, information design, david veksler, tech, philosophy, ai safety, bitcoin, leadership, custom cheatsheets"> + <!-- Other meta tags (author, canonical, OG, Twitter) remain similar --> + <link rel="canonical" href="<?php echo htmlspecialchars($baseUrl); ?>"> + <meta property="og:title" content="David Veksler's Interactive Cheatsheet Gallery"> + <meta property="og:description" content="Explore an interactive gallery of expertly crafted cheatsheets. Filter by topic, view details dynamically."> + <meta property="og:image" content="<?php echo htmlspecialchars($baseUrl); ?>images/cheatsheets-og-gallery.png"> <!-- Suggest creating a gallery-specific OG image --> + <meta property="og:image:alt" content="Interactive Cheatsheet Gallery Showcase"> + <!-- Twitter card meta --> + + <!-- Bootstrap CSS & Icons --> + <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css"> + + <!-- Custom CSS for Awesome Gallery --> + <style> + :root { + --gallery-bg-start: #f8f9fa; + --gallery-bg-end: #e9ecef; + --card-hover-scale: 1.03; + --card-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); + --card-hover-shadow: 0 8px 25px rgba(0, 0, 0, 0.12); + --expanded-view-bg: rgba(255, 255, 255, 0.98); + --expanded-view-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + --filter-active-bg: #0d6efd; + --filter-active-color: #fff; + } + html { scroll-behavior: smooth; } + body { + display: flex; + flex-direction: column; + min-height: 100vh; + background-image: linear-gradient(135deg, var(--gallery-bg-start) 0%, var(--gallery-bg-end) 100%); + overflow-x: hidden; /* Prevent horizontal scroll during animations */ + } + .main-content { flex: 1; } + .navbar { background-image: linear-gradient(to bottom, #343a40, #212529); } + + /* --- Filter Buttons --- */ + .filter-buttons .btn { + margin: 0.25rem; + transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; + } + .filter-buttons .btn.active { + background-color: var(--filter-active-bg); + color: var(--filter-active-color); + border-color: var(--filter-active-bg); + } + + /* --- Portfolio Grid & Items --- */ + #cheatsheetGrid { + position: relative; /* Needed for positioning expanded item */ + } + .portfolio-item { + /* Styles for the column */ + transition: opacity 0.4s ease-out, transform 0.4s ease-out; + } + .portfolio-item.filtered-out { + opacity: 0; + transform: scale(0.9); + /* We'll add d-none via JS after transition */ + pointer-events: none; /* Avoid interacting while hiding */ + position: absolute; /* Take out of flow smoothly */ + z-index: -1; + } + .portfolio-card { + cursor: pointer; + border-radius: 8px; + overflow: hidden; + background-color: #fff; + box-shadow: var(--card-shadow); + transition: transform 0.3s ease-out, box-shadow 0.3s ease-out; + height: 100%; /* Fill the column height */ + display: flex; + flex-direction: column; + } + .portfolio-card:hover { + transform: scale(var(--card-hover-scale)); + box-shadow: var(--card-hover-shadow); + z-index: 10; /* Bring slightly forward on hover */ + } + .card-thumbnail { + aspect-ratio: 16 / 9; + background-color: #e9ecef; + background-size: cover; + background-position: center; + display: flex; + align-items: center; + justify-content: center; + color: #adb5bd; + border-bottom: 1px solid #dee2e6; + } + .card-thumbnail::before { /* Placeholder Icon */ + font-family: 'bootstrap-icons'; + content: "\F48B"; /* bi-image-alt */ + font-size: 2.5rem; + opacity: 0.5; + } + /* Hide placeholder if image is set via style */ + .card-thumbnail[style*="background-image"]::before { + display: none; + } + .portfolio-card .card-body { + padding: 1rem; + flex-grow: 1; /* Allow body to take remaining space */ + } + .portfolio-card .card-title { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0; /* Title is main element here */ + } + + /* --- Expanded View --- */ + .expanded-view-container { + display: none; /* Hidden initially */ + position: fixed; /* Overlay */ + top: 0; left: 0; right: 0; bottom: 0; + background-color: rgba(0, 0, 0, 0.6); /* Dim background */ + z-index: 1050; /* Above most content */ + padding: 2rem; + overflow-y: auto; + cursor: pointer; /* Indicate clicking outside closes */ + } + .expanded-view-content { + position: relative; + background-color: var(--expanded-view-bg); + border-radius: 10px; + box-shadow: var(--expanded-view-shadow); + max-width: 800px; /* Control max width */ + margin: 2rem auto; /* Centered */ + padding: 1.5rem 2rem; + cursor: default; /* Standard cursor inside content */ + transform: scale(0.9); + opacity: 0; + transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s ease-out; /* Nice bounce effect */ + } + .expanded-view-container.visible { + display: block; + } + .expanded-view-container.visible .expanded-view-content { + transform: scale(1); + opacity: 1; + } + + .expanded-view-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; border-bottom: 1px solid #eee; padding-bottom: 0.8rem;} + .expanded-view-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 0; } + .expanded-view-close { font-size: 1.8rem; line-height: 1; background: none; border: none; opacity: 0.6; transition: opacity 0.2s; } + .expanded-view-close:hover { opacity: 1; } + + .expanded-view-body { display: flex; flex-direction: column; gap: 1.5rem; } + .expanded-preview { + width: 100%; + aspect-ratio: 16 / 9; + background-color: #f0f0f0; + border-radius: 5px; + overflow: hidden; + display: flex; align-items: center; justify-content: center; + } + .expanded-preview iframe { width: 100%; height: 100%; border: none; } + .expanded-preview img { width: 100%; height: 100%; object-fit: cover; } + .expanded-description { color: #333; line-height: 1.6; } + .expanded-actions { text-align: center; margin-top: 1rem; } + + /* Spinner for iframe loading */ + .spinner-container { position: absolute; top:0; left:0; right:0; bottom:0; background:rgba(255,255,255,0.8); display: flex; align-items: center; justify-content: center; z-index: 5; opacity:0; transition: opacity 0.3s; pointer-events: none; } + .spinner-container.loading { opacity: 1; pointer-events: auto;} + + /* Footer & Subtle CTA */ + .footer { background-color: #e9ecef; color: #6c757d; border-top: 1px solid #ced4da; } + .cta-scroll-link { font-size: 0.9rem; text-decoration: none; } + .cta-scroll-link:hover { text-decoration: underline; } + .cta-section-bottom { border-top: 1px solid #dee2e6; padding-top: 2rem; padding-bottom: 1rem; margin-top: 3rem; } + </style> +</head> +<body class="d-flex flex-column min-vh-100"> + <nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top shadow-sm"> + <div class="container"> + <a class="navbar-brand" href="<?php echo htmlspecialchars($baseUrl); ?>"> + <i class="bi bi-grid-3x3-gap-fill me-2"></i>David Veksler's Interactive Gallery + </a> + </div> + </nav> + + <main class="main-content container mt-4 mb-5"> + <header class="text-center mb-4"> + <h1 class="display-5 fw-bold">Cheatsheet Showcase</h1> + <p class="lead text-muted">Explore interactive examples. Filter by category or search.</p> + <a href="#custom-cheatsheets" class="cta-scroll-link link-secondary d-block mb-3"><i class="bi bi-tools me-1"></i>Need a custom cheatsheet?</a> + </header> + + <!-- Filter Buttons --> + <div id="filterButtons" class="filter-buttons text-center mb-4"> + <?php foreach ($categories as $category): ?> + <button class="btn btn-sm <?php echo ($category === 'all') ? 'btn-primary active' : 'btn-outline-secondary'; ?>" data-filter="<?php echo htmlspecialchars($category); ?>"> + <?php echo htmlspecialchars(ucfirst($category)); ?> + </button> + <?php endforeach; ?> + </div> + + <!-- Text Filter Input (Optional) --> + <div class="row mb-4 justify-content-center"> + <div class="col-md-8 col-lg-6"> + <div class="input-group shadow-sm"> + <span class="input-group-text bg-white border-end-0" id="search-addon"><i class="bi bi-search text-primary"></i></span> + <input type="search" id="searchInput" class="form-control border-start-0" placeholder="Search within category..." aria-label="Search cheatsheets" aria-describedby="search-addon"> + </div> + </div> + </div> + + <!-- Error Display --> + <?php if (!empty($errors)): ?> + <div class="alert alert-warning">... errors ...</div> + <?php endif; ?> + <?php if (empty($cheatsheets) && empty($errors)): ?> + <div class="alert alert-info text-center">No cheatsheet examples found.</div> + <?php endif; ?> + + <!-- Cheatsheet Grid --> + <div id="cheatsheetGrid" class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4"> + <?php if (!empty($cheatsheets)): ?> + <?php foreach ($cheatsheets as $sheet): ?> + <div class="col portfolio-item" data-category="<?php echo htmlspecialchars($sheet['category']); ?>" data-title="<?php echo htmlspecialchars(strtolower($sheet['title'])); ?>" id="<?php echo htmlspecialchars($sheet['id']); ?>"> + <div class="portfolio-card" role="button" tabindex="0" aria-label="View details for <?php echo htmlspecialchars($sheet['title']); ?>"> + <div class="card-thumbnail" <?php if (!empty($sheet['image'])): ?> style="background-image: url('<?php echo htmlspecialchars($sheet['image']); ?>');" <?php endif; ?>> + <!-- Placeholder handled by CSS --> + </div> + <div class="card-body"> + <h5 class="card-title"><?php echo htmlspecialchars($sheet['title']); ?></h5> + </div> + <!-- Hidden data for expanded view --> + <script type="application/json" class="cheatsheet-data"> + <?php echo json_encode([ + 'title' => $sheet['title'], + 'description' => $sheet['description'], + 'url' => $sheet['url'], + 'image' => $sheet['image'] + ]); ?> + </script> + </div> + </div> + <?php endforeach; ?> + <?php endif; ?> + </div> + + <!-- No Results Message --> + <div id="noResults" class="alert alert-warning text-center mt-4 d-none" role="alert"> + <i class="bi bi-emoji-frown me-2"></i>No cheatsheets match your criteria. + </div> + + <!-- Call to Action Section --> + <section id="custom-cheatsheets" class="cta-section-bottom text-center"> + <h3 class="h4 fw-normal mb-3">Need a Custom Cheatsheet?</h3> + <p class="text-muted mb-3 mx-auto" style="max-width: 600px;">I design professional, tailored cheatsheets for documentation, training, marketing, and more.</p> + <a href="https://www.linkedin.com/in/davidveksler/" target="_blank" rel="noopener noreferrer" class="btn btn-outline-primary btn-sm"> + <i class="bi bi-linkedin me-1"></i> Discuss Your Project on LinkedIn + </a> + </section> + </main> + + <!-- Expanded View Structure (Initially Hidden) --> + <div id="expandedView" class="expanded-view-container"> + <div class="expanded-view-content" role="dialog" aria-modal="true" aria-labelledby="expandedViewTitle"> + <div class="expanded-view-header"> + <h2 id="expandedViewTitle" class="expanded-view-title">Cheatsheet Title</h2> + <button type="button" class="expanded-view-close" aria-label="Close dialog">×</button> + </div> + <div class="expanded-view-body"> + <div class="expanded-preview"> + <!-- Content (image or iframe) added by JS --> + <div class="spinner-container"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div> + </div> + <p class="expanded-description">Description goes here...</p> + </div> + <div class="expanded-actions"> + <a href="#" id="expandedViewLink" target="_blank" rel="noopener noreferrer" class="btn btn-primary"> + View Full Cheatsheet <i class="bi bi-box-arrow-up-right ms-1"></i> + </a> + </div> + </div> + </div> + + + <!-- Footer --> + <footer class="footer py-4 mt-auto border-top bg-light"> + <div class="container text-center"> + <p class="mb-2 text-muted"> + Interactive Cheatsheet Gallery by David Veksler © <?php echo date("Y"); ?> + </p> + <div> + <a href="https://www.linkedin.com/in/davidveksler/" title="David Veksler on LinkedIn" target="_blank" rel="noopener noreferrer" class="mx-2 link-secondary"><i class="bi bi-linkedin"></i> LinkedIn</a> + <span class="text-muted mx-1">|</span> + <a href="<?php echo htmlspecialchars($baseUrl); ?>" title="Reload Gallery" class="mx-2 link-secondary"><i class="bi bi-collection"></i> Gallery Home</a> + </div> + </div> + </footer> + + <!-- Bootstrap Bundle --> + <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script> + + <!-- Custom JS for Gallery --> + <script> + document.addEventListener('DOMContentLoaded', function() { + const grid = document.getElementById('cheatsheetGrid'); + const items = grid.querySelectorAll('.portfolio-item'); + const filterButtonsContainer = document.getElementById('filterButtons'); + const searchInput = document.getElementById('searchInput'); + const noResultsMessage = document.getElementById('noResults'); + const expandedViewContainer = document.getElementById('expandedView'); + const expandedViewContent = expandedViewContainer.querySelector('.expanded-view-content'); + const expandedViewClose = expandedViewContainer.querySelector('.expanded-view-close'); + const expandedViewTitle = expandedViewContainer.querySelector('#expandedViewTitle'); + const expandedPreview = expandedViewContainer.querySelector('.expanded-preview'); + const expandedDescription = expandedViewContainer.querySelector('.expanded-description'); + const expandedLink = expandedViewContainer.querySelector('#expandedViewLink'); + const spinnerContainer = expandedPreview.querySelector('.spinner-container'); + + let currentFilter = 'all'; // Track current category filter + let currentSearch = ''; // Track current search term + + // --- Filtering Logic --- + function applyFilters() { + let itemsVisible = 0; + items.forEach(item => { + const categoryMatch = currentFilter === 'all' || item.dataset.category === currentFilter; + const searchMatch = currentSearch === '' || item.dataset.title.includes(currentSearch); // Simple title search + + const shouldShow = categoryMatch && searchMatch; + + if (shouldShow) { + if (item.classList.contains('filtered-out')) { + // Make visible: remove d-none first, then transition classes + item.classList.remove('d-none'); + // Use setTimeout to allow the 'd-none' removal to render before starting transition + setTimeout(() => { + item.classList.remove('filtered-out'); + item.classList.remove('position-absolute'); // Restore flow + item.classList.remove('z-index:-1') + item.style.pointerEvents = ''; + }, 10); // Small delay + } + itemsVisible++; + } else { + if (!item.classList.contains('filtered-out')) { + item.classList.add('filtered-out'); + // Add d-none after the transition might have completed + item.addEventListener('transitionend', () => { + if (item.classList.contains('filtered-out')) { // Check if still filtered out + item.classList.add('d-none'); + } + }, { once: true }); + } + } + }); + + noResultsMessage.classList.toggle('d-none', itemsVisible > 0); + } + + // Category Filter Button Clicks + filterButtonsContainer.addEventListener('click', function(e) { + if (e.target.tagName === 'BUTTON') { + const filterValue = e.target.dataset.filter; + if (filterValue === currentFilter) return; // No change + + filterButtonsContainer.querySelector('.active').classList.remove('active', 'btn-primary'); + filterButtonsContainer.querySelector('.active').classList.add('btn-outline-secondary'); // Make previously active outlined + e.target.classList.add('active', 'btn-primary'); + e.target.classList.remove('btn-outline-secondary'); // Make currently active primary + + currentFilter = filterValue; + applyFilters(); + } + }); + + // Search Input + searchInput.addEventListener('input', function() { + currentSearch = searchInput.value.toLowerCase().trim(); + applyFilters(); + }); + + + // --- Expanded View Logic --- + function showExpandedView(itemData) { + expandedViewTitle.textContent = itemData.title; + expandedDescription.textContent = itemData.description; + expandedLink.href = itemData.url; + expandedPreview.innerHTML = ''; // Clear previous content + + // Re-add spinner + expandedPreview.appendChild(spinnerContainer.cloneNode(true)); + const currentSpinner = expandedPreview.querySelector('.spinner-container'); + currentSpinner.classList.add('loading'); // Show spinner immediately + + // Decide whether to show image or iframe + if (itemData.image) { + const img = document.createElement('img'); + img.src = itemData.image; + img.alt = `Preview for ${itemData.title}`; + img.loading = 'lazy'; + img.onload = () => currentSpinner.classList.remove('loading'); // Hide spinner on load + img.onerror = () => { // Fallback to iframe if image fails + console.warn("Image failed to load, trying iframe:", itemData.image); + loadIframePreview(itemData.url, currentSpinner); + }; + expandedPreview.appendChild(img); + } else { + // Load iframe directly if no image + loadIframePreview(itemData.url, currentSpinner); + } + + expandedViewContainer.classList.add('visible'); + document.body.style.overflow = 'hidden'; // Prevent background scroll + } + + function loadIframePreview(url, spinner) { + const iframe = document.createElement('iframe'); + iframe.src = url; + iframe.title = `Preview of ${expandedViewTitle.textContent}`; + iframe.setAttribute('loading', 'lazy'); + iframe.setAttribute('frameborder', '0'); + iframe.setAttribute('referrerpolicy', 'no-referrer'); + // Hide spinner when iframe is considered loaded (this is not always perfect) + iframe.onload = () => spinner.classList.remove('loading'); + expandedPreview.appendChild(iframe); + } + + + function closeExpandedView() { + expandedViewContainer.classList.remove('visible'); + document.body.style.overflow = ''; // Restore scrolling + // Optional: Clear content immediately or after transition + // expandedPreview.innerHTML = ''; + } + + // Grid Item Clicks + grid.addEventListener('click', function(e) { + const card = e.target.closest('.portfolio-card'); + if (card) { + const dataElement = card.querySelector('.cheatsheet-data'); + if (dataElement) { + try { + const itemData = JSON.parse(dataElement.textContent); + showExpandedView(itemData); + } catch (err) { + console.error("Failed to parse cheatsheet data:", err); + } + } + } + }); + + // Close button click + expandedViewClose.addEventListener('click', closeExpandedView); + + // Click outside the content to close + expandedViewContainer.addEventListener('click', function(e) { + if (e.target === expandedViewContainer) { // Only if clicking the backdrop itself + closeExpandedView(); + } + }); + + // Keyboard accessibility for closing + expandedViewContainer.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + closeExpandedView(); + } + }); + + // Keyboard accessibility for opening + grid.addEventListener('keydown', function(e) { + const card = e.target.closest('.portfolio-card'); + if (card && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); // Prevent space from scrolling + const dataElement = card.querySelector('.cheatsheet-data'); + if (dataElement) { + try { + const itemData = JSON.parse(dataElement.textContent); + showExpandedView(itemData); + } catch (err) { console.error("Failed to parse cheatsheet data:", err); } + } + } + }); + + }); + </script> + +</body> +</html> \ No newline at end of file