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

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 &rarr;
</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>
);
}