diff --git a/package.json b/package.json index bd8f3f3..4b724a7 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "http-proxy-middleware": "^3.0.5", "isomorphic-dompurify": "^2.25.0", "js-cookie": "^3.0.5", + "lucide-react": "^0.544.0", "next": "^15.3.5", "next-themes": "^0.2.1", "react": "^18.2.0", diff --git a/public/images/bg-cartoon.svg b/public/images/bg-cartoon.svg new file mode 100644 index 0000000..79c3dc3 --- /dev/null +++ b/public/images/bg-cartoon.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/checkout/[service]/[plan]/page.tsx b/src/app/checkout/[service]/[plan]/page.tsx index 34618e6..a68ba1b 100644 --- a/src/app/checkout/[service]/[plan]/page.tsx +++ b/src/app/checkout/[service]/[plan]/page.tsx @@ -166,7 +166,6 @@ const CheckoutPageComponent = () => { const rawPlanName = params?.plan || ''; const planName = decodeURIComponent(rawPlanName); - // Create a clean version without the "+" for API use const cleanPlanName = planName.replace(/\+/g, ""); const isMinecraft = service.toLowerCase() === "minecraft"; @@ -180,10 +179,8 @@ const CheckoutPageComponent = () => { setSelectedCycle(billingFromUrl); } - // Verificar se existe um código de afiliado no cookie const utmCode = getCookie('utm_code'); if (utmCode) { - console.log(`Checkout detectou código de afiliado: ${utmCode}`); } }, [searchParams]); @@ -201,12 +198,10 @@ const CheckoutPageComponent = () => { } }, [authContext, router, service, planName, searchParams]); - // Set default values for MTA and SAMP services useEffect(() => { if (isMTA || isSAMP) { - // Set default server name and egg ID for MTA and SAMP setServerName(isMTA ? "MTA Server" : "SAMP Server"); - setEggId(isMTA ? "13" : "14"); // Assuming these are the correct egg IDs + setEggId(isMTA ? "13" : "14"); } }, [isMTA, isSAMP]); @@ -385,13 +380,11 @@ const CheckoutPageComponent = () => { return; } - // Create request body based on service type let body: any = { pid: plan.id, billingcycle: selectedCycle, }; - // Only add customfields if it's NOT MTA or SAMP if (!isMTA && !isSAMP) { if (isMinecraft) { body.customfields = { server_name: serverName, egg_id: eggId }; @@ -399,34 +392,25 @@ const CheckoutPageComponent = () => { body.customfields = { OS: osId }; } } - // For MTA and SAMP, no customfields at all if (appliedCoupon) { body.promocode = appliedCoupon.code; } - // Verificar se existe utm_code no cookie e adicionar ao body const utmCode = getCookie('utm_code'); if (utmCode) { body.utm_code = utmCode; - console.log(`Código de afiliado detectado: ${utmCode}`); } - // Use specific API endpoints for MTA and SAMP services const endpoint = isMTA ? `/v1/users/payment/create` : isSAMP ? `/v1/users/payment/create` : `/v1/users/payment/create`; - - // For plan-specific endpoints like MTA/SAMP, make sure we're passing the clean plan name - // without any "+" characters + if (isMTA || isSAMP) { - // Add the clean plan name to the body body.plan = cleanPlanName; } - // Make sure we're only sending what's necessary const requestBody = JSON.stringify(body); - console.log(`Sending request to ${endpoint}:`, requestBody); const response = await fetch(`${apiUrl}${endpoint}`, { method: "POST", diff --git a/src/app/dashboard/app/[identifier]/page.tsx b/src/app/dashboard/app/[identifier]/page.tsx index 81beee3..703b2a9 100644 --- a/src/app/dashboard/app/[identifier]/page.tsx +++ b/src/app/dashboard/app/[identifier]/page.tsx @@ -109,7 +109,6 @@ export default function ServerDetailsPage() { const [isConsoleExpanded, setIsConsoleExpanded] = useState(false); const [connectionStatus, setConnectionStatus] = useState("Desconectado"); - // Handle hash navigation useEffect(() => { const hash = window.location.hash.replace("#", ""); if (hash && ["console", "stats", "files", "settings"].includes(hash)) { @@ -187,19 +186,16 @@ export default function ServerDetailsPage() { wsRef.current = ws; ws.onConnected(() => { - console.log("Callback onConnected chamado!"); if (!isMounted) return; setConsoleMessages(prev => { const newMsg = "[Sistema] Conexão com o console estabelecida."; const msgWithTimestamp = `${new Date().toISOString()}|${newMsg}`; - // Verifica se a mensagem já existe para evitar duplicação if (prev.some(msg => msg.includes(newMsg))) return prev; return [...prev, msgWithTimestamp]; }); setIsConnected(true); setIsConnecting(false); setConnectionStatus("Conectado"); - console.log("Estados atualizados - isConnected: true, isConnecting: false"); }); ws.onDisconnected(() => { @@ -207,7 +203,6 @@ export default function ServerDetailsPage() { setConsoleMessages(prev => { const newMsg = "[Sistema] Conexão com o console perdida."; const msgWithTimestamp = `${new Date().toISOString()}|${newMsg}`; - // Verifica se a mensagem já existe para evitar duplicação if (prev.some(msg => msg.includes(newMsg))) return prev; return [...prev, msgWithTimestamp]; }); @@ -224,16 +219,14 @@ export default function ServerDetailsPage() { const timestamp = new Date().toISOString(); const msgWithTimestamp = `${timestamp}|${data.data}`; - // Verifica duplicação por timestamp e conteúdo const isDuplicate = prev.some(existingMsg => { const existingContent = existingMsg.split('|').slice(1).join('|'); const newContent = data.data; const existingTimestamp = existingMsg.split('|')[0]; - // Se o conteúdo é igual e a diferença de tempo é menor que 1 segundo, é duplicado if (existingContent === newContent) { const timeDiff = new Date(timestamp).getTime() - new Date(existingTimestamp).getTime(); - return timeDiff < 1000; // menos de 1 segundo + return timeDiff < 1000; } return false; }); @@ -254,7 +247,6 @@ export default function ServerDetailsPage() { ws.onError((errorMessage) => { if (!isMounted) return; console.warn("Aviso WebSocket:", errorMessage); - // Só definir erro se for um erro crítico, não problemas de conexão temporários if (errorMessage.includes("Falha ao") || errorMessage.includes("renovar")) { setError(errorMessage); } @@ -264,9 +256,7 @@ export default function ServerDetailsPage() { }); try { - console.log("Iniciando conexão WebSocket..."); await ws.connect(); - console.log("Comando de conexão WebSocket enviado"); } catch (error) { console.error("Erro ao conectar WebSocket:", error); if (isMounted) { @@ -317,9 +307,9 @@ export default function ServerDetailsPage() { const result = await getServerFiles(identifier, accessKey, path); if (result.success && result.data) { setServerFiles(result.data); - setCurrentPath(path); // Ensure currentPath is updated - setFilesError(null); // Clear any previous errors - setSelectedFiles([]); // Limpa seleção ao recarregar arquivos + setCurrentPath(path); + setFilesError(null); + setSelectedFiles([]); } else { const errorMessage = typeof result.error === 'string' ? result.error : "Failed to load files."; setFilesError(errorMessage); @@ -334,16 +324,13 @@ export default function ServerDetailsPage() { const handleFileClick = (file: FileAttributes) => { if (!file.is_file) { - // It's a directory, navigate into it const newPath = currentPath === "/" ? `/${file.name}` : `${currentPath}/${file.name}`; fetchFiles(newPath); } else { - // It's a file, open for editing if it's a text file if (isEditableFile(file)) { handleEditFile(file); } else { - console.log("Clicked on file:", file.name); } } }; @@ -362,7 +349,6 @@ export default function ServerDetailsPage() { const handleBreadcrumbClick = (pathSegmentIndex: number) => { if (pathSegmentIndex === -1) { - // Handle "Root" click fetchFiles("/"); } else { const segments = currentPath.split("/").filter(Boolean); @@ -371,7 +357,6 @@ export default function ServerDetailsPage() { } }; - // Removed auto-scroll to prevent unwanted scrolling behavior const sendPowerAction = async (signal: string) => { if (!accessKey || powerLoading || server?.suspended || server?.is_suspended) return; @@ -379,7 +364,6 @@ export default function ServerDetailsPage() { setPowerLoading(true); try { - console.log(`Sending power signal ${signal} to server ${identifier}`); const result = await sendServerPowerSignal(identifier, signal); if (!result.success) { @@ -387,7 +371,6 @@ export default function ServerDetailsPage() { throw new Error(result.message || `Falha ao enviar sinal ${signal}`); } - console.log("Power signal success:", result); setServer((prev) => prev ? { ...prev, status: getPendingStatus(signal) } : null, @@ -463,7 +446,6 @@ export default function ServerDetailsPage() { 'url' in result.data && typeof (result.data as any).url === 'string' ) { - // Download via link direto (força download) const a = document.createElement('a'); a.href = (result.data as any).url; a.download = file.name; @@ -472,7 +454,6 @@ export default function ServerDetailsPage() { a.click(); document.body.removeChild(a); } else { - // Download via blob if (result.data instanceof Blob) { const url = window.URL.createObjectURL(result.data); const a = document.createElement('a'); @@ -486,7 +467,7 @@ export default function ServerDetailsPage() { setFilesError('Erro inesperado ao baixar arquivo.'); } } - setFilesError(null); // Clear any previous errors + setFilesError(null); } else { const errorMessage = typeof result.error === 'string' ? result.error : "Erro ao baixar arquivo"; setFilesError(errorMessage); @@ -527,7 +508,6 @@ export default function ServerDetailsPage() { throw new Error(errorMessage); } - // Refresh file list after saving fetchFiles(currentPath); }; @@ -560,7 +540,7 @@ export default function ServerDetailsPage() { try { const result = await deleteFiles(identifier, accessKey, currentPath, [file.name]); if (result.success) { - setFilesError(null); // Clear any previous errors + setFilesError(null); fetchFiles(currentPath); } else { const errorMessage = typeof result.error === 'string' ? result.error : "Erro ao excluir arquivo"; @@ -590,9 +570,9 @@ export default function ServerDetailsPage() { try { const result = await compressFiles(identifier, accessKey, currentPath, files); if (result.success) { - setFilesError(null); // Clear any previous errors + setFilesError(null); fetchFiles(currentPath); - setSelectedFiles([]); // Limpa seleção após comprimir + setSelectedFiles([]); } else { const errorMessage = typeof result.error === 'string' ? result.error : "Erro ao comprimir arquivos"; setFilesError(errorMessage); @@ -609,7 +589,7 @@ export default function ServerDetailsPage() { try { const result = await decompressFile(identifier, accessKey, currentPath, file.name); if (result.success) { - setFilesError(null); // Clear any previous errors + setFilesError(null); fetchFiles(currentPath); } else { const errorMessage = typeof result.error === 'string' ? result.error : "Erro ao descomprimir arquivo"; @@ -624,7 +604,6 @@ export default function ServerDetailsPage() { const handleDecompressSelectedFiles = async () => { if (!accessKey || !selectedFiles.length) return; - // Filtrar apenas arquivos comprimidos const compressedFiles = selectedFiles.filter(fileName => isCompressedFile(fileName)); if (compressedFiles.length === 0) return; @@ -632,17 +611,14 @@ export default function ServerDetailsPage() { setFilesLoading(true); try { - // Descomprimir cada arquivo selecionado for (const fileName of compressedFiles) { try { - // Encontrar o arquivo na lista de arquivos const fileObj = serverFiles.find(f => f.name === fileName); if (fileObj) { await decompressFile(identifier, accessKey, currentPath, fileName); } } catch (innerErr) { console.error(`Erro ao descomprimir ${fileName}:`, innerErr); - // Continua com os próximos arquivos mesmo se um falhar } } @@ -663,9 +639,8 @@ export default function ServerDetailsPage() { try { setUploadProgress(0); - setFilesError(null); // Clear any previous errors + setFilesError(null); - // Check file sizes (100MB limit) const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB in bytes for (let i = 0; i < files.length; i++) { if (files[i].size > MAX_FILE_SIZE) { @@ -673,18 +648,14 @@ export default function ServerDetailsPage() { } } - // Get upload URL const urlResult = await getUploadUrl(identifier, accessKey, currentPath); if (!urlResult.success || !urlResult.data) { throw new Error(typeof urlResult.error === 'string' ? urlResult.error : "Erro ao obter URL de upload"); } - // Upload each file individually with proper progress tracking for (let i = 0; i < files.length; i++) { const file = files[i]; - console.log(`Starting upload for ${file.name} (${i + 1}/${files.length})`); - // Update progress to show current file const baseProgress = (i / files.length) * 100; setUploadProgress(baseProgress); @@ -700,11 +671,8 @@ export default function ServerDetailsPage() { }); if (response.ok) { - console.log(`✅ Upload successful for ${file.name}`); - // Update progress to completion for this file setUploadProgress(((i + 1) / files.length) * 100); } else { - // Handle HTTP errors let httpErrorMessage = `Erro HTTP ${response.status} ao fazer upload de ${file.name}`; try { const errorData = await response.json(); @@ -716,56 +684,42 @@ export default function ServerDetailsPage() { } } catch (fetchError: any) { - // Check if this is a network error that indicates the upload might have succeeded if (fetchError.message && (fetchError.message.includes('NetworkError') || fetchError.message.includes('Failed to fetch') || fetchError.message.includes('CORS'))) { - console.log(`⚠️ Network/CORS error for ${file.name} - checking if upload was successful...`); - // Wait a moment and refresh file list to check if file was uploaded await new Promise(resolve => setTimeout(resolve, 2000)); try { - // Get fresh file list from server const freshResult = await getServerFiles(identifier, accessKey, currentPath); if (freshResult.success && freshResult.data) { - // Check if the file now exists in the fresh server list const fileExists = freshResult.data.some(serverFile => serverFile.name === file.name); if (fileExists) { - console.log(`✅ File ${file.name} was actually uploaded successfully despite network error`); - // Update local state with fresh data setServerFiles(freshResult.data); setUploadProgress(((i + 1) / files.length) * 100); - continue; // Move to next file + continue; } else { console.error(`❌ Upload failed for ${file.name}:`, fetchError.message); - console.log(`❌ File ${file.name} was not found on server after network error`); } } } catch (checkError) { console.error(`❌ Upload failed for ${file.name}:`, fetchError.message); - console.log(`❌ Failed to verify upload for ${file.name}:`, checkError); } - // If we get here, the upload truly failed or we couldn't verify it - console.log(`🚨 Treating as genuine upload failure for ${file.name}`); throw new Error(`Erro de rede ao fazer upload de ${file.name}. Verifique sua conexão e tente novamente.`); } else { - // Other types of errors - log immediately console.error(`❌ Upload failed for ${file.name}:`, fetchError.message); throw new Error(`Erro ao fazer upload de ${file.name}: ${fetchError.message}`); } } } - // Final refresh and cleanup await fetchFiles(currentPath); setUploadProgress(null); - console.log('🎉 All uploads completed successfully'); } catch (err: any) { const errorMessage = typeof err === 'string' ? err : (err?.message || "Erro ao fazer upload"); @@ -774,13 +728,11 @@ export default function ServerDetailsPage() { console.error('Upload error:', err); } - // Reset input if (event && event.target) { (event.target as HTMLInputElement).value = ''; } }; - // Drag and Drop handlers const handleDrag = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -806,7 +758,6 @@ export default function ServerDetailsPage() { setUploadProgress(0); setFilesError(null); - // Check file sizes (100MB limit) const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB in bytes for (let i = 0; i < droppedFiles.length; i++) { if (droppedFiles[i].size > MAX_FILE_SIZE) { @@ -814,18 +765,14 @@ export default function ServerDetailsPage() { } } - // Get upload URL const urlResult = await getUploadUrl(identifier, accessKey, currentPath); if (!urlResult.success || !urlResult.data) { throw new Error(typeof urlResult.error === 'string' ? urlResult.error : "Erro ao obter URL de upload"); } - // Upload each file individually with proper progress tracking for (let i = 0; i < droppedFiles.length; i++) { const file = droppedFiles[i]; - console.log(`Starting upload for ${file.name} (${i + 1}/${droppedFiles.length})`); - // Update progress to show current file const baseProgress = (i / droppedFiles.length) * 100; setUploadProgress(baseProgress); @@ -841,11 +788,8 @@ export default function ServerDetailsPage() { }); if (response.ok) { - console.log(`✅ Upload successful for ${file.name}`); - // Update progress to completion for this file setUploadProgress(((i + 1) / droppedFiles.length) * 100); } else { - // Handle HTTP errors let httpErrorMessage = `Erro HTTP ${response.status} ao fazer upload de ${file.name}`; try { const errorData = await response.json(); @@ -856,13 +800,11 @@ export default function ServerDetailsPage() { throw new Error(httpErrorMessage); } } catch (fetchError: any) { - // Similar error handling as handleFileUpload if (fetchError.message && (fetchError.message.includes('NetworkError') || fetchError.message.includes('Failed to fetch') || fetchError.message.includes('CORS'))) { - console.log(`⚠️ Network/CORS error for ${file.name} - checking if upload was successful...`); await new Promise(resolve => setTimeout(resolve, 2000)); try { @@ -871,7 +813,6 @@ export default function ServerDetailsPage() { const fileExists = freshResult.data.some(serverFile => serverFile.name === file.name); if (fileExists) { - console.log(`✅ File ${file.name} was actually uploaded successfully despite network error`); setServerFiles(freshResult.data); setUploadProgress(((i + 1) / droppedFiles.length) * 100); continue; @@ -889,11 +830,9 @@ export default function ServerDetailsPage() { } } - // Final refresh and cleanup await fetchFiles(currentPath); setUploadProgress(null); - console.log('🎉 All uploads completed successfully'); } catch (err: any) { const errorMessage = typeof err === 'string' ? err : (err?.message || "Erro ao fazer upload"); @@ -903,7 +842,6 @@ export default function ServerDetailsPage() { } }; - // Trigger file input click const triggerFileInput = () => { if (fileInputRef.current && uploadProgress === null) { fileInputRef.current.click(); @@ -917,7 +855,7 @@ export default function ServerDetailsPage() { const result = await deleteFiles(identifier, accessKey, currentPath, files); if (result.success) { setFilesError(null); - setSelectedFiles([]); // Limpa seleção após deletar + setSelectedFiles([]); fetchFiles(currentPath); } else { const errorMessage = typeof result.error === 'string' ? result.error : "Erro ao excluir arquivos"; @@ -938,7 +876,6 @@ export default function ServerDetailsPage() { } if (error) { - // Se o servidor estiver suspenso, mostrar uma mensagem de erro específica if (error === "Servidor Suspenso") { return (
@@ -963,7 +900,6 @@ export default function ServerDetailsPage() { ); } - // Erro padrão return (
@@ -988,7 +924,7 @@ export default function ServerDetailsPage() {
{/* Left - Back & Server Info */} -
+
-

+

{server.name}

@@ -1012,9 +948,9 @@ export default function ServerDetailsPage() {

{/* Right - Status & Actions */} -
+
{/* Status Badge */} -
+
{/* Quick Actions */} -
+
+ - {isMobileMenuOpen && ( -
- {accountMenuItems.map((item) => ( - - ))} -
- )} + + {isMobileMenuOpen && ( + + {accountMenuItems.map((item) => ( + handleMobileSubTabChange(item.id)} + whileHover={{ backgroundColor: "rgba(239, 68, 68, 0.1)" }} + whileTap={{ scale: 0.98 }} + className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-all duration-300 ${ + activeSubTab === item.id + ? "bg-gradient-to-br from-red-600/20 to-red-800/20 text-red-400" + : "text-gray-300 hover:text-white" + }`} + > + {item.icon} + {item.label} + + ))} + + )} +
); } return ( -
-
-

Minha Conta

+ +
+

Minha Conta

-
+ ); } diff --git a/src/components/dashboard/BillingContent.tsx b/src/components/dashboard/BillingContent.tsx index e43ea97..2dbb787 100644 --- a/src/components/dashboard/BillingContent.tsx +++ b/src/components/dashboard/BillingContent.tsx @@ -84,7 +84,6 @@ export default function BillingContent() { ); } - // Calcular total gasto este mês const totalSpentThisMonth = invoices .filter((inv) => { const invoiceDate = new Date(inv.date); @@ -98,7 +97,6 @@ export default function BillingContent() { .reduce((sum, inv) => sum + parseFloat(inv.total), 0) .toFixed(2); - // Contar faturas pendentes (não pagas e não canceladas) const pendingInvoicesCount = invoices.filter( (inv) => inv.status.toLowerCase() !== "paid" && @@ -106,23 +104,18 @@ export default function BillingContent() { inv.status.toLowerCase() !== "refunded" ).length; - // Calcular próxima cobrança baseada na fatura mais antiga paga const getNextBillingInfo = () => { - // Pegar todas as faturas pagas para calcular o padrão de cobrança const paidInvoices = invoices .filter((inv) => inv.status.toLowerCase() === "paid") .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); if (paidInvoices.length > 0) { - // Pegar a fatura paga mais antiga para calcular a próxima cobrança const oldestPaidInvoice = paidInvoices[0]; const lastBillingDate = new Date(oldestPaidInvoice.date); - // Assumir cobrança mensal - adicionar 1 mês const nextBillingDate = new Date(lastBillingDate); nextBillingDate.setMonth(nextBillingDate.getMonth() + 1); - // Se a próxima cobrança já passou, continuar adicionando meses até encontrar uma data futura const today = new Date(); while (nextBillingDate <= today) { nextBillingDate.setMonth(nextBillingDate.getMonth() + 1); @@ -134,7 +127,6 @@ export default function BillingContent() { }; } - // Se não há faturas pagas, verificar se há pendentes const pendingInvoices = invoices .filter((inv) => inv.status.toLowerCase() !== "paid" && @@ -156,7 +148,6 @@ export default function BillingContent() { const nextBilling = getNextBillingInfo(); - // Função para abrir a fatura const handleViewInvoice = (invoice: Invoice) => { const centralUrl = process.env.NEXT_PUBLIC_CENTRAL_URL; if (centralUrl) { @@ -225,7 +216,26 @@ export default function BillingContent() {
- {/* Invoices */} +
+
+

+ Área Financeira do Cliente +

+

+ Quer renovar, fazer upgrade ou ter suas faturas detalhadas? +

+ + + Gerenciar Faturas e Serviços + +
+
+

diff --git a/src/components/dashboard/ContentTransition.tsx b/src/components/dashboard/ContentTransition.tsx index ad372a4..0b354e3 100644 --- a/src/components/dashboard/ContentTransition.tsx +++ b/src/components/dashboard/ContentTransition.tsx @@ -1,6 +1,7 @@ "use client"; -import { ReactNode, useEffect, useState } from "react"; +import { ReactNode, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; interface ContentTransitionProps { children: ReactNode; @@ -11,24 +12,17 @@ export default function ContentTransition({ children, activeKey, }: ContentTransitionProps) { - const [isVisible, setIsVisible] = useState(true); - - useEffect(() => { - setIsVisible(false); - const timer = setTimeout(() => { - setIsVisible(true); - }, 150); - - return () => clearTimeout(timer); - }, [activeKey]); - return ( -
- {children} -
+ + + {children} + + ); } diff --git a/src/components/dashboard/DashboardLayout.tsx b/src/components/dashboard/DashboardLayout.tsx index bf89612..ecedb0f 100644 --- a/src/components/dashboard/DashboardLayout.tsx +++ b/src/components/dashboard/DashboardLayout.tsx @@ -1,9 +1,9 @@ "use client"; -import { ReactNode, useEffect } from "react"; +import { ReactNode } from "react"; import Footer from "@/components/layout/Footer"; import { UserDashboardProvider } from "@/contexts/UserDashboardContext"; import { ServerProvider } from "@/contexts/ServerContext"; -import { useAuth } from "@/contexts/AuthContext"; +import { motion } from "framer-motion"; interface DashboardLayoutProps { children: ReactNode; @@ -13,24 +13,44 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) { return ( -
+
{/* Background Effects */}
-
-
+ + +
{/* Main Content Container */}
- {/* Dashboard Content - Takes at least full viewport height */} -
+ {/* Dashboard Content */} +
{children}
- {/* Spacer to create distance from footer */} -
- - {/* Footer - Will be below the viewport */} + {/* Footer */}
diff --git a/src/components/dashboard/DashboardNavbar.tsx b/src/components/dashboard/DashboardNavbar.tsx index 41d2e8d..c865b3e 100644 --- a/src/components/dashboard/DashboardNavbar.tsx +++ b/src/components/dashboard/DashboardNavbar.tsx @@ -3,6 +3,8 @@ import { useAuth } from "@/contexts/AuthContext"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import { motion } from "framer-motion"; +import Image from "next/image"; import { FaServer, FaCreditCard, @@ -10,7 +12,7 @@ import { FaUser, FaSignOutAlt, FaChartLine, - FaBoxOpen, // Ícone adicionado + FaBoxOpen, } from "react-icons/fa"; interface DashboardNavbarProps { @@ -25,15 +27,14 @@ export default function DashboardNavbar({ const { user, logout } = useAuth(); const router = useRouter(); - // Array de itens de menu, agora com o item "Blob" e a propriedade "disabled" const mainMenuItems = [ { id: "overview", label: "Visão Geral", icon: }, - { id: "services", label: "Meus Serviços", icon: }, + { id: "services", label: "Serviços", icon: }, { id: "blob", label: "Blob", icon: , - disabled: true, // Item desativado + disabled: true, }, { id: "billing", label: "Faturamento", icon: }, { id: "account", label: "Minha Conta", icon: }, @@ -44,56 +45,65 @@ export default function DashboardNavbar({ router.push("/"); }; - // Esta função não precisa de alteração, pois o item desabilitado ainda - // ocupa espaço no layout, mantendo o cálculo do índice correto. const getActiveIndex = () => { const index = mainMenuItems.findIndex((item) => { if (item.id === "account") { - return ["profile", "settings"].includes(activeTab); + return ["profile", "settings", "security", "affiliate"].includes(activeTab); } return item.id === activeTab; }); return index >= 0 ? index : 0; }; + + const getIndicatorPosition = (index: number) => { + return index * 115; + }; return ( - + ); } \ No newline at end of file diff --git a/src/components/dashboard/FileEditorModal.tsx b/src/components/dashboard/FileEditorModal.tsx index f1d930a..9a43772 100644 --- a/src/components/dashboard/FileEditorModal.tsx +++ b/src/components/dashboard/FileEditorModal.tsx @@ -37,7 +37,6 @@ export default function FileEditorModal({ try { const result = await onGetContent(); try { - // Tenta fazer parse do JSON se for uma string JSON const jsonContent = JSON.parse(result); if (jsonContent.contents) { setContent(jsonContent.contents); @@ -45,7 +44,6 @@ export default function FileEditorModal({ setContent(result); } } catch { - // Se não for JSON, usa o conteúdo como está setContent(result); } } catch (err: any) { diff --git a/src/components/dashboard/OverviewContent.tsx b/src/components/dashboard/OverviewContent.tsx index 8d506f7..8322911 100644 --- a/src/components/dashboard/OverviewContent.tsx +++ b/src/components/dashboard/OverviewContent.tsx @@ -15,6 +15,7 @@ import { useAuth } from "@/contexts/AuthContext"; import { fetchUserInvoicesAPI } from "@/services/api/user"; import { Invoice, Transaction } from "@/types/user"; import { useServer } from "@/contexts/ServerContext"; +import { motion } from "framer-motion"; import BillingContent from "@/components/dashboard/BillingContent"; @@ -56,24 +57,40 @@ export default function OverviewContent({ onTabChange }: { onTabChange?: (tab: s if (loading) { return ( -
- -

Carregando dados...

-
+ +
+
+
+ Logo +
+
+

Carregando dados...

+
); } if (error) { return ( -
- - - Erro ao carregar dados do painel: {error} - -
+
+ +
+
+

Erro ao carregar dados

+

+ {error} +

+
+ ); } @@ -134,127 +151,194 @@ export default function OverviewContent({ onTabChange }: { onTabChange?: (tab: s return (
{/* Stats Cards */} -
-
+ +
-

+

Servidores Ativos

-

+

{activeServersCount}

-
- +
+
-
+
-
+
-

+

Uptime

-

+

99.9%

-
- +
+
-
+
-
+
-

+

Faturas Pendentes

-

+

{openTicketsCount}

-
- +
+
-
+
-
+
-

+

Próxima Cobrança

-

+

{nextDueDateInfo.amount}

{nextDueDateInfo.date}

-
- +
+
-
-
+ + {/* Quick Actions */} -
-

- Ações Rápidas -

-
- - - +
-
+ {/* Recent Activity */} -
-

- Faturas Recentes -

+ +
+
+

+ Faturas Recentes +

+
+ {invoices.length > 0 ? ( -
+
{invoices .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) .slice(0, 3) .map((invoice: Invoice) => ( -
-
- +
+
-

+

Fatura #{invoice.id}

@@ -267,29 +351,34 @@ export default function OverviewContent({ onTabChange }: { onTabChange?: (tab: s

{invoice.status} -
+ ))}
) : ( -

- Nenhuma fatura encontrada. -

+
+
+ +
+

+ Nenhuma fatura encontrada. +

+
)} -
+
); } diff --git a/src/components/dashboard/ProfileContent.tsx b/src/components/dashboard/ProfileContent.tsx index 830efa9..09d65d2 100644 --- a/src/components/dashboard/ProfileContent.tsx +++ b/src/components/dashboard/ProfileContent.tsx @@ -68,7 +68,6 @@ export default function ProfileContent({ user }: ProfileContentProps) { setSuccess(null); try { - // Atualizar nome se mudou if (formData.firstname !== user?.client?.firstname) { const result = await updateField('UpdateFirstName', { firstname: formData.firstname }); if (!result.success) { @@ -78,7 +77,6 @@ export default function ProfileContent({ user }: ProfileContentProps) { } } - // Atualizar sobrenome se mudou if (formData.lastname !== user?.client?.lastname) { const result = await updateField('UpdateLastName', { lastname: formData.lastname }); if (!result.success) { @@ -88,7 +86,6 @@ export default function ProfileContent({ user }: ProfileContentProps) { } } - // Atualizar email se mudou if (formData.email !== user?.client?.email) { const result = await updateField('UpdateEmail', { email: formData.email }); if (!result.success) { @@ -101,7 +98,6 @@ export default function ProfileContent({ user }: ProfileContentProps) { setSuccess('Perfil atualizado com sucesso!'); setIsEditing(false); - // Limpar mensagem de sucesso após 3 segundos setTimeout(() => setSuccess(null), 3000); } catch (error) { diff --git a/src/components/dashboard/SecurityContent.tsx b/src/components/dashboard/SecurityContent.tsx index a7b476c..c6f2651 100644 --- a/src/components/dashboard/SecurityContent.tsx +++ b/src/components/dashboard/SecurityContent.tsx @@ -75,7 +75,6 @@ export default function SecurityContent() { if (response.ok) { const data = await response.json(); - console.log('Social connections status:', data); setSocialConnections({ google: data.google || false, discord: data.discord || false, @@ -103,10 +102,10 @@ export default function SecurityContent() { const showTemporaryMessage = (message: string, isError: boolean = false) => { if (isError) { setError(message); - setTimeout(() => setError(null), 5000); // 5 segundos + setTimeout(() => setError(null), 5000); } else { setSuccess(message); - setTimeout(() => setSuccess(null), 5000); // 5 segundos + setTimeout(() => setSuccess(null), 5000); } }; @@ -142,20 +141,13 @@ export default function SecurityContent() { } } - console.log('OAuth callback detected:', { code: code ? 'present' : 'absent', state, socialType }); if (code && socialType && currentToken) { const completeOAuthLink = async () => { try { setIsFetchingSocial(true); - console.log(`Completing ${socialType} OAuth flow with code`); - console.log(`Sending OAuth2 code completion for ${socialType}:`, { - url: `${apiUrl}/v1/users/me/login/social?action=link&type=${socialType}&code=...`, - token: currentToken ? `${currentToken.substring(0, 10)}...` : 'missing', - codeLength: code.length - }); const response = await fetch(`${apiUrl}/v1/users/me/login/social?action=link&type=${socialType}&code=${encodeURIComponent(code)}`, { method: 'POST', @@ -175,20 +167,14 @@ export default function SecurityContent() { redirected: response.redirected }; - console.log(`${socialType} link completion response:`, responseDebug); if (response.ok) { const data = await response.json(); - console.log(`${socialType} link completion data:`, data); if (data.result === 'success') { showTemporaryMessage(data.message || `Conta ${socialType === 'google' ? 'Google' : 'Discord'} vinculada com sucesso!`); setSocialConnections(prev => ({...prev, [socialType]: true})); - console.log(`${socialType} account linked successfully:`, { - user: data.user || 'No user data returned' - }); - fetchSocialConnections(); } else { showTemporaryMessage(`Erro ao vincular conta ${socialType === 'google' ? 'Google' : 'Discord'}`, true); diff --git a/src/components/dashboard/ServersContent.tsx b/src/components/dashboard/ServersContent.tsx index b6b46f5..315a47f 100644 --- a/src/components/dashboard/ServersContent.tsx +++ b/src/components/dashboard/ServersContent.tsx @@ -109,7 +109,6 @@ export default function ServersContent() { const formatUptime = (uptime: string | undefined) => { if (!uptime) return "N/A"; - // Aqui você pode implementar a formatação do uptime se necessário return uptime; }; @@ -137,7 +136,6 @@ export default function ServersContent() { {servers && servers.length > 0 ? (
{servers.map((server: Server) => { - // Parse CPU usage from "0.275/Unlimited" format const cpuParts = server.cpu ? server.cpu.split("/") : ["0", "0"]; const cpuUsed = parseFloat(cpuParts[0]) || 0; const cpuTotal = @@ -147,7 +145,6 @@ export default function ServersContent() { const cpuPercent = cpuTotal > 0 ? Math.min((cpuUsed / cpuTotal) * 100, 100) : 0; - // Parse RAM usage from "478/1024MB" format const ramParts = server.ram ? server.ram.replace("MB", "").split("/") : ["0", "1024"]; @@ -155,7 +152,6 @@ export default function ServersContent() { const ramTotal = parseInt(ramParts[1]) || 1024; const ramPercent = ramTotal > 0 ? (ramUsed / ramTotal) * 100 : 0; - // Parse disk usage from "116/Unlimited" format const diskParts = server.disk ? server.disk.split("/") : ["0", "0"]; const diskUsed = parseInt(diskParts[0]) || 0; const diskTotal = diff --git a/src/components/dashboard/SettingsContent.tsx b/src/components/dashboard/SettingsContent.tsx index 59fc8ed..43a4672 100644 --- a/src/components/dashboard/SettingsContent.tsx +++ b/src/components/dashboard/SettingsContent.tsx @@ -49,8 +49,6 @@ export default function SettingsContent() { }; const handleSaveSettings = () => { - console.log("Salvando configurações:", { notifications, security }); - // Implementar lógica de salvamento }; const handleChangePassword = () => { @@ -58,7 +56,6 @@ export default function SettingsContent() { alert("As senhas não coincidem!"); return; } - console.log("Alterando senha..."); }; return ( diff --git a/src/components/dashboard/app/FileUploadArea.tsx b/src/components/dashboard/app/FileUploadArea.tsx index f9aa739..cbb12bc 100644 --- a/src/components/dashboard/app/FileUploadArea.tsx +++ b/src/components/dashboard/app/FileUploadArea.tsx @@ -34,8 +34,7 @@ const FileUploadArea: FC = ({ onUpload, disabled, uploadPro const files = e.dataTransfer.files; if (!files || files.length === 0) return; - // Check for files larger than 100MB - const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB in bytes + const MAX_FILE_SIZE = 100 * 1024 * 1024; for (let i = 0; i < files.length; i++) { if (files[i].size > MAX_FILE_SIZE) { alert(`O arquivo "${files[i].name}" excede o limite de 100MB permitido para upload.`); @@ -49,8 +48,7 @@ const FileUploadArea: FC = ({ onUpload, disabled, uploadPro const handleFileChange = async (e: ChangeEvent) => { if (!e.target.files || disabled) return; - // Check for files larger than 100MB - const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB in bytes + const MAX_FILE_SIZE = 100 * 1024 * 1024; for (let i = 0; i < e.target.files.length; i++) { if (e.target.files[i].size > MAX_FILE_SIZE) { alert(`O arquivo "${e.target.files[i].name}" excede o limite de 100MB permitido para upload.`); @@ -106,7 +104,7 @@ const FileUploadArea: FC = ({ onUpload, disabled, uploadPro cx="16" cy="16" r="12" - stroke="rgb(75 85 99)" // gray-600 + stroke="rgb(75 85 99)" strokeWidth="3" fill="none" className="opacity-25" @@ -116,7 +114,7 @@ const FileUploadArea: FC = ({ onUpload, disabled, uploadPro cx="16" cy="16" r="12" - stroke="rgb(59 130 246)" // blue-500 + stroke="rgb(59 130 246)" strokeWidth="3" fill="none" strokeDasharray={`${2 * Math.PI * 12}`} diff --git a/src/components/dashboard/app/ServerNavbar.tsx b/src/components/dashboard/app/ServerNavbar.tsx index 2668db2..ca73fb0 100644 --- a/src/components/dashboard/app/ServerNavbar.tsx +++ b/src/components/dashboard/app/ServerNavbar.tsx @@ -29,7 +29,6 @@ export default function ServerNavbar({ activeTab, onTabChange, serverName }: Ser
-
+
{consoleMessages.length > 0 ? ( @@ -97,8 +97,8 @@ export default function ConsoleContent({ )}
-
-
+
+
diff --git a/src/components/dashboard/minecraft/CustomizationContent.tsx b/src/components/dashboard/minecraft/CustomizationContent.tsx index cb4564f..fa2fdf5 100644 --- a/src/components/dashboard/minecraft/CustomizationContent.tsx +++ b/src/components/dashboard/minecraft/CustomizationContent.tsx @@ -53,7 +53,6 @@ export default function CustomizationContent({ server }: CustomizationContentPro const [hasCustomIcon, setHasCustomIcon] = useState(false); const canvasRef = useRef(null); - // Minecraft settings const [maxPlayers, setMaxPlayers] = useState(20); const [gamemode, setGamemode] = useState("survival"); const [difficulty, setDifficulty] = useState("easy"); @@ -76,32 +75,47 @@ export default function CustomizationContent({ server }: CustomizationContentPro try { const token = accessKey || getCookie('access_key'); const filesResponse = await getServerFiles(identifier, token || '', '/'); - + if (filesResponse.success && filesResponse.data) { - // Check if server-icon.png exists - const iconFile = filesResponse.data.find(file => + const serverIconFile = filesResponse.data.find(file => file.name === 'server-icon.png' && file.is_file ); - - if (iconFile) { - // If it exists, set the icon URL and mark as having custom icon - setServerImage(`${config.api.baseUrl}/v1/users/me/servers/games/${identifier}/files?action=download&file=/server-icon.png&token=${token}`); - setHasCustomIcon(true); + + if (serverIconFile) { + const downloadResponse = await fetch( + `${config.api.baseUrl}/v1/users/me/servers/games/${identifier}/files?action=download&file=/server-icon.png`, + { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + }, + } + ); + + if (downloadResponse.ok) { + const downloadData = await downloadResponse.json(); + if (downloadData.url) { + setServerImage(downloadData.url); + setHasCustomIcon(true); + } else { + setServerImage('/images/default-minecraft-server.png'); + setHasCustomIcon(false); + } + } else { + setServerImage('/images/default-minecraft-server.png'); + setHasCustomIcon(false); + } } else { - // If it doesn't exist, use default icon setServerImage('/images/default-minecraft-server.png'); setHasCustomIcon(false); } } } catch (err) { - console.error('Error checking for server icon:', err); - // Use default icon if there's an error setServerImage('/images/default-minecraft-server.png'); setHasCustomIcon(false); } }; - // Function to resize an image to 64x64 pixels const resizeImage = (file: File): Promise => { return new Promise((resolve, reject) => { const img = new Image(); @@ -109,21 +123,19 @@ export default function CustomizationContent({ server }: CustomizationContentPro if (!canvasRef.current) { canvasRef.current = document.createElement('canvas'); } - + const canvas = canvasRef.current; canvas.width = 64; canvas.height = 64; const ctx = canvas.getContext('2d'); - + if (!ctx) { reject(new Error('Failed to get canvas context')); return; } - - // Draw image at the new size + ctx.drawImage(img, 0, 0, 64, 64); - - // Convert canvas to PNG blob + canvas.toBlob( (blob) => { if (blob) { @@ -132,7 +144,8 @@ export default function CustomizationContent({ server }: CustomizationContentPro reject(new Error('Failed to create blob from canvas')); } }, - 'image/png' + 'image/jpeg', + 0.9 ); }; img.onerror = () => reject(new Error('Failed to load image')); @@ -140,51 +153,66 @@ export default function CustomizationContent({ server }: CustomizationContentPro }); }; - // Function to handle server icon upload const handleIconUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file || !server?.identifier || !accessKey) { return; } - - // Check if file is an image + if (!file.type.startsWith('image/')) { setError('Por favor, selecione um arquivo de imagem válido'); return; } - + try { setUploadingIcon(true); setError(null); - - // Resize image to 64x64 pixels + const resizedBlob = await resizeImage(file); - - // Create a new File from the blob - const pngFile = new File([resizedBlob], 'server-icon.png', { type: 'image/png' }); - - // Get upload URL + + const jpegFile = new File([resizedBlob], 'server-icon.png', { type: 'image/png' }); + const token = accessKey || getCookie('access_key'); const uploadUrlResponse = await getUploadUrl(server.identifier, token || '', '/'); - + if (uploadUrlResponse.success && uploadUrlResponse.data) { - // Upload the file const formData = new FormData(); - formData.append('files', pngFile); // Changed from 'file' to 'files' to match API expectations - + formData.append('files', jpegFile); + const uploadResponse = await fetch(uploadUrlResponse.data.url, { method: 'POST', body: formData }); - + if (!uploadResponse.ok) { throw new Error(`Failed to upload server icon: ${uploadResponse.status}`); } - - // Refresh icon display - checkForServerIcon(server.identifier); - setSuccess('Ícone do servidor atualizado com sucesso!'); - setTimeout(() => setSuccess(null), 5000); + + const downloadResponse = await fetch( + `${config.api.baseUrl}/v1/users/me/servers/games/${server.identifier}/files?action=download&file=/server-icon.png`, + { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + }, + } + ); + + if (downloadResponse.ok) { + const downloadData = await downloadResponse.json(); + if (downloadData.url) { + setServerImage(downloadData.url); + setHasCustomIcon(true); + setSuccess('Ícone do servidor atualizado com sucesso!'); + setTimeout(() => setSuccess(null), 5000); + } else { + setServerImage('/images/default-minecraft-server.png'); + setHasCustomIcon(false); + } + } else { + setServerImage('/images/default-minecraft-server.png'); + setHasCustomIcon(false); + } } else { throw new Error('Failed to get upload URL'); } @@ -199,7 +227,7 @@ export default function CustomizationContent({ server }: CustomizationContentPro const fetchServerSettings = async (identifier: string) => { setLoading(true); setError(null); - + try { const token = accessKey || getCookie('access_key'); const response = await fetch(`${config.api.baseUrl}/v1/users/me/servers/games/${identifier}/minesettings`, { @@ -209,19 +237,16 @@ export default function CustomizationContent({ server }: CustomizationContentPro 'Content-Type': 'application/json', } }); - + if (!response.ok) { throw new Error(`Failed to fetch server settings: ${response.status}`); } - + const responseData: MinecraftSettingsResponse = await response.json(); - - console.log('API Response:', responseData); - + if (responseData && responseData.settings) { const settings = responseData.settings; - - // Update state with retrieved settings + setPvp(settings.pvp || false); setSpawnMonsters(settings.monsters || false); setOnlineMode(settings.online_mode || false); @@ -233,27 +258,24 @@ export default function CustomizationContent({ server }: CustomizationContentPro setDifficulty(settings.difficulty || "easy"); setGamemode(settings.gamemode || "survival"); } - + } catch (err: any) { - console.error('Error fetching server settings:', err); setError(`Failed to load server settings: ${err.message}`); } finally { setLoading(false); } }; - // Removida função handleImageUpload que não é mais necessária - const handleSaveSettings = async () => { if (!server?.identifier || !accessKey) { setError("Identificador do servidor ou informações de autenticação ausentes"); return; } - + setIsSaving(true); setError(null); setSuccess(null); - + try { const token = accessKey || getCookie('access_key'); const response = await fetch(`${config.api.baseUrl}/v1/users/me/servers/games/${server.identifier}/minesettings`, { @@ -275,21 +297,19 @@ export default function CustomizationContent({ server }: CustomizationContentPro gamemode }) }); - + if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.message || `Erro ${response.status}: ${response.statusText}`; throw new Error(errorMessage); } - - // Atualizar as configurações locais com os dados retornados da API + fetchServerSettings(server.identifier); - + setSuccess("Configurações salvas com sucesso!"); - setTimeout(() => setSuccess(null), 5000); // Limpa mensagem de sucesso após 5 segundos - + setTimeout(() => setSuccess(null), 5000); + } catch (err: any) { - console.error('Erro ao atualizar configurações do servidor:', err); setError(`Falha ao salvar configurações: ${err.message}`); } finally { setIsSaving(false); @@ -354,12 +374,16 @@ export default function CustomizationContent({ server }: CustomizationContentPro
- Ícone do Servidor { - (e.target as HTMLImageElement).src = "/images/default-minecraft-server.png"; + const target = e.target as HTMLImageElement; + if (target.src !== "/images/default-minecraft-server.png") { + target.src = "/images/default-minecraft-server.png"; + setHasCustomIcon(false); + } }} /> {hasCustomIcon && ( diff --git a/src/components/dashboard/minecraft/FilesContent.tsx b/src/components/dashboard/minecraft/FilesContent.tsx index 9ca9f60..4ef1b8f 100644 --- a/src/components/dashboard/minecraft/FilesContent.tsx +++ b/src/components/dashboard/minecraft/FilesContent.tsx @@ -56,7 +56,6 @@ export default function FilesContent({ } }, [accessKey, identifier, currentPath]); - // Escutar eventos de refresh da lista de arquivos useEffect(() => { const handleRefresh = () => { if (accessKey && identifier) { @@ -213,12 +212,11 @@ export default function FilesContent({ try { const filePath = currentPath === "/" ? `/${file.name}` : `${currentPath}/${file.name}`; - // Decompressão requer root e file separados const result = await decompressFile( identifier, accessKey, - currentPath, // root (diretório) - file.name // nome do arquivo + currentPath, + file.name ); if (result.success) { fetchFiles(currentPath); @@ -239,12 +237,10 @@ export default function FilesContent({ try { for (let i = 0; i < files.length; i++) { const file = files[i]; - // getUploadUrl recebe diretório, não precisa do nome do arquivo const uploadUrl = await getUploadUrl(identifier, accessKey, currentPath); if (uploadUrl.success && uploadUrl.data) { const formData = new FormData(); - // Important: Use "files" as the key name, not "file" formData.append('files', file); const uploadResponse = await fetch(uploadUrl.data.url, { @@ -314,8 +310,6 @@ export default function FilesContent({ setDragActive(false); if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { - // We don't need to assign to fileInputRef.current.files anymore - // Just directly handle the files from dataTransfer handleFileUpload({ target: { files: e.dataTransfer.files } } as any); } }} @@ -391,7 +385,6 @@ export default function FilesContent({ {selectedFiles.length > 0 && (
{selectedFiles.length === 1 && isCompressedFile(selectedFiles[0]) ? ( - // Botão de descompactar para arquivos compactados ) : ( - // Botão de compactar para arquivos normais {isCompressedFile(file.name) ? ( - // Botão de descomprimir para arquivos compactados ) : ( - // Botão de comprimir para arquivos e pastas normais (file.is_file || !isCompressedFile(file.name)) && ( -
- -
-

{server.name}

-

Servidor Minecraft

+
+ +
+

{server.name}

+

Servidor Minecraft

{/* Server Status & Controls */} -
-
+
+
- + {server.status === "running" ? "Online" : server.status === "offline" || server.status === "stopped" @@ -79,7 +79,7 @@ export default function MinecraftServerHeader({
{!server.suspended && !server.is_suspended && ( -
+
)} diff --git a/src/components/dashboard/minecraft/MinecraftServerManager.tsx b/src/components/dashboard/minecraft/MinecraftServerManager.tsx index cbeea31..de38cfa 100644 --- a/src/components/dashboard/minecraft/MinecraftServerManager.tsx +++ b/src/components/dashboard/minecraft/MinecraftServerManager.tsx @@ -4,21 +4,25 @@ import { useEffect, useState, useCallback, useRef } from "react"; import { useParams, useRouter } from "next/navigation"; import { useAuth } from "@/contexts/AuthContext"; import { useServer } from "@/contexts/ServerContext"; +import { NotificationsProvider, useNotifications } from "@/contexts/NotificationsContext"; import { FaServer, FaExclamationTriangle, FaSpinner } from "react-icons/fa"; +import { SiMinecraft } from "react-icons/si"; import { ServerConsoleWebSocket } from "@/services/ServerConsoleWebSocket"; +import { motion, AnimatePresence } from "framer-motion"; -import MinecraftNavbar from "@/components/dashboard/minecraft/MinecraftNavbar"; +import MinecraftSidebar from "@/components/dashboard/minecraft/MinecraftSidebar"; import MinecraftServerHeader from "@/components/dashboard/minecraft/MinecraftServerHeader"; import ConsoleContent from "@/components/dashboard/minecraft/ConsoleContent"; -import ServerConsole from "@/components/dashboard/minecraft/ServerConsole"; import StatsContent from "@/components/dashboard/minecraft/StatsContent"; -import FilesContent from "@/components/dashboard/minecraft/FilesContent"; +import NewFilesContent from "@/components/dashboard/minecraft/NewFilesContent"; import CustomizationContent from "@/components/dashboard/minecraft/CustomizationContent"; import MinecraftSettingsContent from "@/components/dashboard/minecraft/MinecraftSettingsContent"; +import WorldsContent from "@/components/dashboard/minecraft/WorldsContent"; +import NetworkContent from "@/components/dashboard/minecraft/NetworkContent"; import { FileAttributes } from "@/types/server"; import FileEditorModal from "@/components/dashboard/FileEditorModal"; import RenameModal from "@/components/dashboard/RenameModal"; @@ -55,9 +59,10 @@ type ServerDetails = { is_suspended?: boolean; }; -export default function MinecraftServerManager() { +function MinecraftServerManagerContent() { const { accessKey, isLoading } = useAuth(); const { sendServerPowerSignal, getServerDetail, getServerConsoleCredentials } = useServer(); + const { showNotification } = useNotifications(); const router = useRouter(); const { identifier } = useParams() as { identifier: string }; const [server, setServer] = useState(null); @@ -65,10 +70,9 @@ export default function MinecraftServerManager() { const [error, setError] = useState(null); const [powerLoading, setPowerLoading] = useState(false); - // Ref para o WebSocket const wsRef = useRef(null); + const wsInitializedRef = useRef(false); - // Console state const [consoleState, setConsoleState] = useState({ consoleMessages: [] as string[], isConnected: false, @@ -77,13 +81,12 @@ export default function MinecraftServerManager() { stats: null as ServerStats | null }); - // Tab state + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [activeTab, setActiveTab] = useState("console"); - // File management state const [selectedFiles, setSelectedFiles] = useState([]); - // File management modals const [fileEditorModal, setFileEditorModal] = useState<{ isOpen: boolean; fileName: string; @@ -98,29 +101,30 @@ export default function MinecraftServerManager() { const [createFolderModal, setCreateFolderModal] = useState(false); - // Authentication check + const [eulaModal, setEulaModal] = useState<{ + isOpen: boolean; + accepting: boolean; + }>({ isOpen: false, accepting: false }); + useEffect(() => { if (!isLoading && (!accessKey || error === 'No access token provided.' || error === 'Unauthorized')) { router.replace('/login'); } }, [accessKey, error, isLoading, router]); - // Handle hash navigation useEffect(() => { const hash = window.location.hash.replace("#", ""); - if (hash && ["console", "stats", "files", "customization", "settings"].includes(hash)) { + if (hash && ["console", "stats", "files", "worlds", "network", "customization", "settings"].includes(hash)) { setActiveTab(hash); } }, []); - // Reset selected files when switching tabs useEffect(() => { if (activeTab !== "files") { setSelectedFiles([]); } }, [activeTab]); - // Server details fetching useEffect(() => { if (!accessKey || !identifier) return; @@ -136,6 +140,7 @@ export default function MinecraftServerManager() { if (serverResult.server.suspended || serverResult.server.is_suspended) { setError("Servidor Suspenso"); + showNotification("Este servidor está suspenso. Entre em contato com o suporte.", "error"); setLoading(false); return; } @@ -146,87 +151,157 @@ export default function MinecraftServerManager() { setLoading(false); } catch (err: any) { setError(err.message); + showNotification(err.message, "error"); setLoading(false); } }; fetchServerDetails(); - }, [accessKey, identifier, getServerDetail]); + }, [accessKey, identifier, getServerDetail, showNotification]); - // Efeito para manter a conexão WebSocket sempre ativa useEffect(() => { - if (!accessKey || !identifier) return; + if (!accessKey || !identifier || loading || error) return; - let isMounted = true; + wsInitializedRef.current = false; - // Estabelecer conexão direta com WebSocket - não usando ServerConsole para evitar montar/desmontar + let isMounted = true; + let connectionNotified = false; + let reconnectTimer: NodeJS.Timeout | null = null; + let isConnecting = false; + const credentialProvider = async () => { - const result = await getServerConsoleCredentials(identifier); - if (result.success && result.socket && result.key && result.expiresAt) { - return { - socket: result.socket, - key: result.key, - expiresAt: result.expiresAt, - }; + try { + const result = await getServerConsoleCredentials(identifier); + if (result.success && result.socket && result.key && result.expiresAt) { + return { + socket: result.socket, + key: result.key, + expiresAt: result.expiresAt, + }; + } + return { error: result.error || "Falha ao obter credenciais" }; + } catch (err) { + console.error("Erro ao solicitar credenciais:", err); + return { error: "Erro ao solicitar credenciais do WebSocket" }; } - return { error: result.error || "Falha ao obter credenciais" }; }; - const ws = new ServerConsoleWebSocket(credentialProvider); - wsRef.current = ws; + const connectWebSocket = () => { + if (isConnecting || !isMounted) return; + + if (wsRef.current) { + wsRef.current.disconnect(); + wsRef.current = null; + } - ws.onConnected(() => { - if (!isMounted) return; + isConnecting = true; setConsoleState(prev => ({ ...prev, - isConnected: true, - isConnecting: false, - connectionStatus: "Conectado", - consoleMessages: [...prev.consoleMessages, `${new Date().toISOString()}|[Sistema] Conexão com o console estabelecida.`] + isConnecting: true, + connectionStatus: "Conectando..." })); - }); + - ws.onDisconnected(() => { - if (!isMounted) return; - setConsoleState(prev => ({ - ...prev, - isConnected: false, - isConnecting: false, - connectionStatus: "Desconectado", - consoleMessages: [...prev.consoleMessages, `${new Date().toISOString()}|[Sistema] Conexão com o console perdida.`] - })); - }); + const ws = new ServerConsoleWebSocket(credentialProvider); + wsRef.current = ws; - ws.onMessage((data) => { - if (!isMounted) return; - handleConsoleUpdate(data); - }); + ws.onConnected(() => { + if (!isMounted) return; - ws.onError((errorMessage) => { - if (!isMounted) return; - console.warn("Aviso WebSocket:", errorMessage); - if (errorMessage.includes("Falha ao") || errorMessage.includes("renovar")) { - setError(errorMessage); - } - setConsoleState(prev => ({ - ...prev, - isConnected: false, - isConnecting: false, - connectionStatus: "Erro na conexão" - })); - }); + isConnecting = false; - // Conectar imediatamente - ws.connect(); + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + + setConsoleState(prev => ({ + ...prev, + isConnected: true, + isConnecting: false, + connectionStatus: "Conectado", + consoleMessages: !prev.isConnected + ? [...prev.consoleMessages, `${new Date().toISOString()}|[Sistema] Conexão com o console estabelecida.`] + : prev.consoleMessages + })); + + if (!connectionNotified) { + showNotification("Conexão com o console estabelecida", "success"); + connectionNotified = true; + setTimeout(() => { + if (isMounted) connectionNotified = false; + }, 30000); + } + }); + + ws.onDisconnected(() => { + if (!isMounted) return; + + isConnecting = false; + + setConsoleState(prev => ({ + ...prev, + isConnected: false, + isConnecting: false, + connectionStatus: "Desconectado", + consoleMessages: prev.isConnected + ? [...prev.consoleMessages, `${new Date().toISOString()}|[Sistema] Conexão com o console perdida.`] + : prev.consoleMessages + })); + + if (!reconnectTimer && isMounted) { + reconnectTimer = setTimeout(() => { + if (isMounted && !isConnecting && !wsRef.current) { + connectWebSocket(); + } + }, 5000); + } + }); + + ws.onMessage((data) => { + if (!isMounted) return; + handleConsoleUpdate(data); + }); + + ws.onError((errorMessage) => { + if (!isMounted) return; + + isConnecting = false; + + const isSignificantError = + errorMessage.includes("Falha ao") || + errorMessage.includes("renovar") || + errorMessage.includes("Unauthorized"); + + if (isSignificantError) { + setError(errorMessage); + showNotification(errorMessage, "error"); + } + + setConsoleState(prev => ({ + ...prev, + isConnected: false, + isConnecting: false, + connectionStatus: "Erro na conexão" + })); + }); + + ws.connect(); + }; + + connectWebSocket(); return () => { isMounted = false; - wsRef.current?.disconnect(); - wsRef.current = null; + wsInitializedRef.current = false; + if (reconnectTimer) clearTimeout(reconnectTimer); + if (wsRef.current) { + wsRef.current.disconnect(); + wsRef.current = null; + } }; }, [accessKey, identifier, getServerConsoleCredentials]); - // Console updates handler const handleConsoleUpdate = useCallback((data: any) => { if (data.type === 'connection') { setConsoleState(prev => ({ @@ -234,28 +309,33 @@ export default function MinecraftServerManager() { isConnected: data.connected !== undefined ? data.connected : prev.isConnected, isConnecting: data.connecting !== undefined ? data.connecting : prev.isConnecting, connectionStatus: data.status || prev.connectionStatus, - consoleMessages: data.message + consoleMessages: data.message ? [...prev.consoleMessages, `${new Date().toISOString()}|${data.message}`] : prev.consoleMessages })); } else if (data.type === 'console') { + const messageContent = data.data; + + if (messageContent.includes("You need to agree to the EULA in order to run the server")) { + setEulaModal({ isOpen: true, accepting: false }); + } + setConsoleState(prev => { const timestamp = new Date().toISOString(); const msgWithTimestamp = `${timestamp}|${data.data}`; - - // Prevent duplicate messages + const isDuplicate = prev.consoleMessages.some(existingMsg => { const existingContent = existingMsg.split('|').slice(1).join('|'); const newContent = data.data; const existingTimestamp = existingMsg.split('|')[0]; - + if (existingContent === newContent) { const timeDiff = new Date(timestamp).getTime() - new Date(existingTimestamp).getTime(); return timeDiff < 1000; } return false; }); - + if (isDuplicate) return prev; return { ...prev, @@ -278,15 +358,16 @@ export default function MinecraftServerManager() { })); if (data.error && data.error.includes("Falha ao")) { setError(data.error); + showNotification(data.error, "error"); } } - }, []); + }, [showNotification]); - // Power action handler const handlePowerAction = async (signal: string) => { if (!accessKey || powerLoading || server?.suspended || server?.is_suspended) return; setPowerLoading(true); + showNotification(`Enviando sinal ${signal} ao servidor...`, "info"); try { const result = await sendServerPowerSignal(identifier, signal); @@ -299,7 +380,6 @@ export default function MinecraftServerManager() { prev ? { ...prev, status: getPendingStatus(signal) } : null, ); - // Add console message about the power action setConsoleState(prev => ({ ...prev, consoleMessages: [ @@ -307,29 +387,29 @@ export default function MinecraftServerManager() { `${new Date().toISOString()}|[Sistema] Sinal ${signal} enviado ao servidor.` ] })); + + showNotification(`Sinal ${signal} enviado com sucesso!`, "success"); } catch (err: any) { setError(err.message); + showNotification(err.message, "error"); } finally { setPowerLoading(false); } }; - // File handling functions const handleEditFile = (file: FileAttributes) => { - // FilesContent component will pass the file object with the correct path setFileEditorModal({ isOpen: true, fileName: file.name, - filePath: file.name, // We'll rely on FilesContent to track the correct path + filePath: file.name, }); }; const handleRenameFile = (file: FileAttributes) => { - // FilesContent component will pass the file object with the correct path setRenameModal({ isOpen: true, fileName: file.name, - filePath: file.name, // We'll rely on FilesContent to track the correct path + filePath: file.name, }); }; @@ -337,8 +417,8 @@ export default function MinecraftServerManager() { if (!accessKey || !renameModal.filePath) return; try { - // The renameFile API requires root and from/to names - const root = "/"; // Root directory + showNotification(`Renomeando arquivo...`, "info"); + const root = "/"; const fromName = renameModal.filePath; const toName = newName; @@ -350,14 +430,14 @@ export default function MinecraftServerManager() { toName ); - // Close modal setRenameModal({ isOpen: false, fileName: "", filePath: "" }); - // Trigger file list refresh in FilesContent component const event = new CustomEvent('refreshFileList'); window.dispatchEvent(event); - } catch (err) { - console.error("Error renaming file:", err); + + showNotification(`Arquivo renomeado com sucesso!`, "success"); + } catch (err: any) { + showNotification(`Erro ao renomear arquivo: ${err.message}`, "error"); } }; @@ -365,18 +445,18 @@ export default function MinecraftServerManager() { if (!accessKey) return; try { - // Use root directory as path + showNotification(`Criando pasta ${folderName}...`, "info"); const root = "/"; await createFolder(identifier, accessKey, root, folderName); - // Close modal setCreateFolderModal(false); - // Trigger file list refresh in FilesContent component const event = new CustomEvent('refreshFileList'); window.dispatchEvent(event); - } catch (err) { - console.error("Error creating folder:", err); + + showNotification(`Pasta ${folderName} criada com sucesso!`, "success"); + } catch (err: any) { + showNotification(`Erro ao criar pasta: ${err.message}`, "error"); } }; @@ -386,11 +466,23 @@ export default function MinecraftServerManager() { try { const result = await getFileContents(identifier, accessKey, fileEditorModal.filePath); if (result.success && result.data) { - return result.data; + let content = result.data; + + try { + const jsonContent = JSON.parse(content); + if (jsonContent.contents) { + content = jsonContent.contents; + } + } catch { + } + + return content; } else { + showNotification(`Erro ao carregar conteúdo do arquivo: ${result.error}`, "error"); return ""; } - } catch (err) { + } catch (err: any) { + showNotification(`Erro ao carregar conteúdo do arquivo`, "error"); return ""; } }; @@ -399,10 +491,57 @@ export default function MinecraftServerManager() { if (!accessKey || !fileEditorModal.filePath) return; try { + showNotification(`Salvando arquivo ${fileEditorModal.fileName}...`, "info"); await writeFile(identifier, accessKey, fileEditorModal.filePath, content); setFileEditorModal({ isOpen: false, fileName: "", filePath: "" }); - } catch (err) { - console.error("Error saving file:", err); + showNotification(`Arquivo ${fileEditorModal.fileName} salvo com sucesso!`, "success"); + } catch (err: any) { + showNotification(`Erro ao salvar arquivo: ${err.message}`, "error"); + } + }; + + const handleAcceptEula = async () => { + if (!accessKey) return; + + setEulaModal(prev => ({ ...prev, accepting: true })); + + try { + showNotification("Aceitando EULA do Minecraft...", "info"); + + const eulaPath = "/eula.txt"; + + try { + const result = await getFileContents(identifier, accessKey, eulaPath); + if (result.success && result.data) { + let content = result.data; + + try { + const jsonContent = JSON.parse(content); + if (jsonContent.contents) { + content = jsonContent.contents; + } + } catch { + } + + const updatedContent = content.replace(/eula=false/g, "eula=true"); + await writeFile(identifier, accessKey, eulaPath, updatedContent); + } else { + await writeFile(identifier, accessKey, eulaPath, "eula=true"); + } + } catch (error) { + await writeFile(identifier, accessKey, eulaPath, "eula=true"); + } + + setEulaModal({ isOpen: false, accepting: false }); + showNotification("EULA aceito com sucesso! Reiniciando servidor...", "success"); + + setTimeout(() => { + handlePowerAction("start"); + }, 2000); + + } catch (error: any) { + setEulaModal({ isOpen: false, accepting: false }); + showNotification(`Erro ao aceitar EULA: ${error.message}`, "error"); } }; @@ -421,13 +560,16 @@ export default function MinecraftServerManager() { } }; - // Loading states if (loading) { return ( -
+
- -

Carregando servidor Minecraft...

+
+ + +
+

Carregando servidor...

+

Preparando ambiente Minecraft

); @@ -435,14 +577,14 @@ export default function MinecraftServerManager() { if (error) { return ( -
-
+
+
-

Erro

-

{error}

+

Erro

+

{error}

@@ -453,14 +595,16 @@ export default function MinecraftServerManager() { if (!server) { return ( -
-
- -

Servidor não encontrado

-

O servidor solicitado não foi encontrado.

+
+
+
+ +
+

Servidor não encontrado

+

O servidor solicitado não está disponível ou não existe.

@@ -470,76 +614,127 @@ export default function MinecraftServerManager() { } return ( -
- {/* Header */} - +
+
+ + +
+
- {/* Navigation */} - - {/* Content */} -
-
- {/* Tab Content */} - {activeTab === "console" && ( - { - // Enviar comando via WebSocket direto - if (wsRef.current && consoleState.isConnected) { - wsRef.current.sendCommand(command); - } +
+ setSidebarCollapsed(!sidebarCollapsed)} + /> + +
+
+ { - // Reconectar o WebSocket - if (wsRef.current && !consoleState.isConnecting) { - setConsoleState(prev => ({ - ...prev, - isConnecting: true, - connectionStatus: 'Reconectando...' - })); - wsRef.current.renewCredentials(); - } - }} - /> - )} - - {activeTab === "stats" && ( - - )} - - {activeTab === "files" && ( - setCreateFolderModal(true)} - /> - )} - - {activeTab === "customization" && ( - - )} - - {activeTab === "settings" && ( - - )} -
+ className="space-y-6 w-full" + > + {activeTab === "console" && ( + { + if (wsRef.current && consoleState.isConnected) { + wsRef.current.sendCommand(command); + } + }} + onReconnect={() => { + if (wsRef.current && !consoleState.isConnecting) { + setConsoleState(prev => ({ + ...prev, + isConnecting: true, + connectionStatus: 'Reconectando...' + })); + wsRef.current.renewCredentials(); + } + }} + /> + )} + + {activeTab === "stats" && ( + + )} + + {activeTab === "files" && ( + setCreateFolderModal(true)} + /> + )} + + {activeTab === "worlds" && ( + + )} + + {activeTab === "network" && ( + + )} + + {activeTab === "customization" && ( + + )} + + {activeTab === "settings" && ( + + )} + +
+
- {/* Modals */} setFileEditorModal({ isOpen: false, fileName: "", filePath: "" })} @@ -561,6 +756,80 @@ export default function MinecraftServerManager() { onClose={() => setCreateFolderModal(false)} onCreateFolder={handleCreateFolder} /> + + + {eulaModal.isOpen && ( + setEulaModal({ isOpen: false, accepting: false })} + > + e.stopPropagation()} + > +
+
+ +
+

Minecraft® EULA

+

+ Para iniciar o servidor Minecraft, você precisa aceitar os termos do{" "} + + Minecraft® EULA + + . +

+

+ Nota: A não aceitação desses termos implica na não inicialização do servidor. +

+
+ +
+ + +
+
+
+ )} +
); } + +export default function MinecraftServerManager() { + return ( + + + + ); +} diff --git a/src/components/dashboard/minecraft/MinecraftSettingsContent.tsx b/src/components/dashboard/minecraft/MinecraftSettingsContent.tsx index d7bbdec..c64e146 100644 --- a/src/components/dashboard/minecraft/MinecraftSettingsContent.tsx +++ b/src/components/dashboard/minecraft/MinecraftSettingsContent.tsx @@ -57,7 +57,6 @@ export default function MinecraftSettingsContent({ server }: MinecraftSettingsCo const [changingOs, setChangingOs] = useState(false); const [showOsConfirm, setShowOsConfirm] = useState(false); - // FTP related states const [ftpInfo, setFtpInfo] = useState(null); const [loadingFtp, setLoadingFtp] = useState(false); const [ftpError, setFtpError] = useState(null); @@ -68,7 +67,6 @@ export default function MinecraftSettingsContent({ server }: MinecraftSettingsCo const [newPassword, setNewPassword] = useState(null); const [showNewPasswordModal, setShowNewPasswordModal] = useState(false); - // Fetch FTP information const fetchFtpInfo = async () => { if (!accessKey) return; @@ -90,7 +88,6 @@ export default function MinecraftSettingsContent({ server }: MinecraftSettingsCo } }; - // Reset FTP password const handleResetFtpPassword = async () => { if (!accessKey) return; @@ -103,7 +100,6 @@ export default function MinecraftSettingsContent({ server }: MinecraftSettingsCo if (result.success && result.data) { setFtpInfo(result.data); setShowResetConfirm(false); - // Mostrar a nova senha em um modal if (result.data.password) { setNewPassword(result.data.password); setShowNewPasswordModal(true); @@ -267,7 +263,6 @@ export default function MinecraftSettingsContent({ server }: MinecraftSettingsCo setShowOsConfirm(false); setSelectedOs(null); - // Recarregar configurações após mudança window.location.reload(); } catch (err: any) { setError(err.message); @@ -547,7 +542,6 @@ export default function MinecraftSettingsContent({ server }: MinecraftSettingsCo + )} + {!isDesktop && ( + + )} +
+
+
+ +
+
+ {!isCollapsed && ( +
+
+ + Minecraft Java +
+
+ )} + {isCollapsed && ( +
+
+ +
+
+ )} +
+ + +
+
+ ); + + return ( + <> + {!isDesktop && ( + + )} + + {isDesktop && ( + + )} + + {!isDesktop && isMobileOpen && ( +
+ setIsMobileOpen(false)} + /> + + + {sidebarContent} + +
+ )} + + ); +} diff --git a/src/components/dashboard/minecraft/NetworkContent.tsx b/src/components/dashboard/minecraft/NetworkContent.tsx new file mode 100644 index 0000000..0e1b502 --- /dev/null +++ b/src/components/dashboard/minecraft/NetworkContent.tsx @@ -0,0 +1,460 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + FaNetworkWired, + FaGlobe, + FaPlus, + FaCopy, + FaCheck, + FaTrash, + FaEdit, + FaSpinner, + FaExclamationTriangle +} from "react-icons/fa"; +import { motion } from "framer-motion"; +import { useAuth } from "@/contexts/AuthContext"; +import config from "../../../../config.json"; + +interface NetworkContentProps { + server: { + ip?: string; + name?: string; + identifier?: string; + }; +} + +interface NetworkData { + ip: string; + port: number; + subdomain: string; +} + +export default function NetworkContent({ server }: NetworkContentProps) { + const { accessKey } = useAuth(); + const [networkData, setNetworkData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [copiedIp, setCopiedIp] = useState(false); + const [copiedPort, setCopiedPort] = useState(false); + const [copiedSubdomain, setCopiedSubdomain] = useState(false); + const [showCreateSubdomain, setShowCreateSubdomain] = useState(false); + const [showEditSubdomain, setShowEditSubdomain] = useState(false); + const [subdomainName, setSubdomainName] = useState(""); + const [actionLoading, setActionLoading] = useState(false); + + useEffect(() => { + if (server?.identifier && accessKey) { + loadNetworkData(); + } + }, [server?.identifier, accessKey]); + + const loadNetworkData = async () => { + if (!server?.identifier || !accessKey) return; + + try { + setLoading(true); + setError(null); + + const response = await fetch( + `${config.api.baseUrl}/v1/users/me/servers/games/${server.identifier}/network`, + { + method: "GET", + headers: { + Authorization: `Bearer ${accessKey}`, + "Content-Type": "application/json", + }, + } + ); + + if (response.ok) { + const data = await response.json(); + setNetworkData(data); + } else { + const errorData = await response.json(); + setError(errorData.message || "Erro ao carregar dados de rede"); + } + } catch (err: any) { + setError(err.message || "Erro inesperado"); + } finally { + setLoading(false); + } + }; + + const createSubdomain = async () => { + if (!server?.identifier || !accessKey || !subdomainName.trim()) return; + + try { + setActionLoading(true); + setError(null); + + const response = await fetch( + `${config.api.baseUrl}/v1/users/me/servers/games/${server.identifier}/network?subdomain=${encodeURIComponent(subdomainName.trim())}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${accessKey}`, + "Content-Type": "application/json", + }, + } + ); + + if (response.ok) { + await loadNetworkData(); + setShowCreateSubdomain(false); + setSubdomainName(""); + } else { + const errorData = await response.json(); + setError(errorData.message || "Erro ao criar subdomínio"); + } + } catch (err: any) { + setError(err.message || "Erro inesperado"); + } finally { + setActionLoading(false); + } + }; + + const updateSubdomain = async () => { + if (!server?.identifier || !accessKey || !subdomainName.trim()) return; + + try { + setActionLoading(true); + setError(null); + + const response = await fetch( + `${config.api.baseUrl}/v1/users/me/servers/games/${server.identifier}/network?subdomain=${encodeURIComponent(subdomainName.trim())}`, + { + method: "PATCH", + headers: { + Authorization: `Bearer ${accessKey}`, + "Content-Type": "application/json", + }, + } + ); + + if (response.ok) { + await loadNetworkData(); + setShowEditSubdomain(false); + setSubdomainName(""); + } else { + const errorData = await response.json(); + setError(errorData.message || "Erro ao atualizar subdomínio"); + } + } catch (err: any) { + setError(err.message || "Erro inesperado"); + } finally { + setActionLoading(false); + } + }; + + const deleteSubdomain = async () => { + if (!server?.identifier || !accessKey) return; + + try { + setActionLoading(true); + setError(null); + + const response = await fetch( + `${config.api.baseUrl}/v1/users/me/servers/games/${server.identifier}/network`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${accessKey}`, + "Content-Type": "application/json", + }, + } + ); + + if (response.ok) { + await loadNetworkData(); + } else { + const errorData = await response.json(); + setError(errorData.message || "Erro ao remover subdomínio"); + } + } catch (err: any) { + setError(err.message || "Erro inesperado"); + } finally { + setActionLoading(false); + } + }; + + const copyToClipboard = async (text: string, type: "ip" | "port" | "subdomain") => { + try { + await navigator.clipboard.writeText(text); + if (type === "ip") { + setCopiedIp(true); + setTimeout(() => setCopiedIp(false), 2000); + } else if (type === "port") { + setCopiedPort(true); + setTimeout(() => setCopiedPort(false), 2000); + } else { + setCopiedSubdomain(true); + setTimeout(() => setCopiedSubdomain(false), 2000); + } + } catch (err) { + console.error("Failed to copy text: ", err); + } + }; + + const handleEditClick = () => { + setSubdomainName(networkData?.subdomain || ""); + setShowEditSubdomain(true); + }; + + if (loading) { + return ( +
+ +

Carregando dados de rede...

+
+ ); + } + + if (error) { + return ( +
+ + {error} +
+ ); + } + + const ipAndPort = server?.ip?.split(":") || ["", ""]; + const ip = ipAndPort[0]; + const port = ipAndPort[1]; + const hasSubdomain = networkData?.subdomain && networkData.subdomain !== "notconfigured"; + + return ( +
+ +
+
+ +
+

Configurações de Rede

+

+ Gerencie o acesso ao seu servidor Minecraft +

+
+ +
+ +
+

IP

+ +
+
+

+ {ip || "N/A"} +

+
+
+ + +
+

Porta

+ +
+
+

+ {port || "N/A"} +

+
+
+
+ + +
+

Conexão Direta

+
+
+ Online +
+
+
+

+ {server?.ip || "N/A"} +

+

+ Use este endereço para conectar diretamente +

+
+
+
+ + +
+
+ +
+

Subdomínio

+

+ Configure um subdomínio personalizado +

+
+ +
+ {hasSubdomain ? ( +
+
+

Subdomínio Ativo

+
+ + + +
+
+
+

+ {networkData.subdomain}.firehosting.cloud +

+

+ Conecte usando: {networkData.subdomain}.firehosting.cloud +

+
+
+ ) : ( +
+ +

Nenhum subdomínio configurado

+

+ Configure um subdomínio personalizado para facilitar o acesso +

+
+ )} + + {!showCreateSubdomain && !showEditSubdomain && ( + { + setSubdomainName(""); + setShowCreateSubdomain(!hasSubdomain); + setShowEditSubdomain(!!hasSubdomain); + }} + className="inline-flex items-center space-x-2 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white px-6 py-2.5 rounded-lg font-semibold transition-all duration-200 transform hover:scale-105 shadow-lg hover:shadow-purple-900/25 text-sm" + whileHover={{ y: -1 }} + whileTap={{ scale: 0.98 }} + > + + {hasSubdomain ? "Editar Subdomínio" : "Criar Subdomínio"} + + )} + + {(showCreateSubdomain || showEditSubdomain) && ( + +

+ {showEditSubdomain ? "Editar Subdomínio" : "Criar Subdomínio"} +

+
+
+ +
+ setSubdomainName(e.target.value.toLowerCase())} + placeholder="meu-servidor" + className="flex-1 bg-gray-800/50 border border-gray-600/50 rounded-l px-3 py-2 text-white placeholder-gray-500 text-sm focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent" + /> +
+ .firehosting.cloud +
+
+
+
+ + +
+
+
+ )} +
+
+
+ ); +} diff --git a/src/components/dashboard/minecraft/NewFilesContent.tsx b/src/components/dashboard/minecraft/NewFilesContent.tsx new file mode 100644 index 0000000..db188e1 --- /dev/null +++ b/src/components/dashboard/minecraft/NewFilesContent.tsx @@ -0,0 +1,804 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { + FaFolder, + FaPlus, + FaUpload, + FaSpinner, + FaExclamationTriangle, + FaFileAlt, + FaFolderOpen, + FaDownload, + FaEdit, + FaTrashAlt, + FaFileArchive, + FaFileImport, + FaCompress, + FaAngleRight, + FaHome, + FaSearch, + FaSort, + FaSortAmountDown, + FaSortAmountUp +} from "react-icons/fa"; +import { FileAttributes } from "@/types/server"; +import { + getServerFiles, + downloadFile, + deleteFiles, + compressFiles, + decompressFile, + getUploadUrl +} from "@/services/api/server"; +import { useNotifications } from "@/contexts/NotificationsContext"; +import { motion, AnimatePresence } from "framer-motion"; + +interface FilesContentProps { + identifier: string; + accessKey: string; + onEditFile: (file: FileAttributes) => void; + onRenameFile: (file: FileAttributes) => void; + onCreateFolder: () => void; +} + +type SortKey = "name" | "size" | "type" | "modified"; +type SortDirection = "asc" | "desc"; + +export default function FilesContent({ + identifier, + accessKey, + onEditFile, + onRenameFile, + onCreateFolder +}: FilesContentProps) { + const { showNotification } = useNotifications(); + const [serverFiles, setServerFiles] = useState([]); + const [filesLoading, setFilesLoading] = useState(false); + const [filesError, setFilesError] = useState(null); + const [currentPath, setCurrentPath] = useState("/"); + const [selectedFiles, setSelectedFiles] = useState([]); + const [uploadProgress, setUploadProgress] = useState(null); + const [dragActive, setDragActive] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [sortConfig, setSortConfig] = useState<{key: SortKey, direction: SortDirection}>({ + key: "name", + direction: "asc" + }); + + const fileInputRef = useRef(null); + const searchInputRef = useRef(null); + const filesContainerRef = useRef(null); + + useEffect(() => { + if (accessKey && identifier) { + fetchFiles(currentPath); + } + }, [accessKey, identifier, currentPath]); + + useEffect(() => { + const handleRefresh = () => { + if (accessKey && identifier) { + fetchFiles(currentPath); + } + }; + + window.addEventListener('refreshFileList', handleRefresh); + + return () => { + window.removeEventListener('refreshFileList', handleRefresh); + }; + }, [accessKey, identifier, currentPath]); + + const fetchFiles = async (path: string) => { + if (!accessKey) return; + setFilesLoading(true); + setFilesError(null); + try { + const result = await getServerFiles(identifier, accessKey, path); + if (result.success && result.data) { + setServerFiles(result.data); + setCurrentPath(path); + setFilesError(null); + setSelectedFiles([]); + } else { + const errorMessage = typeof result.error === 'string' ? result.error : "Failed to load files."; + setFilesError(errorMessage); + showNotification(errorMessage, "error"); + } + } catch (err: any) { + const errorMessage = typeof err === 'string' ? err : (err?.message || "An error occurred while fetching files."); + setFilesError(errorMessage); + showNotification(errorMessage, "error"); + } finally { + setFilesLoading(false); + } + }; + + const handleFileClick = (file: FileAttributes) => { + if (!file.is_file) { + const newPath = currentPath === "/" ? `/${file.name}` : `${currentPath}/${file.name}`; + fetchFiles(newPath); + } else { + if (isEditableFile(file)) { + onEditFile(file); + } else if (isCompressedFile(file.name)) { + handleDecompressFile(file); + } else { + showNotification(`Este tipo de arquivo não pode ser editado diretamente.`, "info"); + } + } + }; + + const isEditableFile = (file: FileAttributes): boolean => { + const editableExtensions = ['.txt', '.json', '.js', '.ts', '.jsx', '.tsx', '.css', '.html', '.xml', '.yml', '.yaml', '.properties', '.conf', '.cfg', '.ini']; + return editableExtensions.some(ext => file.name.toLowerCase().endsWith(ext)) || + !file.name.includes('.') || + file.mimetype?.includes('text/'); + }; + + const isCompressedFile = (fileName: string): boolean => { + const compressedExtensions = ['.zip', '.tar.gz', '.tar', '.rar', '.7z', '.gz', '.bz2', '.xz']; + return compressedExtensions.some(ext => fileName.toLowerCase().endsWith(ext)); + }; + + const handleBreadcrumbClick = (pathSegmentIndex: number) => { + if (pathSegmentIndex === -1) { + fetchFiles("/"); + } else { + const segments = currentPath.split("/").filter(Boolean); + const newPath = "/" + segments.slice(0, pathSegmentIndex + 1).join("/"); + fetchFiles(newPath); + } + }; + + const handleDownloadFile = async (file: FileAttributes) => { + if (!accessKey) return; + + try { + const filePath = currentPath === "/" ? `/${file.name}` : `${currentPath}/${file.name}`; + showNotification(`Iniciando download de ${file.name}...`, "info"); + + const result = await downloadFile(identifier, accessKey, filePath); + if (result.success && result.data) { + if ('url' in result.data) { + window.open(result.data.url, '_blank'); + showNotification(`Download de ${file.name} iniciado!`, "success"); + } else { + const blob = result.data as Blob; + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = file.name; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + showNotification(`Download de ${file.name} concluído!`, "success"); + } + } else { + setFilesError(result.error || "Failed to download file"); + showNotification(result.error || "Falha ao baixar o arquivo", "error"); + } + } catch (err: any) { + setFilesError(err.message || "An error occurred while downloading the file"); + showNotification(err.message || "Erro ao baixar o arquivo", "error"); + } + }; + + const handleDeleteFile = async (file: FileAttributes) => { + if (!accessKey) return; + + try { + showNotification(`Excluindo ${file.name}...`, "info"); + const result = await deleteFiles(identifier, accessKey, currentPath, [file.name]); + if (result.success) { + fetchFiles(currentPath); + showNotification(`${file.name} excluído com sucesso!`, "success"); + } else { + setFilesError(result.error || "Failed to delete file"); + showNotification(result.error || "Falha ao excluir o arquivo", "error"); + } + } catch (err: any) { + setFilesError(err.message || "An error occurred while deleting the file"); + showNotification(err.message || "Erro ao excluir o arquivo", "error"); + } + }; + + const handleDeleteFiles = async (files: string[]) => { + if (!accessKey || files.length === 0) return; + + try { + showNotification(`Excluindo ${files.length} arquivo(s)...`, "info"); + const result = await deleteFiles(identifier, accessKey, currentPath, files); + if (result.success) { + fetchFiles(currentPath); + setSelectedFiles([]); + showNotification(`${files.length} arquivo(s) excluídos com sucesso!`, "success"); + } else { + setFilesError(result.error || "Failed to delete files"); + showNotification(result.error || "Falha ao excluir os arquivos", "error"); + } + } catch (err: any) { + setFilesError(err.message || "An error occurred while deleting the files"); + showNotification(err.message || "Erro ao excluir os arquivos", "error"); + } + }; + + const handleCompressFiles = async (files: string[]) => { + if (!accessKey || files.length === 0) return; + + try { + showNotification(`Comprimindo ${files.length} arquivo(s)...`, "info"); + const result = await compressFiles(identifier, accessKey, currentPath, files); + if (result.success) { + fetchFiles(currentPath); + setSelectedFiles([]); + showNotification(`Arquivos comprimidos com sucesso!`, "success"); + } else { + setFilesError(result.error || "Failed to compress files"); + showNotification(result.error || "Falha ao comprimir os arquivos", "error"); + } + } catch (err: any) { + setFilesError(err.message || "An error occurred while compressing files"); + showNotification(err.message || "Erro ao comprimir os arquivos", "error"); + } + }; + + const handleDecompressFile = async (file: FileAttributes) => { + if (!accessKey) return; + + try { + showNotification(`Descomprimindo ${file.name}...`, "info"); + const result = await decompressFile( + identifier, + accessKey, + currentPath, + file.name + ); + if (result.success) { + fetchFiles(currentPath); + showNotification(`${file.name} descomprimido com sucesso!`, "success"); + } else { + setFilesError(result.error || "Failed to decompress file"); + showNotification(result.error || "Falha ao descomprimir o arquivo", "error"); + } + } catch (err: any) { + setFilesError(err.message || "An error occurred while decompressing file"); + showNotification(err.message || "Erro ao descomprimir o arquivo", "error"); + } + }; + + const handleFileUpload = async (event: React.ChangeEvent | { target: { files: FileList } }) => { + const files = event.target.files; + if (!files || files.length === 0 || !accessKey) return; + + setUploadProgress(0); + showNotification(`Iniciando upload de ${files.length} arquivo(s)...`, "info"); + + try { + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const uploadUrl = await getUploadUrl(identifier, accessKey, currentPath); + + if (uploadUrl.success && uploadUrl.data) { + const formData = new FormData(); + formData.append('files', file); + + const uploadResponse = await fetch(uploadUrl.data.url, { + method: 'POST', + body: formData, + }); + + if (!uploadResponse.ok) { + const errorText = await uploadResponse.text(); + console.error("Upload error:", errorText); + throw new Error(`Failed to upload ${file.name}: ${uploadResponse.status} ${errorText}`); + } + } + + setUploadProgress(((i + 1) / files.length) * 100); + } + + fetchFiles(currentPath); + showNotification(`${files.length} arquivo(s) enviados com sucesso!`, "success"); + } catch (err: any) { + console.error("Upload error:", err); + setFilesError(err.message || "An error occurred during upload"); + showNotification(err.message || "Erro durante o upload", "error"); + } finally { + setUploadProgress(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + }; + + const formatBytes = (bytes: number) => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; + }; + + const toggleSort = (key: SortKey) => { + setSortConfig({ + key, + direction: sortConfig.key === key && sortConfig.direction === 'asc' ? 'desc' : 'asc' + }); + }; + + const filteredFiles = serverFiles + .filter(file => + searchQuery.trim() === '' || + file.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .sort((a, b) => { + if (sortConfig.key === 'name') { + if (!a.is_file && b.is_file) return sortConfig.direction === 'asc' ? -1 : 1; + if (a.is_file && !b.is_file) return sortConfig.direction === 'asc' ? 1 : -1; + return sortConfig.direction === 'asc' + ? a.name.localeCompare(b.name) + : b.name.localeCompare(a.name); + } + + if (sortConfig.key === 'size') { + if (!a.is_file && !b.is_file) return 0; + if (!a.is_file) return sortConfig.direction === 'asc' ? -1 : 1; + if (!b.is_file) return sortConfig.direction === 'asc' ? 1 : -1; + return sortConfig.direction === 'asc' + ? a.size - b.size + : b.size - a.size; + } + + if (sortConfig.key === 'type') { + if (!a.is_file && !b.is_file) return 0; + if (!a.is_file) return sortConfig.direction === 'asc' ? -1 : 1; + if (!b.is_file) return sortConfig.direction === 'asc' ? 1 : -1; + + const aType = a.mimetype || ''; + const bType = b.mimetype || ''; + return sortConfig.direction === 'asc' + ? aType.localeCompare(bType) + : bType.localeCompare(aType); + } + + if (sortConfig.key === 'modified') { + return sortConfig.direction === 'asc' + ? new Date(a.modified_at).getTime() - new Date(b.modified_at).getTime() + : new Date(b.modified_at).getTime() - new Date(a.modified_at).getTime(); + } + + return 0; + }); + + return ( +
+ { + e.preventDefault(); + e.stopPropagation(); + setDragActive(true); + }} + onDragOver={(e) => { + e.preventDefault(); + e.stopPropagation(); + if (!dragActive) setDragActive(true); + }} + onDragLeave={(e) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + }} + onDrop={(e) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + handleFileUpload({ target: { files: e.dataTransfer.files } } as any); + } + }} + > +
+
+
+ +
+
+

