Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | 1x 7x 7x 7x 3x 3x 3x 1x 1x 1x 2x 1x 1x 1x 1x 1x 1x 7x 7x 1x 2x | import React from 'react';
import { useTranslation } from 'react-i18next';
import {
getProjectImageUrl,
generatePlaceholderSVGDataUrl,
getTechnologyWords,
convertRawToBlob,
} from './projectsUtils';
import ProjectSummary from './ProjectSummary';
/**
* Renders a single project card with image, summary, technologies, and links.
* Handles progressive image loading and a multi-step fallback chain when images fail.
*
* @param {object} project - Project data object from projects.json
* @param {string} image - Primary image URL resolved by the parent
* @param {number} index - Position in the projects array, used as the loadedImages key
* @param {object} loadedImages - Map of index → boolean tracking which images have loaded
* @param {Function} setLoadedImages - Setter for loadedImages state
* @returns {JSX.Element}
*/
const ProjectCard = ({ project, index, loadedImages = {}, setLoadedImages = () => {}, image }) => {
const { t, i18n } = useTranslation();
const initialSrc = image || `/projects_media/${project.name}/project-image.png`;
const handleError = (e) => {
const img = e.currentTarget;
const tries = parseInt(img.getAttribute('data-try') || '0', 10);
// Three-step fallback: local asset → main branch → master branch → SVG placeholder
if (tries === 0) {
img.setAttribute('data-try', '1');
img.src = getProjectImageUrl(project.name, 'main');
return;
}
if (tries === 1) {
img.setAttribute('data-try', '2');
img.src = getProjectImageUrl(project.name, 'master');
return;
}
Eif (tries === 2) {
img.src = generatePlaceholderSVGDataUrl(project.name);
}
setLoadedImages(prev => ({ ...prev, [index]: true }));
};
// Prefer explicit technologies list; fall back to parsing bold tokens from the README
const technologies = project.technologies?.length > 0
? project.technologies
: getTechnologyWords(project.object?.text);
return (
<div className={'project-card ' + (loadedImages[index] ? 'visible' : '')} key={index}>
<div className="image-wrap">
<img
src={initialSrc}
alt={project.name + ' project'}
className={'project-image ' + (loadedImages[index] ? 'loaded' : '')}
loading="lazy"
onLoad={() => setLoadedImages(prev => ({ ...prev, [index]: true }))}
onError={handleError}
/>
</div>
<div className="project-content">
<h3>{project.name}</h3>
<ProjectSummary project={project} language={i18n.language} t={t} />
<div className="technologies">
{technologies.map((word, idx) => (
<span className="tech-box" key={idx}>{word}</span>
))}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '8px' }}>
<a href={project.url} target="_blank" rel="noopener noreferrer" className="project-link">
{t('viewOnGithub')}
</a>
{project.repoDocs?.productionUrl?.link && (
<a
href={convertRawToBlob(project.repoDocs.productionUrl.link)}
target="_blank"
rel="noopener noreferrer"
className="project-link"
style={{ marginTop: '6px' }}
>
{t('urlLabel')}
</a>
)}
</div>
</div>
</div>
);
};
export default ProjectCard;
|