Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
16 changes: 14 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,20 @@

<body>
<div id="header">
<h1>Animation Nation</h1>
<p id="stats"></p>
<div id="header-content">
<h1>Animation Nation</h1>
<p id="stats"></p>
<div id="search-container">
<div id="search-wrapper">
<input
type="text"
id="search-input"
placeholder="Search by art name or author..."
/>
<button id="clear-btn">&times;</button>
</div>
</div>
</div>
</div>

<div class="card-container">
Expand Down
204 changes: 111 additions & 93 deletions public/includes.js
Original file line number Diff line number Diff line change
@@ -1,107 +1,125 @@
// Load the cards.json file using Fetch API
fetch('./public/cards.json')
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json(); // Parse the JSON file content
})
.then((cards) => {
/* Shuffles cards' order */
function shuffle(o) {
for (
let j, x, i = o.length;
i;
j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x
);
return o;
}

/** Creates cards from the array above */
const getCardContents = (cardList) => {
return shuffle(cardList).map((c) => [
`<li class="card">` +
`<a href='${c.pageLink}'>` +
`<img class="art-image" src='${c.imageLink}' alt='${c.artName}' />` +
`</a>` +
`<a class="art-title" href='${c.pageLink}'><h3 >${c.artName}</h3></a>` +
`<p class='author'><a href="${c.githubLink}" target="_blank"><i class="fab fa-github"></i> ${c.author}</a> </p>` +
`</li>`
]);
};

/* Injects cards list HTML into the DOM */
let contents = getCardContents(cards);
document.getElementById('cards').innerHTML = contents;

/* Adds scroll to top arrow button */
window.onscroll = function () {
if (window.scrollY > 100) {
goToTopBtn.classList.add('active');
} else {
goToTopBtn.classList.remove('active');
}
};

// Adds the click event to the button
const goToTopBtn = document.querySelector('.go-to-top');
goToTopBtn.addEventListener('click', function () {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});

// Get element by id "stats" and set the innerHTML to the following
document.getElementById(
'stats'
).innerHTML = `Showcasing ${cards.length} artworks`;
})
.catch((error) => {
console.error('Error fetching the cards.json file:', error);
});
// Shuffles an array's elements into a random order without mutating the original.
function shuffle(array) {
const newArray = [...array];
for (
let j, x, i = newArray.length;
i;
j = parseInt(Math.random() * i),
(x = newArray[--i]),
(newArray[i] = newArray[j]),
(newArray[j] = x)
);
return newArray;
}

// 🎨 Hacktoberfest Card Data
const cardList = [
{
artName: "HACKTOBERFEST",
pageLink: "index.html",
imageLink: "hacktoberfest-logo.png",
author: "Takunda",
githubLink: "https://github.com/Enock12234"
}
];
// Returns a function that delays invoking its callback until after a specified delay.
function debounce(func, delay = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}