Gerenciador de Arquivos

+

Gerencie seus arquivos e pastas

+
+
+ +
+
+ +
+ setSearchQuery(e.target.value)} + className="w-full bg-white/10 border border-white/20 rounded-xl py-3 pl-12 pr-4 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent backdrop-blur-sm" + /> +
+
+ +
+
+ + + + + +
+ + {selectedFiles.length > 0 && ( +
+ + {selectedFiles.length} arquivo(s) selecionado(s) + + + {selectedFiles.length === 1 && isCompressedFile(selectedFiles[0]) ? ( + + ) : ( + + )} + + + + +
+ )} + + {uploadProgress !== null && ( +
+
+ + + + +
+ + {Math.round(uploadProgress)}% + +
+
+
+

Enviando arquivos...

+

Aguarde enquanto processamos seu upload

+
+
+ )} +
+ +
+
+ + {currentPath + .split("/") + .filter(Boolean) + .map((segment, index, segments) => ( +
+ + +
+ ))} +
+
+ + {filesLoading ? ( +
+ +

Carregando arquivos...

+
+ ) : filesError ? ( +
+ + {filesError} +
+ ) : filteredFiles.length === 0 ? ( +
+ + {searchQuery ? "Nenhum arquivo encontrado para sua pesquisa." : "Esta pasta está vazia."} + {searchQuery && ( + + )} +
+ ) : ( +
+ + + + + + + + + + + + + {filteredFiles.map((file, index) => ( + + + + + + + + ))} + + +
+ + + + + + + + Ações
+
+
+ { + if (e.target.checked) { + setSelectedFiles([...selectedFiles, file.name]); + } else { + setSelectedFiles(selectedFiles.filter(f => f !== file.name)); + } + }} + className="rounded border-gray-600 text-green-600 focus:ring-green-500 bg-gray-700" + /> +
+
handleFileClick(file)} + > +
+ {file.is_file ? ( + + ) : ( + + )} +
+ + {file.name} + +
+
+
+ {file.is_file ? formatBytes(file.size) : "-"} + + {file.is_file ? file.mimetype : "Pasta"} + + {new Date(file.modified_at).toLocaleString()} + +
+ {file.is_file && ( + + )} + {file.is_file && isEditableFile(file) && ( + + )} + + + {isCompressedFile(file.name) ? ( + + ) : ( + (file.is_file || !isCompressedFile(file.name)) && ( + + ) + )} +
+
+
+ )} + + {dragActive && ( +
+
+ +

Solte os arquivos para fazer upload

+
+
+ )} +
+
+ ); +} diff --git a/src/components/dashboard/minecraft/ServerConsole.tsx b/src/components/dashboard/minecraft/ServerConsole.tsx index df0df65..d1fdaaf 100644 --- a/src/components/dashboard/minecraft/ServerConsole.tsx +++ b/src/components/dashboard/minecraft/ServerConsole.tsx @@ -33,7 +33,6 @@ export default function ServerConsole({ let isMounted = true; - // Usar o método do contexto para obter as credenciais const credentialProvider = async () => { const result = await getServerConsoleCredentials(identifier); if (result.success && result.socket && result.key && result.expiresAt) { diff --git a/src/components/dashboard/minecraft/WorldsContent.tsx b/src/components/dashboard/minecraft/WorldsContent.tsx new file mode 100644 index 0000000..6cc59df --- /dev/null +++ b/src/components/dashboard/minecraft/WorldsContent.tsx @@ -0,0 +1,447 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { + FaGlobe, + FaUpload, + FaDownload, + FaSpinner, + FaExclamationTriangle, + FaCheckCircle, + FaServer, + FaTrash, + FaArchive +} from "react-icons/fa"; +import { FileAttributes } from "@/types/server"; +import { + getServerFiles, + downloadFile, + deleteFiles, + compressFiles, + decompressFile, + getUploadUrl, + createFolder +} from "@/services/api/server"; +import { useNotifications } from "@/contexts/NotificationsContext"; +import { motion, AnimatePresence } from "framer-motion"; + +interface WorldsContentProps { + identifier: string; + accessKey: string; + onPowerAction?: (signal: string) => void; +} + +type MinecraftType = 'vanilla' | 'bedrock' | 'unknown'; + +export default function WorldsContent({ + identifier, + accessKey, + onPowerAction +}: WorldsContentProps) { + const { showNotification } = useNotifications(); + const [minecraftType, setMinecraftType] = useState('unknown'); + const [worldPath, setWorldPath] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(null); + const [showUploadModal, setShowUploadModal] = useState(false); + const [serverStatus, setServerStatus] = useState('unknown'); + + const fileInputRef = useRef(null); + + useEffect(() => { + detectMinecraftType(); + }, [accessKey, identifier]); + + const detectMinecraftType = async () => { + if (!accessKey) return; + + try { + const result = await getServerFiles(identifier, accessKey, "/"); + if (result.success && result.data) { + const hasWorldFolder = result.data.some(file => file.name === 'world' && !file.is_file); + const hasWorldsFolder = result.data.some(file => file.name === 'worlds' && !file.is_file); + + if (hasWorldsFolder) { + setMinecraftType('bedrock'); + setWorldPath('/worlds/Bedrock level'); + } else if (hasWorldFolder) { + setMinecraftType('vanilla'); + setWorldPath('/world'); + } else { + setMinecraftType('unknown'); + setWorldPath(''); + } + } + } catch (error) { + setMinecraftType('unknown'); + setWorldPath(''); + } + }; + + const handleDownloadWorld = async () => { + if (!accessKey || minecraftType === 'unknown') return; + + setIsLoading(true); + showNotification("Preparando download do mundo...", "info"); + + try { + const worldFolder = minecraftType === 'bedrock' ? 'Bedrock level' : 'world'; + const parentPath = minecraftType === 'bedrock' ? '/worlds' : '/'; + + const compressResult = await compressFiles(identifier, accessKey, parentPath, [worldFolder]); + + if (compressResult.success) { + await new Promise(resolve => setTimeout(resolve, 2000)); + + const filesResult = await getServerFiles(identifier, accessKey, parentPath); + if (filesResult.success && filesResult.data) { + const archiveFile = filesResult.data + .filter(file => file.is_file && file.name.includes('archive-') && file.name.endsWith('.tar.gz')) + .sort((a, b) => new Date(b.modified_at).getTime() - new Date(a.modified_at).getTime())[0]; + + if (archiveFile) { + const downloadResult = await downloadFile(identifier, accessKey, `${parentPath}/${archiveFile.name}`); + + if (downloadResult.success && downloadResult.data) { + if ('url' in downloadResult.data) { + window.open(downloadResult.data.url, '_blank'); + } else { + const blob = downloadResult.data as Blob; + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${worldFolder}.tar.gz`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } + + showNotification("Download do mundo iniciado com sucesso!", "success"); + + await deleteFiles(identifier, accessKey, parentPath, [archiveFile.name]); + } else { + showNotification("Erro ao fazer download do mundo", "error"); + } + } else { + showNotification("Arquivo de compressão não encontrado", "error"); + } + } else { + showNotification("Erro ao listar arquivos após compressão", "error"); + } + } else { + showNotification("Erro ao comprimir o mundo", "error"); + } + } catch (error) { + showNotification("Erro durante o download do mundo", "error"); + } finally { + setIsLoading(false); + } + }; + + const handleUploadWorld = async (file: File) => { + if (!accessKey || minecraftType === 'unknown') return; + + setIsUploading(true); + setUploadProgress(0); + setShowUploadModal(false); + + showNotification("Iniciando upload do novo mundo...", "info"); + + try { + const worldFolder = minecraftType === 'bedrock' ? 'Bedrock level' : 'world'; + const parentPath = minecraftType === 'bedrock' ? '/worlds' : '/'; + + setUploadProgress(10); + showNotification("Desligando servidor...", "info"); + + if (onPowerAction) { + await new Promise((resolve) => { + onPowerAction("kill"); + setTimeout(resolve, 5000); + }); + } + + setUploadProgress(20); + showNotification("Removendo mundo atual...", "info"); + + try { + await deleteFiles(identifier, accessKey, parentPath, [worldFolder]); + } catch (error) { + } + + setUploadProgress(30); + showNotification("Preparando nova pasta...", "info"); + + const createFolderResult = await createFolder(identifier, accessKey, parentPath, worldFolder); + if (!createFolderResult.success) { + throw new Error("Erro ao criar nova pasta do mundo"); + } + + setUploadProgress(40); + showNotification("Enviando novo mundo...", "info"); + + const worldUploadPath = `${parentPath}/${worldFolder}`; + const uploadUrl = await getUploadUrl(identifier, accessKey, worldUploadPath); + if (!uploadUrl.success || !uploadUrl.data) { + throw new Error("Erro ao obter URL de upload"); + } + + const formData = new FormData(); + formData.append('files', file); + + const uploadResponse = await fetch(uploadUrl.data.url, { + method: 'POST', + body: formData, + }); + + if (!uploadResponse.ok) { + throw new Error(`Erro no upload: ${uploadResponse.status}`); + } + + setUploadProgress(60); + showNotification("Extraindo mundo...", "info"); + + const archiveName = file.name; + const decompressResult = await decompressFile( + identifier, + accessKey, + worldUploadPath, + archiveName + ); + + if (!decompressResult.success) { + throw new Error("Erro ao extrair o mundo"); + } + + setUploadProgress(80); + showNotification("Verificando arquivos...", "info"); + + const verifyResult = await getServerFiles(identifier, accessKey, parentPath); + if (verifyResult.success && verifyResult.data) { + const hasWorldFolder = verifyResult.data.some(f => f.name === worldFolder && !f.is_file); + + if (!hasWorldFolder) { + throw new Error("Mundo não foi extraído corretamente"); + } + } + + setUploadProgress(90); + showNotification("Limpando arquivos temporários...", "info"); + + await deleteFiles(identifier, accessKey, worldUploadPath, [archiveName]); + + setUploadProgress(100); + showNotification("Mundo atualizado com sucesso!", "success"); + + if (onPowerAction) { + setTimeout(() => { + onPowerAction("start"); + }, 2000); + } + + detectMinecraftType(); + + } catch (error: any) { + showNotification(`Erro durante o upload: ${error.message}`, "error"); + } finally { + setIsUploading(false); + setUploadProgress(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + }; + + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + if (file.name.endsWith('.zip') || file.name.endsWith('.tar.gz')) { + handleUploadWorld(file); + } else { + showNotification("Por favor, selecione um arquivo ZIP ou TAR.GZ", "error"); + } + } + }; + + return ( +
+ +
+
+ +
+

Seu Mundo

+

+ {minecraftType === 'vanilla' && 'Minecraft Java Edition - Mundo Vanilla'} + {minecraftType === 'bedrock' && 'Minecraft Bedrock Edition - Mundo Bedrock'} + {minecraftType === 'unknown' && 'Tipo de servidor não identificado'} +

+
+ + {minecraftType !== 'unknown' && ( +
+ setShowUploadModal(true)} + disabled={isUploading} + className="group relative overflow-hidden bg-gradient-to-r from-green-600 to-emerald-700 hover:from-green-700 hover:to-emerald-800 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl p-6 transition-all duration-200 transform hover:scale-105 shadow-lg hover:shadow-green-900/25" + whileHover={{ y: -2 }} + whileTap={{ scale: 0.98 }} + > +
+ +

Enviar Novo Mundo

+

+ Substitua o mundo atual por um novo arquivo ZIP +

+
+
+ + + +
+ {isLoading ? ( + + ) : ( + + )} +

Baixar Mundo Atual

+

+ Faça download do seu mundo atual +

+
+
+ +
+ )} + + {minecraftType === 'unknown' && ( +
+ +

+ Não foi possível identificar o tipo de servidor Minecraft. +
+ Verifique se existe uma pasta "world" ou "worlds" no servidor. +

+
+ )} + + {isUploading && uploadProgress !== null && ( +
+
+
+ + + + +
+ + {Math.round(uploadProgress)}% + +
+
+
+

+ {uploadProgress < 20 && "Desligando servidor..."} + {uploadProgress >= 20 && uploadProgress < 30 && "Removendo mundo atual..."} + {uploadProgress >= 30 && uploadProgress < 60 && "Enviando novo mundo..."} + {uploadProgress >= 60 && uploadProgress < 80 && "Extraindo mundo..."} + {uploadProgress >= 80 && uploadProgress < 90 && "Verificando arquivos..."} + {uploadProgress >= 90 && "Finalizando..."} +

+

Aguarde enquanto processamos seu mundo

+
+
+
+ )} + + + + {showUploadModal && ( + setShowUploadModal(false)} + > + e.stopPropagation()} + > +
+
+ +
+

Atenção

+

+ Esta ação irá substituir completamente o mundo atual. +

+ Certifique-se de ter feito backup do mundo atual antes de continuar. +

+ Formatos aceitos: .zip e .tar.gz +

+
+ +
+ + +
+
+
+ )} +
+ + +
+ ); +} diff --git a/src/components/home/CTA.tsx b/src/components/home/CTA.tsx index 7c78133..f6e65b1 100644 --- a/src/components/home/CTA.tsx +++ b/src/components/home/CTA.tsx @@ -2,52 +2,94 @@ import { motion } from "framer-motion"; import Link from "next/link"; -import { FiArrowRight, FiMessageCircle } from "react-icons/fi"; +import { FaRocket, FaShieldAlt, FaHeadset } from "react-icons/fa"; const CTA = () => { return ( -
-
- -

- Pronto para Evoluir Sua - Hospedagem? -

-

- Junte-se a milhares de clientes satisfeitos que confiam na FireHosting para - suas necessidades de jogos e VPS. Experimente a diferença hoje. -

+
+
+
+ +

+ Pronto para Evoluir Sua + Hospedagem? +

+ +

+ Junte-se a milhares de clientes satisfeitos que confiam na FireHosting para + suas necessidades de jogos e VPS. Experimente a diferença hoje. +

-
- - Ver Planos - -
- - {/* Additional Info */} -
-
-
-
30 Dias de Garantia
-
Garantia sem riscos
-
-
-
-
Configuração Instantânea
-
Comece em minutos
+
+ + Ver Nossos Planos + + → + +
-
-
-
Suporte 24/7
-
Sempre aqui para ajudar
+ + {/* Additional Info Cards */} +
+ +
+
+ +
+
+
30 Dias de Garantia
+
Satisfação garantida ou seu dinheiro de volta
+
+ + +
+
+ +
+
+
Configuração Instantânea
+
Seu servidor pronto em minutos
+
+ + +
+
+ +
+
+
Suporte 24/7
+
Equipe especializada sempre disponível
+
-
- + +
); diff --git a/src/components/home/Features.tsx b/src/components/home/Features.tsx index af65fe3..0bd7792 100644 --- a/src/components/home/Features.tsx +++ b/src/components/home/Features.tsx @@ -1,54 +1,47 @@ "use client"; import { motion } from "framer-motion"; -import { - FiZap, - FiShield, - FiUsers, - FiClock, - FiDatabase, - FiTrendingUp, -} from "react-icons/fi"; +import { FaRocket, FaShieldAlt, FaUsers, FaClock, FaDatabase, FaChartLine, FaGamepad, FaServer } from "react-icons/fa"; const Features = () => { const features = [ { - icon: FiZap, + icon: FaRocket, title: "Performance Extrema", description: "Experimente servidores ultra-rápidos com armazenamento NVMe SSD e infraestrutura de rede otimizada para latência mínima.", stats: "< 10ms latência", }, { - icon: FiShield, + icon: FaShieldAlt, title: "Segurança Avançada", description: "Proteção DDoS multicamadas, backups automatizados e medidas de segurança de nível empresarial mantêm seus dados seguros.", stats: "99.9% taxa de proteção", }, { - icon: FiClock, + icon: FaClock, title: "Máxima Disponibilidade", description: "Garantia de uptime líder do setor com infraestrutura redundante e sistemas de monitoramento em tempo real.", stats: "99.9% uptime SLA", }, { - icon: FiUsers, + icon: FaUsers, title: "Suporte Especializado 24/7", description: "Suporte técnico 24 horas por dia com especialistas em jogos e hospedagem que entendem suas necessidades.", stats: "< 2min tempo resposta", }, { - icon: FiDatabase, + icon: FaDatabase, title: "Backups Automáticos", description: "Backups automatizados diários com restauração em um clique garantem que seus dados estejam sempre protegidos e recuperáveis.", stats: "Múltiplos backups diários", }, { - icon: FiTrendingUp, + icon: FaChartLine, title: "Soluções Escaláveis", description: "Escale facilmente seus recursos para cima ou para baixo com base na demanda com nossa infraestrutura de hospedagem flexível.", @@ -57,24 +50,35 @@ const Features = () => { ]; return ( -
-
+
+
+ + + RECURSOS EXCLUSIVOS + + - Por que Escolher a FireHosting + Por que Escolher a FireHosting + - Construída com tecnologia de ponta e apoiada por expertise da indústria + Construída com tecnologia de ponta para garantir a melhor experiência de jogo
@@ -87,30 +91,30 @@ const Features = () => { transition={{ duration: 0.6, delay: index * 0.1 }} className="group" > -
+
{/* Icon */}
-
- +
+
{/* Glow effect */} -
+
{/* Content */}
-

+

{feature.title}

-

+

{feature.description}

{/* Stats */} -
- +
+ {feature.stats}
@@ -125,31 +129,34 @@ const Features = () => { initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, delay: 0.4 }} - className="mt-16 grid grid-cols-2 md:grid-cols-4 gap-6 text-center" + className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8" > -
-
- 10K+ +
+
+ 1K+
-
Servidores Ativos
+
Servidores Ativos
-
-
- 50K+ + +
+
+ 10K+
-
Clientes Satisfeitos
+
Clientes Satisfeitos
-
-
+ +
+
99.9%
-
Uptime SLA
+
Uptime SLA
-
-
+ +
+
24/7
-
Suporte Especializado
+
Suporte Especializado
diff --git a/src/components/home/Hero.tsx b/src/components/home/Hero.tsx index a893bf0..56866ef 100644 --- a/src/components/home/Hero.tsx +++ b/src/components/home/Hero.tsx @@ -2,169 +2,167 @@ import { motion } from "framer-motion"; import Link from "next/link"; -import { FiArrowRight, FiZap, FiShield, FiUsers } from "react-icons/fi"; +import { FaArrowRight } from "react-icons/fa"; +import { FaServer, FaShieldAlt, FaHeadset } from "react-icons/fa"; +import Image from "next/image"; const Hero = () => { return (
- {/* Dark Base Layer */} -
- - {/* 3D Animated Abstract Background */} - - - - - {/* 3D Perspective Grid */} - - - - -
- - {/* Main Heading */} - +
+ {/* Left Column - Text */} + - Hospedagem de - Alta Performance{" "} - para Gamers - + {/* Badge */} + + A melhor hospedagem para seus jogos + + + {/* Main Heading */} + + Hospedagem + Gamer{" "} + de Alta Performance + - {/* Subtitle */} - - Experimente o que há de melhor em hospedagem de jogos, você merece - uma experiência sem lag, com suporte 24/7 e proteção DDoS. - + {/* Subtitle */} + + Experimente o que há de melhor em hospedagem de jogos, você merece + uma experiência sem lag, com suporte 24/7 e proteção DDoS. + - {/* Stats */} - -
- - 99.9*% Uptime -
-
- - Proteção DDoS -
-
- - Suporte 24/7 -
-
+ {/* Feature Cards */} + +
+ +

99.9% Uptime

+

Servidores sempre online

+
+ +
+ +

Anti-DDoS

+

Proteção avançada

+
+ +
+ +

Suporte 24/7

+

Atendimento especializado

+
+
- {/* CTA Buttons */} + {/* CTA Buttons */} + + + Ver Planos + + + + + Falar com Suporte + + +
+ + {/* Right Column - Image */} - - Ver Planos - - +
+ {/* 3D Floating Server Image */} + + Servidor de Jogos + + + {/* Animated Particles/Elements */} + + + + + {/* Decorative Elements */} + +
- - {/* Floating Elements */} - - - +
); diff --git a/src/components/home/Promotions.tsx b/src/components/home/Promotions.tsx index f5eaf99..fd61dfe 100644 --- a/src/components/home/Promotions.tsx +++ b/src/components/home/Promotions.tsx @@ -124,7 +124,7 @@ const Promotions = () => { } return ( -
+

diff --git a/src/components/home/Services.tsx b/src/components/home/Services.tsx index d413964..4e9e1f0 100644 --- a/src/components/home/Services.tsx +++ b/src/components/home/Services.tsx @@ -2,23 +2,16 @@ import { motion } from "framer-motion"; import Link from "next/link"; -import { - FiServer, - FiMonitor, - FiCpu, - FiArrowRight, - FiShield, - FiUsers, - FiGlobe, -} from "react-icons/fi"; -import { FiPlay } from "react-icons/fi"; +import { FaServer, FaGlobe, FaRobot, FaGamepad, FaArrowRight } from "react-icons/fa"; +import { GiStoneBlock } from "react-icons/gi"; import { SiMinecraft } from "react-icons/si"; +import Image from "next/image"; const Services = () => { const services = [ { - icon: SiMinecraft, - title: "Hospedagem de Servidores Minecraft", + icon: GiStoneBlock, + title: "Servidores Minecraft", description: "Hospedagem otimizada para servidores Minecraft com suporte a plugins, mods e alta performance.", features: [ @@ -31,10 +24,13 @@ const Services = () => { ], price: "A partir de R$21.90/mês", color: "text-green-400", + bgColor: "from-green-500/20 to-green-700/20", + borderColor: "border-green-500/30", + image: "/images/default-minecraft-server.png", }, { - icon: FiServer, - title: "Hospedagem de Servidores de VPS", + icon: FaServer, + title: "Servidores VPS", description: "VPS dedicado com recursos escaláveis, alta performance e acesso root completo para personalização total.", features: [ @@ -45,11 +41,14 @@ const Services = () => { "Rede de Baixa Latência", ], price: "A partir de R$125.90/mês", - color: "text-support", + color: "text-blue-400", + bgColor: "from-blue-500/20 to-blue-700/20", + borderColor: "border-blue-500/30", + image: "/images/default-minecraft-server.png", }, { - icon: FiGlobe, - title: "Hospedagem de WebSites", + icon: FaGlobe, + title: "Hospedagem Web", description: "Hospedagem de sites com cPanel, suporte a WordPress e otimização para SEO, ideal para blogs e negócios online.", features: [ @@ -59,11 +58,14 @@ const Services = () => { "Painel de Controle Intuitivo", ], price: "A partir de R$24.90/mês", - color: "text-primary", + color: "text-red-400", + bgColor: "from-red-500/20 to-red-700/20", + borderColor: "border-red-500/30", + image: "/images/default-minecraft-server.png", }, { - icon: FiCpu, - title: "Hospedagem de Aplicações & Bots", + icon: FaRobot, + title: "Aplicações & Bots", description: "Hospedagem de aplicações e bots com suporte a Node.js, Python e outras linguagens, ideal para desenvolvedores.", features: [ @@ -74,27 +76,41 @@ const Services = () => { "Monitoramento em Tempo Real", ], price: "A partir de R$2.90/mês", - color: "text-alert", + color: "text-yellow-400", + bgColor: "from-yellow-500/20 to-yellow-700/20", + borderColor: "border-yellow-500/30", + image: "/images/default-minecraft-server.png", }, ]; return ( -
-
+
+
+ + + NOSSOS PRODUTOS + + - Nossos Serviços + Soluções de Hospedagem + Oferecemos uma ampla gama de serviços de hospedagem, desde servidores Minecraft até VPS e hospedagem de sites, todos projetados @@ -102,7 +118,7 @@ const Services = () => {
-
+
{services.map((service, index) => ( { transition={{ duration: 0.6, delay: index * 0.1 }} className="group" > -
-
- {/* Icon */} -
-
- +
+ {/* Image Background - Faded */} +
+
+ {service.image && ( + {service.title} + )} +
+
+ +
+ {/* Icon and Title */} +
+
+
+

+ {service.title} +

{/* Content */} -
-

- {service.title} -

-

+

+

{service.description}

@@ -134,9 +164,9 @@ const Services = () => { {service.features.map((feature, idx) => (
  • - + {feature}
  • ))} @@ -144,19 +174,19 @@ const Services = () => {
    {/* Price and CTA */} -
    -
    - +
    +
    + {service.price} + + Detalhes + +
    - - Ver Detalhes - -
    @@ -169,9 +199,9 @@ const Services = () => { initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, delay: 0.4 }} - className="text-center mt-12" + className="text-center mt-16" > - + Ver Todos os Serviços diff --git a/src/components/home/Testimonials.tsx b/src/components/home/Testimonials.tsx index f607844..7f6ff52 100644 --- a/src/components/home/Testimonials.tsx +++ b/src/components/home/Testimonials.tsx @@ -10,7 +10,6 @@ import { } from "react-icons/fi"; const Testimonials = () => { - console.log("Testimonials component rendering"); const [currentTestimonial, setCurrentTestimonial] = useState(0); const testimonials = [ diff --git a/src/components/layout/ExternalScripts.tsx b/src/components/layout/ExternalScripts.tsx index 8634c71..97df537 100644 --- a/src/components/layout/ExternalScripts.tsx +++ b/src/components/layout/ExternalScripts.tsx @@ -4,7 +4,6 @@ import Script from "next/script"; import { useEffect } from "react"; export default function ExternalScripts() { - // Log para verificar que os scripts estão sendo carregados useEffect(() => { console.log("External scripts component loaded"); }, []); @@ -19,7 +18,6 @@ export default function ExternalScripts() { return ( <> - {/* Script para notificação de incidentes do BetterStack */}