feat: SAPIENS Mobile App - Initial commit
React Native mobile application for SAPIENS news platform. Consolidated all previous history into single commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
117
client/src/components/SwipeableCarousel.tsx
Normal file
117
client/src/components/SwipeableCarousel.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
interface CarouselItem {
|
||||
id: string;
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
interface SwipeableCarouselProps {
|
||||
items: CarouselItem[];
|
||||
autoScroll?: boolean;
|
||||
autoScrollDelay?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function SwipeableCarousel({
|
||||
items,
|
||||
autoScroll = true,
|
||||
autoScrollDelay = 5000,
|
||||
className = "",
|
||||
}: SwipeableCarouselProps) {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const carouselRef = useRef<HTMLDivElement>(null);
|
||||
const startXRef = useRef(0);
|
||||
const scrollLeftRef = useRef(0);
|
||||
|
||||
// Auto scroll functionality
|
||||
useEffect(() => {
|
||||
if (!autoScroll || isDragging) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentIndex((prev) => (prev + 1) % items.length);
|
||||
}, autoScrollDelay);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [autoScroll, autoScrollDelay, isDragging, items.length]);
|
||||
|
||||
// Handle manual scroll
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
setIsDragging(true);
|
||||
const carousel = carouselRef.current;
|
||||
if (!carousel) return;
|
||||
|
||||
startXRef.current = e.pageX - carousel.offsetLeft;
|
||||
scrollLeftRef.current = carousel.scrollLeft;
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
|
||||
const carousel = carouselRef.current;
|
||||
if (!carousel) return;
|
||||
|
||||
const x = e.pageX - carousel.offsetLeft;
|
||||
const walk = (x - startXRef.current) * 2;
|
||||
carousel.scrollLeft = scrollLeftRef.current - walk;
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
// Touch events for mobile
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
setIsDragging(true);
|
||||
const carousel = carouselRef.current;
|
||||
if (!carousel) return;
|
||||
|
||||
startXRef.current = e.touches[0].pageX - carousel.offsetLeft;
|
||||
scrollLeftRef.current = carousel.scrollLeft;
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const carousel = carouselRef.current;
|
||||
if (!carousel) return;
|
||||
|
||||
const x = e.touches[0].pageX - carousel.offsetLeft;
|
||||
const walk = (x - startXRef.current) * 2;
|
||||
carousel.scrollLeft = scrollLeftRef.current - walk;
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="flex gap-4 overflow-x-auto scrollbar-hide snap-x snap-mandatory px-4"
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
data-testid="carousel-container"
|
||||
style={{ scrollBehavior: isDragging ? 'auto' : 'smooth' }}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex-shrink-0 w-80 snap-start"
|
||||
data-testid={`carousel-item-${index}`}
|
||||
>
|
||||
{item.content}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user