Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recipe Gallery Sphinx Directive #1150

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
129 changes: 129 additions & 0 deletions docs/_static/css/recipe_gallery.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/* Recipe Gallery */

.hidden {
display: none;
}

/* Style used in recipes.rst file only for testing */
#recipe-tag-search-bar {
border: 1px solid black;
}

/* Shared styles for both styles of recipe gallery */
.recipe-gallery-container {
height: auto;
padding: 0 !important;
position: relative;
overflow: hidden;
}

.recipe-gallery {
display: flex;
}

.recipe-card {
padding: 5px !important;
text-align: center;
margin-right: 10px;
margin-left: 0;
margin-top: 10px;
box-sizing: border-box;
height: auto;
border: 2px solid #e5e7eb;
border-radius: 5px;
}

.recipe-image-container {
width: 85%;
aspect-ratio: 1 / 1;
padding: 2% !important;
}

.recipe-link {
width: 100%;
height: 100%;
}

.recipe-card img {
width: 100%;
height: 100%;
object-fit: contain;
margin-top: 0 !important;
}

/* Paragraph elements for recipe tags */
.recipe-tags {
display: none;
}

/* Recipe Gallery with carousel layout */
.carousel {
width: 100%;
box-sizing: border-box;
transition: transform 0.5s ease;
padding: 0 10% !important;
white-space: normal;
position: relative;
margin: 0 !important;
}

.carousel-button-left-container, .carousel-button-right-container {
width: calc(10% - 10px) !important;
align-items: center;
justify-content: center;
height: calc(100% - 10px); /* Account for margin-top of recipe cards so the buttons line up with height */
position: absolute;
z-index: 1000;
padding: 0 !important;
margin: 0 !important;
margin-top: 10px !important; /* Account for margin-top of recipe cards */
top: 0;
}

.carousel-button-left-container {
left: 0;
}

.carousel-button-right-container {
right: 0;
}

.carousel-button-left-container:hover .carousel-button-left, .carousel-button-right-container:hover .carousel-button-right {
opacity: 1;
visibility: visible;
}

.carousel-button-left, .carousel-button-right {
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.650);
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
color: white;
}

.carousel-button-left {
padding-left: 35%;
}

.carousel-button-right {
padding-right: 35%;
}

.carousel .recipe-card {
/* flex: 0 0 20%; */
flex: 0 0 calc((100% - (3 * 10px)) /4);
}

/* Recipe Gallery with multi-row layout */
.multi-row {
width: 100%;
justify-items: start;
padding: 0 !important;
flex-wrap: wrap;
}

.multi-row .recipe-card {
flex: 0 0 calc(25% - 10px);
}
1 change: 1 addition & 0 deletions docs/_static/css/tethys.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,4 @@ pre {
padding-left: 1rem;
padding-right: 1rem;
}

