mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 21:06:41 +02:00
352 lines
7.3 KiB
Text
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>
|