managarten/games/mana-games/apps/web/src/components/HorizontalScroller.astro
Till JS a4184f1bab restore(mana-games): bring back AI browser games platform
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 09:16:58 +02:00

352 lines
7.3 KiB
Text

---
import GameCard from './GameCard.astro';
export interface Props {
title: string;
games: any[];
id?: string;
}
const { title, games, id = 'scroller' } = Astro.props;
---
<section class="scroller-section">
<div class="scroller-header">
<h2>{title}</h2>
<div class="scroller-controls">
<button class="scroll-btn scroll-left" data-scroller={id} aria-label="Nach links scrollen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M15 18L9 12L15 6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"></path>
</svg>
</button>
<button class="scroll-btn scroll-right" data-scroller={id} aria-label="Nach rechts scrollen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M9 18L15 12L9 6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"></path>
</svg>
</button>
</div>
</div>
<div class="scroller-container">
<div class="scroller-gradient-left"></div>
<div class="scroller-gradient-right"></div>
<div class="scroller-track" id={id}>
<div class="scroller-content">
{
games.map((game) => (
<div class="scroller-item">
<GameCard
title={game.title}
description={game.description}
slug={game.slug}
thumbnail={game.thumbnail}
tags={game.tags}
complexity={game.complexity}
codeStats={game.codeStats}
/>
</div>
))
}
</div>
</div>
</div>
</section>
<style>
.scroller-section {
position: relative;
margin-bottom: 3rem;
width: 100%;
}
.scroller-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding: 0 max(1.5rem, calc((100vw - 1400px) / 2));
}
.scroller-header h2 {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text);
margin: 0;
}
.scroller-controls {
display: flex;
gap: 0.5rem;
}
.scroll-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: var(--color-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
opacity: 0.6;
}
.scroll-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.15);
opacity: 1;
transform: scale(1.05);
}
.scroll-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.scroller-container {
position: relative;
width: 100%;
overflow: hidden;
}
.scroller-gradient-left,
.scroller-gradient-right {
position: absolute;
top: 0;
bottom: 0;
width: 100px;
z-index: 2;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.scroller-gradient-left {
left: 0;
background: linear-gradient(90deg, var(--color-bg) 0%, transparent 100%);
}
.scroller-gradient-right {
right: 0;
background: linear-gradient(270deg, var(--color-bg) 0%, transparent 100%);
}
.scroller-container.has-scroll-left .scroller-gradient-left,
.scroller-container.has-scroll-right .scroller-gradient-right {
opacity: 1;
}
.scroller-track {
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
scrollbar-width: none;
-ms-overflow-style: none;
padding: 0.5rem 0 1.5rem;
}
.scroller-track::-webkit-scrollbar {
display: none;
}
.scroller-content {
display: flex;
gap: 1.5rem;
padding: 0 max(1.5rem, calc((100vw - 1400px) / 2));
min-width: min-content;
}
.scroller-item {
flex: 0 0 320px;
max-width: 320px;
opacity: 0;
transform: translateY(20px);
animation: scrollerItemFadeIn 0.4s ease forwards;
}
.scroller-item:nth-child(1) {
animation-delay: 0s;
}
.scroller-item:nth-child(2) {
animation-delay: 0.05s;
}
.scroller-item:nth-child(3) {
animation-delay: 0.1s;
}
.scroller-item:nth-child(4) {
animation-delay: 0.15s;
}
.scroller-item:nth-child(5) {
animation-delay: 0.2s;
}
.scroller-item:nth-child(6) {
animation-delay: 0.25s;
}
.scroller-item:nth-child(n + 7) {
animation-delay: 0.3s;
}
@keyframes scrollerItemFadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
@media (hover: hover) {
.scroller-item {
transition: transform 0.2s ease;
}
.scroller-item:hover {
transform: scale(1.02);
}
}
@media (max-width: 768px) {
.scroller-header {
padding: 0 1rem;
}
.scroller-content {
padding: 0 1rem;
gap: 1rem;
}
.scroller-item {
flex: 0 0 280px;
max-width: 280px;
}
.scroll-btn {
width: 36px;
height: 36px;
}
.scroller-gradient-left,
.scroller-gradient-right {
width: 50px;
}
}
@media (max-width: 480px) {
.scroller-item {
flex: 0 0 240px;
max-width: 240px;
}
.scroller-controls {
display: none;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
const scrollers = document.querySelectorAll('.scroller-track');
scrollers.forEach((scroller) => {
const scrollerId = scroller.id;
const container = scroller.closest('.scroller-container');
const leftBtn = document.querySelector(
`.scroll-left[data-scroller="${scrollerId}"]`
) as HTMLButtonElement;
const rightBtn = document.querySelector(
`.scroll-right[data-scroller="${scrollerId}"]`
) as HTMLButtonElement;
if (!container || !leftBtn || !rightBtn) return;
const updateButtons = () => {
const scrollLeft = scroller.scrollLeft;
const scrollWidth = scroller.scrollWidth;
const clientWidth = scroller.clientWidth;
leftBtn.disabled = scrollLeft <= 0;
rightBtn.disabled = scrollLeft >= scrollWidth - clientWidth - 1;
if (scrollLeft > 0) {
container.classList.add('has-scroll-left');
} else {
container.classList.remove('has-scroll-left');
}
if (scrollLeft < scrollWidth - clientWidth - 1) {
container.classList.add('has-scroll-right');
} else {
container.classList.remove('has-scroll-right');
}
};
const scrollAmount = () => {
const item = scroller.querySelector('.scroller-item') as HTMLElement;
if (!item) return 320;
return item.offsetWidth + 24;
};
leftBtn.addEventListener('click', () => {
scroller.scrollBy({ left: -scrollAmount(), behavior: 'smooth' });
});
rightBtn.addEventListener('click', () => {
scroller.scrollBy({ left: scrollAmount(), behavior: 'smooth' });
});
scroller.addEventListener('scroll', updateButtons);
window.addEventListener('resize', updateButtons);
setTimeout(updateButtons, 100);
let touchStartX = 0;
let touchEndX = 0;
let isSwiping = false;
scroller.addEventListener(
'touchstart',
(e) => {
touchStartX = e.touches[0].clientX;
isSwiping = true;
},
{ passive: true }
);
scroller.addEventListener(
'touchmove',
(e) => {
if (!isSwiping) return;
touchEndX = e.touches[0].clientX;
},
{ passive: true }
);
scroller.addEventListener('touchend', () => {
if (!isSwiping) return;
isSwiping = false;
const swipeDistance = touchEndX - touchStartX;
const threshold = 50;
if (Math.abs(swipeDistance) > threshold) {
if (swipeDistance > 0) {
scroller.scrollBy({ left: -scrollAmount(), behavior: 'smooth' });
} else {
scroller.scrollBy({ left: scrollAmount(), behavior: 'smooth' });
}
}
});
});
});
</script>