All files / src/components/Projects ProjectCard.js

91.3% Statements 21/23
85% Branches 17/20
57.14% Functions 4/7
100% Lines 21/21

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;