diff --git a/blocks/event-teasers/_event-teasers.json b/blocks/event-teasers/_event-teasers.json new file mode 100644 index 0000000..d28e314 --- /dev/null +++ b/blocks/event-teasers/_event-teasers.json @@ -0,0 +1,75 @@ +{ + "definitions": [ + { + "title": "Event Teasers Slider", + "id": "event-teasers", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block", + "template": { + "name": "Event Teasers Slider", + "model": "event-teasers", + "filter": "event-teasers" + } + } + } + } + } + ], + "models": [ + { + "id": "event-teasers", + "fields": [ + { + "component": "text", + "name": "category", + "label": "Event Category" + }, + { + "component": "text", + "name": "title", + "label": "Event Title" + }, + { + "component": "text", + "name": "date", + "label": "Event Date" + }, + { + "component": "text", + "name": "location", + "label": "Event Location" + }, + { + "component": "text", + "name": "link", + "label": "Event Link" + }, + { + "component": "text", + "name": "label", + "value": "LEARN MORE", + "label": "Button Label" + }, + { + "component": "select", + "name": "target", + "label": "Link Target", + "options": [ + { + "name": "Same Window", + "value": "" + }, + { + "name": "New Window", + "value": "_blank" + } + ] + } + ] + } + ], + "filters": [] +} + diff --git a/blocks/event-teasers/event-teasers.css b/blocks/event-teasers/event-teasers.css new file mode 100644 index 0000000..c4dab7d --- /dev/null +++ b/blocks/event-teasers/event-teasers.css @@ -0,0 +1,113 @@ +/* Event Teasers Slider */ + +.event-teasers-wrapper { + position: relative; + width: 100%; + display: flex; + align-items: center; + gap: var(--spacing-m); +} + +.event-teasers-slider { + overflow-x: auto; + scroll-behavior: smooth; + display: flex; + gap: var(--spacing-m); + scroll-snap-type: x mandatory; + flex: 1 1 auto; +} + +.event-teaser-card { + flex: 0 0 calc(100% - var(--spacing-m)); + max-width: calc(100% - var(--spacing-m)); + display: flex; + flex-direction: column; + gap: var(--spacing-s); + padding: var(--spacing-l); + border: 1px solid rgb(var(--color-border)); + border-radius: 4px; + background-color: transparent; + transition: background-color 0.3s; + scroll-snap-align: start; +} + +.event-teaser-card:hover { + background-color: rgb(var(--color-pure-white)); +} + +.event-teaser-info { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.event-divider { + height: 1px; + background-color: rgb(var(--color-border)); + width: 100%; + margin: var(--spacing-xs) 0; +} + +.event-details { + display: flex; + gap: var(--spacing-xs); + font-size: var(--body-font-size-s); + color: rgb(var(--color-secondary-text)); +} + +.event-teaser-button .button { + margin-top: auto; +} + +/* Responsive card widths */ + +@media (width >= 600px) { + .event-teaser-card { + flex-basis: calc(50% - var(--spacing-m)); + max-width: calc(50% - var(--spacing-m)); + } +} + +@media (width >= 900px) { + .event-teaser-card { + flex-basis: calc(33.333% - var(--spacing-m)); + max-width: calc(33.333% - var(--spacing-m)); + } +} + +@media (width >= 1200px) { + .event-teaser-card { + flex-basis: calc(25% - var(--spacing-m)); + max-width: calc(25% - var(--spacing-m)); + } +} + +/* Navigation buttons */ + +.slider-prev, +.slider-next { + background: none; + border: none; + width: 32px; + height: 32px; + cursor: pointer; + mask-size: contain; + mask-repeat: no-repeat; + background-color: rgb(var(--color-text)); + flex: 0 0 auto; +} + +.slider-prev { + mask-image: url('/icons/arrow-left.svg'); +} + +.slider-next { + mask-image: url('/icons/arrow-right.svg'); +} + +.slider-prev:disabled, +.slider-next:disabled { + opacity: 0.3; + cursor: default; +} + diff --git a/blocks/event-teasers/event-teasers.js b/blocks/event-teasers/event-teasers.js new file mode 100644 index 0000000..929bbc2 --- /dev/null +++ b/blocks/event-teasers/event-teasers.js @@ -0,0 +1,178 @@ +import { setBlockItemOptions } from '../../scripts/utils.js'; +import { renderButton } from '../../components/button/button.js'; + +/* + * Event-Teasers block + * ─────────────────── + * Expected authoring markup (rows → cols): + * | Category | Title | Date | Location | Link | Label | Target | + * Each row represents one event teaser. + * The block converts these rows into teaser cards and arranges them + * in an horizontally scrollable slider that shows 4 items per “page” + * on desktop, fewer on smaller breakpoints. + */ + +function buildTeaserCard(cfg) { + const wrapper = document.createElement('article'); + wrapper.className = 'event-teaser-card'; + + // Event info container + const info = document.createElement('div'); + info.className = 'event-teaser-info'; + + const category = document.createElement('div'); + category.className = 'event-category'; + category.textContent = cfg.category || 'Event'; + + const title = document.createElement('h3'); + title.className = 'event-title'; + title.textContent = cfg.title || 'Untitled Event'; + + const divider = document.createElement('div'); + divider.className = 'event-divider'; + + const details = document.createElement('div'); + details.className = 'event-details'; + + if (cfg.date) { + const date = document.createElement('span'); + date.className = 'event-date'; + date.textContent = cfg.date; + details.append(date); + } + + if (cfg.location) { + const loc = document.createElement('span'); + loc.className = 'event-location'; + loc.textContent = cfg.location; + details.append(loc); + } + + info.append(category, title, divider, details); + + const btnContainer = document.createElement('div'); + btnContainer.className = 'event-teaser-button'; + + if (cfg.label) { + const btn = renderButton({ + link: cfg.link, + label: cfg.label, + target: cfg.target, + block: wrapper, + }); + btn.classList.add('outline'); + btnContainer.append(btn); + } + + wrapper.append(info, btnContainer); + + return wrapper; +} + +function collectConfigs(block) { + const map = [ + { name: 'category' }, + { name: 'title' }, + { name: 'date' }, + { name: 'location' }, + { name: 'link' }, + { name: 'label' }, + { name: 'target' }, + ]; + + const configs = []; + [...block.children].forEach((row) => { + setBlockItemOptions(row, map, configs); + }); + return configs; +} + +function buildNavigationButton(dir, label) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = `slider-${dir}`; + btn.setAttribute('aria-label', label); + return btn; +} + +export default function decorate(block) { + // Collect data from authoring rows + const configs = collectConfigs(block); + + // Build main DOM structure + const wrapper = document.createElement('div'); + wrapper.className = 'event-teasers-wrapper'; + + const slider = document.createElement('div'); + slider.className = 'event-teasers-slider'; + + configs.forEach((cfg) => { + const card = buildTeaserCard(cfg); + slider.append(card); + }); + + // Navigation + const prevBtn = buildNavigationButton('prev', 'Previous events'); + const nextBtn = buildNavigationButton('next', 'Next events'); + + wrapper.append(prevBtn, slider, nextBtn); + + // Clean and inject + block.textContent = ''; + block.append(wrapper); + + // Slider logic + const getPageWidth = () => slider.getBoundingClientRect().width; + const cardsPerPage = () => { + if (window.innerWidth >= 1200) return 4; + if (window.innerWidth >= 900) return 3; + if (window.innerWidth >= 600) return 2; + return 1; + }; + + let pageIndex = 0; + const maxPage = () => Math.max(0, Math.ceil(configs.length / cardsPerPage()) - 1); + + const update = () => { + const scrollTo = pageIndex * getPageWidth(); + slider.scrollTo({ left: scrollTo, behavior: 'smooth' }); + prevBtn.disabled = pageIndex === 0; + nextBtn.disabled = pageIndex === maxPage(); + }; + + prevBtn.addEventListener('click', () => { + pageIndex = Math.max(0, pageIndex - 1); + update(); + }); + nextBtn.addEventListener('click', () => { + pageIndex = Math.min(maxPage(), pageIndex + 1); + update(); + }); + + // Swipe support + let touchStartX = 0; + slider.addEventListener('touchstart', (e) => { + touchStartX = e.touches[0].clientX; + }); + slider.addEventListener('touchend', (e) => { + const diff = e.changedTouches[0].clientX - touchStartX; + if (Math.abs(diff) > 50) { + if (diff < 0) { + pageIndex = Math.min(maxPage(), pageIndex + 1); + } else { + pageIndex = Math.max(0, pageIndex - 1); + } + update(); + } + }); + + window.addEventListener('resize', () => { + // Reset page index when cards per page changes to keep slider in bounds + pageIndex = Math.min(pageIndex, maxPage()); + update(); + }); + + // Initial state + update(); +} + diff --git a/component-definition.json b/component-definition.json index f58cade..f761f37 100644 --- a/component-definition.json +++ b/component-definition.json @@ -276,6 +276,22 @@ } } }, + { + "title": "Event Teasers Slider", + "id": "event-teasers", + "plugins": { + "xwalk": { + "page": { + "resourceType": "core/franklin/components/block/v1/block", + "template": { + "name": "Event Teasers Slider", + "model": "event-teasers", + "filter": "event-teasers" + } + } + } + } + }, { "title": "Fragment", "id": "fragment", diff --git a/component-filters.json b/component-filters.json index ff802ed..35475bd 100644 --- a/component-filters.json +++ b/component-filters.json @@ -26,6 +26,7 @@ "video", "gallery", "event-teaser", + "event-teasers", "framed-grid" ] }, diff --git a/component-models.json b/component-models.json index 97e7276..0e1f3ac 100644 --- a/component-models.json +++ b/component-models.json @@ -631,6 +631,57 @@ } ] }, + { + "id": "event-teasers", + "fields": [ + { + "component": "text", + "name": "category", + "label": "Event Category" + }, + { + "component": "text", + "name": "title", + "label": "Event Title" + }, + { + "component": "text", + "name": "date", + "label": "Event Date" + }, + { + "component": "text", + "name": "location", + "label": "Event Location" + }, + { + "component": "text", + "name": "link", + "label": "Event Link" + }, + { + "component": "text", + "name": "label", + "value": "LEARN MORE", + "label": "Button Label" + }, + { + "component": "select", + "name": "target", + "label": "Link Target", + "options": [ + { + "name": "Same Window", + "value": "" + }, + { + "name": "New Window", + "value": "_blank" + } + ] + } + ] + }, { "id": "fragment", "fields": [ diff --git a/models/_section.json b/models/_section.json index 94c06a5..8c84b08 100644 --- a/models/_section.json +++ b/models/_section.json @@ -68,6 +68,7 @@ "video", "gallery", "event-teaser", + "event-teasers", "framed-grid" ] }