Upload 3 files
Browse files- js/cards/ArtifactSummaryCard.js +225 -0
- js/cards/HomeAreaCard.js +46 -0
- js/cards/ResourceCard.js +130 -0
js/cards/ArtifactSummaryCard.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ArtifactSummaryCard.js - Summary card component for artifacts
|
| 2 |
+
import { areasData } from '../data/areas.js';
|
| 3 |
+
import { featuredArtifacts } from '../data/artifacts.js';
|
| 4 |
+
|
| 5 |
+
export function createArtifactSummaryCard(artifact, index) {
|
| 6 |
+
const { title, date, type, description, areaTags, subAreaTags, sourceUrl } = artifact;
|
| 7 |
+
|
| 8 |
+
// Type-based styling (simplified - just icon and text color)
|
| 9 |
+
const typeStyles = {
|
| 10 |
+
'blog': {
|
| 11 |
+
icon: '📝',
|
| 12 |
+
textColor: 'text-blue-700'
|
| 13 |
+
},
|
| 14 |
+
'paper': {
|
| 15 |
+
icon: '📄',
|
| 16 |
+
textColor: 'text-green-700'
|
| 17 |
+
},
|
| 18 |
+
'dataset': {
|
| 19 |
+
icon: '📊',
|
| 20 |
+
textColor: 'text-purple-700'
|
| 21 |
+
},
|
| 22 |
+
'space': {
|
| 23 |
+
icon: '🚀',
|
| 24 |
+
textColor: 'text-orange-700'
|
| 25 |
+
},
|
| 26 |
+
'external': {
|
| 27 |
+
icon: '🔗',
|
| 28 |
+
textColor: 'text-gray-700'
|
| 29 |
+
}
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
const style = typeStyles[type] || typeStyles['external'];
|
| 33 |
+
|
| 34 |
+
// Use imported areas data
|
| 35 |
+
const areaData = areasData;
|
| 36 |
+
|
| 37 |
+
const primaryArea = areaTags[0];
|
| 38 |
+
const primaryAreaData = areaData[primaryArea];
|
| 39 |
+
const backgroundImage = primaryAreaData?.image || '';
|
| 40 |
+
const imageCredit = primaryAreaData?.imageAttribution || '';
|
| 41 |
+
|
| 42 |
+
const areaTagsHtml = areaTags.map(tag => {
|
| 43 |
+
const area = areaData[tag];
|
| 44 |
+
return area ? `<span class="inline-block px-2 py-1 text-xs rounded-full ${area.color}">${area.name}</span>` : '';
|
| 45 |
+
}).join('');
|
| 46 |
+
|
| 47 |
+
const subAreaTagsHtml = subAreaTags.map(subAreaKey => {
|
| 48 |
+
// Find the sub-area name and color by looking through all areas
|
| 49 |
+
let subAreaName = subAreaKey; // fallback to key if not found
|
| 50 |
+
let textColor = 'text-gray-700'; // fallback color
|
| 51 |
+
|
| 52 |
+
for (const areaKey in areaData) {
|
| 53 |
+
const area = areaData[areaKey];
|
| 54 |
+
if (area.subAreas && area.subAreas[subAreaKey]) {
|
| 55 |
+
const subArea = area.subAreas[subAreaKey];
|
| 56 |
+
subAreaName = typeof subArea === 'string' ? subArea : subArea.name;
|
| 57 |
+
// Extract just the text color from the sub-area color
|
| 58 |
+
if (typeof subArea === 'object' && subArea.color) {
|
| 59 |
+
const colorMatch = subArea.color.match(/text-(\w+)-(\d+)/);
|
| 60 |
+
if (colorMatch) {
|
| 61 |
+
textColor = `text-${colorMatch[1]}-700`; // Use consistent 700 shade
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
break;
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
return `<span class="inline-block px-2 py-0.5 text-xs bg-gray-200 ${textColor} rounded">${subAreaName}</span>`;
|
| 68 |
+
}).join('');
|
| 69 |
+
|
| 70 |
+
const cardId = `artifact-card-${index}`;
|
| 71 |
+
|
| 72 |
+
return `
|
| 73 |
+
<div class="flex-none w-80 h-60 border border-gray-200 rounded-lg overflow-hidden bg-white relative group hover:shadow-lg transition-shadow duration-200">
|
| 74 |
+
<!-- Background image -->
|
| 75 |
+
${backgroundImage ? `
|
| 76 |
+
<div class="absolute inset-0 opacity-10">
|
| 77 |
+
<img src="images/${backgroundImage}" alt="" class="w-full h-full object-cover">
|
| 78 |
+
</div>
|
| 79 |
+
` : ''}
|
| 80 |
+
|
| 81 |
+
<!-- Toggle indicator -->
|
| 82 |
+
<div class="absolute top-2 right-2 z-10">
|
| 83 |
+
<button class="toggle-btn w-6 h-6 bg-white bg-opacity-80 hover:bg-opacity-100 rounded-full flex items-center justify-center text-gray-600 hover:text-gray-800 transition-all shadow-sm" onclick="toggleCardView('${cardId}')">
|
| 84 |
+
<svg class="w-3 h-3 expand-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 85 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
| 86 |
+
</svg>
|
| 87 |
+
<svg class="w-3 h-3 collapse-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 88 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
|
| 89 |
+
</svg>
|
| 90 |
+
</button>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<!-- Content -->
|
| 94 |
+
<div class="relative p-4 h-full flex flex-col overflow-hidden" id="${cardId}">
|
| 95 |
+
<!-- Default view -->
|
| 96 |
+
<div class="default-view">
|
| 97 |
+
<!-- Header: Type and Date -->
|
| 98 |
+
<div class="flex justify-between items-start mb-3 pr-8">
|
| 99 |
+
<div class="flex items-center space-x-2">
|
| 100 |
+
<span class="text-lg">${style.icon}</span>
|
| 101 |
+
<span class="text-xs font-medium font-bold uppercase tracking-wide">${type}</span>
|
| 102 |
+
</div>
|
| 103 |
+
<span class="text-xs text-gray-600">${date}</span>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<!-- Title -->
|
| 107 |
+
<div class="mb-4 flex-grow min-h-0">
|
| 108 |
+
<h3 class="font-semibold text-gray-900 text-sm leading-tight line-clamp-3">${title}</h3>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<!-- Bottom section with tags and image -->
|
| 112 |
+
<div class="flex justify-between items-end">
|
| 113 |
+
<!-- Left: Tags -->
|
| 114 |
+
<div class="flex-1 mr-4">
|
| 115 |
+
<!-- Area Tags -->
|
| 116 |
+
<div class="flex flex-wrap gap-1 mb-2">
|
| 117 |
+
${areaTagsHtml}
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
<!-- Sub-area Tags -->
|
| 121 |
+
${subAreaTags.length > 0 ? `
|
| 122 |
+
<div class="flex flex-wrap gap-1">
|
| 123 |
+
${subAreaTagsHtml}
|
| 124 |
+
</div>
|
| 125 |
+
` : ''}
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<!-- Right: Area image with credit on hover -->
|
| 129 |
+
${backgroundImage ? `
|
| 130 |
+
<div class="relative group/image">
|
| 131 |
+
<div class="w-12 h-12 rounded-lg overflow-hidden bg-gray-100 flex items-center justify-center cursor-help" title="${imageCredit}">
|
| 132 |
+
<img src="images/${backgroundImage}" alt="${primaryAreaData.name}" class="w-full h-full object-cover opacity-80">
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
` : ''}
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
<!-- Description view (hidden by default) -->
|
| 140 |
+
<div class="description-view hidden h-full flex flex-col min-h-0">
|
| 141 |
+
<!-- Title (single line with overflow) -->
|
| 142 |
+
<div class="mb-3 flex-shrink-0">
|
| 143 |
+
<h3 class="font-semibold text-gray-900 text-sm leading-tight truncate" title="${title}">${title}</h3>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<!-- Description (scrollable, takes remaining space) -->
|
| 147 |
+
<div class="flex-grow overflow-y-auto min-h-0">
|
| 148 |
+
<p class="text-xs text-gray-700 leading-relaxed">${description}</p>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<!-- URL link (always visible) -->
|
| 153 |
+
${sourceUrl ? `
|
| 154 |
+
<div class="absolute bottom-2 right-2">
|
| 155 |
+
<a href="${sourceUrl}" target="_blank" rel="noopener noreferrer" class="text-gray-400 hover:text-blue-600 transition-colors">
|
| 156 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 157 |
+
<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"></path>
|
| 158 |
+
</svg>
|
| 159 |
+
</a>
|
| 160 |
+
</div>
|
| 161 |
+
` : ''}
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
`;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
export function createArtifactCarousel(artifacts, containerId) {
|
| 168 |
+
const container = document.getElementById(containerId);
|
| 169 |
+
if (!container) return;
|
| 170 |
+
|
| 171 |
+
const cardsHtml = artifacts.map((artifact, index) => createArtifactSummaryCard(artifact, index)).join('');
|
| 172 |
+
|
| 173 |
+
container.innerHTML = `
|
| 174 |
+
<div class="relative">
|
| 175 |
+
<!-- Carousel container -->
|
| 176 |
+
<div class="flex overflow-x-auto scrollbar-hide space-x-4 pb-4" id="${containerId}-scroll">
|
| 177 |
+
${cardsHtml}
|
| 178 |
+
</div>
|
| 179 |
+
|
| 180 |
+
<!-- Navigation arrows -->
|
| 181 |
+
<button class="absolute left-0 top-1/2 transform -translate-y-1/2 bg-white shadow-lg rounded-full p-2 hover:bg-gray-50 transition-colors z-10" onclick="scrollCarousel('${containerId}-scroll', -320)">
|
| 182 |
+
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 183 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
| 184 |
+
</svg>
|
| 185 |
+
</button>
|
| 186 |
+
<button class="absolute right-0 top-1/2 transform -translate-y-1/2 bg-white shadow-lg rounded-full p-2 hover:bg-gray-50 transition-colors z-10" onclick="scrollCarousel('${containerId}-scroll', 320)">
|
| 187 |
+
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 188 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
| 189 |
+
</svg>
|
| 190 |
+
</button>
|
| 191 |
+
</div>
|
| 192 |
+
`;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
// Utility function for carousel navigation
|
| 197 |
+
window.scrollCarousel = function(containerId, scrollAmount) {
|
| 198 |
+
const container = document.getElementById(containerId);
|
| 199 |
+
if (container) {
|
| 200 |
+
container.scrollBy({ left: scrollAmount, behavior: 'smooth' });
|
| 201 |
+
}
|
| 202 |
+
};
|
| 203 |
+
|
| 204 |
+
// Add toggle function
|
| 205 |
+
window.toggleCardView = function(cardId) {
|
| 206 |
+
const card = document.getElementById(cardId);
|
| 207 |
+
const defaultView = card.querySelector('.default-view');
|
| 208 |
+
const descriptionView = card.querySelector('.description-view');
|
| 209 |
+
const expandIcon = card.parentElement.querySelector('.expand-icon');
|
| 210 |
+
const collapseIcon = card.parentElement.querySelector('.collapse-icon');
|
| 211 |
+
|
| 212 |
+
if (defaultView.classList.contains('hidden')) {
|
| 213 |
+
// Show default view
|
| 214 |
+
defaultView.classList.remove('hidden');
|
| 215 |
+
descriptionView.classList.add('hidden');
|
| 216 |
+
expandIcon.classList.remove('hidden');
|
| 217 |
+
collapseIcon.classList.add('hidden');
|
| 218 |
+
} else {
|
| 219 |
+
// Show description view
|
| 220 |
+
defaultView.classList.add('hidden');
|
| 221 |
+
descriptionView.classList.remove('hidden');
|
| 222 |
+
expandIcon.classList.add('hidden');
|
| 223 |
+
collapseIcon.classList.remove('hidden');
|
| 224 |
+
}
|
| 225 |
+
};
|
js/cards/HomeAreaCard.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// HomeAreaCard.js - Area card component for homepage
|
| 2 |
+
import { areasData } from '../data/areas.js';
|
| 3 |
+
|
| 4 |
+
export function createHomeAreaCard(id, title, description, openness, subAreas, imagePosition = 'left', imageAttribution = null, altText = null) {
|
| 5 |
+
const flexDirection = imagePosition === 'left' ? 'lg:flex-row' : 'lg:flex-row-reverse';
|
| 6 |
+
|
| 7 |
+
const subAreasList = subAreas.map(area => `<li>${area}</li>`).join('');
|
| 8 |
+
|
| 9 |
+
// Use provided alt text or fallback to title
|
| 10 |
+
const imgAlt = altText || title;
|
| 11 |
+
|
| 12 |
+
// Create attribution text if provided
|
| 13 |
+
const attribution = imageAttribution ?
|
| 14 |
+
`<p class="text-xs text-gray-500 mt-2 text-center">${imageAttribution}</p>` : '';
|
| 15 |
+
|
| 16 |
+
return `
|
| 17 |
+
<div id="${id}" class="bg-white rounded-lg shadow-sm overflow-hidden" style="min-height: 300px;">
|
| 18 |
+
<div class="flex flex-col ${flexDirection}">
|
| 19 |
+
<div class="lg:w-2/3 p-8">
|
| 20 |
+
<h2 class="text-2xl font-bold text-gray-900 mb-4">${title}</h2>
|
| 21 |
+
<p class="text-gray-700 mb-6">${description}</p>
|
| 22 |
+
|
| 23 |
+
${openness ? `
|
| 24 |
+
<div class="mb-6 px-6 pt-4 pb-6 bg-gradient-to-r from-orange-50 to-yellow-50 border-l-4 border-orange-300 rounded-r-lg">
|
| 25 |
+
<p class="font-bold text-orange-900 mb-3">The Role of Openness</p>
|
| 26 |
+
<p class="text-orange-800 italic leading-relaxed">${openness}</p>
|
| 27 |
+
</div>
|
| 28 |
+
` : ''}
|
| 29 |
+
|
| 30 |
+
<div class="mb-6">
|
| 31 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-3">Sub-areas</h3>
|
| 32 |
+
<ul class="list-disc list-inside text-gray-700 space-y-1">
|
| 33 |
+
${subAreasList}
|
| 34 |
+
</ul>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
<div class="lg:w-1/3 bg-gray-100">
|
| 38 |
+
<div class="h-full flex flex-col items-center justify-center p-8">
|
| 39 |
+
<img src="images/${id}.png" alt="${imgAlt}" class="max-w-full max-h-full object-contain">
|
| 40 |
+
${attribution}
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
`;
|
| 46 |
+
}
|
js/cards/ResourceCard.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// cards/ResourceCard.js - Resource card component for SPA
|
| 2 |
+
import { areasData } from '../data/areas.js';
|
| 3 |
+
|
| 4 |
+
export function createResourceCard(resource) {
|
| 5 |
+
// Get the primary area for this resource
|
| 6 |
+
const primaryArea = areasData[resource.areaTags[0]];
|
| 7 |
+
const primaryColor = primaryArea?.primaryColor || 'gray';
|
| 8 |
+
|
| 9 |
+
// Format date
|
| 10 |
+
const formattedDate = new Date(resource.date).toLocaleDateString('en-US', {
|
| 11 |
+
year: 'numeric',
|
| 12 |
+
month: 'short',
|
| 13 |
+
day: 'numeric'
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
// Generate area tags
|
| 17 |
+
const areaTagsHtml = resource.areaTags.map(areaId => {
|
| 18 |
+
const area = areasData[areaId];
|
| 19 |
+
if (!area) return '';
|
| 20 |
+
return `<span class="px-2 py-1 rounded-full text-xs font-medium ${area.colors.medium}">${area.name}</span>`;
|
| 21 |
+
}).join('');
|
| 22 |
+
|
| 23 |
+
// Generate sub-area tags
|
| 24 |
+
const subAreaTagsHtml = resource.subAreaTags.map(subAreaKey => {
|
| 25 |
+
// Find the sub-area name and color by looking through all areas
|
| 26 |
+
let subAreaName = subAreaKey; // fallback to key if not found
|
| 27 |
+
let textColor = 'text-gray-700'; // fallback color
|
| 28 |
+
|
| 29 |
+
for (const areaKey in areasData) {
|
| 30 |
+
const area = areasData[areaKey];
|
| 31 |
+
if (area.subAreas && area.subAreas[subAreaKey]) {
|
| 32 |
+
const subArea = area.subAreas[subAreaKey];
|
| 33 |
+
subAreaName = typeof subArea === 'string' ? subArea : subArea.name;
|
| 34 |
+
// Extract just the text color from the sub-area color
|
| 35 |
+
if (typeof subArea === 'object' && subArea.color) {
|
| 36 |
+
const colorMatch = subArea.color.match(/text-(\w+)-(\d+)/);
|
| 37 |
+
if (colorMatch) {
|
| 38 |
+
textColor = `text-${colorMatch[1]}-700`; // Use consistent 700 shade
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
break;
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
return `<span class="inline-block px-2 py-0.5 text-xs bg-gray-200 ${textColor} rounded">${subAreaName}</span>`;
|
| 45 |
+
}).join('');
|
| 46 |
+
|
| 47 |
+
// Type badge
|
| 48 |
+
const typeBadge = resource.type === 'dataset' ? 'Dataset' : 'Tool';
|
| 49 |
+
const typeColor = resource.type === 'dataset' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800';
|
| 50 |
+
|
| 51 |
+
return `
|
| 52 |
+
<div class="resource-card bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden flex-shrink-0 relative" style="height: 600px; width: 600px;">
|
| 53 |
+
<!-- Top Section - Single view with all information -->
|
| 54 |
+
<div class="resource-card-top h-48 bg-gradient-to-br from-${primaryColor}-50 to-${primaryColor}-100 relative overflow-hidden">
|
| 55 |
+
<!-- Background image for top section -->
|
| 56 |
+
<div class="absolute inset-0 opacity-10">
|
| 57 |
+
<img src="images/${primaryArea?.image || 'ai.png'}" alt="" class="w-full h-full object-cover">
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<div class="relative z-10 p-4 h-full flex flex-col">
|
| 61 |
+
<!-- Header with type, link, and date -->
|
| 62 |
+
<div class="flex items-center justify-between mb-3">
|
| 63 |
+
<div class="flex items-center gap-2">
|
| 64 |
+
<span class="px-2 py-1 rounded-full text-xs font-medium ${typeColor}">${typeBadge}</span>
|
| 65 |
+
<a href="${resource.url}" target="_blank" class="text-xs text-blue-600 hover:text-blue-800 flex items-center">
|
| 66 |
+
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 67 |
+
<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"></path>
|
| 68 |
+
</svg>
|
| 69 |
+
View Resource
|
| 70 |
+
</a>
|
| 71 |
+
</div>
|
| 72 |
+
<span class="text-xs text-gray-600">${formattedDate}</span>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<!-- Title -->
|
| 76 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-3 leading-tight">${resource.title}</h3>
|
| 77 |
+
|
| 78 |
+
<!-- Description -->
|
| 79 |
+
<p class="text-sm text-gray-700 mb-3 flex-1 overflow-hidden" style="display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;">
|
| 80 |
+
${resource.description}
|
| 81 |
+
</p>
|
| 82 |
+
|
| 83 |
+
<!-- Footer with tags and area image -->
|
| 84 |
+
<div class="flex items-center justify-between">
|
| 85 |
+
<div class="flex flex-wrap gap-1 flex-1">
|
| 86 |
+
${areaTagsHtml}
|
| 87 |
+
${subAreaTagsHtml}
|
| 88 |
+
</div>
|
| 89 |
+
<div class="w-8 h-8 rounded-full bg-white bg-opacity-50 flex items-center justify-center ml-3 flex-shrink-0">
|
| 90 |
+
<img src="images/${primaryArea?.image || 'ai.png'}" alt="" class="w-6 h-6 rounded-full object-cover">
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<!-- Bottom Section - 70% of height -->
|
| 97 |
+
<div class="resource-card-bottom relative" style="height: 420px;">
|
| 98 |
+
${resource.embedUrl ? `
|
| 99 |
+
<!-- Iframe embed -->
|
| 100 |
+
<iframe
|
| 101 |
+
src="${resource.embedUrl}"
|
| 102 |
+
class="w-full h-full border-0"
|
| 103 |
+
loading="lazy"
|
| 104 |
+
title="${resource.title}"
|
| 105 |
+
></iframe>
|
| 106 |
+
` : `
|
| 107 |
+
<!-- Fallback with background image and text -->
|
| 108 |
+
<div class="w-full h-full relative overflow-hidden">
|
| 109 |
+
<div class="absolute inset-0 opacity-50">
|
| 110 |
+
<img src="images/${primaryArea?.image || 'ai.png'}" alt="" class="w-full h-full object-cover">
|
| 111 |
+
</div>
|
| 112 |
+
<div class="absolute inset-0 bg-black bg-opacity-20 flex items-center justify-center">
|
| 113 |
+
<div class="text-center text-white">
|
| 114 |
+
<div class="text-lg font-semibold mb-2">Iframe not available</div>
|
| 115 |
+
<a href="${resource.url}" target="_blank" class="text-sm text-blue-200 hover:text-blue-100 underline">
|
| 116 |
+
View on external site
|
| 117 |
+
</a>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
`}
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
`;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
export function initializeResourceCards() {
|
| 128 |
+
// Resource cards now have a single view, no toggle functionality needed
|
| 129 |
+
// This function is kept for consistency with the calling code
|
| 130 |
+
}
|