|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { renderTagSet } from '../utils/tags.js'; |
|
|
|
|
|
|
|
|
const areasData = window.areasData; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function createCard(type, data, options = {}) { |
|
|
const renderers = { |
|
|
artifact: renderArtifactCard, |
|
|
area: renderAreaCard |
|
|
}; |
|
|
|
|
|
const renderer = renderers[type]; |
|
|
if (!renderer) { |
|
|
console.warn(`Unknown card type: ${type}`); |
|
|
return ''; |
|
|
} |
|
|
|
|
|
return renderer(data, options); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderArtifactCard(artifact, options = {}) { |
|
|
const { index = 0 } = options; |
|
|
|
|
|
|
|
|
const title = artifact.title; |
|
|
const date = artifact.date; |
|
|
const type = artifact.type; |
|
|
const description = artifact.description; |
|
|
const areaTags = artifact.areas || artifact.areaTags || []; |
|
|
const subAreaTags = artifact.topics || artifact.subAreaTags || []; |
|
|
const sourceUrl = artifact.url || artifact.sourceUrl || ''; |
|
|
|
|
|
|
|
|
const typeStyles = { |
|
|
'blog': { icon: 'π', textColor: 'text-blue-700' }, |
|
|
'paper': { icon: 'π', textColor: 'text-green-700' }, |
|
|
'dataset': { icon: 'π', textColor: 'text-purple-700' }, |
|
|
'space': { icon: 'π', textColor: 'text-orange-700' }, |
|
|
'external': { icon: 'π', textColor: 'text-gray-700' } |
|
|
}; |
|
|
|
|
|
const style = typeStyles[type] || typeStyles['external']; |
|
|
|
|
|
|
|
|
const primaryArea = areaTags[0]; |
|
|
const primaryAreaData = areasData[primaryArea]; |
|
|
const backgroundImage = primaryAreaData?.image || ''; |
|
|
const imageCredit = primaryAreaData?.imageAttribution || ''; |
|
|
|
|
|
|
|
|
const { areaTagsHtml, topicTagsHtml } = renderTagSet(areaTags, subAreaTags); |
|
|
|
|
|
const cardId = `artifact-card-${index}`; |
|
|
|
|
|
return ` |
|
|
<div class="flex-none w-80 h-60 border border-gray-300 rounded-lg overflow-hidden bg-white/95 shadow-sm relative group hover:shadow-lg transition-shadow duration-200"> |
|
|
<!-- Background image --> |
|
|
${backgroundImage ? ` |
|
|
<div class="absolute inset-0 opacity-10"> |
|
|
<img src="/images/${backgroundImage}" alt="" class="w-full h-full object-cover" loading="lazy"> |
|
|
</div> |
|
|
` : ''} |
|
|
|
|
|
<!-- Toggle indicator --> |
|
|
<div class="absolute top-2 right-2 z-10"> |
|
|
<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}')"> |
|
|
<svg class="w-3 h-3 expand-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> |
|
|
</svg> |
|
|
<svg class="w-3 h-3 collapse-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path> |
|
|
</svg> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<!-- Content --> |
|
|
<div class="relative p-4 h-full flex flex-col overflow-hidden" id="${cardId}"> |
|
|
<!-- Default view --> |
|
|
<div class="default-view"> |
|
|
<!-- Header: Type and Date --> |
|
|
<div class="flex justify-between items-start mb-3 pr-8"> |
|
|
<div class="flex items-center space-x-2"> |
|
|
<span class="text-lg">${style.icon}</span> |
|
|
<span class="text-sm font-bold uppercase tracking-wide">${type}</span> |
|
|
</div> |
|
|
<span class="text-sm text-gray-500">${date}</span> |
|
|
</div> |
|
|
|
|
|
<!-- Title --> |
|
|
<div class="mb-4 flex-grow min-h-0"> |
|
|
<h3 class="font-semibold text-gray-900 text-base leading-snug line-clamp-3">${title}</h3> |
|
|
</div> |
|
|
|
|
|
<!-- Bottom section with tags and image --> |
|
|
<div class="flex justify-between items-end"> |
|
|
<!-- Left: Tags --> |
|
|
<div class="flex-1 mr-4"> |
|
|
<!-- Area Tags --> |
|
|
<div class="flex flex-wrap gap-1 mb-2"> |
|
|
${areaTagsHtml} |
|
|
</div> |
|
|
|
|
|
<!-- Sub-area Tags --> |
|
|
${subAreaTags.length > 0 ? ` |
|
|
<div class="flex flex-wrap gap-1"> |
|
|
${topicTagsHtml} |
|
|
</div> |
|
|
` : ''} |
|
|
</div> |
|
|
|
|
|
<!-- Right: Area image with credit on hover --> |
|
|
${backgroundImage ? ` |
|
|
<div class="relative group/image"> |
|
|
<div class="w-12 h-12 rounded-lg overflow-hidden bg-gray-100 flex items-center justify-center cursor-help" title="${imageCredit}"> |
|
|
<img src="/images/${backgroundImage}" alt="${primaryAreaData.name}" class="w-full h-full object-cover opacity-80" loading="lazy"> |
|
|
</div> |
|
|
</div> |
|
|
` : ''} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Description view (hidden by default) --> |
|
|
<div class="description-view hidden h-full flex flex-col min-h-0"> |
|
|
<!-- Title (single line with overflow) --> |
|
|
<div class="mb-3 flex-shrink-0"> |
|
|
<h3 class="font-semibold text-gray-900 text-base leading-tight truncate" title="${title}">${title}</h3> |
|
|
</div> |
|
|
|
|
|
<!-- Description (scrollable, takes remaining space) --> |
|
|
<div class="flex-grow overflow-y-auto min-h-0"> |
|
|
<p class="text-sm text-gray-700" style="line-height: 1.6;">${description}</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- URL link (always visible) --> |
|
|
${sourceUrl ? ` |
|
|
<div class="absolute bottom-2 right-2"> |
|
|
<a href="${sourceUrl}" target="_blank" rel="noopener noreferrer" class="text-gray-400 hover:text-blue-600 transition-colors"> |
|
|
<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"></path> |
|
|
</svg> |
|
|
</a> |
|
|
</div> |
|
|
` : ''} |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderAreaCard(area, options = {}) { |
|
|
|
|
|
const shortDesc = area.description?.short || area.description.split('.')[0] + '.'; |
|
|
|
|
|
|
|
|
const topics = Object.values(area.topics).map(topic => { |
|
|
const topicName = topic.navName || topic.name; |
|
|
let bgColor = 'bg-gray-200'; |
|
|
let textColor = 'text-gray-700'; |
|
|
|
|
|
|
|
|
if (topic.color) { |
|
|
const bgMatch = topic.color.match(/bg-(\w+)-(\d+)/); |
|
|
const textMatch = topic.color.match(/text-(\w+)-(\d+)/); |
|
|
|
|
|
if (bgMatch) { |
|
|
bgColor = `bg-${bgMatch[1]}-${bgMatch[2]}`; |
|
|
} |
|
|
if (textMatch) { |
|
|
textColor = `text-${textMatch[1]}-700`; |
|
|
} |
|
|
} |
|
|
|
|
|
return { |
|
|
name: topicName, |
|
|
bgColor, |
|
|
textColor |
|
|
}; |
|
|
}); |
|
|
|
|
|
return ` |
|
|
<a href="/${area.id}" |
|
|
class="group relative block border border-gray-300 rounded-lg overflow-hidden bg-white/95 shadow-sm hover:shadow-lg transition-all duration-200 h-64"> |
|
|
|
|
|
<!-- Background image with low opacity --> |
|
|
${area.image ? ` |
|
|
<div class="absolute inset-0 opacity-15 group-hover:opacity-30 transition-opacity"> |
|
|
<img src="/images/${area.image}" alt="" class="w-full h-full object-cover" loading="lazy"> |
|
|
</div> |
|
|
` : ''} |
|
|
|
|
|
<!-- Content --> |
|
|
<div class="relative p-5 h-full flex flex-col"> |
|
|
<!-- Header --> |
|
|
<div class="flex justify-between items-start mb-3 flex-shrink-0"> |
|
|
<h3 class="text-xl font-bold text-gray-900 leading-tight">${area.navTitle}</h3> |
|
|
<svg class="w-5 h-5 text-gray-400 group-hover:text-blue-600 transition-colors flex-shrink-0 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> |
|
|
</svg> |
|
|
</div> |
|
|
|
|
|
<!-- Description - scrollable --> |
|
|
<div class="text-base text-gray-700 mb-4 flex-grow overflow-y-auto pr-2"> |
|
|
<p style="line-height: 1.6;">${shortDesc}</p> |
|
|
</div> |
|
|
|
|
|
<!-- Topics with colors from subAreas --> |
|
|
<div class="flex-shrink-0"> |
|
|
<p class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">Topics:</p> |
|
|
<div class="flex flex-wrap gap-2"> |
|
|
${topics.map(topic => ` |
|
|
<span class="inline-block px-3 py-1.5 text-sm ${topic.bgColor} ${topic.textColor} rounded"> |
|
|
${topic.name} |
|
|
</span> |
|
|
`).join('')} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Image Attribution --> |
|
|
${area.imageAttribution ? ` |
|
|
<p class="text-xs text-gray-500 mt-3 pt-2 border-t border-gray-100 flex-shrink-0">${area.imageAttribution}</p> |
|
|
` : ''} |
|
|
</div> |
|
|
</a> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
export { renderArtifactCard, renderAreaCard }; |
|
|
|
|
|
|