--- import type { CollectionEntry } from 'astro:content'; import { formatDate, processMarkdown, isValidDate } from '../utils/markdown'; import { siteConfig, getPostCardAspectRatio } from '../config'; import { normalizeStatus, getStatusDisplayText, hasStatusStyling, getStatusClasses } from '../utils/status'; import { optimizeContentImagePath, getOptimizedFormat } from '../utils/images'; import ImageWrapper from './ImageWrapper.astro'; import Icon from './Icon.astro'; interface Props { project: CollectionEntry<'projects'>; featured?: boolean; eager?: boolean; context?: 'featured' | 'recent' | 'projects' | 'tags' | 'home'; showCategories?: boolean; } const { project, featured = false, eager = false, context = 'projects', showCategories = true } = Astro.props; // Get excerpt from markdown processing const { excerpt } = processMarkdown(project.body || ''); const { title, description: rawDescription, date, image: rawImage, imageAlt } = project.data; // Process Obsidian bracket syntax in image field (copy from PostCard) const image = (() => { if (!rawImage) return rawImage; // Handle case where rawImage is an array (unquoted YAML syntax) let imageValue = rawImage; if (Array.isArray(rawImage)) { // If it's an array, take the first element imageValue = rawImage[0]; } // Ensure imageValue is a string before calling string methods if (typeof imageValue !== 'string') { console.warn('ProjectCard: imageValue is not a string:', imageValue); return imageValue; } // Check if it's Obsidian double bracket syntax if (imageValue.startsWith('[[') && imageValue.endsWith(']]')) { // Extract the content inside the double brackets return imageValue.slice(2, -2); } return imageValue; })(); const description = rawDescription || excerpt; const categories = project.data.categories || []; const status = normalizeStatus(project.data.status); const repositoryUrl = project.data.repositoryUrl; const projectUrl = project.data.projectUrl || project.data.demoUrl || project.data.demoURL; // Determine content availability for layout const hasDescription = description && description.trim().length > 0; const hasCategories = showCategories && categories.length > 0; const hasHoverContent = hasDescription || hasCategories; // Determine if we should show the cover image // Projects should always show images when available (not controlled by postOptions) const shouldShowCoverImage = true; ---
{image && shouldShowCoverImage && (
{ // Clean the image path by removing Obsidian brackets let cleanImagePath = image; if (!cleanImagePath || typeof cleanImagePath !== 'string') return ''; if (cleanImagePath.startsWith('[[') && cleanImagePath.endsWith(']]')) { cleanImagePath = cleanImagePath.slice(2, -2); } // Remove Obsidian subfolder prefixes (images/, attachments/) if (typeof cleanImagePath === 'string') { // Extract just the filename if it includes attachments/ if (cleanImagePath.includes('attachments/')) { cleanImagePath = cleanImagePath.split('attachments/').pop() || ''; } // Remove images/ prefix if present (Obsidian subfolder setting) else if (cleanImagePath.startsWith('images/')) { cleanImagePath = cleanImagePath.replace('images/', ''); } } const finalSrc = cleanImagePath || ''; // Convert to WebP if applicable (sync-images.js creates WebP versions) return getOptimizedFormat(finalSrc); })()} basePath={(() => { // 1. External URLs - no basePath needed if (typeof image === 'string' && image.startsWith('http')) { return ''; } // 2. Clean the image path to check for attachments/ prefix let cleanImagePath = image; if (typeof cleanImagePath === 'string' && cleanImagePath.startsWith('[[') && cleanImagePath.endsWith(']]')) { cleanImagePath = cleanImagePath.slice(2, -2); } // 3. Detect folder-based vs file-based: if image path starts with 'attachments/', // it's a single-file project (shared attachments folder) if (typeof cleanImagePath === 'string' && cleanImagePath.includes('attachments/')) { // Single-file project with image in attachments folder // The sync script copies these to /projects/attachments/ return "/projects/attachments/"; } // 4. Folder-based project - sync script copies images to project folder root // Safety check: ensure project.id exists before using it if (project.id) { return `/projects/${project.id}/`; } else { // Fallback to attachments directory if project.id is undefined return '/projects/attachments/'; } })()} alt={imageAlt || `Cover image for project: ${title}`} width={800} height={600} loading={eager ? 'eager' : 'lazy'} fetchpriority={eager ? 'high' : 'auto'} class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
)} {!image && (
)} {status && (
{getStatusDisplayText(status)}
)} {isValidDate(date) && (
)}

{title}

{hasHoverContent ? (
{hasDescription && (

{description}

)} {hasCategories && (
{categories.slice(0, 3).map(category => ( {category} ))} {categories.length > 3 && ( +{categories.length - 3} more )}
)}
) : (
{hasDescription && (

{description}

)} {hasCategories && (
{categories.slice(0, 3).map(category => ( {category} ))} {categories.length > 3 && ( +{categories.length - 3} more )}
)}
)}