Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 18 additions & 30 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 57 additions & 14 deletions frontend/src/components/ControlledCarousel.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,52 @@ import teal from '.././img/teal-bg.png';
import red from '.././img/red-bg.png';
import LoadingSpinner from '.././LoadingSpinner';

const GITHUB_REPO_URL_REGEX = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/?#]+)/i;
const GITHUB_OPENGRAPH_BASE_URL = "https://opengraph.githubassets.com/1";

const isValidUrl = (url) => {
try {
const parsed = new URL(url);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch (e) {
return false;
}
};

const getFallbackImage = (color) => {
switch (color) {
case "red":
return red;
case "teal":
return teal;
case "navy":
return navy;
default:
return red;
}
};

const getGithubRepositoryImage = (link) => {
if (!link || !isValidUrl(link)) {
return null;
}

const match = link.match(GITHUB_REPO_URL_REGEX);
if (!match) {
return null;
}

return `${GITHUB_OPENGRAPH_BASE_URL}/${match[1]}/${match[2]}`;
};

const getPortfolioItemImage = (item) => {
if (isValidUrl(item?.img)) {
return item.img;
}

return getGithubRepositoryImage(item?.link) || getFallbackImage(item?.img);
Comment thread
conorheffron marked this conversation as resolved.
};

class ControlledCarousel extends Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -39,19 +85,6 @@ class ControlledCarousel extends Component {
);
}

const handleColor = (color) => {
switch (color) {
case "red":
return red;
case "teal":
return teal;
case "navy":
return navy;
default:
return red;
}
};

return (
<div className="App">
<AppNavbar />
Expand All @@ -60,7 +93,17 @@ class ControlledCarousel extends Component {
{portfolioItems.map((item, index) => (
<Carousel.Item key={index} interval={500}>
<a href={item.link} target="_blank" rel="noreferrer">
<img className="d-block w-100" src={handleColor(item.img)} alt={item.alt} />
<img
className="d-block w-100"
src={getPortfolioItemImage(item)}
alt={item.alt}
loading="lazy"
referrerPolicy="no-referrer"
onError={(event) => {
event.currentTarget.onerror = null;
event.currentTarget.src = getFallbackImage(item?.img);
}}
/>
<Carousel.Caption>
Comment thread
conorheffron marked this conversation as resolved.
Comment thread
conorheffron marked this conversation as resolved.
<h1><u>{item.title}</u></h1>
<h2>{item.description}</h2>
Expand Down
52 changes: 51 additions & 1 deletion frontend/src/components/__tests__/ControlledCarousel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const mockPortfolioItems = [
title: "booking-sys",
description: "Sample Reservations & Viewer System",
techStack: "Python & Django Web App, JavaScript, SQLite3 or MySQL database."
},
{
link: "https://example.com/custom-project",
img: "red",
alt: "red3",
title: "custom-project",
description: "Sample custom project",
techStack: "React, JavaScript"
}
];

Expand Down Expand Up @@ -71,5 +79,47 @@ describe('Portfolio Controlled Carousel', () => {
// Verify that the component does not show the loading spinner
expect(container.querySelector('.LoadingSpinner')).not.toBeInTheDocument();
});
});

test('uses github snapshot images for github links and fallback image for non-github links', async () => {
render(
<MemoryRouter>
<ControlledCarousel />
</MemoryRouter>
);

const githubImage = await screen.findByAltText('navy1');
expect(githubImage).toHaveAttribute(
'src',
'https://opengraph.githubassets.com/1/conorheffron/ironoc-db'
);

const fallbackImage = await screen.findByAltText('red3');
expect(fallbackImage.getAttribute('src')).toContain('red-bg');
});

test('uses item.img directly when it is a valid absolute URL, taking precedence over GitHub snapshot', async () => {
const absoluteImgUrl = 'https://example.com/my-project-image.png';
const itemsWithAbsoluteImg = [
{
link: 'https://github.com/conorheffron/ironoc-db',
img: absoluteImgUrl,
alt: 'absolute-img',
title: 'absolute-img-project',
description: 'Project with absolute img URL',
techStack: 'React'
}
];
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
json: jest.fn().mockResolvedValue(itemsWithAbsoluteImg)
});

render(
<MemoryRouter>
<ControlledCarousel />
</MemoryRouter>
);

const img = await screen.findByAltText('absolute-img');
expect(img).toHaveAttribute('src', absoluteImgUrl);
});
});
Loading