// 🔀 Optional shuffle function (add if not defined)
function shuffle(array) {
return array.sort(() => 0.5 - Math.random());
// Renders a generic message ("No results") into a specified container.
function renderMessage(container, message) {
if (!container) return;
container.innerHTML = `<p class="no-results">${message}</p>`;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function naming

Nice to see the refactored function 👌.

The current function name, renderMessage feels quite generic.
As it stands, it could be mistaken for an utility that renders any "type" of message.
However the function has a very specific purpose: rendering a styled message with "no-results" class (statically applied)

To make the intent cleare for others reading or reusing the function, consider renaming it

  • Rename the function to clarify its use (E.g.: renderNoResults, renderNoResultsMessage )


// 🖼️ Generate HTML cards
const getCardContents = (cardList) => {
return shuffle(cardList)
.map((c) => `
// Renders a list of card objects into the main card container.
function renderCards(container, cardList) {
if (!container) return;
if (cardList.length === 0) {
renderMessage(container, "No artworks found.");
return;
}
const html = cardList
.map((card) => `
<li class="card">
<a href='${c.pageLink}'>
<img class="art-image" src='${c.imageLink}' alt='${c.artName}' />
<a href='${card.pageLink || "#"}'>
<img class="art-image" src='${card.imageLink || ""}' alt='${card.artName || "Untitled"}' />
</a>
<a class="art-title" href='${c.pageLink}'>
<h3>${c.artName}</h3>
<a class="art-title" href='${card.pageLink || "#"}'>
<h3>${card.artName || "Untitled"}</h3>
</a>
<p class='author'>
<a href="${c.githubLink}" target="_blank">
<i class="fab fa-github"></i> ${c.author}
<a href="${card.githubLink || "#"}" target="_blank">
<i class="fab fa-github"></i> ${card.author || "Unknown"}
</a>
</p>
</li>
`)
.join('');
};
.join("");
container.innerHTML = html;
}

// Filters cards, updates stats, and triggers re-renders based on search input.
function handleSearch(elements, masterCardList) {
const { searchInput, cardsContainer, statsElement, clearBtn } = elements;
const query = searchInput.value.toLowerCase().trim();

// 🧩 Inject into the DOM
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById("cardContainer");
if (container) {
container.innerHTML = getCardContents(cardList);
clearBtn.classList.toggle("visible", query.length > 0);

if (query === "") {
statsElement.innerHTML = `Showcasing ${masterCardList.length} artworks`;
renderCards(cardsContainer, shuffle(masterCardList));
} else {
const filteredList = masterCardList.filter((card) => {
if (!card) return false;
const artName = (card.artName || "").toLowerCase();
const author = (card.author || "").toLowerCase();
return artName.includes(query) || author.includes(query);
});
statsElement.innerHTML = `Showcasing ${masterCardList.length} artworks | ${filteredList.length} found`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StatsElement similar action

Context: l66 and l76.
Like the renderMessage code you've address, these two lines are sharing a same purpose :
rendering the stats.

Improvements
You could definitely make a function out of is instead of repeating how the message should display

I would suggest a function that accept two parameters:

  • the length of the card list
  • an optional length for the filtered card list

Then the logic would be if there is the second param render the extra bit of states ( found result from the search : "| found" )

Todos

  • Extract the repeated logic to a function :)

renderCards(cardsContainer, filteredList);
}
}

// Fetches initial data and sets up all application event listeners.
async function initApp() {
const elements = {
searchInput: document.getElementById("search-input"),
cardsContainer: document.getElementById("cards"),
statsElement: document.getElementById("stats"),
goToTopBtn: document.querySelector(".go-to-top"),
clearBtn: document.getElementById("clear-btn"),
};

try {
const response = await fetch("./public/cards.json");
if (!response.ok) throw new Error("Failed to fetch cards.json");
const data = await response.json();

const masterCardList = data;

const debouncedSearch = debounce(() => handleSearch(elements, masterCardList), 300);
elements.searchInput.addEventListener("input", debouncedSearch);

elements.clearBtn.addEventListener("click", () => {
elements.searchInput.value = "";
handleSearch(elements, masterCardList);
elements.searchInput.blur();
});

// Manages the initial render, including handling the browser back button state.
handleSearch(elements, masterCardList);

// Sets up the go-to-top button functionality.
window.onscroll = () => {
elements.goToTopBtn.classList.toggle("active", window.scrollY > 100);
};
elements.goToTopBtn.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});

} catch (error) {
console.error("Error initializing app:", error);
renderMessage(elements.cardsContainer, "Error: Could not load artworks.");
}
});
}

// Kicks off the application once the DOM is fully loaded.
document.addEventListener("DOMContentLoaded", initApp);
102 changes: 81 additions & 21 deletions public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,17 @@ ul {
position: fixed;
top: 0;
width: 100%;
padding: 20px;
z-index: 1000;
background: #1a1a1ade;
border-bottom: 2px solid var(--main-20);
}

#header-content {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}

#header h1 {
font-size: 2.5rem;
margin: 0;
Expand All @@ -61,8 +66,61 @@ ul {
color: var(--main-50);
}

#search-container {
margin-top: 20px;
display: flex;
justify-content: center;
padding-top: 20px;
border-top: 2px solid var(--main-20);
}

#search-wrapper {
position: relative;
width: 90%;
max-width: 400px;
display: flex;
}

#search-input {
width: 100%;
padding: 12px;
padding-right: 40px;
border: 1px solid var(--main-40);
border-radius: 8px;
background-color: #2c2c2c;
color: #f1f1f1;
font-size: 1rem;
font-family: 'Comfortaa';
}

#search-input:focus {
outline: none;
border-color: var(--main);
box-shadow: 0 0 8px var(--main-50);
}

#clear-btn {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
background: none;
border: none;
color: #888;
font-size: 24px;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}

#clear-btn.visible {
opacity: 1;
visibility: visible;
}

.card-container {
padding-top: 50px;
padding-top: 220px;
}


Expand All @@ -82,35 +140,29 @@ ul {
flex-wrap: wrap;
}

.flex-content {

}

#cards {
display: flex;
flex-wrap: wrap;
margin: 3vw;
padding: 50px;
grid-gap: 20px;
overflow: hidden;
justify-content: center;
gap: 40px;
padding: 20px;
margin: 0 auto;
max-width: 1400px;
}

.card {
background-color: #1a1a1ace;
padding: 30px;
backdrop-filter: blur(1px);
display: flex;
flex-direction: column;
justify-content: center;
align-content: space-between;
height: 40vh;
min-width: 250px;
width: 18vw;
max-width: 250px;
border: 2px solid var(--main-30);
border-radius: 10px;
margin-top: 20px;
transition: 0.5s ease;
flex-basis: 260px;
flex-grow: 0;
max-width: 260px;
display: flex;
flex-direction: column;
align-items: center;
height: 330px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
overflow: hidden;
}

Expand Down Expand Up @@ -229,4 +281,12 @@ ul {
/* Handles on scrollbar hover */
::-webkit-scrollbar-thumb:hover {
background: rgb(88, 194, 250);
}

.no-results {
color: #888;
font-size: 1.2rem;
width: 100%;
text-align: center;
padding: 50px 0;
}