Files
WebFTP/components/SiteManagerModal.tsx
2026-01-19 11:04:36 +09:00

347 lines
15 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { X, Server, FolderPlus, Trash2, Save, Power, Monitor, Settings2 } from 'lucide-react';
import { SiteConfig } from '../types';
interface SiteManagerModalProps {
isOpen: boolean;
onClose: () => void;
onConnect: (site: SiteConfig) => void;
onSaveSites: (sites: SiteConfig[]) => void;
initialSites: SiteConfig[];
}
const SiteManagerModal: React.FC<SiteManagerModalProps> = ({
isOpen,
onClose,
onConnect,
onSaveSites,
initialSites
}) => {
const [sites, setSites] = useState<SiteConfig[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [formData, setFormData] = useState<SiteConfig | null>(null);
const [isDirty, setIsDirty] = useState(false);
const [activeTab, setActiveTab] = useState<'general' | 'transfer'>('general');
// Initialize
useEffect(() => {
if (isOpen) {
setSites(initialSites);
if (initialSites.length > 0 && !selectedId) {
selectSite(initialSites[0]);
}
}
}, [isOpen, initialSites]);
const selectSite = (site: SiteConfig) => {
setSelectedId(site.id);
setFormData({ ...site });
setIsDirty(false);
setActiveTab('general');
};
const handleNewSite = () => {
const newSite: SiteConfig = {
id: `site-${Date.now()}`,
name: '새 사이트',
protocol: 'ftp',
host: '',
port: '21',
user: '',
pass: '',
passiveMode: true
};
const newSites = [...sites, newSite];
setSites(newSites);
selectSite(newSite);
setIsDirty(true);
};
const handleDelete = () => {
if (!selectedId) return;
if (!window.confirm('선택한 사이트를 삭제하시겠습니까?')) return;
const newSites = sites.filter(s => s.id !== selectedId);
setSites(newSites);
onSaveSites(newSites); // Auto save on delete
if (newSites.length > 0) {
selectSite(newSites[0]);
} else {
setSelectedId(null);
setFormData(null);
}
};
const handleSave = () => {
if (!formData) return;
const newSites = sites.map(s => s.id === formData.id ? formData : s);
setSites(newSites);
onSaveSites(newSites);
setIsDirty(false);
};
const handleConnectClick = () => {
if (formData) {
if (isDirty) {
handleSave();
}
onConnect(formData);
}
};
const updateForm = (field: keyof SiteConfig, value: any) => {
if (!formData) return;
const updated = { ...formData, [field]: value };
// Auto port update based on protocol
if (field === 'protocol') {
if (value === 'sftp') updated.port = '22';
if (value === 'ftp') updated.port = '21';
}
setFormData(updated);
if (field === 'name') {
setSites(sites.map(s => s.id === updated.id ? updated : s));
}
setIsDirty(true);
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/20 backdrop-blur-sm p-4">
<div className="bg-white border border-slate-200 rounded-lg shadow-2xl w-full max-w-3xl flex flex-col h-[600px] max-h-[90vh]">
{/* Header */}
<div className="flex items-center justify-between p-3 border-b border-slate-200 bg-slate-50 rounded-t-lg">
<h2 className="text-sm font-bold text-slate-800 flex items-center gap-2">
<Server size={18} className="text-blue-600" />
(Site Manager)
</h2>
<button onClick={onClose} className="text-slate-400 hover:text-slate-600 transition-colors">
<X size={18} />
</button>
</div>
<div className="flex flex-1 min-h-0">
{/* Left: Site List */}
<div className="w-1/3 border-r border-slate-200 flex flex-col bg-slate-50">
<div className="p-2 border-b border-slate-200 flex gap-2">
<button
onClick={handleNewSite}
className="flex-1 bg-white hover:bg-slate-50 text-slate-700 text-xs py-1.5 rounded border border-slate-300 flex items-center justify-center gap-1 transition-colors shadow-sm"
>
<FolderPlus size={14} />
</button>
<button
onClick={handleDelete}
disabled={!selectedId}
className="bg-white hover:bg-red-50 hover:border-red-200 text-slate-500 hover:text-red-500 text-xs px-2 py-1.5 rounded border border-slate-300 transition-colors disabled:opacity-50 shadow-sm"
>
<Trash2 size={14} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1">
{sites.map(site => (
<div
key={site.id}
onClick={() => selectSite(site)}
className={`flex items-center gap-2 px-3 py-2 rounded cursor-pointer text-sm select-none transition-colors ${
selectedId === site.id
? 'bg-blue-100 text-blue-900 border border-blue-200'
: 'text-slate-600 hover:bg-slate-200'
}`}
>
<Server size={14} className={selectedId === site.id ? 'text-blue-600' : 'text-slate-400'} />
<span className="truncate">{site.name}</span>
</div>
))}
{sites.length === 0 && (
<div className="text-center text-slate-400 text-xs mt-10">
.
</div>
)}
</div>
</div>
{/* Right: Site Details Form */}
<div className="w-2/3 flex flex-col bg-white">
{formData ? (
<>
{/* Tabs */}
<div className="flex border-b border-slate-200 px-4">
<button
onClick={() => setActiveTab('general')}
className={`px-4 py-2 text-xs font-medium border-b-2 transition-colors ${
activeTab === 'general' ? 'border-blue-500 text-blue-600' : 'border-transparent text-slate-500'
}`}
>
(General)
</button>
<button
onClick={() => setActiveTab('transfer')}
className={`px-4 py-2 text-xs font-medium border-b-2 transition-colors ${
activeTab === 'transfer' ? 'border-blue-500 text-blue-600' : 'border-transparent text-slate-500'
}`}
>
(Transfer)
</button>
</div>
<div className="p-6 flex-1 overflow-y-auto">
{activeTab === 'general' ? (
<div className="space-y-4">
<div className="grid grid-cols-4 gap-4 items-center">
<label className="text-xs text-slate-500 text-right"> </label>
<input
type="text"
value={formData.name}
onChange={(e) => updateForm('name', e.target.value)}
className="col-span-3 bg-white border border-slate-300 rounded px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none text-slate-800"
/>
</div>
<hr className="border-slate-200 my-4" />
<div className="grid grid-cols-4 gap-4 items-center">
<label className="text-xs text-slate-500 text-right"></label>
<select
value={formData.protocol}
onChange={(e) => updateForm('protocol', e.target.value as any)}
className="col-span-3 bg-white border border-slate-300 rounded px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none text-slate-800"
>
<option value="ftp">FTP - </option>
<option value="sftp">SFTP - SSH </option>
</select>
</div>
<div className="grid grid-cols-4 gap-4 items-center">
<label className="text-xs text-slate-500 text-right"></label>
<div className="col-span-3 flex gap-2">
<input
type="text"
value={formData.host}
onChange={(e) => updateForm('host', e.target.value)}
placeholder="ftp.example.com"
className="flex-1 bg-white border border-slate-300 rounded px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none text-slate-800 placeholder:text-slate-400"
/>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">:</span>
<input
type="text"
value={formData.port}
onChange={(e) => updateForm('port', e.target.value)}
className="w-16 bg-white border border-slate-300 rounded px-2 py-1.5 text-sm text-center focus:border-blue-500 focus:outline-none text-slate-800"
/>
</div>
</div>
</div>
<div className="grid grid-cols-4 gap-4 items-center">
<label className="text-xs text-slate-500 text-right"> (ID)</label>
<input
type="text"
value={formData.user}
onChange={(e) => updateForm('user', e.target.value)}
className="col-span-3 bg-white border border-slate-300 rounded px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none text-slate-800"
/>
</div>
<div className="grid grid-cols-4 gap-4 items-center">
<label className="text-xs text-slate-500 text-right"></label>
<input
type="password"
value={formData.pass || ''}
onChange={(e) => updateForm('pass', e.target.value)}
placeholder="저장하지 않으려면 비워두세요"
className="col-span-3 bg-white border border-slate-300 rounded px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none text-slate-800 placeholder:text-slate-400"
/>
</div>
</div>
) : (
<div className="space-y-6">
<div className="space-y-3">
<h3 className="text-sm font-bold text-slate-800 border-b border-slate-200 pb-2 mb-4"> </h3>
<div className="flex items-center gap-3">
<label className="flex items-center gap-2 cursor-pointer group">
<div className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${formData.passiveMode !== false ? 'bg-blue-600 border-blue-500' : 'bg-white border-slate-400'}`}>
{formData.passiveMode !== false && <div className="w-2 h-2 bg-white rounded-sm" />}
</div>
<input
type="checkbox"
checked={formData.passiveMode !== false}
onChange={(e) => updateForm('passiveMode', e.target.checked)}
className="hidden"
/>
<div className="text-sm text-slate-700 group-hover:text-blue-600 transition-colors"> (Passive Mode) </div>
</label>
</div>
<p className="text-xs text-slate-500 ml-6">
/NAT .
(FTP )
</p>
</div>
<div className="space-y-3">
<h3 className="text-sm font-bold text-slate-800 border-b border-slate-200 pb-2 mb-4"> </h3>
<div className="grid grid-cols-4 gap-4 items-center">
<label className="text-xs text-slate-500"> :</label>
<input type="number" defaultValue={2} disabled className="bg-slate-100 border border-slate-300 rounded px-2 py-1 text-sm text-slate-500" />
<span className="text-xs text-slate-400 col-span-2">( )</span>
</div>
</div>
</div>
)}
</div>
</>
) : (
<div className="flex-1 flex flex-col items-center justify-center text-slate-400">
<Server size={48} className="mb-4 opacity-20" />
<p> </p>
<p>'새 사이트' .</p>
</div>
)}
</div>
</div>
{/* Footer Actions */}
<div className="p-3 border-t border-slate-200 bg-slate-50 flex justify-between items-center rounded-b-lg">
<button
onClick={onClose}
className="px-4 py-2 text-slate-500 hover:text-slate-800 text-sm transition-colors"
>
</button>
<div className="flex gap-2">
<button
onClick={handleSave}
disabled={!formData}
className={`px-4 py-2 text-sm rounded flex items-center gap-2 transition-colors shadow-sm ${
isDirty
? 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-emerald-500/20'
: 'bg-white border border-slate-300 text-slate-500'
}`}
>
<Save size={16} />
</button>
<button
onClick={handleConnectClick}
disabled={!formData}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded text-sm flex items-center gap-2 shadow-md shadow-blue-500/20"
>
<Power size={16} />
</button>
</div>
</div>
</div>
</div>
);
};
export default SiteManagerModal;