diff --git a/css/pages/_hub.css b/css/pages/_hub.css index ed63d1fa..04784437 100644 --- a/css/pages/_hub.css +++ b/css/pages/_hub.css @@ -76,21 +76,27 @@ .hub-story-text p { font-size: 17px; line-height: 28px; color: rgba(255, 255, 255, 0.9); margin: 0 0 16px; } .hub-story-text p:last-child { margin-bottom: 0; } -/* ---------- Use cases (image cards, continuous auto-scrolling marquee) ---------- */ +/* ---------- Use cases (image cards, auto-scrolling + drag + dots) ---------- */ .hub-carousel { position: relative; } -/* masked viewport: cards fade out at both edges instead of being hard-cut */ +/* masked, scrollable viewport: cards fade out at both edges instead of being + hard-cut. Auto-scroll, click-and-drag and touch swiping are driven by JS + (setupHubCarousel); the native scrollbar is hidden. */ .hub-uses { - overflow: hidden; + overflow-x: auto; overflow-y: hidden; padding: 6px 0 10px; -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 2.5%, #000 97.5%, transparent 100%); mask-image: linear-gradient(90deg, transparent 0, #000 2.5%, #000 97.5%, transparent 100%); + cursor: grab; + -webkit-user-select: none; user-select: none; + scrollbar-width: none; -ms-overflow-style: none; + scroll-behavior: auto; } -/* the moving track = two identical groups; translateX(-50%) loops seamlessly */ +.hub-uses::-webkit-scrollbar { display: none; } +.hub-uses.is-dragging { cursor: grabbing; } +/* the track = three identical groups so the JS loop is seamless in both directions */ .hub-track { display: flex; width: max-content; - animation: hub-marquee 55s linear infinite; } -.hub-carousel:hover .hub-track { animation-play-state: paused; } .hub-group { display: flex; gap: 18px; padding-right: 18px; flex: 0 0 auto; } @@ -106,17 +112,11 @@ .hub-use:nth-child(3n+2) { background: linear-gradient(135deg, #FA7850 0%, #E8613B 100%); } .hub-use:nth-child(3n+3) { background: linear-gradient(135deg, #3B82C4 0%, #2C6BA6 100%); } .hub-use:hover { transform: translateY(-4px); box-shadow: 0 18px 36px rgba(16, 24, 40, 0.18); } -@keyframes hub-marquee { - from { transform: translateX(0); } - to { transform: translateX(-50%); } -} -@media (prefers-reduced-motion: reduce) { - .hub-track { animation: none; } -} .hub-use-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.4s ease; + -webkit-user-drag: none; user-select: none; pointer-events: none; } .hub-use:hover .hub-use-img { transform: scale(1.05); } /* gradient rising from the bottom for text legibility */ @@ -138,6 +138,25 @@ .hub-use-body h3 { font-size: 18px; line-height: 1.25; color: #fff; margin: 0; font-weight: 700; } .hub-use-body p { font-size: 13px; line-height: 19px; color: rgba(255, 255, 255, 0.9); margin: 0; } +/* ---------- Scroll-position dots ---------- */ +.hub-dots { + display: flex; justify-content: center; align-items: center; + gap: 9px; margin-top: 18px; +} +.hub-dot { + width: 8px; height: 8px; padding: 0; border: 0; border-radius: 50%; + background: #CBD5DE; cursor: pointer; flex: 0 0 auto; + transition: width 0.25s ease, background-color 0.25s ease, + opacity 0.25s ease, transform 0.25s ease; +} +.hub-dot:hover { background: #9AA9B7; } +.hub-dot.is-near { width: 7px; height: 7px; } +.hub-dot.is-far { width: 6px; height: 6px; opacity: 0.6; } +.hub-dot.is-active { + width: 24px; height: 8px; border-radius: 6px; + background: #2D738C; opacity: 1; +} + /* ---------- Amenities ---------- */ .hub-amenities { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; diff --git a/css/pages/_member.css b/css/pages/_member.css index 824ce453..e15d284c 100644 --- a/css/pages/_member.css +++ b/css/pages/_member.css @@ -77,27 +77,22 @@ .mm-numbers-section .mm-note { color: rgba(255,255,255,0.6); margin-top: 24px; } /* ---- ways (hub-style auto-scrolling slider) ---- */ -/* Mechanics (.hub-carousel/.hub-uses/.hub-track/.hub-group) and the marquee - animation are reused from _hub.css; the JS clone step (setupHubCarousel) runs - on every page and picks up the .hub-track here. These rules only restyle the - card body for icon-based cards (no photo). */ +/* Mechanics (.hub-carousel/.hub-uses/.hub-track/.hub-group), the marquee + animation, the photo (.hub-use-img), the legibility gradient (::after) and + the bottom-anchored body are all reused from _hub.css; the JS clone step + (setupHubCarousel) runs on every page and picks up the .hub-track here. + These rules only tweak card size and add a small icon badge over the photo. */ .mm-ways-carousel { margin-top: 4px; } -.mm-use { - display: flex; flex-direction: column; justify-content: space-between; - gap: 18px; padding: 24px 22px; - width: clamp(240px, 24vw, 300px); - aspect-ratio: 5 / 6; -} -/* No photo here, so drop the hub image-legibility overlay. */ -.mm-use::after { display: none; } +.mm-use { width: clamp(240px, 24vw, 300px); aspect-ratio: 5 / 6; } +/* Brand icon as a subtle glass badge floating over the photo. */ .mm-use-ico { + position: absolute; top: 14px; left: 14px; z-index: 3; display: inline-flex; align-items: center; justify-content: center; - width: 54px; height: 54px; border-radius: 14px; - background: rgba(255, 255, 255, 0.18); - color: #fff; font-size: 23px; flex: 0 0 auto; + width: 44px; height: 44px; border-radius: 12px; + background: rgba(10, 18, 26, 0.42); + -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); + color: #fff; font-size: 19px; } -/* Body is absolute-positioned for photo cards in _hub.css; here it flows normally. */ -.mm-use .hub-use-body { position: static; padding: 0; gap: 6px; } .mm-use .hub-use-body h3 { font-size: 19px; line-height: 1.25; } .mm-use .hub-use-body p { font-size: 14px; line-height: 21px; } diff --git a/en/become-a-member/index.html b/en/become-a-member/index.html index e3c15eab..dd9a5285 100644 --- a/en/become-a-member/index.html +++ b/en/become-a-member/index.html @@ -76,8 +76,8 @@ @@ -135,8 +135,8 @@ @@ -209,7 +209,7 @@

Why become a member?

- Macedonian students gathered together in Ljubljana + Macedonian students gathered together in Ljubljana
Real events, real friendships
@@ -241,7 +241,7 @@

Feel at home away from home

Belong to a community where you never have to explain where you come from.

- Students writing their names in Cyrillic at Macedonian Student Night + Students writing their names in Cyrillic at Macedonian Student Night
A piece of home in Slovenia
@@ -266,26 +266,32 @@
+ Stay connected

Stay connected

Receive updates and know what is happening.

+ Join the community

Join the community

Come to events, meet people and take part when it suits you.

+ Volunteer sometimes

Volunteer sometimes

Help with selected activities without a long-term commitment.

+ Join projects

Join projects

Contribute to specific initiatives, campaigns, guides or collaborations.

+ Organise events

Organise events

Help plan, coordinate, promote and deliver activities.

+ Take responsibility

Take responsibility

Explore leadership, governance, representation or local team roles.

diff --git a/images/mm-benefit-piece-of-home.webp b/images/mm-benefit-piece-of-home.webp new file mode 100644 index 00000000..4aca1f2c Binary files /dev/null and b/images/mm-benefit-piece-of-home.webp differ diff --git a/images/mm-benefit-real-events.webp b/images/mm-benefit-real-events.webp new file mode 100644 index 00000000..d8ef6d03 Binary files /dev/null and b/images/mm-benefit-real-events.webp differ diff --git a/images/mm-way-community.webp b/images/mm-way-community.webp new file mode 100644 index 00000000..f40c220d Binary files /dev/null and b/images/mm-way-community.webp differ diff --git a/images/mm-way-events.webp b/images/mm-way-events.webp new file mode 100644 index 00000000..7c827025 Binary files /dev/null and b/images/mm-way-events.webp differ diff --git a/images/mm-way-projects.webp b/images/mm-way-projects.webp new file mode 100644 index 00000000..975e9eb0 Binary files /dev/null and b/images/mm-way-projects.webp differ diff --git a/images/mm-way-responsibility.webp b/images/mm-way-responsibility.webp new file mode 100644 index 00000000..65a565d6 Binary files /dev/null and b/images/mm-way-responsibility.webp differ diff --git a/images/mm-way-stay-connected.webp b/images/mm-way-stay-connected.webp new file mode 100644 index 00000000..dba2fe82 Binary files /dev/null and b/images/mm-way-stay-connected.webp differ diff --git a/images/mm-way-volunteer.webp b/images/mm-way-volunteer.webp new file mode 100644 index 00000000..41c34db4 Binary files /dev/null and b/images/mm-way-volunteer.webp differ diff --git a/main.js b/main.js index 2ecc76de..2cc3fc98 100644 --- a/main.js +++ b/main.js @@ -931,22 +931,174 @@ document.addEventListener('DOMContentLoaded', function() { /** * =================================================================== - * HUB USE-CASE CAROUSEL (auto-scrolling) + * HUB USE-CASE CAROUSEL (auto-scrolling + drag + position dots) * =================================================================== + * Drives every .hub-carousel on the page (Hub "What you can do here" + * and the "ways to be part of MSOS" slider). The track is built from + * three identical .hub-group copies so the scroll loops seamlessly in + * both directions; auto-scroll is JS-driven (not a CSS animation) so it + * can co-exist with click-and-drag and touch swiping. */ function setupHubCarousel() { - const trackEl = document.querySelector('.hub-track'); - if (!trackEl) return; - const group = trackEl.querySelector('.hub-group'); - if (!group) return; + const carousels = document.querySelectorAll('.hub-carousel'); + if (!carousels.length) return; + const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const LOOP_MS = 55000; // time to travel one group width (matches old 55s marquee) - // Duplicate the group so translateX(-50%) loops back seamlessly (waterslide flow). - // Guard against double-cloning if the script somehow runs twice. - if (trackEl.querySelectorAll('.hub-group').length < 2) { - const clone = group.cloneNode(true); - clone.setAttribute('aria-hidden', 'true'); - trackEl.appendChild(clone); - } + carousels.forEach(function (carousel) { + if (carousel.dataset.carouselInit) return; + carousel.dataset.carouselInit = '1'; + + const scroller = carousel.querySelector('.hub-uses'); + const track = carousel.querySelector('.hub-track'); + if (!scroller || !track) return; + const firstGroup = track.querySelector('.hub-group'); + if (!firstGroup) return; + + // Need 3 identical groups so a full neighbour is always visible at each wrap point. + while (track.querySelectorAll('.hub-group').length < 3) { + const clone = firstGroup.cloneNode(true); + clone.setAttribute('aria-hidden', 'true'); + track.appendChild(clone); + } + + const cards = firstGroup.children; + const cardCount = cards.length; + if (!cardCount) return; + + // --- Position dots --- + const dotsWrap = document.createElement('div'); + dotsWrap.className = 'hub-dots'; + dotsWrap.setAttribute('role', 'tablist'); + dotsWrap.setAttribute('aria-label', 'Carousel position'); + const dotEls = []; + for (let i = 0; i < cardCount; i++) { + const b = document.createElement('button'); + b.type = 'button'; + b.className = 'hub-dot'; + b.setAttribute('aria-label', 'Go to item ' + (i + 1)); + b.addEventListener('click', (function (idx) { + return function () { goTo(idx); }; + })(i)); + dotsWrap.appendChild(b); + dotEls.push(b); + } + carousel.appendChild(dotsWrap); + + // --- Measurements (recomputed on resize / image load) --- + let groupWidth = 0, step = 0; + function measure() { + groupWidth = firstGroup.offsetWidth; + step = cardCount > 1 ? (cards[1].offsetLeft - cards[0].offsetLeft) : groupWidth; + if (!step) step = groupWidth / cardCount; + } + + // Keep scroll within the middle copy [groupWidth, 2*groupWidth) so it loops both ways. + function wrap() { + if (!groupWidth) return; + if (scroller.scrollLeft >= 2 * groupWidth) scroller.scrollLeft -= groupWidth; + else if (scroller.scrollLeft < groupWidth) scroller.scrollLeft += groupWidth; + } + + function updateDots() { + if (!step || !groupWidth) return; + const within = ((scroller.scrollLeft % groupWidth) + groupWidth) % groupWidth; + const active = Math.round(within / step) % cardCount; + for (let i = 0; i < cardCount; i++) { + const dist = Math.min(Math.abs(i - active), cardCount - Math.abs(i - active)); + dotEls[i].classList.toggle('is-active', i === active); + dotEls[i].classList.toggle('is-near', dist === 1); + dotEls[i].classList.toggle('is-far', dist >= 2); + } + } + + function reset() { + measure(); + if (groupWidth) scroller.scrollLeft = groupWidth; + updateDots(); + } + reset(); + window.addEventListener('load', reset); + window.addEventListener('resize', function () { measure(); wrap(); updateDots(); }); + + // --- Jump to a specific card when a dot is clicked --- + // Custom eased scroll (not native smooth) so it never fights the + // rAF auto-scroll loop. Target always sits in the middle copy + // [groupWidth, 2*groupWidth), so wrap() stays a no-op during it. + let programmatic = 0; // ms remaining where auto-scroll yields + let animId = 0; + function goTo(idx) { + if (!step) measure(); + const start = scroller.scrollLeft; + const target = groupWidth + idx * step; + const dur = 450, t0 = performance.now(); + const ease = function (t) { return 1 - Math.pow(1 - t, 3); }; + cancelAnimationFrame(animId); + programmatic = dur + 80; + (function anim(now) { + const t = Math.min(1, (now - t0) / dur); + scroller.scrollLeft = start + (target - start) * ease(t); + updateDots(); + if (t < 1) animId = requestAnimationFrame(anim); + })(t0); + } + + // --- Auto-scroll loop --- + let paused = false; + let dragging = false; + let last = null; + function tick(ts) { + if (last == null) last = ts; + const dt = ts - last; + last = ts; + if (!reduceMotion && !paused && programmatic <= 0 && groupWidth) { + scroller.scrollLeft += (groupWidth / LOOP_MS) * dt; + } + if (programmatic > 0) programmatic -= dt; + wrap(); + updateDots(); + requestAnimationFrame(tick); + } + requestAnimationFrame(tick); + + // Pause on hover (desktop). + carousel.addEventListener('mouseenter', function () { paused = true; }); + carousel.addEventListener('mouseleave', function () { if (!dragging) paused = false; }); + + // --- Click-and-drag (mouse) + pause during touch (native scroll handles the swipe) --- + let lastX = 0; + scroller.addEventListener('pointerdown', function (e) { + paused = true; + if (e.pointerType !== 'mouse') return; // touch/pen: let the browser scroll natively + dragging = true; + lastX = e.clientX; + scroller.classList.add('is-dragging'); + try { scroller.setPointerCapture(e.pointerId); } catch (err) {} + }); + scroller.addEventListener('pointermove', function (e) { + if (!dragging) return; + const dx = e.clientX - lastX; + lastX = e.clientX; + scroller.scrollLeft -= dx; + wrap(); + updateDots(); + e.preventDefault(); + }); + function endDrag(e) { + if (dragging) { + dragging = false; + scroller.classList.remove('is-dragging'); + try { scroller.releasePointerCapture(e.pointerId); } catch (err) {} + } + // Resume auto-scroll unless the cursor is still hovering the carousel. + if (!carousel.matches(':hover')) paused = false; + } + scroller.addEventListener('pointerup', endDrag); + scroller.addEventListener('pointercancel', endDrag); + + // Prevent the browser's native image drag-ghost while dragging. + track.addEventListener('dragstart', function (e) { e.preventDefault(); }); + }); } /** diff --git a/mk/become-a-member/index.html b/mk/become-a-member/index.html index 8a7d26fa..99f1ba45 100644 --- a/mk/become-a-member/index.html +++ b/mk/become-a-member/index.html @@ -76,8 +76,8 @@ @@ -135,8 +135,8 @@ @@ -209,7 +209,7 @@

Зошто да станеш член?

- Македонски студенти собрани во Љубљана + Македонски студенти собрани во Љубљана
Вистински настани, вистински пријателства
@@ -241,7 +241,7 @@

Почувствувај се како дома

Припаѓај на заедница каде никогаш не мораш да објаснуваш од каде доаѓаш.

- Студенти ги пишуваат своите имиња на кирилица на македонската студентска вечер + Студенти ги пишуваат своите имиња на кирилица на македонската студентска вечер
Дом далеку од дома
@@ -265,26 +265,32 @@
+ Остани поврзан

Остани поврзан

Примај новости и знај што се случува.

+ Приклучи се на заедницата

Приклучи се на заедницата

Доаѓај на настани, запознавај луѓе и учествувај кога ти одговара.

+ Волонтирај повремено

Волонтирај повремено

Помагај на избрани активности без долгорочна обврска.

+ Приклучи се на проекти

Приклучи се на проекти

Придонеси на конкретни иницијативи, кампањи, водичи или соработки.

+ Организирај настани

Организирај настани

Помагај да се планираат, координираат, промовираат и реализираат активности.

+ Преземи одговорност

Преземи одговорност

Истражи лидерство, управување, застапување или улоги во локален тим.

diff --git a/si/become-a-member/index.html b/si/become-a-member/index.html index 635cfba1..56aa868e 100644 --- a/si/become-a-member/index.html +++ b/si/become-a-member/index.html @@ -76,8 +76,8 @@ @@ -135,8 +135,8 @@ @@ -209,7 +209,7 @@

Zakaj postati član?

- Makedonski študenti zbrani v Ljubljani + Makedonski študenti zbrani v Ljubljani
Pravi dogodki, prava prijateljstva
@@ -241,7 +241,7 @@

Počuti se kot doma

Pripadaj skupnosti, kjer ti nikoli ni treba pojasnjevati, od kod prihajaš.

- Študenti pišejo svoja imena v cirilici na makedonskem študentskem večeru + Študenti pišejo svoja imena v cirilici na makedonskem študentskem večeru
Košček doma v Sloveniji
@@ -266,26 +266,32 @@
+ Ostani povezan

Ostani povezan

Prejemaj novice in vedi, kaj se dogaja.

+ Pridruži se skupnosti

Pridruži se skupnosti

Obišči dogodke, spoznaj ljudi in sodeluj, ko ti ustreza.

+ Občasno prostovoljči

Občasno prostovoljči

Pomagaj pri izbranih dejavnostih brez dolgoročne zaveze.

+ Pridruži se projektom

Pridruži se projektom

Prispevaj k pobudam, kampanjam, vodnikom ali sodelovanjem.

+ Organiziraj dogodke

Organiziraj dogodke

Pomagaj načrtovati, usklajevati, promovirati in izvesti dejavnosti.

+ Prevzemi odgovornost

Prevzemi odgovornost

Razišči vodenje, upravljanje, zastopanje ali vloge v lokalni ekipi.