166 lines
7.7 KiB
TypeScript
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>
|
|
);
|
|
}
|