o2o-infinith-demo/src/components/plan/RepurposingProposal.tsx

166 lines
7.7 KiB
TypeScript

import { useState } from 'react';
import { motion } from 'motion/react';
import {
YoutubeFilled,
InstagramFilled,
FacebookFilled,
GlobeFilled,
TiktokFilled,
VideoFilled,
RefreshFilled,
BoltFilled,
} from '../icons/FilledIcons';
import { SectionWrapper } from '../report/ui/SectionWrapper';
import type { RepurposingProposalItem } from '../../types/plan';
interface RepurposingProposalProps {
proposals: RepurposingProposalItem[];
}
// Maps channel keyword → FilledIcon
function ChannelIcon({ channel, size = 14 }: { channel: string; size?: number }) {
const lower = channel.toLowerCase();
const className = 'shrink-0';
if (lower.includes('youtube')) return <YoutubeFilled size={size} className={className} />;
if (lower.includes('instagram')) return <InstagramFilled size={size} className={className} />;
if (lower.includes('facebook')) return <FacebookFilled size={size} className={className} />;
if (lower.includes('tiktok')) return <TiktokFilled size={size} className={className} />;
if (lower.includes('naver') || lower.includes('blog')) return <GlobeFilled size={size} className={className} />;
return <VideoFilled size={size} className={className} />;
}
const effortConfig: Record<string, { label: string; bg: string; text: string; border: string }> = {
low: { label: '빠른 작업', bg: 'bg-[#F3F0FF]', text: 'text-[#4A3A7C]', border: 'border-[#D5CDF5]' },
medium: { label: '중간 작업', bg: 'bg-[#FFF6ED]', text: 'text-[#7C5C3A]', border: 'border-[#F5E0C5]' },
high: { label: '집중 작업', bg: 'bg-[#FFF0F0]', text: 'text-[#7C3A4B]', border: 'border-[#F5D5DC]' },
};
const priorityConfig: Record<string, { label: string; dot: string }> = {
high: { label: 'P0', dot: 'bg-[#D4889A]' },
medium: { label: 'P1', dot: 'bg-[#D4A872]' },
low: { label: 'P2', dot: 'bg-[#9B8AD4]' },
};
function formatViews(views: number): string {
if (views >= 1_000_000) return `${(views / 1_000_000).toFixed(1)}M`;
if (views >= 1_000) return `${Math.round(views / 1_000)}K`;
return String(views);
}
export default function RepurposingProposal({ proposals }: RepurposingProposalProps) {
const [expandedIdx, setExpandedIdx] = useState<number | null>(0);
return (
<SectionWrapper
id="repurposing-proposal"
title="Repurposing Proposal"
subtitle="콘텐츠 리퍼포징 제안"
>
<div className="space-y-4">
{proposals.map((item, idx) => {
const effort = effortConfig[item.estimatedEffort];
const priority = priorityConfig[item.priority];
const isExpanded = expandedIdx === idx;
return (
<motion.div
key={item.sourceVideo.title}
className="rounded-2xl border border-slate-100 bg-white shadow-[3px_4px_12px_rgba(0,0,0,0.06)] overflow-hidden"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: idx * 0.08 }}
>
{/* Card Header — click to expand */}
<button
className="w-full flex items-center gap-4 px-6 py-4 text-left hover:bg-slate-50/50 transition-colors"
onClick={() => setExpandedIdx(isExpanded ? null : idx)}
>
{/* Source video info */}
<div className="w-10 h-10 rounded-xl bg-[#F3F0FF] flex items-center justify-center shrink-0">
<YoutubeFilled size={20} className="text-[#6C5CE7]" />
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-[#0A1128] text-sm truncate">{item.sourceVideo.title}</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-slate-500">{formatViews(item.sourceVideo.views)} views</span>
<span className="w-1 h-1 rounded-full bg-slate-300" />
<span className="text-xs text-slate-500">{item.sourceVideo.type === 'Short' ? 'Shorts' : 'Long-form'}</span>
</div>
</div>
{/* Badges */}
<div className="flex items-center gap-2 shrink-0">
{/* Output count */}
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-[#F3F0FF] border border-[#D5CDF5]">
<RefreshFilled size={11} className="text-[#6C5CE7]" />
<span className="text-xs font-semibold text-[#4A3A7C]">{item.outputs.length} </span>
</div>
{/* Effort */}
<span className={`text-xs font-medium px-2.5 py-1 rounded-full border ${effort.bg} ${effort.text} ${effort.border} hidden sm:inline-flex`}>
{effort.label}
</span>
{/* Priority */}
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-slate-50 border border-slate-200">
<span className={`w-1.5 h-1.5 rounded-full ${priority.dot}`} />
<span className="text-xs font-bold text-slate-600">{priority.label}</span>
</div>
{/* Chevron */}
<svg
className={`w-4 h-4 text-slate-400 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
{/* Expanded: Repurpose outputs */}
{isExpanded && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="border-t border-slate-100"
>
<div className="px-6 py-4">
<div className="flex items-center gap-2 mb-4">
<BoltFilled size={14} className="text-[#6C5CE7]" />
<p className="text-xs font-semibold text-slate-500 uppercase tracking-widest">REPURPOSE AS</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{item.outputs.map((output, oIdx) => (
<motion.div
key={output.format}
className="flex items-start gap-3 p-4 rounded-xl bg-slate-50 border border-slate-100"
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: oIdx * 0.06 }}
>
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#6C5CE7]/10 to-[#4F1DA1]/10 flex items-center justify-center shrink-0">
<ChannelIcon channel={output.channel} size={16} />
</div>
<div className="min-w-0">
<p className="font-semibold text-[#0A1128] text-sm">{output.format}</p>
<p className="text-xs text-[#6C5CE7] font-medium mb-1">{output.channel}</p>
<p className="text-xs text-slate-500 leading-relaxed">{output.description}</p>
</div>
</motion.div>
))}
</div>
</div>
</motion.div>
)}
</motion.div>
);
})}
</div>
</SectionWrapper>
);
}