201 lines
7.8 KiB
TypeScript
201 lines
7.8 KiB
TypeScript
import { useState } from 'react';
|
|
import { motion } from 'motion/react';
|
|
import { YoutubeFilled } from '../icons/FilledIcons';
|
|
import { SectionWrapper } from '../report/ui/SectionWrapper';
|
|
import AssetDetailModal from './AssetDetailModal';
|
|
import type { AssetCollectionData, AssetCard, YouTubeRepurposeItem, AssetSource, AssetStatus, AssetType } from '../../types/plan';
|
|
|
|
type ModalAsset =
|
|
| { kind: 'asset'; data: AssetCard }
|
|
| { kind: 'youtube'; data: YouTubeRepurposeItem };
|
|
|
|
interface AssetCollectionProps {
|
|
data: AssetCollectionData;
|
|
}
|
|
|
|
const filterTabs = [
|
|
{ key: 'all', label: '전체' },
|
|
{ key: 'homepage', label: '홈페이지' },
|
|
{ key: 'naver_place', label: '네이버' },
|
|
{ key: 'blog', label: '블로그' },
|
|
{ key: 'social', label: '소셜미디어' },
|
|
{ key: 'youtube', label: 'YouTube' },
|
|
] as const;
|
|
|
|
type FilterKey = (typeof filterTabs)[number]['key'];
|
|
|
|
const sourceBadgeColors: Record<AssetSource, string> = {
|
|
homepage: 'bg-slate-100 text-slate-700',
|
|
naver_place: 'bg-[#F3F0FF] text-[#4A3A7C]',
|
|
blog: 'bg-[#EFF0FF] text-[#3A3F7C]',
|
|
social: 'bg-[#FFF0F0] text-[#7C3A4B]',
|
|
youtube: 'bg-[#FFF0F0] text-[#7C3A4B]',
|
|
};
|
|
|
|
const typeBadgeColors: Record<AssetType, string> = {
|
|
photo: 'bg-[#EFF0FF] text-[#3A3F7C]',
|
|
video: 'bg-[#F3F0FF] text-[#4A3A7C]',
|
|
text: 'bg-[#FFF6ED] text-[#7C5C3A]',
|
|
};
|
|
|
|
const statusConfig: Record<AssetStatus, { className: string; label: string }> = {
|
|
collected: { className: 'bg-[#F3F0FF] text-[#4A3A7C]', label: '수집완료' },
|
|
pending: { className: 'bg-[#FFF6ED] text-[#7C5C3A]', label: '수집 대기' },
|
|
needs_creation: { className: 'bg-[#FFF0F0] text-[#7C3A4B]', label: '제작 필요' },
|
|
};
|
|
|
|
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 AssetCollection({ data }: AssetCollectionProps) {
|
|
const [activeFilter, setActiveFilter] = useState<FilterKey>('all');
|
|
const [selectedAsset, setSelectedAsset] = useState<ModalAsset | null>(null);
|
|
|
|
const filteredAssets =
|
|
activeFilter === 'all'
|
|
? data.assets
|
|
: data.assets.filter((a) => a.source === activeFilter);
|
|
|
|
return (
|
|
<SectionWrapper
|
|
id="asset-collection"
|
|
title="Asset Collection"
|
|
subtitle="에셋 수집 & 리퍼포징 소스"
|
|
>
|
|
{/* Source Filter Tabs */}
|
|
<div className="flex flex-wrap gap-2 mb-8">
|
|
{filterTabs.map((tab) => (
|
|
<button
|
|
key={tab.key}
|
|
onClick={() => setActiveFilter(tab.key)}
|
|
className={`rounded-full px-4 py-2 text-sm font-medium transition-all ${
|
|
activeFilter === tab.key
|
|
? 'bg-gradient-to-r from-[#4F1DA1] to-[#021341] text-white shadow-lg'
|
|
: 'bg-white border border-slate-200 text-slate-600 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Asset Cards Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-12">
|
|
{filteredAssets.map((asset, i) => {
|
|
const statusInfo = statusConfig[asset.status];
|
|
return (
|
|
<motion.div
|
|
key={asset.id}
|
|
className="rounded-2xl border border-slate-100 bg-white shadow-sm p-5 cursor-pointer hover:shadow-[3px_4px_12px_rgba(0,0,0,0.06)] hover:border-[#D5CDF5] transition-all"
|
|
onClick={() => setSelectedAsset({ kind: 'asset', data: asset })}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: i * 0.05 }}
|
|
>
|
|
{/* Top badges row */}
|
|
<div className="flex items-center gap-2 mb-3 flex-wrap">
|
|
<span
|
|
className={`rounded-full px-3 py-1 text-xs font-medium ${sourceBadgeColors[asset.source]}`}
|
|
>
|
|
{asset.sourceLabel}
|
|
</span>
|
|
<span
|
|
className={`rounded-full px-3 py-1 text-xs font-medium ${typeBadgeColors[asset.type]}`}
|
|
>
|
|
{asset.type}
|
|
</span>
|
|
<span
|
|
className={`rounded-full px-3 py-1 text-xs font-medium ml-auto ${statusInfo.className}`}
|
|
>
|
|
{statusInfo.label}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Title & Description */}
|
|
<h4 className="font-semibold text-[#0A1128] mb-1">{asset.title}</h4>
|
|
<p className="text-sm text-slate-600 mb-3">{asset.description}</p>
|
|
|
|
{/* Repurposing suggestions */}
|
|
{asset.repurposingSuggestions.length > 0 && (
|
|
<div>
|
|
<p className="text-xs font-semibold text-slate-500 uppercase mb-2">
|
|
Repurposing →
|
|
</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{asset.repurposingSuggestions.map((suggestion, j) => (
|
|
<span
|
|
key={j}
|
|
className="rounded-full bg-[#F3F0FF] text-[#4A3A7C] text-xs px-2 py-1"
|
|
>
|
|
{suggestion}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* YouTube Repurpose Section */}
|
|
{data.youtubeRepurpose.length > 0 && (
|
|
<div>
|
|
<h3 className="font-serif text-2xl font-bold text-[#0A1128] mb-4">
|
|
YouTube Top Videos for Repurposing
|
|
</h3>
|
|
<div className="flex overflow-x-auto gap-4 pb-4 scrollbar-hide" style={{ scrollbarWidth: 'none' }}>
|
|
{data.youtubeRepurpose.map((video, i) => (
|
|
<motion.div
|
|
key={video.title}
|
|
className="min-w-[280px] rounded-2xl border border-slate-100 bg-white shadow-sm p-5 shrink-0 cursor-pointer hover:shadow-[3px_4px_12px_rgba(0,0,0,0.06)] hover:border-[#D5CDF5] transition-all"
|
|
onClick={() => setSelectedAsset({ kind: 'youtube', data: video })}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.4, delay: i * 0.1 }}
|
|
>
|
|
<div className="flex items-start gap-2 mb-3">
|
|
<YoutubeFilled size={18} className="text-[#D4889A] shrink-0 mt-0.5" />
|
|
<h4 className="font-semibold text-sm text-[#0A1128]">{video.title}</h4>
|
|
</div>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<span className="rounded-full bg-slate-100 text-slate-700 px-3 py-1 text-xs font-medium">
|
|
{formatViews(video.views)} views
|
|
</span>
|
|
<span
|
|
className={`rounded-full px-3 py-1 text-xs font-medium ${
|
|
video.type === 'Short'
|
|
? 'bg-[#F3F0FF] text-[#4A3A7C]'
|
|
: 'bg-[#EFF0FF] text-[#3A3F7C]'
|
|
}`}
|
|
>
|
|
{video.type}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs font-semibold text-slate-500 uppercase mb-2">
|
|
Repurpose As:
|
|
</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{video.repurposeAs.map((suggestion, j) => (
|
|
<span
|
|
key={j}
|
|
className="rounded-full bg-[#F3F0FF] text-[#4A3A7C] text-xs px-2 py-1"
|
|
>
|
|
{suggestion}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<AssetDetailModal
|
|
asset={selectedAsset}
|
|
onClose={() => setSelectedAsset(null)}
|
|
/>
|
|
</SectionWrapper>
|
|
);
|
|
}
|