All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 1m13s
Build & Deploy / 🧪 QA (push) Successful in 1m44s
Build & Deploy / 🏗️ Build (push) Successful in 3m14s
Build & Deploy / 🚀 Deploy (push) Successful in 34s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
195 lines
7.9 KiB
TypeScript
195 lines
7.9 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { m } from 'framer-motion';
|
|
import Link from 'next/link';
|
|
import Image from 'next/image';
|
|
import { HoverShineOverlay } from '@/components/ui/HoverShineOverlay';
|
|
import { useTranslations, useLocale } from 'next-intl';
|
|
|
|
export interface Reference {
|
|
id: string;
|
|
title: string;
|
|
slug: string;
|
|
category: string;
|
|
image?: {
|
|
url: string;
|
|
alt?: string;
|
|
} | string | null;
|
|
}
|
|
|
|
interface ReferencesSliderProps {
|
|
references?: Reference[];
|
|
badge?: string;
|
|
title?: string;
|
|
description?: string;
|
|
ctaLabel?: string;
|
|
ctaHref?: string;
|
|
data?: {
|
|
badge?: string;
|
|
title?: string;
|
|
description?: string;
|
|
ctaLabel?: string;
|
|
ctaHref?: string;
|
|
}
|
|
}
|
|
|
|
export function ReferencesSlider(props: ReferencesSliderProps) {
|
|
const t = useTranslations('ReferencesSlider');
|
|
const locale = useLocale();
|
|
|
|
const { data } = props;
|
|
const references = props.references || [];
|
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
const dragDistanceRef = React.useRef(0);
|
|
const startXRef = React.useRef(0);
|
|
const scrollLeftRef = React.useRef(0);
|
|
const isDraggingRef = React.useRef(false);
|
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
|
|
|
|
const badge = props.badge || data?.badge || t('badge');
|
|
const title = props.title || data?.title || t('title');
|
|
const description = props.description || data?.description || t('description');
|
|
const ctaLabel = props.ctaLabel || data?.ctaLabel || t('ctaLabel');
|
|
const ctaHref = props.ctaHref || data?.ctaHref || `/${locale}/referenzen`;
|
|
|
|
if (!references || references.length === 0) return null;
|
|
|
|
// Fallback images pool if CMS doesn't provide one
|
|
const fallbacks = [
|
|
'/assets/photos/DSC01123.JPG',
|
|
'/assets/photos/DSC00850.JPG',
|
|
'/assets/photos/DSC01129.JPG',
|
|
'/assets/photos/DSC00010.JPG',
|
|
];
|
|
|
|
const onMouseDown = (e: React.MouseEvent) => {
|
|
if (!containerRef.current) return;
|
|
setIsDragging(true);
|
|
isDraggingRef.current = true;
|
|
const startXVal = e.clientX - (containerRef.current.offsetLeft || 0);
|
|
startXRef.current = startXVal;
|
|
scrollLeftRef.current = containerRef.current.scrollLeft || 0;
|
|
dragDistanceRef.current = 0;
|
|
};
|
|
|
|
const onMouseLeave = () => {
|
|
setIsDragging(false);
|
|
isDraggingRef.current = false;
|
|
};
|
|
|
|
const onMouseUp = () => {
|
|
setIsDragging(false);
|
|
isDraggingRef.current = false;
|
|
};
|
|
|
|
const onMouseMove = (e: React.MouseEvent) => {
|
|
if (!isDraggingRef.current || !containerRef.current) return;
|
|
e.preventDefault();
|
|
const x = e.clientX - (containerRef.current.offsetLeft || 0);
|
|
const walk = (x - startXRef.current) * 2; // Scroll-fast
|
|
containerRef.current.scrollLeft = scrollLeftRef.current - walk;
|
|
dragDistanceRef.current = Math.abs(x - startXRef.current);
|
|
};
|
|
|
|
|
|
return (
|
|
<section id="referenzen" className="pt-16 pb-8 md:py-24 lg:py-32 bg-neutral-dark text-white relative overflow-hidden">
|
|
<div className="absolute inset-0 bg-gradient-to-b from-neutral-dark via-neutral-900 to-neutral-dark z-0" />
|
|
|
|
<div className="container relative z-10 mb-12 flex flex-col items-center justify-center text-center gap-6">
|
|
<div>
|
|
<h2 className="text-primary-light font-bold tracking-wider uppercase text-sm mb-3">{badge}</h2>
|
|
<h3 className="font-heading text-3xl md:text-5xl font-extrabold">{title}</h3>
|
|
</div>
|
|
<div className="max-w-3xl mx-auto mb-4 md:mb-8 text-neutral-300">
|
|
{description}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative z-10">
|
|
{/* Fade edges for peek effect */}
|
|
<div className="absolute left-0 top-0 bottom-0 w-16 bg-gradient-to-r from-neutral-dark to-transparent z-20 pointer-events-none" />
|
|
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-neutral-dark to-transparent z-20 pointer-events-none" />
|
|
|
|
{/* Carousel: centered first card with peek on both sides */}
|
|
|
|
<div
|
|
ref={containerRef}
|
|
onMouseDown={onMouseDown}
|
|
onMouseLeave={onMouseLeave}
|
|
onMouseUp={onMouseUp}
|
|
onMouseMove={onMouseMove}
|
|
className={`select-none flex gap-6 overflow-x-auto pb-8 pt-4 px-[calc(50vw-160px)] md:px-[calc(50vw-240px)] [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden ${isDragging ? 'cursor-grabbing snap-none' : 'cursor-grab snap-x snap-mandatory'}`}
|
|
>
|
|
{references.map((ref, i) => {
|
|
const imgSrc = ref.image
|
|
? (typeof ref.image === 'string' ? ref.image : ref.image.url)
|
|
: fallbacks[i % fallbacks.length];
|
|
|
|
return (
|
|
<m.div
|
|
key={ref.id}
|
|
data-card
|
|
initial={{ opacity: 0, x: 20 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true, margin: "-50px" }}
|
|
transition={{ delay: i * 0.1, duration: 1.0, ease: [0.16, 1, 0.3, 1] }}
|
|
className="flex-shrink-0 w-[320px] md:w-[480px] snap-center group pointer-events-auto"
|
|
>
|
|
<Link
|
|
href={`/${locale}/referenzen#${ref.slug}`}
|
|
onClick={(e) => {
|
|
if (dragDistanceRef.current > 5) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
}}
|
|
onDragStart={(e) => e.preventDefault()}
|
|
draggable={false}
|
|
data-testid="reference-tile"
|
|
className="block relative select-none aspect-[16/10] bg-neutral-800 rounded-2xl overflow-hidden mb-5 border border-white/5 shadow-2xl cursor-pointer"
|
|
>
|
|
<Image
|
|
draggable={false}
|
|
src={imgSrc}
|
|
alt={ref.title}
|
|
fill
|
|
quality={50}
|
|
sizes="(max-width: 768px) 320px, 480px"
|
|
className="object-cover saturate-50 opacity-70 group-hover:scale-105 group-hover:opacity-100 group-hover:saturate-100 transition-all duration-700 ease-in-out pointer-events-none"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/30 to-transparent opacity-80 group-hover:opacity-90 transition-opacity duration-300 pointer-events-none" />
|
|
|
|
<HoverShineOverlay />
|
|
|
|
<div className="absolute bottom-6 left-6 right-6 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-500 pointer-events-none bg-black/60 backdrop-blur-sm p-4 rounded-xl border border-white/10">
|
|
<span className="inline-block py-1 px-3 rounded-full bg-primary/20 text-white text-[10px] font-bold uppercase tracking-widest mb-2 border border-primary/30">
|
|
{ref.category}
|
|
</span>
|
|
<h4 className="font-heading text-xl md:text-2xl font-bold leading-tight break-words">{ref.title}</h4>
|
|
</div>
|
|
</Link>
|
|
</m.div>
|
|
);
|
|
})}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="container relative z-10 mt-4 flex justify-center">
|
|
<Link
|
|
href={ctaHref}
|
|
className="px-8 py-4 rounded-xl bg-white/5 border border-white/20 hover:bg-white hover:text-neutral-dark hover:shadow-[0_0_20px_rgba(255,255,255,0.3)] transition-all font-bold flex items-center gap-3 backdrop-blur-sm"
|
|
>
|
|
{ctaLabel}
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
|
</Link>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|