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:
jungwoo choi
2025-10-23 14:30:25 +09:00
commit 919afe56f2
1516 changed files with 64072 additions and 0 deletions

View 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>
);
}