feat(frontend): X 봇 관리 UI 추가
- XBotDialog 컴포넌트 생성 - ScheduleBots 페이지에 X 봇 추가/수정/삭제 통합 - X 섹션에 canAdd 활성화 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ec0e587434
commit
eeb5e7234c
3 changed files with 391 additions and 7 deletions
353
frontend/src/components/pc/admin/bot/XBotDialog.jsx
Normal file
353
frontend/src/components/pc/admin/bot/XBotDialog.jsx
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
/**
|
||||||
|
* X 봇 추가/수정 다이얼로그
|
||||||
|
*/
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Twitter, Search, X, ChevronDown, Loader2 } from 'lucide-react';
|
||||||
|
import { getXBot, createXBot, updateXBot, lookupXProfile } from '@/api/admin/bots';
|
||||||
|
|
||||||
|
// 동기화 간격 옵션
|
||||||
|
const INTERVAL_OPTIONS = [
|
||||||
|
{ value: 1, label: '1분' },
|
||||||
|
{ value: 2, label: '2분' },
|
||||||
|
{ value: 5, label: '5분' },
|
||||||
|
{ value: 10, label: '10분' },
|
||||||
|
{ value: 30, label: '30분' },
|
||||||
|
{ value: 60, label: '1시간' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 커스텀 드롭다운 컴포넌트 (Portal 사용)
|
||||||
|
*/
|
||||||
|
function Dropdown({ value, options, onChange, placeholder = '선택', className = '' }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||||
|
const buttonRef = useRef(null);
|
||||||
|
const menuRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (
|
||||||
|
buttonRef.current &&
|
||||||
|
!buttonRef.current.contains(event.target) &&
|
||||||
|
menuRef.current &&
|
||||||
|
!menuRef.current.contains(event.target)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && buttonRef.current) {
|
||||||
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
|
setPosition({
|
||||||
|
top: rect.bottom + 4,
|
||||||
|
left: rect.left,
|
||||||
|
width: rect.width,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const selectedOption = options.find((opt) => opt.value === value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative ${className}`}>
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="flex items-center gap-2 w-full px-4 py-2.5 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm transition-colors justify-between"
|
||||||
|
>
|
||||||
|
<span className={selectedOption ? 'text-gray-900' : 'text-gray-400'}>
|
||||||
|
{selectedOption?.label || placeholder}
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
size={14}
|
||||||
|
className={`text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{createPortal(
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
ref={menuRef}
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: position.top,
|
||||||
|
left: position.left,
|
||||||
|
width: position.width,
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
className="bg-white rounded-xl shadow-lg border border-gray-200 py-1 max-h-60 overflow-y-auto"
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onChange(opt.value);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
className={`w-full px-4 py-2 text-left hover:bg-gray-50 transition-colors text-sm ${
|
||||||
|
value === opt.value ? 'bg-sky-50 text-sky-600' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const isEdit = !!botId;
|
||||||
|
|
||||||
|
// 폼 상태
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [profileInfo, setProfileInfo] = useState(null);
|
||||||
|
const [lookupLoading, setLookupLoading] = useState(false);
|
||||||
|
const [interval, setInterval] = useState(1);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// X 봇 상세 조회 (수정 모드)
|
||||||
|
const { data: bot, isLoading: botLoading } = useQuery({
|
||||||
|
queryKey: ['admin', 'x-bot', botId],
|
||||||
|
queryFn: () => getXBot(botId),
|
||||||
|
enabled: isOpen && !!botId,
|
||||||
|
staleTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 다이얼로그 열릴 때 데이터 설정
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
if (bot) {
|
||||||
|
// 수정 모드
|
||||||
|
setUsername(bot.username || '');
|
||||||
|
setProfileInfo({
|
||||||
|
username: bot.username,
|
||||||
|
displayName: bot.display_name,
|
||||||
|
avatarUrl: bot.avatar_url,
|
||||||
|
});
|
||||||
|
setInterval(bot.cron_interval || 1);
|
||||||
|
} else if (!botId) {
|
||||||
|
// 추가 모드
|
||||||
|
setUsername('');
|
||||||
|
setProfileInfo(null);
|
||||||
|
setInterval(1);
|
||||||
|
}
|
||||||
|
}, [isOpen, bot, botId]);
|
||||||
|
|
||||||
|
// 프로필 조회
|
||||||
|
const handleLookup = async () => {
|
||||||
|
if (!username.trim()) return;
|
||||||
|
setLookupLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await lookupXProfile(username);
|
||||||
|
setProfileInfo({
|
||||||
|
username: data.username,
|
||||||
|
displayName: data.displayName,
|
||||||
|
avatarUrl: data.avatarUrl,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('프로필 조회 실패:', error);
|
||||||
|
alert(error.message || '프로필을 찾을 수 없습니다.');
|
||||||
|
} finally {
|
||||||
|
setLookupLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 제출
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!profileInfo) return;
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
username: profileInfo.username,
|
||||||
|
display_name: profileInfo.displayName,
|
||||||
|
avatar_url: profileInfo.avatarUrl,
|
||||||
|
cron_interval: interval,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
await updateXBot(botId, data);
|
||||||
|
} else {
|
||||||
|
await createXBot(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐시 무효화
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['admin', 'bots'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['admin', 'x-bot'] });
|
||||||
|
|
||||||
|
onSuccess?.();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('봇 저장 실패:', error);
|
||||||
|
alert(error.message || '봇 저장에 실패했습니다.');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.95, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.95, opacity: 0 }}
|
||||||
|
className="bg-white rounded-2xl w-full max-w-lg mx-4 shadow-xl max-h-[90vh] overflow-hidden flex flex-col"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-sky-50 flex items-center justify-center">
|
||||||
|
<Twitter size={20} className="text-sky-500" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-bold text-gray-900">
|
||||||
|
{isEdit ? 'X 봇 수정' : 'X 봇 추가'}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 본문 */}
|
||||||
|
{botLoading ? (
|
||||||
|
<div className="flex-1 flex items-center justify-center p-12">
|
||||||
|
<Loader2 size={32} className="animate-spin text-sky-500" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-6 space-y-5">
|
||||||
|
{/* Username */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">@</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="realfromis_9"
|
||||||
|
disabled={isEdit}
|
||||||
|
className="w-full pl-8 pr-4 py-2.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500/20 focus:border-sky-500 disabled:bg-gray-50 disabled:text-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!isEdit && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleLookup}
|
||||||
|
disabled={lookupLoading || !username.trim()}
|
||||||
|
className="px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{lookupLoading ? (
|
||||||
|
<span className="w-4 h-4 border-2 border-gray-400/30 border-t-gray-400 rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Search size={18} />
|
||||||
|
)}
|
||||||
|
조회
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* 프로필 정보 표시 */}
|
||||||
|
{profileInfo && (
|
||||||
|
<div className="mt-3 p-4 bg-gray-50 rounded-lg flex items-center gap-4">
|
||||||
|
{profileInfo.avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={profileInfo.avatarUrl}
|
||||||
|
alt={profileInfo.displayName}
|
||||||
|
className="w-12 h-12 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 bg-sky-100 rounded-full flex items-center justify-center">
|
||||||
|
<Twitter size={24} className="text-sky-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-medium text-gray-900 truncate">
|
||||||
|
{profileInfo.displayName}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">@{profileInfo.username}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 동기화 간격 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
동기화 간격
|
||||||
|
</label>
|
||||||
|
<Dropdown
|
||||||
|
value={interval}
|
||||||
|
options={INTERVAL_OPTIONS}
|
||||||
|
onChange={setInterval}
|
||||||
|
placeholder="간격 선택"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 푸터 */}
|
||||||
|
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-100 bg-gray-50">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={submitting}
|
||||||
|
className="px-4 py-2.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!profileInfo || submitting || botLoading}
|
||||||
|
className="px-4 py-2.5 bg-sky-500 text-white rounded-lg hover:bg-sky-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{submitting && <Loader2 size={16} className="animate-spin" />}
|
||||||
|
{isEdit ? '수정' : '추가'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default XBotDialog;
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export { default as BotCard, XIcon, MeilisearchIcon, BotListItem, BotMiniCard, BotTableRow, BotTable } from './BotCard';
|
export { default as BotCard, XIcon, MeilisearchIcon, BotListItem, BotMiniCard, BotTableRow, BotTable } from './BotCard';
|
||||||
export { default as YouTubeBotDialog } from './YouTubeBotDialog';
|
export { default as YouTubeBotDialog } from './YouTubeBotDialog';
|
||||||
|
export { default as XBotDialog } from './XBotDialog';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Home, ChevronRight, Bot, CheckCircle, XCircle, RefreshCw, Plus, Youtube } from 'lucide-react';
|
import { Home, ChevronRight, Bot, CheckCircle, XCircle, RefreshCw, Plus, Youtube } from 'lucide-react';
|
||||||
import { Toast, Tooltip, AnimatedNumber } from '@/components/common';
|
import { Toast, Tooltip, AnimatedNumber } from '@/components/common';
|
||||||
import { AdminLayout, XIcon, MeilisearchIcon, BotTableRow, BotTable, YouTubeBotDialog } from '@/components/pc/admin';
|
import { AdminLayout, XIcon, MeilisearchIcon, BotTableRow, BotTable, YouTubeBotDialog, XBotDialog } from '@/components/pc/admin';
|
||||||
import { useAdminAuth } from '@/hooks/pc/admin';
|
import { useAdminAuth } from '@/hooks/pc/admin';
|
||||||
import { useToast } from '@/hooks/common';
|
import { useToast } from '@/hooks/common';
|
||||||
import * as botsApi from '@/api/admin/bots';
|
import * as botsApi from '@/api/admin/bots';
|
||||||
|
|
@ -32,6 +32,7 @@ const SECTIONS = {
|
||||||
color: 'text-gray-700',
|
color: 'text-gray-700',
|
||||||
bgColor: 'bg-gray-50',
|
bgColor: 'bg-gray-50',
|
||||||
borderColor: 'border-gray-200',
|
borderColor: 'border-gray-200',
|
||||||
|
canAdd: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -60,8 +61,10 @@ function ScheduleBots() {
|
||||||
const [isInitialLoad, setIsInitialLoad] = useState(true); // 첫 로드 여부 (애니메이션용)
|
const [isInitialLoad, setIsInitialLoad] = useState(true); // 첫 로드 여부 (애니메이션용)
|
||||||
const [syncing, setSyncing] = useState(null); // 동기화 중인 봇 ID
|
const [syncing, setSyncing] = useState(null); // 동기화 중인 봇 ID
|
||||||
const [quotaWarning, setQuotaWarning] = useState(null); // 할당량 경고 상태
|
const [quotaWarning, setQuotaWarning] = useState(null); // 할당량 경고 상태
|
||||||
const [botDialogOpen, setBotDialogOpen] = useState(false); // 봇 추가/수정 다이얼로그
|
const [youtubeDialogOpen, setYoutubeDialogOpen] = useState(false); // YouTube 봇 다이얼로그
|
||||||
|
const [xDialogOpen, setXDialogOpen] = useState(false); // X 봇 다이얼로그
|
||||||
const [editingBotId, setEditingBotId] = useState(null); // 수정 중인 봇 DB ID
|
const [editingBotId, setEditingBotId] = useState(null); // 수정 중인 봇 DB ID
|
||||||
|
const [editingBotType, setEditingBotType] = useState(null); // 수정 중인 봇 타입
|
||||||
const [deletingBot, setDeletingBot] = useState(null); // 삭제할 봇
|
const [deletingBot, setDeletingBot] = useState(null); // 삭제할 봇
|
||||||
|
|
||||||
// 봇 목록 조회
|
// 봇 목록 조회
|
||||||
|
|
@ -161,7 +164,11 @@ function ScheduleBots() {
|
||||||
if (!deletingBot) return;
|
if (!deletingBot) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await botsApi.deleteYouTubeBot(deletingBot.db_id);
|
if (deletingBot.type === 'youtube') {
|
||||||
|
await botsApi.deleteYouTubeBot(deletingBot.db_id);
|
||||||
|
} else if (deletingBot.type === 'x') {
|
||||||
|
await botsApi.deleteXBot(deletingBot.db_id);
|
||||||
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'bots'] });
|
queryClient.invalidateQueries({ queryKey: ['admin', 'bots'] });
|
||||||
setToast({ type: 'success', message: `${deletingBot.name} 봇이 삭제되었습니다.` });
|
setToast({ type: 'success', message: `${deletingBot.name} 봇이 삭제되었습니다.` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -255,10 +262,23 @@ function ScheduleBots() {
|
||||||
<AdminLayout user={user}>
|
<AdminLayout user={user}>
|
||||||
<Toast toast={toast} onClose={() => setToast(null)} />
|
<Toast toast={toast} onClose={() => setToast(null)} />
|
||||||
<YouTubeBotDialog
|
<YouTubeBotDialog
|
||||||
isOpen={botDialogOpen}
|
isOpen={youtubeDialogOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setBotDialogOpen(false);
|
setYoutubeDialogOpen(false);
|
||||||
setEditingBotId(null);
|
setEditingBotId(null);
|
||||||
|
setEditingBotType(null);
|
||||||
|
}}
|
||||||
|
botId={editingBotId}
|
||||||
|
onSuccess={() => {
|
||||||
|
setToast({ type: 'success', message: editingBotId ? '봇이 수정되었습니다.' : '봇이 추가되었습니다.' });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<XBotDialog
|
||||||
|
isOpen={xDialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setXDialogOpen(false);
|
||||||
|
setEditingBotId(null);
|
||||||
|
setEditingBotType(null);
|
||||||
}}
|
}}
|
||||||
botId={editingBotId}
|
botId={editingBotId}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
|
|
@ -432,7 +452,12 @@ function ScheduleBots() {
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingBotId(null);
|
setEditingBotId(null);
|
||||||
setBotDialogOpen(true);
|
setEditingBotType(type);
|
||||||
|
if (type === 'youtube') {
|
||||||
|
setYoutubeDialogOpen(true);
|
||||||
|
} else if (type === 'x') {
|
||||||
|
setXDialogOpen(true);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
|
|
@ -478,7 +503,12 @@ function ScheduleBots() {
|
||||||
onToggle={toggleBot}
|
onToggle={toggleBot}
|
||||||
onEdit={(bot) => {
|
onEdit={(bot) => {
|
||||||
setEditingBotId(bot.db_id);
|
setEditingBotId(bot.db_id);
|
||||||
setBotDialogOpen(true);
|
setEditingBotType(bot.type);
|
||||||
|
if (bot.type === 'youtube') {
|
||||||
|
setYoutubeDialogOpen(true);
|
||||||
|
} else if (bot.type === 'x') {
|
||||||
|
setXDialogOpen(true);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onDelete={(bot) => setDeletingBot(bot)}
|
onDelete={(bot) => setDeletingBot(bot)}
|
||||||
onAnimationComplete={() =>
|
onAnimationComplete={() =>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue