117 lines
4.3 KiB
JavaScript
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>
|
|
);
|
|
}
|