Files
ProjectSS/client/src/components/SwipeCard.jsx

117 lines
4.3 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { motion, useMotionValue, useTransform, useAnimation } from 'framer-motion';
import { HandCoins, Droplet, Eye, Shield, Building2, EyeOff } from 'lucide-react';
import './SwipeCard.css';
const renderStats = (stats) => {
if (!stats) return null;
const items = [];
if (stats.res !== 0) items.push({ icon: '✊', label: '저항', val: stats.res, isDanger: true });
if (stats.ent !== 0) items.push({ icon: '🌀', label: '엔트로피', val: stats.ent, isDanger: true });
if (stats.rsk !== 0) items.push({ icon: '⚠️', label: '위험', val: stats.rsk, isDanger: true });
if (stats.bud !== 0) items.push({ icon: <HandCoins size={14} />, label: '예산', val: stats.bud });
if (stats.per !== 0) items.push({ icon: <Droplet size={14} />, label: '인력', val: stats.per });
if (stats.inf !== 0) items.push({ icon: <Eye size={14} />, label: '정보', val: stats.inf });
if (stats.wea !== 0) items.push({ icon: <Building2 size={14} />, label: '재력', val: stats.wea });
if (stats.for !== 0) items.push({ icon: <Shield size={14} />, label: '무력', val: stats.for });
if (stats.sur !== 0) items.push({ icon: <EyeOff size={14} />, label: '감시', val: stats.sur });
if (items.length === 0) return null;
return (
<div className="stats-preview">
{items.map((item, idx) => {
const sign = item.val > 0 ? '+' : '';
const isBad = item.isDanger ? item.val > 0 : item.val < 0;
const colorClass = isBad ? 'stat-bad' : 'stat-good';
return (
<div key={idx} className={`stat-item ${colorClass}`}>
<span className="stat-icon">{item.icon}</span>
<span className="stat-label">{item.label}</span>
<span className="stat-val">{sign}{item.val}</span>
</div>
);
})}
</div>
);
};
export default function SwipeCard({ card, onSwipe, phase }) {
const x = useMotionValue(0);
const controls = useAnimation();
// Dynamic friction based on phase
const friction = phase === 3 ? 1.5 : phase === 2 ? 1.2 : 0.8;
// Rotations and opacity
const rotate = useTransform(x, [-200, 200], [-15 * friction, 15 * friction]);
const leftOpacity = useTransform(x, [-100, -50, 0], [1, 0.5, 0]);
const rightOpacity = useTransform(x, [0, 50, 100], [0, 0.5, 1]);
const [exitX, setExitX] = useState(0);
// Reset when card changes
useEffect(() => {
setExitX(0);
x.set(0);
controls.start({ x: 0, opacity: 1, rotate: 0, transition: { duration: 0 } });
}, [card, controls, x]);
const handleDragEnd = async (event, info) => {
const threshold = 100;
if (info.offset.x > threshold) {
setExitX(300);
await controls.start({ x: 300, opacity: 0, transition: { duration: 0.3 } });
onSwipe('right');
} else if (info.offset.x < -threshold) {
setExitX(-300);
await controls.start({ x: -300, opacity: 0, transition: { duration: 0.3 } });
onSwipe('left');
} else {
// Snap back
controls.start({ x: 0, y: 0, transition: { type: 'spring', stiffness: 300, damping: 20 } });
}
};
return (
<div className="card-container">
<motion.div
className="swipe-card"
drag="x"
dragConstraints={{ left: 0, right: 0 }}
style={{ x, rotate }}
onDragEnd={handleDragEnd}
animate={controls}
whileTap={{ scale: 0.98, cursor: 'grabbing' }}
>
<div className="card-content">
<div className="card-header">
<span className="card-category">[{card.type}]</span>
</div>
<div className="card-body">
<p className="card-text">{card.text}</p>
</div>
{card.flavor && (
<div className="card-flavor-container">
<p className="card-flavor">"{card.flavor}"</p>
</div>
)}
</div>
{/* Swipe Intents */}
<motion.div className="swipe-intent left-intent" style={{ opacity: leftOpacity }}>
<span className="intent-title">{card.left?.text}</span>
{renderStats(card.left?.stats)}
</motion.div>
<motion.div className="swipe-intent right-intent" style={{ opacity: rightOpacity }}>
<span className="intent-title">{card.right?.text}</span>
{renderStats(card.right?.stats)}
</motion.div>
</motion.div>
</div>
);
}