164 changes: 164 additions & 0 deletions docs/_static/js/recipe_gallery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
function prepareCarousel(carouselContainer) {
let leftButton = carouselContainer.querySelector(".carousel-button-left");
let rightButton = carouselContainer.querySelector(".carousel-button-right");
let carousel = carouselContainer.querySelector(".carousel");

function getCards() {
// Function to get all visible cards in the carousel that are not cloned
return Array.from(carousel.querySelectorAll(".recipe-card")).filter(card => !card.classList.contains("hidden") && !card.classList.contains("cloned"));
}

function getNumOfCards() {
return getCards().length;
}

function getMaxIndex() {
// Function to get the maximum index of the carousel accounting for the cloned cards at the beginning
return getCards().length + 5;
}

let cards = getCards();

let cardWidth;
let cardMargin;
let extraCardsInLastSlide;

if(getNumOfCards() > 0) {
cardWidth = cards[0].getBoundingClientRect().width;
cardMargin = parseInt(getComputedStyle(cards[0]).marginRight);
extraCardsInLastSlide = getNumOfCards() % 4;
}

// Set the initial index to 5 to account for cloned cards at the beginning
let currentIndex = 5;

// Only show and prepare buttons if there are more than 4 cards
if (getNumOfCards() > 4) {
leftButton.classList.remove("hidden");
rightButton.classList.remove("hidden");

// Clone the first and last 5 cards to the opposite end of the carousel to create the illusion of infinite scrolling
let lastSlideClones = cards.slice(-5).map(card => {
let clone = card.cloneNode(true);
clone.classList.add("cloned");
return clone;
});
let firstSlideClones = cards.slice(0, 4).map(card => {
let clone = card.cloneNode(true);
clone.classList.add("cloned");
return clone;
});
lastSlideClones.forEach(clone => carousel.insertBefore(clone, cards[0]));
firstSlideClones.forEach(clone => carousel.appendChild(clone));

setInitialPosition();

leftButton.addEventListener("click", () => {
updateCarousel(-4);
});

rightButton.addEventListener("click", () => {
updateCarousel(4);
});
} else {
leftButton.classList.add("hidden");
rightButton.classList.add("hidden");
setTimeout(() => {
carousel.style.transition = "none";
carousel.style.transform = `translateX(${0}px)`;
setTimeout(() => carousel.style.transition = "transform 0.5s ease", 50); // Re-enable transition
}, 500);
}

function updateCarousel(offset) {
// Function to update the carousel position by the given offset
cards = getCards();
let maxIndex = getMaxIndex();
let remainingCards = maxIndex - (currentIndex + offset);

// Check if there is less than 4 cards remaining in the carousel, and if so, move that many remaining cards to the right.
if (offset > 0 && remainingCards <= 4 && remainingCards > 0 && currentIndex < maxIndex) {
currentIndex += remainingCards
// Check if returning to the first slide with less than 4 cards to reach the beginning of the carousel, if so, move that many cards to the left.
} else if (offset < 0 && currentIndex + offset < 5 && currentIndex > 5) {
currentIndex = 5;
} else {
currentIndex += offset;
}
// Calculate the new position of the carousel
let shift = -currentIndex * (cardWidth + cardMargin);
carousel.style.transform = `translateX(${shift}px)`;
checkBounds();
}

function checkBounds() {
// Check if carousel is at either end of the slides, and if so, reset the position to the
// opposite end seamlessly without any transition to create an illusion of infinite scrolling
let numOfCards = getNumOfCards();
let isAtFirstSlide = currentIndex >= numOfCards + 5;
let isAtLastSlide = currentIndex < 5;

if (isAtLastSlide || isAtFirstSlide) {
setTimeout(() => {
carousel.style.transition = "none"; // Disable transition
if (isAtFirstSlide) {
currentIndex = 5;
}
if (isAtLastSlide) {
currentIndex = numOfCards + 1;
}

let shift = -currentIndex * (cardWidth + cardMargin);
carousel.style.transform = `translateX(${shift}px)`;
setTimeout(() => carousel.style.transition = "transform 0.5s ease", 50); // Re-enable transition
}, 500);
}
}

function setInitialPosition() {
// Set the initial position of the carousel to the first slide
carousel.style.transition = "none"; // Disable transition to avoid animation on initial load
let shift = -currentIndex * (cardWidth + cardMargin);
carousel.style.transform = `translateX(${shift}px)`;
setTimeout(() => carousel.style.transition = "transform 0.5s ease-in-out", 50); // Re-enable transition
}
}

document.addEventListener("DOMContentLoaded", () => {
let carouselContainers = document.querySelectorAll(".carousel-container");
// Initialize all carousel containers
carouselContainers.forEach(carouselContainer => {
prepareCarousel(carouselContainer);
});

// Add event listener to search bar to filter recipes by tags
var searchBar = document.querySelector("#recipe-tag-search-bar");
if (searchBar) {
searchBar.addEventListener("input", () => {
let searchValue = searchBar.value.toLowerCase();

carouselContainers.forEach(carouselContainer => {
let carousel = carouselContainer.querySelector(".carousel");

// Remove all cloned cards in preparation to re-initialize the carousel
let clonedCards = carousel.querySelectorAll(".recipe-card.cloned");
clonedCards.forEach(clone => clone.remove());

// Hide all cards without tags that match the search terms
let cards = document.querySelectorAll(".recipe-card");
cards.forEach(card => {
// Check if the tags of the card contain the search value
let tags = card.querySelector(".recipe-tags").textContent.toLowerCase();
if (tags.includes(searchValue)) {
card.classList.remove("hidden");
} else {
card.classList.add("hidden");
}
});

// Re-initialize the carousel container
prepareCarousel(carouselContainer);
});
});
}
});
13 changes: 13 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
from sphinxawesome_theme import ThemeOptions, LinkIcon
from sphinxawesome_theme.postprocess import Icons

from directives import RecipeGallery

# Add the current directory to sys.path
sys.path.insert(0, str(Path(__file__).parent))

# Mock Dependencies
# NOTE: No obvious way to automatically anticipate all the sub modules without
# installing the package, which is what we are trying to avoid.
Expand Down Expand Up @@ -273,8 +278,12 @@ def __getattr__(cls, name):
html_static_path = ["_static"]
html_css_files = [
"css/tethys.css",
"css/recipe_gallery.css",
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css", # Font Awesome for arrow icons in recipe carousels
]

html_js_files = ["js/recipe_gallery.js"]

html_theme = "sphinxawesome_theme"
theme_options = ThemeOptions(
main_nav_links={
Expand Down Expand Up @@ -302,3 +311,7 @@ def __getattr__(cls, name):

# Link icon for header links instead of pharagraph icons that are the default
html_permalinks_icon = Icons.permalinks_icon


def setup(app):
app.add_directive("recipe-gallery", RecipeGallery)
Loading
Loading