diff --git a/privat/CV/andreknie.de/content/meta/testimonials.yaml b/privat/CV/andreknie.de/content/meta/testimonials.yaml index 8675fc6..1eb0312 100644 --- a/privat/CV/andreknie.de/content/meta/testimonials.yaml +++ b/privat/CV/andreknie.de/content/meta/testimonials.yaml @@ -1,6 +1,6 @@ -- quote: "André hat unsere strategische Ausrichtung entscheidend vorangebracht, die Workshops konzipiert und herausragend moderiert. Die Menschen wurden abgeholt und begeistert. Wir setzen auch in der weiteren KI-Reise auf seine Expertise und Kommunikationsstärke." - name: "Kunde - Organisationsentwicklung" - role: "Öffentliche Einrichtung" +- quote: "André verbindet etwas, das ich in dieser Form nur selten erlebe: tiefes technisches Verständnis für KI mit einem echten Gespür für Menschen und Organisationen. Er kennt die Möglichkeiten moderner KI, versteht aber gleichzeitig die Besonderheiten von Hochschulen. Dadurch gelingt es ihm, komplexe Technologien verständlich zu machen und Organisationen zu befähigen, eigene tragfähige Lösungen zu entwickeln. Dabei geht er nicht den einfachsten Weg, sondern sucht gemeinsam nach der bestmöglichen Lösung. Besonders schätze ich die Zusammenarbeit im Tandem: Die Kombination aus unserer internen Perspektive und seiner externen Expertise hat unsere KI-Reise entscheidend vorangebracht." + name: "Maike Mach" + role: "Stellvertretende Leitung Organisationsentwicklung, Universität Kassel" - quote: "André bringt uns voran, begeistert Menschen, zeigt auf was sinnvoll ist und was nicht. Durch ihn sind wir Größenordnungen schneller und sicherer. Sein Team und er befähigen uns, Dinge umzusetzen, von denen wir vorher nicht geträumt hätten." name: "Kunde - Projektleitung Digitalisierung" diff --git a/privat/CV/andreknie.de/src/components/TestimonialSlider.css b/privat/CV/andreknie.de/src/components/TestimonialSlider.css index f804e7e..c6c7e1a 100644 --- a/privat/CV/andreknie.de/src/components/TestimonialSlider.css +++ b/privat/CV/andreknie.de/src/components/TestimonialSlider.css @@ -2,6 +2,33 @@ padding: var(--section-padding) 20px; } +.testimonial-slider-controls { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-bottom: 18px; +} + +.testimonial-nav-button { + width: 42px; + height: 42px; + border: 1px solid var(--glass-border); + border-radius: 50%; + background: var(--bg-elevated); + color: var(--text-primary); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: border-color var(--transition-fast), color var(--transition-fast), transform var(--transition-fast); +} + +.testimonial-nav-button:hover { + border-color: var(--accent-color); + color: var(--accent-color); + transform: translateY(-1px); +} + .testimonial-scroll-container { display: flex; overflow-x: auto; @@ -9,11 +36,20 @@ scroll-behavior: smooth; gap: 24px; padding-bottom: 32px; /* Space for scrollbar and shadow */ + cursor: grab; + user-select: none; /* Hide scrollbar for cleaner look, or keep it subtle */ scrollbar-width: none; /* Firefox */ -ms-overflow-style: none; /* IE/Edge */ } + +.testimonial-scroll-container.dragging { + cursor: grabbing; + scroll-snap-type: none; + scroll-behavior: auto; +} + .testimonial-scroll-container::-webkit-scrollbar { display: none; /* Chrome/Safari */ } diff --git a/privat/CV/andreknie.de/src/components/TestimonialSlider.jsx b/privat/CV/andreknie.de/src/components/TestimonialSlider.jsx index 6d056ec..822ae59 100644 --- a/privat/CV/andreknie.de/src/components/TestimonialSlider.jsx +++ b/privat/CV/andreknie.de/src/components/TestimonialSlider.jsx @@ -1,4 +1,5 @@ -import React from 'react' +import React, { useEffect, useRef, useState } from 'react' +import { ChevronLeft, ChevronRight } from 'lucide-react' import './TestimonialSlider.css' /** @@ -6,13 +7,114 @@ import './TestimonialSlider.css' * Equal heights, horizontal scroll, no autoplay. */ export default function TestimonialSlider({ testimonials = [] }) { + const scrollerRef = useRef(null) + const dragStateRef = useRef({ isDragging: false, startX: 0, scrollLeft: 0 }) + const [isDragging, setIsDragging] = useState(false) + const canLoop = testimonials.length > 1 + const visibleTestimonials = canLoop ? [...testimonials, ...testimonials, ...testimonials] : testimonials + + const getCardStep = () => { + const scroller = scrollerRef.current + const firstCard = scroller?.querySelector('.testimonial-card') + if (!scroller || !firstCard) return 0 + + const gap = parseFloat(window.getComputedStyle(scroller).columnGap || '0') + return firstCard.offsetWidth + gap + } + + useEffect(() => { + const scroller = scrollerRef.current + if (!canLoop || !scroller) return + + const cardStep = getCardStep() + if (!cardStep) return + + scroller.scrollLeft = cardStep * testimonials.length + }, [canLoop, testimonials.length]) + + const scrollByCard = (direction) => { + const scroller = scrollerRef.current + const cardStep = getCardStep() + if (!scroller || !cardStep) return + + scroller.scrollBy({ + left: direction * cardStep, + behavior: 'smooth', + }) + } + + const keepLoopCentered = () => { + const scroller = scrollerRef.current + const cardStep = getCardStep() + if (!canLoop || !scroller || !cardStep) return + + const segmentWidth = cardStep * testimonials.length + if (scroller.scrollLeft < segmentWidth * 0.5) { + scroller.scrollLeft += segmentWidth + } else if (scroller.scrollLeft > segmentWidth * 1.5) { + scroller.scrollLeft -= segmentWidth + } + } + + const handlePointerDown = (event) => { + if (event.pointerType === 'touch') return + const scroller = scrollerRef.current + if (!scroller) return + + dragStateRef.current = { + isDragging: true, + startX: event.clientX, + scrollLeft: scroller.scrollLeft, + } + setIsDragging(true) + scroller.setPointerCapture(event.pointerId) + } + + const handlePointerMove = (event) => { + const scroller = scrollerRef.current + const dragState = dragStateRef.current + if (!scroller || !dragState.isDragging) return + + scroller.scrollLeft = dragState.scrollLeft - (event.clientX - dragState.startX) + } + + const stopDragging = (event) => { + const scroller = scrollerRef.current + dragStateRef.current.isDragging = false + setIsDragging(false) + keepLoopCentered() + + if (scroller?.hasPointerCapture?.(event.pointerId)) { + scroller.releasePointerCapture(event.pointerId) + } + } + if (testimonials.length === 0) return null return (
-
- {testimonials.map((current, i) => ( -
+
+ + +
+
{ + if (dragStateRef.current.isDragging) stopDragging(event) + }} + > + {visibleTestimonials.map((current, i) => ( +
"

{current.quote}