143 lines
6.1 KiB
TypeScript
143 lines
6.1 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|
import { Category, Prompt } from '../types';
|
|
import { FolderIcon, FileIcon, PlusIcon, TrashIcon } from './Icons';
|
|
|
|
interface SidebarProps {
|
|
categories: Category[];
|
|
prompts: Prompt[];
|
|
onSelectPrompt: (id: string) => void;
|
|
onAddCategory: (parentId: string | null) => void;
|
|
onAddPrompt: (categoryId: string) => void;
|
|
onDeleteCategory: (id: string) => void;
|
|
selectedPromptId: string | null;
|
|
}
|
|
|
|
const Sidebar: React.FC<SidebarProps> = ({
|
|
categories,
|
|
prompts,
|
|
onSelectPrompt,
|
|
onAddCategory,
|
|
onAddPrompt,
|
|
onDeleteCategory,
|
|
selectedPromptId
|
|
}) => {
|
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
|
|
|
const toggleExpand = (id: string) => {
|
|
setExpanded(prev => ({ ...prev, [id]: !prev[id] }));
|
|
};
|
|
|
|
const renderCategory = (category: Category, depth: number = 0) => {
|
|
const categoryPrompts = prompts.filter(p => p.categoryId === category.id);
|
|
const isExpanded = expanded[category.id];
|
|
|
|
return (
|
|
<div key={category.id} className="mb-0.5">
|
|
<div
|
|
className={`group flex items-center justify-between px-3 py-2 rounded-lg cursor-pointer transition-all duration-200 ${isExpanded ? 'bg-slate-50' : 'hover:bg-slate-50'}`}
|
|
onClick={() => toggleExpand(category.id)}
|
|
>
|
|
<div className="flex items-center space-x-2.5" style={{ paddingLeft: `${depth * 12}px` }}>
|
|
<span className={`transform transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}>
|
|
<svg className="w-3 h-3 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</span>
|
|
<FolderIcon className={`w-4 h-4 ${isExpanded ? 'text-indigo-500' : 'text-slate-400'}`} />
|
|
<span className={`text-sm font-semibold truncate ${isExpanded ? 'text-slate-900' : 'text-slate-600'}`}>{category.name}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); onAddPrompt(category.id); }}
|
|
className="p-1 hover:bg-white hover:shadow-sm rounded text-slate-500 transition-all"
|
|
title="프롬프트 추가"
|
|
>
|
|
<PlusIcon className="w-3.5 h-3.5" />
|
|
</button>
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); onDeleteCategory(category.id); }}
|
|
className="p-1 hover:bg-red-50 hover:text-red-500 rounded text-slate-400 transition-all"
|
|
title="분야 삭제"
|
|
>
|
|
<TrashIcon className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{isExpanded && (
|
|
<div className="mt-0.5 border-l border-slate-100 ml-4.5 pl-0">
|
|
{categoryPrompts.map(prompt => (
|
|
<div
|
|
key={prompt.id}
|
|
onClick={() => onSelectPrompt(prompt.id)}
|
|
className={`flex items-center space-x-3 px-4 py-2 my-0.5 cursor-pointer rounded-lg text-sm transition-all duration-200
|
|
${selectedPromptId === prompt.id
|
|
? 'bg-indigo-600 text-white shadow-md shadow-indigo-100'
|
|
: 'hover:bg-indigo-50 text-slate-500 hover:text-indigo-600'}
|
|
`}
|
|
style={{ marginLeft: `${depth * 4}px` }}
|
|
>
|
|
<FileIcon className={`w-4 h-4 ${selectedPromptId === prompt.id ? 'text-white' : 'text-slate-300'}`} />
|
|
<span className="truncate font-medium">{prompt.name}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<aside className="w-80 sidebar-border bg-white flex flex-col h-screen overflow-hidden">
|
|
<div className="px-6 py-8">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex flex-col">
|
|
<h1 className="text-xl font-bold text-slate-900 leading-tight tracking-tight">SAM 프롬프트 관리</h1>
|
|
<a href="https://codebridge-x.com" target="_blank" className="text-[10px] font-black text-indigo-500 uppercase tracking-widest hover:underline transition-all">
|
|
codebridge-x.com
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => onAddCategory(null)}
|
|
className="mt-6 w-full flex items-center justify-center space-x-2 py-2.5 bg-slate-900 text-white rounded-xl hover:bg-slate-800 transition-all shadow-lg active:scale-95"
|
|
>
|
|
<PlusIcon className="w-4 h-4" />
|
|
<span className="text-sm font-bold">새 연구 분야 추가</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto px-4 pb-10 space-y-1">
|
|
{categories.length === 0 ? (
|
|
<div className="text-center py-20 px-6">
|
|
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
<FolderIcon className="w-6 h-6 text-slate-300" />
|
|
</div>
|
|
<p className="text-sm text-slate-400 font-medium">등록된 연구 분야가 없습니다.</p>
|
|
</div>
|
|
) : (
|
|
categories.map(cat => renderCategory(cat))
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-auto p-6 bg-slate-50/50 border-t border-slate-100">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="relative">
|
|
<div className="w-10 h-10 rounded-2xl bg-indigo-600 flex items-center justify-center text-white font-black text-sm shadow-inner">
|
|
CB
|
|
</div>
|
|
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-emerald-500 border-2 border-white rounded-full"></div>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<p className="text-sm font-black text-slate-900 leading-none mb-1">프롬프트 연구소</p>
|
|
<p className="text-[11px] text-slate-500 font-bold">v2.1 안정화 버전</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
};
|
|
|
|
export default Sidebar;
|