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
75 changes: 75 additions & 0 deletions blocks/event-teasers/_event-teasers.json
Original file line number Diff line number Diff line change
@@ -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": []
}

113 changes: 113 additions & 0 deletions blocks/event-teasers/event-teasers.css
Original file line number Diff line number Diff line change
@@ -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;
}

178 changes: 178 additions & 0 deletions blocks/event-teasers/event-teasers.js
Original file line number Diff line number Diff line change
@@ -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();
}

16 changes: 16 additions & 0 deletions component-definition.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions component-filters.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"video",
"gallery",
"event-teaser",
"event-teasers",
"framed-grid"
]
},
Expand Down
Loading