From 80dd19257c03ba3c2ddb744953b6005f5df80508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Mummert?= Date: Wed, 4 Mar 2026 17:50:10 +0100 Subject: [PATCH] Align pinboard assets with /assets path convention --- contao/templates/frontend/pinnwand.html.twig | 4 +- src/Resources/public/assets/pinboard.css | 110 ++++++++++++++++ src/Resources/public/assets/pinboard.js | 125 +++++++++++++++++++ 3 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 src/Resources/public/assets/pinboard.css create mode 100644 src/Resources/public/assets/pinboard.js diff --git a/contao/templates/frontend/pinnwand.html.twig b/contao/templates/frontend/pinnwand.html.twig index e99b165..0b08fdc 100644 --- a/contao/templates/frontend/pinnwand.html.twig +++ b/contao/templates/frontend/pinnwand.html.twig @@ -1,6 +1,6 @@ {% use '@Contao/component/_figure.html.twig' %} - +
@@ -33,4 +33,4 @@
- + diff --git a/src/Resources/public/assets/pinboard.css b/src/Resources/public/assets/pinboard.css new file mode 100644 index 0000000..09094cf --- /dev/null +++ b/src/Resources/public/assets/pinboard.css @@ -0,0 +1,110 @@ +.pinboard { + width: 100%; + max-width: 1400px; + margin: 0 auto; + padding: 1.5rem; +} + +.pinboard__surface { + position: relative; + min-height: 42rem; + border-radius: 1.2rem; + overflow: hidden; + background: + radial-gradient(circle at 20% 10%, rgba(255, 255, 255, 0.25), transparent 42%), + radial-gradient(circle at 80% 90%, rgba(50, 30, 10, 0.2), transparent 36%), + repeating-linear-gradient(35deg, rgba(75, 40, 15, 0.18), rgba(75, 40, 15, 0.18) 3px, rgba(93, 52, 23, 0.12) 3px, rgba(93, 52, 23, 0.12) 9px), + linear-gradient(145deg, #b77944 0%, #a66a38 45%, #8d552a 100%); + box-shadow: inset 0 0 0 1px rgba(65, 33, 12, 0.25), inset 0 0 18px rgba(45, 22, 8, 0.35); +} + +.pin-note { + position: absolute; + width: clamp(220px, 28vw, 340px); + min-height: 190px; + padding: 1rem 1rem 1.25rem; + border-radius: 0.3rem; + background: linear-gradient(160deg, #fff8a8 0%, #f5eb85 100%); + color: #2a241c; + box-shadow: 0 10px 22px rgba(20, 10, 5, 0.34); + cursor: grab; + user-select: none; + touch-action: none; + transition: box-shadow 160ms ease, transform 160ms ease; +} + +.pin-note::before { + content: ''; + position: absolute; + top: -9px; + left: 50%; + width: 18px; + height: 18px; + transform: translateX(-50%); + border-radius: 50%; + background: radial-gradient(circle at 35% 35%, #f2f2f2, #979797 68%, #707070 100%); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35); +} + +.pin-note.is-highlighted { + background: linear-gradient(160deg, #ffd99b 0%, #f7c46f 100%); +} + +.pin-note.is-dragging { + cursor: grabbing; + box-shadow: 0 18px 34px rgba(20, 10, 5, 0.46); +} + +.pin-note__image-wrap { + margin: 0 0 0.7rem; +} + +.pin-note__image { + display: block; + width: 100%; + height: auto; + border-radius: 0.25rem; +} + +.pin-note__headline { + margin: 0 0 0.5rem; + font-size: 1.1rem; + line-height: 1.25; +} + +.pin-note__text { + font-size: 0.96rem; + line-height: 1.45; + max-height: 14rem; + overflow: hidden; +} + +.pin-note__link-wrap { + margin: 0.8rem 0 0; +} + +.pin-note__link { + color: inherit; + font-weight: 600; +} + +.pinboard__empty { + margin: 0; + padding: 1.5rem; + color: #fff; + font-weight: 600; +} + +@media (max-width: 768px) { + .pinboard { + padding: 0.8rem; + } + + .pinboard__surface { + min-height: 36rem; + } + + .pin-note { + width: min(84vw, 290px); + } +} diff --git a/src/Resources/public/assets/pinboard.js b/src/Resources/public/assets/pinboard.js new file mode 100644 index 0000000..6f7684a --- /dev/null +++ b/src/Resources/public/assets/pinboard.js @@ -0,0 +1,125 @@ +(() => { + const board = document.querySelector('[data-pinboard-surface]'); + + if (!board) { + return; + } + + const notes = Array.from(board.querySelectorAll('[data-pin-note]')); + + if (!notes.length) { + return; + } + + let zCounter = 200; + + const randomBetween = (min, max) => Math.random() * (max - min) + min; + + const placeNotes = () => { + const boardRect = board.getBoundingClientRect(); + const maxXBase = Math.max(24, boardRect.width - 300); + const maxYBase = Math.max(24, boardRect.height - 220); + + notes.forEach((note, index) => { + const maxX = Math.max(16, maxXBase - note.offsetWidth * 0.2); + const maxY = Math.max(16, maxYBase - note.offsetHeight * 0.2); + + const x = randomBetween(12, maxX); + const y = randomBetween(12, maxY); + const rotation = randomBetween(-7, 7); + const highlighted = note.dataset.highlighted === '1'; + const level = highlighted ? 1000 + index : 100 + index; + + note.dataset.baseRotation = String(rotation); + note.dataset.x = String(x); + note.dataset.y = String(y); + note.style.zIndex = String(level); + note.style.transform = `translate(${x}px, ${y}px) rotate(${rotation}deg)`; + }); + }; + + const clampToBoard = (note, x, y) => { + const boardRect = board.getBoundingClientRect(); + const noteRect = note.getBoundingClientRect(); + const maxX = Math.max(0, boardRect.width - noteRect.width); + const maxY = Math.max(0, boardRect.height - noteRect.height); + + return { + x: Math.min(Math.max(0, x), maxX), + y: Math.min(Math.max(0, y), maxY), + }; + }; + + const enableDrag = (note) => { + let dragging = false; + let pointerId = null; + let offsetX = 0; + let offsetY = 0; + + note.addEventListener('pointerdown', (event) => { + if (event.target instanceof Element && event.target.closest('a, button, input, select, textarea, label')) { + return; + } + + dragging = true; + pointerId = event.pointerId; + note.setPointerCapture(pointerId); + note.classList.add('is-dragging'); + note.style.zIndex = String(++zCounter + 2000); + + const startX = Number.parseFloat(note.dataset.x ?? '0'); + const startY = Number.parseFloat(note.dataset.y ?? '0'); + offsetX = event.clientX - startX; + offsetY = event.clientY - startY; + }); + + note.addEventListener('pointermove', (event) => { + if (!dragging || event.pointerId !== pointerId) { + return; + } + + const baseRotation = Number.parseFloat(note.dataset.baseRotation ?? '0'); + const nextX = event.clientX - offsetX; + const nextY = event.clientY - offsetY; + const clamped = clampToBoard(note, nextX, nextY); + + note.dataset.x = String(clamped.x); + note.dataset.y = String(clamped.y); + note.style.transform = `translate(${clamped.x}px, ${clamped.y}px) rotate(${baseRotation}deg)`; + }); + + const releaseDrag = (event) => { + if (!dragging || event.pointerId !== pointerId) { + return; + } + + dragging = false; + note.classList.remove('is-dragging'); + note.releasePointerCapture(pointerId); + pointerId = null; + }; + + note.addEventListener('pointerup', releaseDrag); + note.addEventListener('pointercancel', releaseDrag); + }; + + const adjustBoardHeight = () => { + let requiredHeight = 620; + + notes.forEach((note) => { + const y = Number.parseFloat(note.dataset.y ?? '0'); + requiredHeight = Math.max(requiredHeight, y + note.offsetHeight + 36); + }); + + board.style.minHeight = `${Math.ceil(requiredHeight)}px`; + }; + + placeNotes(); + notes.forEach(enableDrag); + adjustBoardHeight(); + + window.addEventListener('resize', () => { + placeNotes(); + adjustBoardHeight(); + }); +})();