Yacine Jernite
visuals
5daef86
raw
history blame
11.1 kB
// Card.js - Unified card component
// Uses template strings for HTML generation (simple, clear, and efficient for our use case)
// All card types are defined here for consistency and maintainability
import { renderTagSet } from '../utils/tags.js';
// Use global areasData (loaded in index.html <head>)
const areasData = window.areasData;
/**
* Main card creation function
* @param {string} type - Card type: 'artifact', 'area'
* @param {object} data - Card data
* @param {object} options - Additional options
* @returns {string} HTML string for the card
*/
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);
}
/**
* Renders an artifact card (for carousels)
*/
function renderArtifactCard(artifact, options = {}) {
const { index = 0 } = options;
// Handle both old and new field names
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 || '';
// Type-based styling
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'];
// Get area data for background
const primaryArea = areaTags[0];
const primaryAreaData = areasData[primaryArea];
const backgroundImage = primaryAreaData?.image || '';
const imageCredit = primaryAreaData?.imageAttribution || '';
// Render tags
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>
`;
}
/**
* Renders an area card (for homepage)
*/
function renderAreaCard(area, options = {}) {
// Get short description
const shortDesc = area.description?.short || area.description.split('.')[0] + '.';
// Get topic names and colors from the topics
const topics = Object.values(area.topics).map(topic => {
const topicName = topic.navName || topic.name;
let bgColor = 'bg-gray-200';
let textColor = 'text-gray-700';
// Extract background and text color from the topic color class
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 individual renderers for backward compatibility
export { renderArtifactCard, renderAreaCard };