From 5477191d39f8e5203f28e6de3eb80cfd3c0cb54d Mon Sep 17 00:00:00 2001 From: dereklseitz Date: Tue, 9 Sep 2025 14:52:48 -0500 Subject: [PATCH] feat: Refactor Community Events page Refactored Community Events page to be dynamic and API-driven. Implemented a vertical carousel with category filtering and color-coded event cards. Added new data pipeline to fetch event details from Zoho Calendar API at build time. Fixed infinite rebuild loop in Eleventy by configuring the file watcher to ignore the intermediary data file. --- src/_data/eventsJson.js | 54 ++++++++ src/_data/zohoCalendarEvents.js | 33 ++++- src/community.njk | 56 +++++--- src/events.json.njk | 4 + src/scripts/events-carousel.js | 201 +++++++++++++++++++++++++++++ src/styles/community.css | 222 +++++++++++++++++++++++--------- 6 files changed, 488 insertions(+), 82 deletions(-) create mode 100644 src/_data/eventsJson.js create mode 100644 src/events.json.njk create mode 100644 src/scripts/events-carousel.js diff --git a/src/_data/eventsJson.js b/src/_data/eventsJson.js new file mode 100644 index 0000000..c2492b2 --- /dev/null +++ b/src/_data/eventsJson.js @@ -0,0 +1,54 @@ +module.exports = { normalizedEvents: [ + { + "calid": "3521964000000010003", + "title": "Gardening 101 with Bethany Bloom", + "uid": "6788b6fe00ef48d18749a55366207c76@zoho.com", + "start": "20250909T183000-0500", + "end": "20250909T203000-0500", + "isallday": false, + "location": "Bloom Valley Nursery", + "displayDate": "Tuesday, September 9, 2025 • 6:30 PM – 8:30 PM", + "color": "#FC6060", + "description": "Join Bethany Bloom as she shares her tips for creating a themed decor through-out the home that you will be delighted to share with your guests!\n\n\n\n**This​ is a demo event for demonstration purposes only.**\n\n", + "category": "workshop" + }, + { + "calid": "3521964000000010003", + "title": "Labor Day Sale", + "uid": "1ae512d837304fcc91c8920aa90a1f44@zoho.com", + "start": "20250901", + "end": "20250902", + "isallday": true, + "location": "Bloom Valley Nursery", + "displayDate": "Monday, September 1, 2025", + "color": "#7CAA56", + "description": "Celebrate Labor Day with Bloom Valley Nursery! Enjoy special discounts on select plants, gardening tools, and seasonal decor. This one-day event is a perfect opportunity to stock up on everything you need to make your home and garden thrive. \n\n**This is a demo event for demonstration purposes only—no actual sales will take place.**\n\n", + "category": "sales" + }, + { + "calid": "3521964000000010003", + "title": "Landscaping 101 with Vincent Bloom", + "uid": "60085fc27f5045a5bf462c38c02677b0@zoho.com", + "start": "20250916T183000-0500", + "end": "20250916T203000-0500", + "isallday": false, + "location": "Bloom Valley Nursery", + "displayDate": "Tuesday, September 16, 2025 • 6:30 PM – 8:30 PM", + "color": "#FC6060", + "description": "Come learn from Vincent Bloom, one of Bloom Valley Nursery's family owners, as he guides you through the art and science of landscaping. This workshop is designed for community members who want to create beautiful, sustainable outdoor spaces. We'll explore local plant varieties, discuss eco-friendly landscaping methods, and share tips for a garden that not only looks great but also supports our local ecosystem.\n\n", + "category": "workshop" + }, + { + "calid": "3521964000000010003", + "title": "Johnny Appleseed Day", + "uid": "e0394985af2845fd8c1de4b782c139fb@zoho.com", + "start": "20250926T173000-0500", + "end": "20250926T193000-0500", + "isallday": false, + "location": "Bloom Valley Nursery", + "displayDate": "Friday, September 26, 2025 • 5:30 PM – 7:30 PM", + "color": "#FFC464", + "description": "Celebrate the legacy of Johnny Appleseed with us! Join our hands-on workshop where you'll get to pick fresh, crisp apples right from our orchard and then learn how to properly plant your very own apple tree sapling. Our expert growers will share tips on nurturing your tree so it can thrive for years to come. This is a wonderful, educational event for the whole family to connect with nature and grow something beautiful.\n\n", + "category": "community" + } +] }; \ No newline at end of file diff --git a/src/_data/zohoCalendarEvents.js b/src/_data/zohoCalendarEvents.js index b330cbd..6290532 100644 --- a/src/_data/zohoCalendarEvents.js +++ b/src/_data/zohoCalendarEvents.js @@ -83,7 +83,7 @@ module.exports = async function() { const eventsListUrl = `${zohoApiUrl}/events?range={"start":"${startDate}","end":"${endDate}"}`; console.log(`Making initial API call to: ${eventsListUrl}`); console.log(`Using access token`); - + let response = await fetch(eventsListUrl, { headers: { Authorization: `Zoho-oauthtoken ${accessToken}` } }); @@ -115,36 +115,55 @@ module.exports = async function() { isallday: event.isallday, location: event.location, displayDate: formatEventDate(event.dateandtime.start, event.dateandtime.end, event.isallday), + color: event.color, }; - - console.log(`Processing event with UID: ${basicEvent.uid}`); + // This logic is necessary to get the description from a second API call if (basicEvent.uid) { const eventDetailsUrl = `${zohoApiUrl}/events/${basicEvent.uid}`; console.log(`Making details API call to: ${eventDetailsUrl}`); - + const detailsResponse = await fetch(eventDetailsUrl, { headers: { Authorization: `Zoho-oauthtoken ${accessToken}` } }); if (detailsResponse.ok) { const detailsData = await detailsResponse.json(); - console.log(`Details API call for UID ${basicEvent.uid} successful. Received data:`, detailsData); - if (detailsData.events && detailsData.events.length > 0) { basicEvent.description = detailsData.events[0].description; console.log(`Description added for event UID: ${basicEvent.uid}`); } } else { console.warn(`Failed to fetch details for event UID: ${basicEvent.uid}`); - console.log(`Details response status: ${detailsResponse.status}`); } } + + // Assign category based on color + switch (basicEvent.color) { + case '#7CAA56': + basicEvent.category = 'sales'; + break; + case '#FC6060': + basicEvent.category = 'workshop'; + break; + case '#FFC464': + basicEvent.category = 'community'; + break; + default: + basicEvent.category = 'uncategorized'; + } + return basicEvent; }); const normalizedEvents = await Promise.all(normalizedEventsPromises); console.log("All events processed. Final normalized events array:", normalizedEvents); + + const filePath = path.resolve(__dirname, '..', '_data', 'eventsJson.js'); + const fileContents = `module.exports = { normalizedEvents: ${JSON.stringify(normalizedEvents, null, 2)} };`; + fs.writeFileSync(filePath, fileContents, 'utf-8'); + console.log(`Data successfully written to ${filePath}`); + return normalizedEvents; } catch (error) { diff --git a/src/community.njk b/src/community.njk index 7b6ec52..55c9d8a 100644 --- a/src/community.njk +++ b/src/community.njk @@ -13,6 +13,7 @@ stylesheet: fontAwesome: "https://kit.fontawesome.com/c42448086d.js" currentPage: "community" pageScripts: + - "/scripts/events-carousel.js" - "/scripts/cart.js" - "/scripts/newsletter.js" --- @@ -20,25 +21,50 @@ pageScripts:

Mark Your Calendars!

+

Upcoming Events

- {% for event in zohoCalendarEvents %} -
- {% if event.title %} -

{{ event.title }}

- {% endif %} -

{{ event.displayDate }}

- {% if event.location %} -

Location: {{ event.location }}

- {% endif %} - {% if event.description %} -

{{ event.description }}

- {% endif %} + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
- {% endfor %}
-
-
diff --git a/src/events.json.njk b/src/events.json.njk new file mode 100644 index 0000000..87708b5 --- /dev/null +++ b/src/events.json.njk @@ -0,0 +1,4 @@ +--- +permalink: /data/events.json +--- +{{ eventsJson.normalizedEvents | dump | safe}} \ No newline at end of file diff --git a/src/scripts/events-carousel.js b/src/scripts/events-carousel.js new file mode 100644 index 0000000..803fbda --- /dev/null +++ b/src/scripts/events-carousel.js @@ -0,0 +1,201 @@ +// events-carousel.js + +// --- State Variables --- +let allEvents = []; +let selectedEvents = []; +let currentIndex = 0; + +// --- Helpers --- +const parseZohoTimestamp = (raw) => { + if (!raw && raw !== 0) return new Date(0); + const s = String(raw).trim(); + + let m = s.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})([+-]\d{4}|Z)?$/i); + if (m) { + const [, Y, Mo, D, hh, mm, ss, tz] = m; + let tzPart = ''; + if (tz && tz.toUpperCase() !== 'Z') tzPart = tz.slice(0,3) + ':' + tz.slice(3); + else if (tz && tz.toUpperCase() === 'Z') tzPart = 'Z'; + const iso = `${Y}-${Mo}-${D}T${hh}:${mm}:${ss}${tzPart}`; + const d = new Date(iso); + if (!isNaN(d)) return d; + } + + m = s.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})([+-]\d{4}|Z)?$/i); + if (m) { + const [, Y, Mo, D, hh, mm, tz] = m; + let tzPart = ''; + if (tz && tz.toUpperCase() !== 'Z') tzPart = tz.slice(0,3) + ':' + tz.slice(3); + else if (tz && tz.toUpperCase() === 'Z') tzPart = 'Z'; + const iso = `${Y}-${Mo}-${D}T${hh}:${mm}:00${tzPart}`; + const d = new Date(iso); + if (!isNaN(d)) return d; + } + + m = s.match(/^(\d{4})(\d{2})(\d{2})$/); + if (m) { + const [, Y, Mo, D] = m; + return new Date(Number(Y), Number(Mo) - 1, Number(D)); + } + + const tryDate = new Date(s); + if (!isNaN(tryDate)) return tryDate; + + const mmdd = s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})$/); + if (mmdd) { + let month = parseInt(mmdd[1], 10); + let day = parseInt(mmdd[2], 10); + let year = parseInt(mmdd[3], 10); + if (year < 100) year += 2000; + return new Date(year, month - 1, day); + } + + return new Date(0); +}; + +const eventTimestamp = (ev) => { + if (!ev) return 0; + const candidate = ev.start || ev.displayDate || ev.date || ev.eventDate || ev.createdAt || ev.created; + return parseZohoTimestamp(candidate).getTime(); +}; + +// --- Render the visible items --- +const renderEvents = () => { + const eventList = document.getElementById("event-list"); + if (!eventList) return; + eventList.innerHTML = ""; + + if (selectedEvents.length === 0) { + eventList.innerHTML = `

No events found for this category.

`; + return; + } + + const lastIndex = selectedEvents.length - 1; + let visibleIndexes = []; + + if (selectedEvents.length <= 3) { + visibleIndexes = Array.from({ length: selectedEvents.length }, (_, i) => i); + } else if (currentIndex === 0) { + visibleIndexes = [0, 1, 2]; + } else if (currentIndex === lastIndex) { + visibleIndexes = [lastIndex - 2, lastIndex - 1, lastIndex]; + } else { + visibleIndexes = [currentIndex - 1, currentIndex, currentIndex + 1]; + } + + visibleIndexes = visibleIndexes.filter(i => i >= 0 && i <= lastIndex); + + visibleIndexes.forEach((index) => { + const event = selectedEvents[index]; + if (!event) return; + + const card = document.createElement("div"); + card.className = "event-card"; + + card.addEventListener('click', () => { + if (index !== currentIndex) { + currentIndex = index; + renderEvents(); + } + }); + + if (index === currentIndex) { + card.classList.add("is-expanded"); + card.innerHTML = ` +

${event.title || ''}

+

${event.displayDate || event.start || ''}

+

${event.location ? `Location: ${event.location}` : ''}

+

${event.description ? event.description : ''}

+ `; + } else { + card.classList.add("is-collapsed"); + card.innerHTML = ` +

${event.title || ''}

+

${event.displayDate || event.start || ''}

+ `; + card.style.cursor = 'pointer'; + } + + eventList.appendChild(card); + }); +}; + +// --- Navigation Functions --- +const scrollUp = () => { + if (currentIndex > 0) { + currentIndex--; + renderEvents(); + } +}; + +const scrollDown = () => { + if (currentIndex < selectedEvents.length - 1) { + currentIndex++; + renderEvents(); + } +}; + +// --- Category Update and Filtering --- +const updateEventList = (category) => { + const scrollUpButton = document.getElementById('scroll-up'); + const scrollDownButton = document.getElementById('scroll-down'); + if (scrollUpButton) scrollUpButton.removeEventListener('click', scrollUp); + if (scrollDownButton) scrollDownButton.removeEventListener('click', scrollDown); + + if (category === 'all') { + selectedEvents = allEvents.slice(); + } else { + selectedEvents = allEvents.filter(event => event.category === category); + } + + selectedEvents.sort((a, b) => eventTimestamp(a) - eventTimestamp(b)); + + currentIndex = 0; + renderEvents(); + + if (scrollUpButton) scrollUpButton.addEventListener('click', scrollUp); + if (scrollDownButton) scrollDownButton.addEventListener('click', scrollDown); +}; + +// --- Initialization --- +document.addEventListener('DOMContentLoaded', () => { + fetch("../data/events.json") + .then(response => { + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); + return response.json(); + }) + .then(data => { + allEvents = Array.isArray(data) ? data : (data.normalizedEvents || []); + console.log("Events data loaded successfully:", allEvents); + + const categoryButtons = document.querySelectorAll(".cat-btn"); + categoryButtons.forEach(button => { + button.addEventListener('click', (event) => { + event.preventDefault(); + + // --- Active button logic --- + categoryButtons.forEach(btn => btn.parentElement.classList.remove('active')); + button.parentElement.classList.add('active'); + + const url = new URL(button.href); + const category = url.searchParams.get('category'); + updateEventList(category); + }); + }); + + // Set default category and active button + const currentUrl = new URL(window.location.href); + const defaultCategory = currentUrl.searchParams.get('category') || 'all'; + updateEventList(defaultCategory); + categoryButtons.forEach(btn => { + const url = new URL(btn.href); + const btnCategory = url.searchParams.get('category'); + if (btnCategory === defaultCategory) btn.parentElement.classList.add('active'); + }); + }) + .catch(error => { + console.error('Problem fetching events data:', error); + const eventList = document.getElementById("event-list"); + if (eventList) eventList.innerHTML = `

Failed to load events. Please try again later.

`; + }); +}); diff --git a/src/styles/community.css b/src/styles/community.css index c68255f..4d0d5d6 100644 --- a/src/styles/community.css +++ b/src/styles/community.css @@ -2,9 +2,10 @@ .community { display: flex; flex-direction: column; + justify-content: center; align-items: center; text-align: center; - max-width: 900px; + max-width: 1000px; } h1 { @@ -27,9 +28,106 @@ h1 { border: none; } +.upcoming-events { + display: flex; + flex-direction: column; + gap: 25px; +} + +#events-navigation { + display: flex; + flex-direction: row; + text-align: center; + align-items: center; + margin: 0; + padding: 0; + +} + +ul.event-categories { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + text-align: center; + list-style-type: none; + gap: 50px; + margin: 10px auto; +} + +li.enav { + display: flex; + width: auto; + margin: 0; + border: solid 1px var(--color-dark-green); + background-color: var(--color-destin-sand); + border-radius: 10px; + white-space: nowrap; + box-shadow: var(--shadow-medium); + transition: background-color 0.3s ease, color 0.3s ease; +} + +.cat-btn { + display: block; + justify-content: center; + align-items: center; + margin: 0; + padding: 2px 50px; + width: 100%; + height: 100%; + color: var(--color-dark-green); + font-size: 1.4rem; + text-decoration: none; + font-weight: 600; +} + +.sales-btn:hover { + background-color: rgba(124, 170, 86, 0.5); /*#7CAA56*/ +} + +.celebrations-btn:hover { + background: rgba(255, 196, 100, 0.5); /*#FFC464*/ +} + +.workshops-btn:hover { + background: rgba(252, 96, 96, 0.5); /*#FC6060*/ +} + +.all-btn:hover { + background: var(--color-light-teal); +} + +/* --- Active button states --- */ +.event-categories li.enav.active {} +.workshops-btn.active { + background-color: rgba(252, 96, 96, 0.5); +} +.celebrations-btn.active { + background-color: rgba(255, 196, 100, 0.5); +} +.sales-btn.active { + background-color: rgba(124, 170, 86, 0.5); +} +.all-btn.active { + background-color: var(--color-light-teal); +} + +.interactive-events { + display:flex; + flex-direction: row; + align-items: center; + gap: 30px; +} + +.events-left { + width: 50%; + align-items: center; + justify-content: center; +} + .calendar iframe { - width: 975px; - height: 750px; + width: 600px; + height: 550px; margin-top: 25px; margin-bottom: 20px; background-color: var(--color-light-teal); @@ -37,26 +135,56 @@ h1 { box-shadow: var(--shadow-subtle); } -.upcoming-events { - display: flex; - flex-direction: column; - gap: 25px; +.events-right { + width: 50%; + align-items: center; + justify-content: center; } -.events { +.scroll-direction { + display: flex; + justify-content: center; + width: 100%; + padding: 10px 0; +} + +#scroll-up, #scroll-down { + cursor: pointer; + background-color: var(--color-dark-green); + color: white; + border: none; + padding: 10px; + border-radius: 50%; + font-size: 1.5rem; + display: flex; + justify-content: center; + align-items: center; +} + +#event-list { + display: flex; + flex-direction: column; + gap: 20px; + width: auto; + height: 450px; + overflow-y: hidden; + justify-content: center; +} + +.event-card { display: flex; flex-direction: column; align-items: center; text-align: center; - background-color: var(--color-soft-golden); + background-color: var(--color-destin-sand); border: solid var(--color-dark-green); border-radius: 15px; padding: 10px; - width: 80vw; + width: 600px; box-shadow: var(--shadow-subtle); } -.events h2 { +.upcoming-events h2 { font-size: 1.875rem; text-align: center; text-shadow: 0.5px 0.5px .5px var(--color-dark-green), @@ -67,6 +195,8 @@ h1 { background-color: var(--color-mid-green); border: solid var(--color-destin-sand); border-radius: 10px; + margin-top: 20px; + margin-botom: 0; margin-left: auto; margin-right: auto; width: fit-content; @@ -88,65 +218,37 @@ h1 { box-shadow: var(--shadow-subtle); } -.zoho-event { - background-color: var(--color-destin-sand); - border: solid var(--color-dark-green) 2px; - border-radius: 15px; - box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.8); - max-width: 1200px; +.event-card h3, +.event-card p { + padding: 0; + margin: 0; } -.zoho-event h3 { - font-size: 1.875rem; - text-align: center; - text-shadow: 0.5px 0.5px .5px var(--color-destin-sand), - -0.5px -0.5px .5px var(--color-destin-sand), - -0.5px 0.5px .5px var(--color-destin-sand), - 0.5px -0.5px .5px var(--color-destin-sand); - color: var(--color-dark-green); - background-color: var(--color-light-teal); - border: solid var(--color-destin-sand); +.event-card h3 { + border: solid 0.5px var(--color-dark-green); border-radius: 10px; - margin-left: auto; - margin-right: auto; - width: fit-content; - padding: 0 50px; - box-shadow: var(--shadow-medium); + padding: 2px 20px; } -.zoho-event p { - margin-left: auto; - margin-right: auto; +.event-card h3.workshop { + background-color: rgba(252, 96, 96, 0.5); } -.display-date, .location, .add-events { - font-weight: heavy; - font-size: 1.5rem; +.event-card h3.community { + background-color: rgba(255, 196, 100, 0.5); } -.description { - font-size: 1.3rem; - width: 80%; - height: auto; +.event-card h3.sales { + background-color: rgba(124, 170, 86, 0.5); } -.event-list ul { - list-style-type: none; +.event-card.is-collapsed:hover { + cursor: pointer; + transform: translateY(-2px); + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + transition: transform 0.2s ease, box-shadow 0.2s ease; } -.event-list li { - background-color: var(--color-destin-sand); - border: solid var(--color-light-teal); - border-radius: 15px; - text-align: center; - padding: 5px 10px; - margin-bottom: 10px; - box-shadow: var(--shadow-subtle); -} - -.events strong { - color: var(--color-dark-green); - font-size: 1.1rem; -} - -/* | ↑-↑-↑ End Community.html ↑-↑-↑--| */ \ No newline at end of file +.add-events p { + font-size: 1.2rem; +} \ No newline at end of file