From 298a4b9939cf6712d5aad4a38e2b07188338b6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Zanella?= Date: Sat, 6 Sep 2025 16:29:58 -0300 Subject: [PATCH 1/5] Refactor Hero, Promotions, Services, and Header components for improved design and functionality - Updated Hero component with new animations, layout, and icons for a modern look. - Simplified Promotions section by removing unnecessary background color. - Enhanced Services component with new icons, images, and layout adjustments for better presentation. - Revamped Header component with updated icons, styles, and improved user experience. - Modified Tailwind CSS configuration to include new colors, fonts, animations, and background images. --- public/images/bg-cartoon.svg | 91 +++++++++ src/app/globals.css | 127 +++++++++--- src/app/layout.tsx | 8 +- src/app/page.tsx | 78 +++++++- src/app/partnerships/page.tsx | 10 +- src/components/home/CTA.tsx | 122 ++++++++---- src/components/home/Features.tsx | 89 +++++---- src/components/home/Hero.tsx | 302 ++++++++++++++--------------- src/components/home/Promotions.tsx | 2 +- src/components/home/Services.tsx | 134 ++++++++----- src/components/layout/Header.tsx | 67 ++++--- tailwind.config.js | 43 +++- 12 files changed, 706 insertions(+), 367 deletions(-) create mode 100644 public/images/bg-cartoon.svg 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/globals.css b/src/app/globals.css index a5e8b1b..9ad8559 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -14,9 +14,9 @@ } } +/* Animações */ @keyframes float { - 0%, - 100% { + 0%, 100% { transform: translateY(0) rotate(0deg); } 25% { @@ -31,8 +31,7 @@ } @keyframes pulse-glow { - 0%, - 100% { + 0%, 100% { opacity: 0.3; filter: blur(80px); } @@ -67,8 +66,7 @@ } @keyframes morph { - 0%, - 100% { + 0%, 100% { border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; } 25% { @@ -83,8 +81,7 @@ } @keyframes rotate3d-slow { - 0%, - 100% { + 0%, 100% { transform: perspective(1000px) rotateX(15deg) rotateY(-10deg); } 50% { @@ -93,8 +90,7 @@ } @keyframes rotate3d-slow-reverse { - 0%, - 100% { + 0%, 100% { transform: perspective(1000px) rotateX(-15deg) rotateY(10deg); } 50% { @@ -104,12 +100,43 @@ @keyframes grid-move { 0% { - transform: perspective(1000px) rotateX(60deg) translateZ(-100px) - translateY(0) scale(2); + transform: perspective(1000px) rotateX(60deg) translateZ(-100px) translateY(0) scale(2); } 100% { - transform: perspective(1000px) rotateX(60deg) translateZ(-100px) - translateY(-50px) scale(2); + transform: perspective(1000px) rotateX(60deg) translateZ(-100px) translateY(-50px) scale(2); + } +} + +@keyframes bounce-button { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-4px); + } +} + +@keyframes shake { + 0%, 100% { + transform: translateX(0); + } + 25% { + transform: translateX(-4px); + } + 75% { + transform: translateX(4px); + } +} + +@keyframes rotate-color { + 0% { + filter: hue-rotate(0deg); + } + 50% { + filter: hue-rotate(15deg); + } + 100% { + filter: hue-rotate(0deg); } } @@ -147,16 +174,58 @@ animation: grid-move 15s linear infinite alternate; } + /* Elementos 3D */ + .card-3d { + @apply relative overflow-hidden rounded-xl transition-all duration-300; + transform-style: preserve-3d; + perspective: 1000px; + box-shadow: 0 10px 30px -15px rgba(255, 0, 0, 0.25); + background: linear-gradient(135deg, rgba(255, 60, 56, 0.1), rgba(20, 20, 20, 0.8)); + border: 1px solid rgba(255, 60, 56, 0.2); + backdrop-filter: blur(5px); + } + + .card-3d:hover { + transform: translateY(-5px) rotateX(5deg); + box-shadow: 0 15px 35px -15px rgba(255, 60, 56, 0.4); + border: 1px solid rgba(255, 60, 56, 0.4); + } + + .btn-3d { + @apply relative overflow-hidden font-bold text-white rounded-lg transition-all duration-300 px-6 py-3; + background: linear-gradient(135deg, #ff3c38, #be1e2d); + box-shadow: + 0 4px 0 0 #be1e2d, + 0 10px 15px -3px rgba(190, 30, 45, 0.3); + transform-style: preserve-3d; + transform: translateZ(0); + } + + .btn-3d:hover { + transform: translateY(-2px); + box-shadow: + 0 6px 0 0 #be1e2d, + 0 15px 20px -3px rgba(190, 30, 45, 0.4); + animation: rotate-color 3s infinite; + } + + .btn-3d:active { + transform: translateY(2px); + box-shadow: + 0 2px 0 0 #be1e2d, + 0 8px 10px -5px rgba(190, 30, 45, 0.3); + } + .btn-primary { - @apply bg-primary text-white px-6 py-3 rounded-lg font-semibold hover:bg-primary/90 transition-all duration-300 transform hover:scale-105 active:scale-95; + @apply bg-gradient-to-r from-red-600 to-red-700 text-white px-6 py-3 rounded-lg font-semibold transition-all duration-300 transform hover:scale-105 active:scale-95 hover:shadow-lg hover:shadow-red-600/30; } .btn-secondary { - @apply bg-transparent border-2 border-support text-support px-6 py-3 rounded-lg font-semibold hover:bg-support hover:text-secondary transition-all duration-300; + @apply bg-transparent border-2 border-red-500 text-red-500 px-6 py-3 rounded-lg font-semibold hover:bg-red-500 hover:text-white transition-all duration-300; } .card { - @apply bg-secondary/50 border border-detail rounded-xl p-6 backdrop-blur-sm hover:border-support/50 transition-all duration-300; + @apply bg-slate-900/80 border border-red-500/30 rounded-xl p-6 backdrop-blur-sm hover:border-red-500/50 transition-all duration-300 hover:shadow-lg hover:shadow-red-600/20; } .section-padding { @@ -168,7 +237,7 @@ } .text-gradient { - @apply text-[#ff3c38]; + @apply text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-red-700; } .glow-effect { @@ -176,12 +245,20 @@ } .tech-border { - background: linear-gradient( - 45deg, - transparent, - rgba(0, 191, 255, 0.1), - transparent - ); - border: 1px solid rgba(141, 153, 174, 0.2); + background: linear-gradient(45deg, transparent, rgba(255, 60, 56, 0.1), transparent); + border: 1px solid rgba(255, 60, 56, 0.2); + } + + .cartoon-bg { + background-image: url('/images/bg-cartoon.svg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + } + + .animated-gradient-red { + background: linear-gradient(135deg, #ff3c38, #be1e2d, #ff3c38); + background-size: 200% 200%; + animation: move-gradient 10s ease infinite; } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 86ec5d6..06f153a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata, Viewport } from "next"; -import { Inter } from "next/font/google"; +import { Rubik, Exo_2, Orbitron } from "next/font/google"; import "./globals.css"; import { AuthProvider } from "@/contexts/AuthContext"; import ConsoleWarning from "@/components/layout/ConsoleWarning"; @@ -9,7 +9,9 @@ import UtmTracker from "@/components/tracking/UtmTracker"; import { SpeedInsights } from "@vercel/speed-insights/next"; import { Analytics } from "@vercel/analytics/react"; -const inter = Inter({ subsets: ["latin"] }); +const rubik = Rubik({ subsets: ["latin"], variable: "--font-rubik" }); +const exo2 = Exo_2({ subsets: ["latin"], variable: "--font-exo2" }); +const orbitron = Orbitron({ subsets: ["latin"], variable: "--font-orbitron" }); export const viewport: Viewport = { width: "device-width", @@ -52,7 +54,7 @@ export default function RootLayout({ diff --git a/src/app/page.tsx b/src/app/page.tsx index e03be34..81b7fce 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,6 @@ +"use client"; + +import { motion } from "framer-motion"; import Hero from "@/components/home/Hero"; import Features from "@/components/home/Features"; import Services from "@/components/home/Services"; @@ -7,13 +10,72 @@ import CTA from "@/components/home/CTA"; export default function Home() { return ( - <> - - - - - - - +
+
+
+
+ + + +
+
+ + + + + + +
+
); } diff --git a/src/app/partnerships/page.tsx b/src/app/partnerships/page.tsx index 0b6f260..d55be72 100644 --- a/src/app/partnerships/page.tsx +++ b/src/app/partnerships/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import Link from "next/link"; -import { FiExternalLink, FiYoutube } from "react-icons/fi"; +import { FaExternalLinkAlt, FaYoutube } from "react-icons/fa"; import { SiDiscord } from "react-icons/si"; import { fetchPartnersAPI, Partner } from "@/services/api/user"; @@ -167,7 +167,7 @@ export default function PartnershipsPage() { > Discord - + )} @@ -178,9 +178,9 @@ export default function PartnershipsPage() { rel="noopener noreferrer" className="bg-[#FF0000] text-white px-6 py-3 rounded-lg font-medium flex items-center justify-center hover:bg-[#CC0000] transition-colors" > - + YouTube - + )} @@ -192,7 +192,7 @@ export default function PartnershipsPage() { className="btn-primary flex items-center justify-center" > Visitar - + )} 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..04c1726 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+
-
Servidores Ativos
+
Servidores Ativos
-
-
+ +
+
50K+
-
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/layout/Header.tsx b/src/components/layout/Header.tsx index dd519a7..f037cfd 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { motion, AnimatePresence } from "framer-motion"; -import { FiMenu, FiX, FiExternalLink } from "react-icons/fi"; +import { FaBars, FaTimes, FaExternalLinkAlt } from "react-icons/fa"; import { SiDiscord } from "react-icons/si"; import { GrUserManager } from "react-icons/gr"; import { FaUserCircle, FaSignOutAlt } from "react-icons/fa"; @@ -46,22 +46,20 @@ const Header = () => {
    -
    +
    {/* Logo */} -
    FireHosting Logo -
    - + FireHosting @@ -72,15 +70,16 @@ const Header = () => { {item.name} + {pathname === item.href && ( )} @@ -94,32 +93,32 @@ const Header = () => {
    {showUserMenu && ( -
    +
    - Dashboard + Dashboard
    )} @@ -128,13 +127,13 @@ const Header = () => { <> Entrar Cadastrar @@ -145,12 +144,12 @@ const Header = () => { {/* Mobile Menu Button */}
    @@ -163,15 +162,15 @@ const Header = () => { animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} transition={{ duration: 0.3 }} - className="lg:hidden mt-4 space-y-4 border-t border-detail/30 pt-4" + className="lg:hidden mt-4 space-y-4 border-t border-red-500/20 pt-4 bg-slate-900/50 backdrop-blur-sm rounded-lg" > {navItems.map((item) => ( setIsMenuOpen(false)} - className={`block py-2 text-sm font-medium transition-colors duration-300 hover:text-support ${ - pathname === item.href ? "text-support" : "text-light" + className={`block py-3 px-4 text-sm font-gaming font-bold transition-all duration-300 hover:bg-red-500/10 ${ + pathname === item.href ? "text-red-500" : "text-light" }`} > {item.name} @@ -179,19 +178,19 @@ const Header = () => { ))} {/* Mobile Auth Buttons */} -
    +
    {isAuthenticated ? ( <> -
    - - +
    + + {user?.client?.firstname || "Usuário"}
    setIsMenuOpen(false)} - className="block py-2 text-sm font-medium text-light hover:text-support transition-colors duration-300" + className="block py-3 px-4 text-sm font-gaming text-light hover:bg-red-500/10 transition-all duration-300" > Dashboard @@ -200,7 +199,7 @@ const Header = () => { logout(); setIsMenuOpen(false); }} - className="block py-2 text-sm font-medium text-red-400 hover:text-red-300 transition-colors duration-300" + className="block w-full text-left py-3 px-4 text-sm font-gaming text-red-400 hover:bg-red-500/10 hover:text-red-500 transition-all duration-300" > Sair @@ -210,14 +209,14 @@ const Header = () => { setIsMenuOpen(false)} - className="block py-2 text-sm font-medium text-light hover:text-support transition-colors duration-300" + className="block py-3 px-4 text-sm font-gaming font-bold text-light hover:text-red-500 hover:bg-red-500/10 transition-all duration-300" > Entrar setIsMenuOpen(false)} - className="block bg-gradient-to-r from-primary to-red-600 hover:from-red-600 hover:to-primary text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300 w-fit" + className="block mx-4 my-2 py-3 px-4 bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-600 text-white rounded-lg text-sm font-gaming font-bold text-center shadow-lg shadow-red-600/20 transition-all duration-300" > Cadastrar diff --git a/tailwind.config.js b/tailwind.config.js index f3b895e..b966d79 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -9,23 +9,30 @@ module.exports = { extend: { colors: { primary: "#FF3C38", - secondary: "#121212", - support: "#00BFFF", - light: "#ECECEC", + secondary: "#0f0f1a", + support: "#ff5722", + light: "#f8f8f8", alert: "#FFC857", detail: "#8D99AE", + dark: "#05051a", + accent: "#be1e2d", }, fontFamily: { - sans: ["Inter", "system-ui", "sans-serif"], + sans: ["'Rubik'", "system-ui", "sans-serif"], + gaming: ["'Orbitron'", "sans-serif"], + title: ["'Exo 2'", "sans-serif"] }, animation: { "fade-in": "fadeIn 0.6s ease-in-out", "slide-up": "slideUp 0.6s ease-out", "bounce-slow": "bounce 2s infinite", "pulse-slow": "pulse 3s infinite", - float: "float 6s ease-in-out infinite", - morph: "morph 8s ease-in-out infinite", + "float": "float 6s ease-in-out infinite", + "morph": "morph 8s ease-in-out infinite", "background-pan": "backgroundPan 15s linear infinite alternate", + "shake": "shake 0.8s ease-in-out", + "bounce-button": "bounceButton 2s infinite ease-in-out", + "rotate-color": "rotateColor 3s infinite linear", }, keyframes: { fadeIn: { @@ -48,7 +55,31 @@ module.exports = { "0%": { backgroundPosition: "0% 0%" }, "100%": { backgroundPosition: "100% 100%" }, }, + shake: { + "0%, 100%": { transform: "translateX(0)" }, + "25%": { transform: "translateX(-4px)" }, + "75%": { transform: "translateX(4px)" }, + }, + bounceButton: { + "0%, 100%": { transform: "translateY(0)" }, + "50%": { transform: "translateY(-4px)" }, + }, + rotateColor: { + "0%": { filter: "hue-rotate(0deg)" }, + "50%": { filter: "hue-rotate(15deg)" }, + "100%": { filter: "hue-rotate(0deg)" }, + }, + }, + backgroundImage: { + 'cartoon-pattern': "url('/images/bg-cartoon.svg')", + 'gaming-grid': "linear-gradient(rgba(255,60,56,0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,60,56,0.1) 1px, transparent 1px)", + 'gradient-radial-red': 'radial-gradient(circle, rgba(255,60,56,0.2) 0%, rgba(15,15,26,0) 70%)', }, + boxShadow: { + 'neon-red': '0 0 5px rgba(255,60,56,0.5), 0 0 20px rgba(255,60,56,0.3)', + '3d-button': '0 4px 0 0 #be1e2d, 0 10px 15px -3px rgba(190,30,45,0.3)', + '3d-button-hover': '0 6px 0 0 #be1e2d, 0 15px 20px -3px rgba(190,30,45,0.4)', + } }, }, plugins: [], From 5ecc334c0b754936c05232027490581716bba34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Zanella?= Date: Sat, 6 Sep 2025 20:27:43 -0300 Subject: [PATCH 2/5] feat: add Minecraft worlds management component with upload and download functionality - Implemented WorldsContent component for managing Minecraft worlds. - Added functionality to detect Minecraft server type (vanilla/bedrock). - Included upload and download features for world files (.zip, .tar.gz). - Integrated notifications for user feedback during operations. - Created NotificationsContext for managing notification state and display. --- src/app/dashboard/page.tsx | 59 +- src/components/dashboard/AccountSidebar.tsx | 86 +- .../dashboard/ContentTransition.tsx | 32 +- src/components/dashboard/DashboardLayout.tsx | 42 +- src/components/dashboard/DashboardNavbar.tsx | 106 ++- src/components/dashboard/OverviewContent.tsx | 255 ++++-- .../minecraft/CustomizationContent.tsx | 155 ++-- .../minecraft/MinecraftServerHeader.tsx | 6 +- .../minecraft/MinecraftServerManager.tsx | 625 ++++++++++---- .../dashboard/minecraft/MinecraftSidebar.tsx | 244 ++++++ .../dashboard/minecraft/NetworkContent.tsx | 228 +++++ .../dashboard/minecraft/NewFilesContent.tsx | 804 ++++++++++++++++++ .../dashboard/minecraft/WorldsContent.tsx | 447 ++++++++++ src/components/home/Features.tsx | 4 +- src/components/layout/Footer.tsx | 158 ++-- src/contexts/NotificationsContext.tsx | 143 ++++ src/services/ServerConsoleWebSocket.ts | 10 +- tailwind.config.js | 6 +- 18 files changed, 2897 insertions(+), 513 deletions(-) create mode 100644 src/components/dashboard/minecraft/MinecraftSidebar.tsx create mode 100644 src/components/dashboard/minecraft/NetworkContent.tsx create mode 100644 src/components/dashboard/minecraft/NewFilesContent.tsx create mode 100644 src/components/dashboard/minecraft/WorldsContent.tsx create mode 100644 src/contexts/NotificationsContext.tsx diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 44f8b79..0411957 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -3,6 +3,7 @@ import { useAuth } from "@/contexts/AuthContext"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; +import { motion } from "framer-motion"; import DashboardNavbar from "@/components/dashboard/DashboardNavbar"; import AccountSidebar from "@/components/dashboard/AccountSidebar"; @@ -27,25 +28,69 @@ export default function DashboardPage() { } }, [accessKey, isLoading, router]); + useEffect(() => { + const hash = window.location.hash.replace("#", ""); + if (hash === "account") { + setActiveTab("profile"); + } else if (hash && hash !== "overview") { + setActiveTab(hash); + } else { + setActiveTab("overview"); + } + + const handleHashChange = () => { + const newHash = window.location.hash.replace("#", ""); + if (newHash === "account") { + setActiveTab("profile"); + } else if (newHash && newHash !== "overview") { + setActiveTab(newHash); + } else { + setActiveTab("overview"); + } + }; + + window.addEventListener("hashchange", handleHashChange); + return () => window.removeEventListener("hashchange", handleHashChange); + }, []); + const handleTabChange = (tab: string) => { if (tab === "account") { setActiveTab("profile"); + window.location.hash = "profile"; } else { setActiveTab(tab); + if (tab === "overview") { + window.history.replaceState(null, "", window.location.pathname); + } else { + window.location.hash = tab; + } } }; const handleAccountSubTabChange = (subTab: string) => { setActiveTab(subTab); + window.location.hash = subTab; }; if (isLoading) { return ( -
    -
    -
    -

    Carregando...

    -
    +
    + +
    +
    +
    +
    + Logo +
    +
    +
    +

    Carregando...

    +
    ); } @@ -98,7 +143,7 @@ export default function DashboardPage() { {/* Main Content */}
    -
    +
    {/* Mobile Account Sidebar */} {isAccountSection && (
    @@ -111,7 +156,7 @@ export default function DashboardPage() { )} {/* Content Container - Centered */} -
    +
    {renderContent()} diff --git a/src/components/dashboard/AccountSidebar.tsx b/src/components/dashboard/AccountSidebar.tsx index fdc435c..06ad196 100644 --- a/src/components/dashboard/AccountSidebar.tsx +++ b/src/components/dashboard/AccountSidebar.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { FaUser, FaCog, @@ -26,7 +27,6 @@ export default function AccountSidebar({ const accountMenuItems = [ { id: "profile", label: "Minha Conta", icon: }, - //{ id: "settings", label: "Configurações", icon: }, { id: "security", label: "Segurança", icon: }, { id: "affiliate", label: "Afiliado", icon: }, ]; @@ -43,69 +43,87 @@ export default function AccountSidebar({ if (isMobile) { return (
    - + - {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/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..f2fb00b 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,66 @@ 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; }; + + // Função para calcular a posição exata do indicador ativo + const getIndicatorPosition = (index: number) => { + return index * 115; // Usando 115px como largura exata do botão + }; return ( - + ); } \ No newline at end of file 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/minecraft/CustomizationContent.tsx b/src/components/dashboard/minecraft/CustomizationContent.tsx index cb4564f..22274c0 100644 --- a/src/components/dashboard/minecraft/CustomizationContent.tsx +++ b/src/components/dashboard/minecraft/CustomizationContent.tsx @@ -76,32 +76,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 +124,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 +145,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 +154,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 +228,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 +238,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 +259,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 +298,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 +375,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/MinecraftServerHeader.tsx b/src/components/dashboard/minecraft/MinecraftServerHeader.tsx index 8bab0f2..d318edd 100644 --- a/src/components/dashboard/minecraft/MinecraftServerHeader.tsx +++ b/src/components/dashboard/minecraft/MinecraftServerHeader.tsx @@ -38,13 +38,13 @@ export default function MinecraftServerHeader({ const router = useRouter(); return ( -
    -
    +
    +
    diff --git a/src/components/dashboard/minecraft/MinecraftServerManager.tsx b/src/components/dashboard/minecraft/MinecraftServerManager.tsx index cbeea31..e2026e1 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,163 @@ 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; + // Limpar qualquer estado de inicialização anterior + 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, - }; + console.log("Solicitando credenciais WebSocket para:", identifier); + try { + const result = await getServerConsoleCredentials(identifier); + console.log("Resposta de credenciais:", result); + 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; + + // Limpar qualquer conexão anterior + if (wsRef.current) { + console.log("Desconectando WebSocket anterior"); + 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..." })); - }); + + console.log("Iniciando nova conexão WebSocket para console do Minecraft"); - 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 +315,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 +364,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 +386,6 @@ export default function MinecraftServerManager() { prev ? { ...prev, status: getPendingStatus(signal) } : null, ); - // Add console message about the power action setConsoleState(prev => ({ ...prev, consoleMessages: [ @@ -307,29 +393,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 +423,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 +436,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 +451,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 +472,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 +497,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 +566,16 @@ export default function MinecraftServerManager() { } }; - // Loading states if (loading) { return ( -
    +
    - -

    Carregando servidor Minecraft...

    +
    + + +
    +

    Carregando servidor...

    +

    Preparando ambiente Minecraft

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

    Erro

    -

    {error}

    +

    Erro

    +

    {error}

    @@ -453,14 +601,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 +620,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 +762,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/MinecraftSidebar.tsx b/src/components/dashboard/minecraft/MinecraftSidebar.tsx new file mode 100644 index 0000000..92aa50e --- /dev/null +++ b/src/components/dashboard/minecraft/MinecraftSidebar.tsx @@ -0,0 +1,244 @@ +"use client"; + +import { motion } from "framer-motion"; +import { + FaTerminal, + FaChartBar, + FaFolder, + FaCog, + FaPalette, + FaCubes, + FaBars, + FaTimes, + FaChevronLeft, + FaChevronRight, + FaGlobe, + FaNetworkWired +} from "react-icons/fa"; +import { GiStoneBlock } from "react-icons/gi"; +import { useState, useEffect } from "react"; + +interface MinecraftSidebarProps { + activeTab: string; + onTabChange: (tab: string) => void; + serverName: string; + serverStatus?: string; + isCollapsed?: boolean; + onToggleCollapse?: () => void; +} + +export default function MinecraftSidebar({ + activeTab, + onTabChange, + serverName, + serverStatus = "offline", + isCollapsed: externalIsCollapsed, + onToggleCollapse +}: MinecraftSidebarProps) { + const [isMobileOpen, setIsMobileOpen] = useState(false); + const [internalIsCollapsed, setInternalIsCollapsed] = useState(false); + const isCollapsed = externalIsCollapsed !== undefined ? externalIsCollapsed : internalIsCollapsed; + + const toggleCollapse = () => { + if (onToggleCollapse) { + onToggleCollapse(); + } else { + setInternalIsCollapsed(!internalIsCollapsed); + } + }; + const [windowWidth, setWindowWidth] = useState( + typeof window !== 'undefined' ? window.innerWidth : 0 + ); + + useEffect(() => { + const handleResize = () => { + setWindowWidth(window.innerWidth); + if (window.innerWidth >= 1024) { + setIsMobileOpen(false); + } + }; + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + const isDesktop = windowWidth >= 1024; + + const tabs = [ + { key: "console", label: "Console", icon: }, + { key: "stats", label: "Estatísticas", icon: }, + { key: "files", label: "Arquivos", icon: }, + { key: "worlds", label: "Mundos", icon: }, + { key: "network", label: "Rede", icon: }, + { key: "customization", label: "Personalização", icon: }, + { key: "settings", label: "Configurações", icon: }, + ]; + + const statusColor = { + online: "text-green-500", + starting: "text-yellow-500", + stopping: "text-yellow-500", + offline: "text-gray-400", + restarting: "text-blue-500", + running: "text-green-500", + installing: "text-purple-500", + }[serverStatus?.toLowerCase() || "offline"] || "text-gray-400"; + + const statusBg = { + online: "bg-green-500/10", + starting: "bg-yellow-500/10", + stopping: "bg-yellow-500/10", + offline: "bg-gray-500/10", + restarting: "bg-blue-500/10", + running: "bg-green-500/10", + installing: "bg-purple-500/10", + }[serverStatus?.toLowerCase() || "offline"] || "bg-gray-500/10"; + + const sidebarContent = ( +
    +
    +
    + {!isCollapsed && ( +
    +
    + +
    +
    +

    + {serverName} +

    +
    + + + + {serverStatus} + + +
    +
    +
    + )} +
    + {isDesktop && ( + + )} + {!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..fbac46b --- /dev/null +++ b/src/components/dashboard/minecraft/NetworkContent.tsx @@ -0,0 +1,228 @@ +"use client"; + +import { useState } from "react"; +import { + FaNetworkWired, + FaGlobe, + FaPlus, + FaCopy, + FaCheck +} from "react-icons/fa"; +import { motion } from "framer-motion"; + +interface NetworkContentProps { + server: { + ip?: string; + name?: string; + }; +} + +export default function NetworkContent({ server }: NetworkContentProps) { + const [copiedIp, setCopiedIp] = useState(false); + const [copiedPort, setCopiedPort] = useState(false); + const [showCreateSubdomain, setShowCreateSubdomain] = useState(false); + const [subdomainName, setSubdomainName] = useState(""); + + const ipAndPort = server?.ip?.split(":") || ["", ""]; + const ip = ipAndPort[0]; + const port = ipAndPort[1]; + + const copyToClipboard = async (text: string, type: "ip" | "port") => { + try { + await navigator.clipboard.writeText(text); + if (type === "ip") { + setCopiedIp(true); + setTimeout(() => setCopiedIp(false), 2000); + } else { + setCopiedPort(true); + setTimeout(() => setCopiedPort(false), 2000); + } + } catch (err) { + console.error("Failed to copy text: ", err); + } + }; + + const handleCreateSubdomain = () => { + if (subdomainName.trim()) { + console.log(`Creating subdomain: ${subdomainName}.firehosting.cloud`); + setShowCreateSubdomain(false); + setSubdomainName(""); + } + }; + + 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 +

    +
    + +
    +
    + +

    Nenhum subdomínio configurado

    +

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

    +
    + + {!showCreateSubdomain ? ( + setShowCreateSubdomain(true)} + 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 }} + > + + Criar 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/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/Features.tsx b/src/components/home/Features.tsx index 04c1726..0bd7792 100644 --- a/src/components/home/Features.tsx +++ b/src/components/home/Features.tsx @@ -133,14 +133,14 @@ const Features = () => { >
    - 10K+ + 1K+
    Servidores Ativos
    - 50K+ + 10K+
    Clientes Satisfeitos
    diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index f68a82c..1a9451b 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,7 +1,8 @@ import Link from "next/link"; -import { FiMail, FiPhone, FiMapPin, FiExternalLink } from "react-icons/fi"; +import { FaEnvelope, FaPhone, FaExternalLinkAlt, FaGamepad } from "react-icons/fa"; import { SiDiscord, SiTwitter, SiGithub, SiInstagram, SiTiktok, SiYoutube } from "react-icons/si"; import { VscSourceControl } from "react-icons/vsc"; +import { motion } from "framer-motion"; const Footer = () => { const currentYear = new Date().getFullYear(); @@ -32,82 +33,101 @@ const Footer = () => { }; return ( -
    -
    +
    + {/* Background Elements */} +
    + + + +
    {/* Main Footer Content */}
    {/* Company Info */} -
    - -
    +
    + FireHosting Logo -
    - + FireHosting - -

    + +

    + +
    + +

    FireHosting é uma empresa de hospedagem de jogos que oferece soluções de alta performance e confiabilidade para gamers.

    +
    {/* Services Links */}
    -

    Serviços

    -
      +

      + Serviços +

      +
        {footerLinks.services.map((link) => (
      • + {link.name}
      • @@ -117,8 +137,8 @@ const Footer = () => { {/* Support Links */}
        -

        Suporte

        -
          +

          Suporte

          +
            {footerLinks.support.map((link) => (
          • {link.external ? ( @@ -126,16 +146,18 @@ const Footer = () => { href={link.href} target="_blank" rel="noopener noreferrer" - className="text-detail hover:text-support transition-colors duration-300 text-sm flex items-center space-x-1" + className="text-gray-400 hover:text-red-400 transition-colors duration-300 text-sm flex items-center space-x-1 group" > + {link.name} - + ) : ( + {link.name} )} @@ -146,56 +168,66 @@ const Footer = () => { {/* Contact Info */}
            -

            Contato

            -
            - {/* Bottom Footer */} -
            +
            -
            +
            © {currentYear} FireHosting - Todos os direitos reservados.
            - CNPJ: 61.967.228/0001-40 -
            - Desenvolvido com ❤️ e muito ☕ por{" "} + Desenvolvido por{" "} - Túlio Cadilhac + Túlio Cadilhac
            - v0.92 - Pré-Beta (Pode conter bugs) + + + v1 - Beta +
            -
            +
            {footerLinks.legal.map((link) => ( {link.name} diff --git a/src/contexts/NotificationsContext.tsx b/src/contexts/NotificationsContext.tsx new file mode 100644 index 0000000..5f132ad --- /dev/null +++ b/src/contexts/NotificationsContext.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { createContext, useContext, useState, ReactNode } from "react"; +import { FaCheck, FaExclamationTriangle, FaInfoCircle, FaTimes } from "react-icons/fa"; +import { AnimatePresence, motion } from "framer-motion"; + +export type NotificationType = "success" | "error" | "info" | "warning"; + +export interface Notification { + id: string; + type: NotificationType; + message: string; +} + +interface NotificationsContextProps { + notifications: Notification[]; + showNotification: (message: string, type: NotificationType) => void; + clearNotification: (id: string) => void; +} + +const NotificationsContext = createContext(undefined); + +export function NotificationsProvider({ children }: { children: ReactNode }) { + const [notifications, setNotifications] = useState([]); + const [notificationCounter, setNotificationCounter] = useState(0); + const maxNotifications = 3; // Máximo de notificações visíveis ao mesmo tempo + + const showNotification = (message: string, type: NotificationType) => { + const id = `${Date.now()}-${notificationCounter}`; + setNotificationCounter(prev => prev + 1); + + // Verificar se já existe uma notificação similar para evitar duplicação + const hasSimilarNotification = notifications.some(notification => + notification.message === message && notification.type === type + ); + + if (hasSimilarNotification) { + // Se já existe uma notificação similar, não mostre outra + return; + } + + setNotifications(current => { + // Limite a quantidade de notificações visíveis + const updatedNotifications = [...current]; + + // Se exceder o máximo, remova as mais antigas + if (updatedNotifications.length >= maxNotifications) { + updatedNotifications.shift(); // Remove a notificação mais antiga + } + + return [ + ...updatedNotifications, + { + id, + type, + message, + } + ]; + }); + + // Auto-remove after 4 seconds + setTimeout(() => { + clearNotification(id); + }, 4000); + }; + + const clearNotification = (id: string) => { + setNotifications(current => + current.filter(notification => notification.id !== id) + ); + }; + + return ( + + {children} + + + ); +} + +export function useNotifications() { + const context = useContext(NotificationsContext); + + if (context === undefined) { + throw new Error("useNotifications must be used within a NotificationsProvider"); + } + + return context; +} + +function Notifications() { + const { notifications, clearNotification } = useNotifications(); + + return ( +
            + + {notifications.map(notification => ( + +
            + {notification.type === "success" && } + {notification.type === "error" && } + {notification.type === "warning" && } + {notification.type === "info" && } +
            +
            +

            + {notification.message} +

            +
            + +
            + ))} +
            +
            + ); +} diff --git a/src/services/ServerConsoleWebSocket.ts b/src/services/ServerConsoleWebSocket.ts index e7c41ea..cafd9d5 100644 --- a/src/services/ServerConsoleWebSocket.ts +++ b/src/services/ServerConsoleWebSocket.ts @@ -31,13 +31,21 @@ export class ServerConsoleWebSocket { } public async connect(): Promise { - if (this.isConnected || this.isAttemptingConnection) { + if (this.isConnected) { + console.log("Já conectado, não iniciando nova conexão"); + return; + } + + if (this.isAttemptingConnection) { + console.log("Já tentando conectar, aguardando..."); return; } this.isAttemptingConnection = true; + console.log("Iniciando processo de conexão WebSocket"); try { + console.log("Solicitando credenciais via provedor..."); const creds = await this.credentialProvider(); if ('error' in creds) { console.error("Erro ao obter credenciais:", creds.error); diff --git a/tailwind.config.js b/tailwind.config.js index b966d79..25b2184 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -71,9 +71,9 @@ module.exports = { }, }, backgroundImage: { - 'cartoon-pattern': "url('/images/bg-cartoon.svg')", - 'gaming-grid': "linear-gradient(rgba(255,60,56,0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,60,56,0.1) 1px, transparent 1px)", - 'gradient-radial-red': 'radial-gradient(circle, rgba(255,60,56,0.2) 0%, rgba(15,15,26,0) 70%)', + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + "grid-pattern": "linear-gradient(to right, rgba(255, 60, 56, 0.05) 1px, transparent 1px), linear-gradient(to bottom, rgba(255, 60, 56, 0.05) 1px, transparent 1px)", }, boxShadow: { 'neon-red': '0 0 5px rgba(255,60,56,0.5), 0 0 20px rgba(255,60,56,0.3)', From 75d4f3d6f80a3c2cc4215e357f14584040d543ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Zanella?= Date: Sat, 6 Sep 2025 20:52:55 -0300 Subject: [PATCH 3/5] feat: enhance background styles with updated opacity and new patterns --- src/app/page.tsx | 4 ++-- tailwind.config.js | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 81b7fce..38c7e0b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -12,8 +12,8 @@ export default function Home() { return (
            -
            -
            +
            +
            Date: Sun, 14 Sep 2025 00:36:40 -0300 Subject: [PATCH 4/5] feat: enhance NetworkContent component with subdomain management features and improved loading/error handling --- src/components/dashboard/BillingContent.tsx | 21 +- .../dashboard/minecraft/NetworkContent.tsx | 320 +++++++++++++++--- 2 files changed, 296 insertions(+), 45 deletions(-) diff --git a/src/components/dashboard/BillingContent.tsx b/src/components/dashboard/BillingContent.tsx index e43ea97..cd95242 100644 --- a/src/components/dashboard/BillingContent.tsx +++ b/src/components/dashboard/BillingContent.tsx @@ -225,7 +225,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/minecraft/NetworkContent.tsx b/src/components/dashboard/minecraft/NetworkContent.tsx index fbac46b..0e1b502 100644 --- a/src/components/dashboard/minecraft/NetworkContent.tsx +++ b/src/components/dashboard/minecraft/NetworkContent.tsx @@ -1,55 +1,229 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { FaNetworkWired, FaGlobe, FaPlus, FaCopy, - FaCheck + 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); - const ipAndPort = server?.ip?.split(":") || ["", ""]; - const ip = ipAndPort[0]; - const port = ipAndPort[1]; + 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", + }, + } + ); - const copyToClipboard = async (text: string, type: "ip" | "port") => { + 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 { + } 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 handleCreateSubdomain = () => { - if (subdomainName.trim()) { - console.log(`Creating subdomain: ${subdomainName}.firehosting.cloud`); - setShowCreateSubdomain(false); - setSubdomainName(""); - } + 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

            +

            Configurações de Rede

            Gerencie o acesso ao seu servidor Minecraft

            -
            +
            -
            +

            IP

            -
            +

            {ip || "N/A"}

            @@ -96,19 +270,19 @@ export default function NetworkContent({ server }: NetworkContentProps) { initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.3, delay: 0.2 }} - className="bg-gradient-to-br from-gray-900/50 to-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-lg p-4" + className="bg-gradient-to-br from-gray-900/50 to-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-lg p-3" > -
            +

            Porta

            -
            +

            {port || "N/A"}

            @@ -122,14 +296,14 @@ export default function NetworkContent({ server }: NetworkContentProps) { transition={{ duration: 0.3, delay: 0.3 }} className="mt-4 p-4 bg-gradient-to-r from-blue-900/20 to-purple-900/20 rounded-xl border border-blue-500/20" > -
            +

            Conexão Direta

            Online
            -
            +

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

            @@ -157,31 +331,80 @@ export default function NetworkContent({ server }: NetworkContentProps) {
            -
            - -

            Nenhum subdomínio configurado

            -

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

            -
            + {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 ? ( + {!showCreateSubdomain && !showEditSubdomain && ( setShowCreateSubdomain(true)} + onClick={() => { + 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 }} > - Criar Subdomínio + {hasSubdomain ? "Editar Subdomínio" : "Criar Subdomínio"} - ) : ( + )} + + {(showCreateSubdomain || showEditSubdomain) && ( -

            Criar Subdomínio

            +

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

            From fbdae5ecca8cd040f807bd3a59d465f2da52d3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Zanella?= Date: Sun, 14 Sep 2025 06:33:19 -0300 Subject: [PATCH 5/5] Refactor: Remove unnecessary comments and console logs across various components and services - Removed console logs from ServerConsole, Testimonials, ExternalScripts, Header, and other components for cleaner code. - Eliminated redundant comments that describe obvious code functionality in ServerConsole, MtaPlans, SampPlans, and Vps components. - Updated tailwind.config.js to include additional screen breakpoints for better responsiveness. - Cleaned up WebSocket connection handling in FireHostingWebSocket and ServerConsoleWebSocket by removing debug logs. - Streamlined API service files by removing comments that do not add value to the code understanding. --- package.json | 1 + src/app/checkout/[service]/[plan]/page.tsx | 20 +- src/app/dashboard/app/[identifier]/page.tsx | 138 +--- src/app/dashboard/vps/[uuid]/page.tsx | 20 +- src/app/forgot-password/page.tsx | 4 - src/app/login/page.tsx | 8 +- src/app/partnerships/page.tsx | 5 - src/app/register/page.tsx | 4 - src/app/sac/page.tsx | 1 - src/app/services/blob/page.tsx | 2 - src/app/services/page.tsx | 4 - src/app/terms/page.tsx | 681 ++++++++---------- src/components/dashboard/BillingContent.tsx | 9 - src/components/dashboard/DashboardNavbar.tsx | 5 +- src/components/dashboard/FileEditorModal.tsx | 2 - src/components/dashboard/ProfileContent.tsx | 4 - src/components/dashboard/SecurityContent.tsx | 18 +- src/components/dashboard/ServersContent.tsx | 4 - src/components/dashboard/SettingsContent.tsx | 3 - .../dashboard/app/FileUploadArea.tsx | 10 +- src/components/dashboard/app/ServerNavbar.tsx | 1 - .../dashboard/app/SettingsContent.tsx | 8 +- .../dashboard/minecraft/ConsoleContent.tsx | 36 +- .../minecraft/CustomizationContent.tsx | 1 - .../dashboard/minecraft/FilesContent.tsx | 14 +- .../dashboard/minecraft/MinecraftNavbar.tsx | 1 - .../minecraft/MinecraftServerHeader.tsx | 50 +- .../minecraft/MinecraftServerManager.tsx | 6 - .../minecraft/MinecraftSettingsContent.tsx | 6 - .../dashboard/minecraft/ServerConsole.tsx | 1 - src/components/home/Testimonials.tsx | 1 - src/components/layout/ExternalScripts.tsx | 2 - src/components/layout/Header.tsx | 2 +- .../minecraft/MinecraftTestimonials.tsx | 1 - src/components/minecraft/ServerShowcase.tsx | 3 - src/components/mta/MtaPlans.tsx | 3 +- src/components/samp/SampPlans.tsx | 3 +- src/components/vps/VpsOverview.tsx | 2 - src/components/vps/VpsPowerControls.tsx | 3 - src/components/vps/VpsResources.tsx | 4 - src/components/vps/VpsStats.tsx | 3 - src/contexts/NotificationsContext.tsx | 9 +- src/contexts/ServerContext.tsx | 1 - src/contexts/UserDashboardContext.tsx | 1 - src/hooks/useUtmTracker.ts | 9 +- src/pages/api/auth/discord-link.ts | 6 - src/pages/api/auth/google-link.ts | 6 - src/pages/api/websocket-proxy.ts | 24 - src/services/ServerConsoleWebSocket.ts | 48 +- src/services/api/ftp.ts | 2 - src/services/api/server.ts | 1 - src/services/api/user.ts | 7 +- src/services/api/vps.ts | 6 +- src/utils/fetchWithRetry.ts | 32 +- src/utils/fireHostingWebSocket.ts | 78 +- tailwind.config.js | 8 + 56 files changed, 419 insertions(+), 913 deletions(-) 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/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 */} -
            +

    - {/* Seção 1 - Objeto do Contrato */} -
    -
    - -

    1. Objeto do Contrato

    -
    -
    -

    - A prestação, pela FIREHOSTING ao CLIENTE, dos Serviços de Hospedagem e Infraestrutura de Internet, - conforme o plano e especificações técnicas contratadas (CPU, RAM, armazenamento, tráfego). -

    -

    - Os serviços são fornecidos na modalidade "como estão" (as is) e "conforme disponíveis" (as available), - com garantias limitadas ao expressamente previsto neste Contrato e no SLA. -

    -
    -
    +
    + {/* --- Sticky Sidebar Navigation --- */} + - {/* Seção 2 - Obrigações do Cliente */} -
    -
    - -

    2. Obrigações do Cliente

    -
    -
    -
      -
    • - -
      - Informações Cadastrais: Fornecer dados precisos, completos e verídicos, - incluindo e-mail principal que não pertença aos domínios hospedados nos próprios serviços. -
      -
    • -
    • - -
      - Segurança da Conta: Manter sigilo das credenciais de acesso. - O CLIENTE é responsável por toda atividade em sua conta. -
      -
    • -
    • - -
      - Responsabilidade pelo Conteúdo: O CLIENTE é único responsável legal, - civil e penalmente por todo conteúdo hospedado. -
      -
    • -
    • - -
      - Backups: Responsabilidade exclusiva pela criação e manutenção de cópias de segurança dos dados. -
      -
    • -
    • - -
      - Conformidade Legal: Utilizar os serviços em conformidade com a legislação brasileira, - Marco Civil da Internet e LGPD. -
      -
    • -
    -
    -
    + {/* --- Main Content --- */} +
    + } title="Preâmbulo: Partes e Aceitação"> + +

    Partes Contratantes

    +
      +
    • CONTRATADA: FIREHOSTING, pessoa jurídica de direito privado.
    • +
    • CONTRATANTE: A pessoa física ou jurídica que adere a estes termos, doravante "CLIENTE".
    • +
    +
    + +

    Natureza e Aceitação do Contrato

    +

    Este é um Contrato de Adesão (Art. 54, CDC). O vínculo contratual se formaliza quando o CLIENTE finaliza a contratação e marca a caixa "Li e concordo com os Termos de Uso", declarando ter lido e compreendido todas as cláusulas aqui presentes.

    +
    +
    - {/* Seção 3 - Obrigações da FireHosting */} -
    -
    - -

    3. Obrigações da FireHosting

    -
    -
    -
      -
    • - - Prover infraestrutura funcional e acessível conforme SLA -
    • -
    • - - Garantir 99,9% de uptime mensal para infraestrutura -
    • -
    • - - Prestar suporte técnico para questões de infraestrutura -
    • -
    • - - Tratar dados com confidencialidade conforme LGPD -
    • -
    -
    -
    + } title="Definições"> +

    Para a correta interpretação deste contrato, os seguintes termos terão os significados abaixo:

    + +
      +
    • Backup: Cópia de segurança dos dados e arquivos do CLIENTE.
    • +
    • Conteúdo: Quaisquer dados, textos, softwares, imagens, vídeos ou outros materiais inseridos pelo CLIENTE na infraestrutura da FIREHOSTING.
    • +
    • Painel de Controle: A interface online para o CLIENTE gerenciar seus serviços.
    • +
    • Serviços: Todos os produtos de hospedagem (sites, VPS, jogos), registro de domínios e outros oferecidos pela FIREHOSTING.
    • +
    • Uptime: Percentual de tempo em que um serviço está operacional e acessível.
    • +
    +
    +
    - {/* Seção 4 - Hospedagem de Jogos */} -
    -
    - -

    4. Termos Específicos para Hospedagem de Jogos

    -
    -
    -

    - Para serviços de Game Hosting (Minecraft, PalWorld, MTA, SAMP), a FIREHOSTING entrega - o servidor pré-configurado com software base estável. -

    -
    -

    Limitações do Suporte Técnico:

    -
      -
    • • Não inclui suporte para mods, plugins ou modpacks
    • -
    • • Não cobre softwares de servidor não padrão
    • -
    • • Não abrange configurações internas do jogo
    • -
    • • Otimização de desempenho é responsabilidade do cliente
    • -
    -
    -

    - O CLIENTE recebe acesso administrativo total e assume responsabilidade por modificações e suas consequências. -

    -
    -
    + } title="Objeto do Contrato"> +

    O objeto é a prestação dos Serviços de Hospedagem e Infraestrutura de Internet, conforme o plano e as especificações técnicas escolhidas pelo CLIENTE no ato da compra e detalhadas em nosso site oficial.

    + +

    Os serviços são fornecidos "como estão" e "conforme disponíveis". As garantias se limitam àquelas previstas neste Contrato e no Acordo de Nível de Serviço (SLA).

    +
    +
    - {/* Seção 5 - Política de Uso Aceitável */} -
    -
    - -

    5. Política de Uso Aceitável (AUP)

    -
    -
    -

    - É PROIBIDO usar nossos serviços para: -

    -
    -
    -
    - - Atividades ilegais ou crimes -
    -
    - - Material de abuso sexual infantil -
    -
    - - Conteúdo odioso ou discriminatório -
    -
    - - Phishing e fraudes -
    -
    -
    -
    - - Mineração de criptomoedas -
    -
    - - Envio de SPAM -
    -
    - - Ataques DDoS ou port scanning -
    -
    - - Proxies abertos ou redes anônimas -
    -
    -
    -
    -
    + } title="Obrigações do Cliente"> +

    São obrigações fundamentais do CLIENTE:

    + +
      +
    • 3.1. Informações Cadastrais: Fornecer dados precisos e verdadeiros (nome, CPF/CNPJ, endereço) e mantê-los atualizados, incluindo um e-mail de contato que não esteja hospedado no próprio serviço contratado.
    • +
    • 3.2. Segurança da Conta: Manter em sigilo suas senhas e credenciais, sendo o único responsável por toda atividade em sua conta.
    • +
    • 3.3. Responsabilidade pelo Conteúdo: Ser o único e exclusivo responsável legal por todo o Conteúdo que hospeda e distribui.
    • +
    • 3.4. Backups: Realizar e manter cópias de segurança (backups) regulares de todo o seu Conteúdo. A responsabilidade final pela salvaguarda dos dados é do CLIENTE.
    • +
    • 3.5. Conformidade Legal: Utilizar os serviços de acordo com a legislação brasileira.
    • +
    • 3.6. Pagamento: Efetuar pontualmente os pagamentos devidos.
    • +
    +
    +
    - {/* Seção 6 - Pagamentos */} -
    -
    - -

    6. Pagamentos e Faturamento

    -
    -
    -

    - Modalidade pré-paga: Pagamento antecipado por período de uso (mensal, trimestral, etc.). -

    -

    - Renovação automática: Para conveniência, todos os serviços renovam automaticamente. - Cancele com 24h de antecedência se não desejar renovar. -

    -
    -

    Inadimplência:

    -
      -
    • • Após 3 dias: suspensão temporária
    • -
    • • Após 7 dias: exclusão definitiva de dados
    • -
    -
    -

    - Preços podem ser reajustados anualmente conforme IGP-M/FGV, com comunicação prévia de 30 dias. -

    -
    -
    + } title="Obrigações da FireHosting"> +

    São obrigações fundamentais da FIREHOSTING:

    + +
      +
    • 4.1. Prestação dos Serviços: Prover e manter a infraestrutura necessária para o funcionamento dos serviços contratados.
    • +
    • 4.2. Acordo de Nível de Serviço (SLA): Cumprir com as garantias de Uptime estipuladas no SLA.
    • +
    • 4.4. Confidencialidade: Tratar os dados do CLIENTE com confidencialidade e em conformidade com a LGPD e a Política de Privacidade.
    • +
    +
    + +

    4.3. Limites do Suporte Técnico

    +

    Nosso Suporte Técnico se limita a questões da nossa infraestrutura. NÃO ABRANGE:

    +
      +
    • Programação, desenvolvimento ou depuração de códigos e scripts do CLIENTE.
    • +
    • Instalação ou configuração de softwares, plugins ou temas de terceiros.
    • +
    • Problemas em softwares no computador do CLIENTE (clientes de e-mail, FTP, etc.).
    • +
    +
    +
    - {/* Seção 7 - Direito de Arrependimento */} -
    -
    - -

    7. Cancelamento e Direito de Arrependimento

    -
    -
    -
    -

    Direito Legal (Art. 49 CDC):

    -

    - Por ser contratação online, você tem 7 dias corridos para desistir do contrato - sem justificativa, com reembolso integral, a partir da confirmação do pagamento. -

    -
    -

    - Para exercer este direito, abra um ticket de suporte manifestando claramente sua decisão. - O reembolso será processado pelo mesmo método de pagamento usado na compra. -

    -

    - Este direito é irrenunciável por lei, mesmo com execução imediata do serviço após pagamento. -

    -
    -
    + {/* O restante das cláusulas permanece o mesmo da versão anterior */} + } title="Termos para Hospedagem de Jogos"> +

    Esta cláusula estabelece condições particulares para planos de Hospedagem de Jogos, delimitando o escopo do serviço e suporte em cumprimento ao Código de Defesa do Consumidor.

    + +

    5.1. Objeto e Entrega do Serviço

    +

    A FIREHOSTING se obriga a entregar o servidor pré-configurado e funcional, com a versão estável mais recente do software base do jogo contratado (Minecraft, PalWorld, etc.).

    +
    + +

    5.2. Garantia de Funcionamento "Como Entregue"

    +

    Garantimos o funcionamento do servidor em sua configuração original, abrangendo conectividade, recursos de hardware contratados (CPU, RAM, disco) e o software base padrão.

    +
    + +

    5.3. Limitação Expressa do Suporte Técnico

    +

    Nosso suporte técnico para Hospedagem de Jogos NÃO ABRANGE:

    +
      +
    • Instalação, configuração ou depuração de mods, modpacks e plugins de terceiros.
    • +
    • Uso de softwares de servidor alternativos (ex: Velocity para Minecraft, se o padrão for PaperMC).
    • +
    • Configurações internas de jogabilidade (gameplay), permissões, economia ou mapas.
    • +
    • Otimização de desempenho relacionada a customizações feitas pelo cliente.
    • +
    +
    + +

    5.4. Autonomia e Responsabilidade do Cliente

    +

    O CLIENTE recebe acesso administrativo e assume total responsabilidade por instalar, modificar e gerenciar seu ambiente. Falhas, instabilidades ou perda de dados resultantes de ações do CLIENTE não são de nossa responsabilidade.

    +
    +
    - {/* Seção 8 - LGPD */} -
    -
    - -

    8. Proteção de Dados (LGPD)

    -
    -
    -

    - Compromisso total com a Lei nº 13.709/2018 (LGPD). Detalhes na nossa Política de Privacidade. -

    -

    - Papel das partes: Somos controladores dos seus dados cadastrais e operadores - dos dados que você hospeda de seus próprios clientes. -

    -

    - Garantimos o pleno exercício dos seus direitos como titular de dados pessoais (acesso, correção, eliminação, etc.). -

    -
    -
    + } title="Isenção de Responsabilidade sobre MODs"> +

    Esta cláusula esclarece que modificações (“MODs” ou “modpacks”) não são parte oficial do serviço, sendo desenvolvidas por terceiros e podendo gerar falhas fora do nosso controle.

    + +

    6.1 & 6.2. Ferramenta de Busca e Ciência de Riscos

    +

    Disponibilizamos uma ferramenta para buscar mods por conveniência, mas não garantimos seu funcionamento ou compatibilidade. O CLIENTE está ciente de que o uso de mods pode causar instabilidade, perda de desempenho ou falhas.

    +
    + +

    6.3. Exclusão de Responsabilidade

    +

    Em nenhuma hipótese a FIREHOSTING será responsabilizada por perdas, danos ou corrupção de dados decorrentes do uso de mods ou modpacks.

    +
    +
    + + } title="Política de Uso Aceitável (AUP)"> +

    A violação desta política resultará na suspensão ou terminação imediata dos serviços, sem direito a reembolso.

    + +

    7.1. Atividades Ilegais & 7.2. Conteúdo Proibido

    +

    É estritamente proibido usar nossos serviços para:

    +
      +
    • Violar propriedade intelectual (direitos autorais, marcas, etc.).
    • +
    • Hospedar ou distribuir Material de Abuso Sexual Infantil (CSAM) - TOLERÂNCIA ZERO.
    • +
    • Disseminar conteúdo de ódio, difamatório ou discriminatório.
    • +
    • Praticar phishing ou qualquer tipo de fraude.
    • +
    +
    + +

    7.3. Abuso de Recursos

    +

    Também é proibido:

    +
      +
    • Consumo excessivo de recursos que prejudique outros clientes em ambientes compartilhados.
    • +
    • Mineração de criptomoedas (exceto em servidores dedicados com consentimento prévio).
    • +
    • Envio de SPAM.
    • +
    • Originar ou ser alvo voluntário de ataques de rede (DDoS).
    • +
    • Operar proxies abertos ou relays da rede Tor.
    • +
    +
    +
    - {/* Seção 9 - SLA */} -
    -
    - -

    9. Acordo de Nível de Serviço (SLA)

    -
    -
    -

    - Garantia de 99,9% de uptime mensal para hospedagem, VPS e servidores dedicados. -

    -
    -

    Créditos por Descumprimento:

    -
    -
    - 99,0% - 99,89% uptime: - 10% de crédito -
    -
    - 95,0% - 98,99% uptime: - 25% de crédito -
    -
    - Abaixo de 95,0% uptime: - 50% de crédito -
    -
    -
    -

    - Créditos devem ser solicitados via ticket em até 5 dias úteis após o mês do problema. -

    -
    -
    + } title="Pagamentos, Faturamento e Renovação"> + +

    8.1-8.3. Pagamento e Renovação

    +

    Os pagamentos são pré-pagos e os serviços configurados para renovação automática. As faturas são enviadas com 7 dias de antecedência. Para não renovar, o cancelamento deve ser solicitado no painel com 24 horas de antecedência do vencimento.

    +
    + +

    8.4. Inadimplência

    +

    O não pagamento resulta em suspensão imediata. Após 7 dias de atraso, a conta poderá ser terminada e os dados permanentemente excluídos.

    +
    + +

    8.5. Reajuste

    +

    Os preços podem ser reajustados anualmente pelo IGP-M/FGV ou em caso de variação abrupta de custos (ex: dólar). O cliente será notificado com 30 dias de antecedência.

    +
    +
    - {/* Seção 10 - Limitação de Responsabilidade */} -
    -
    - -

    10. Limitação de Responsabilidade

    -
    -
    -

    - Marco Civil da Internet (Art. 19): Como provedora de aplicações, só respondemos - por conteúdo de terceiros após ordem judicial específica e se não tomarmos providências no prazo determinado. -

    -

    - Limite de danos: Nossa responsabilidade total não excederá o valor pago pelo serviço - no mês anterior ao evento danoso. -

    -

    - Exclusões: Não respondemos por danos indiretos, lucros cessantes, perda de dados - ou interrupção de negócios decorrentes do uso dos serviços. -

    -
    -
    + } title="Cancelamento e Arrependimento"> +

    O cliente pode cancelar seus serviços a qualquer momento pelo painel de controle. O cancelamento não isenta o pagamento de valores já devidos.

    + +

    9.2. Direito de Arrependimento (Art. 49, CDC)

    +

    Para contratações online, o cliente tem o direito de desistir do contrato em até 7 (sete) dias corridos a contar da data do pagamento, com reembolso dos valores pagos.

    +
      +
    • O exercício do direito deve ser comunicado via ticket de suporte.
    • +
    • Do valor a ser reembolsado, poderemos deduzir custos específicos e comprovados pagos a terceiros (ex: taxas de licenciamento de software) que não sejam recuperáveis.
    • +
    +
    + +

    9.4. Reembolso por Erro Crítico

    +

    Em caso de um erro crítico em nosso sistema, o cliente terá direito a reembolso proporcional. Esta hipótese pode ser invocada no máximo 2 vezes ao ano, com prazo de reembolso de até 120 dias.

    +
    +
    - {/* Seção 11 - Disposições Gerais */} -
    -
    - -

    11. Disposições Gerais

    -
    -
    -

    - Alterações: Podemos alterar estes termos com 30 dias de antecedência por e-mail ou painel. - Uso continuado após notificação implica aceitação. -

    -

    - Força Maior: Não respondemos por falhas devido a eventos imprevisíveis e inevitáveis - (desastres naturais, guerras, falhas de telecomunicações em larga escala). -

    -

    - Foro: Comarca de João Pessoa-PB, ressalvado direito do consumidor de acionar - em seu próprio domicílio conforme CDC. -

    -
    -
    + } title="Proteção de Dados (LGPD)"> +

    Tratamos seus dados em estrita conformidade com a Lei Geral de Proteção de Dados (Lei nº 13.709/2018).

    + +

    10.3. Papel das Partes

    +
      +
    • Controladora: A FIREHOSTING é controladora dos seus dados cadastrais (nome, e-mail, etc.).
    • +
    • Operadora: A FIREHOSTING é operadora do conteúdo que você insere nos servidores. Neste caso, você é o controlador dos dados.
    • +
    +
    +
    + + } title="Limitação de Responsabilidade"> +

    Em conformidade com o Marco Civil da Internet (Art. 19), só seremos responsabilizados por conteúdo gerado por você se, após ordem judicial, não o removermos.

    + +

    11.3 & 11.4. Limitação de Danos

    +

    Nossa responsabilidade total por danos diretos está limitada ao valor pago pelo serviço no mês anterior ao evento. Não nos responsabilizamos por danos indiretos (lucros cessantes, perda de dados).

    +
    +
    - {/* Conclusão */} -
    -

    - - Aceite e Validade -

    -

    - Ao contratar qualquer serviço, você declara estar de acordo com todos os termos acima, - reconhecendo a validade legal deste documento eletrônico que obriga as partes e seus sucessores. -

    -

    - Este contrato é regido pelas leis brasileiras e visa proteger tanto a empresa quanto o cliente, - seguindo as melhores práticas do mercado de hospedagem. -

    - -
    + } title="Acordo de Nível de Serviço (SLA)"> +

    Garantimos um Uptime de Rede e Hardware de 99,9% para Hospedagem de Sites, VPS e Dedicados. Esta garantia não cobre manutenções programadas, falhas causadas pelo cliente, ataques DDoS, entre outros.

    + +

    12.3. Créditos por Descumprimento

    +
    + + + + + + + + + +
    Uptime MensalCrédito
    Entre 99,0% e 99,89%10%
    Entre 95,0% e 98,99%25%
    Abaixo de 95,0%50%
    +
    +
    +
    -
    -

    - Última atualização: 15 de Julho de 2025 -

    + } title="Suspensão e Terminação"> +

    Podemos suspender os serviços por inadimplência (Cláusula 8.4) ou violação da AUP (Cláusula 7). A conta pode ser terminada por violações graves, fraude ou inadimplência prolongada.

    +
    + + } title="Disposições Gerais"> +

    Podemos alterar estes termos a qualquer momento, notificando os clientes com 30 dias de antecedência. O contrato é regido pelas leis do Brasil e o foro eleito é o de João Pessoa-PB, sendo garantido ao consumidor o direito de propor a ação em seu domicílio.

    + +

    E, por estarem assim justas e contratadas, as partes celebram o presente Contrato por meio de aceite eletrônico, que obriga a si e a seus sucessores ao fiel cumprimento de todas as suas cláusulas e condições.

    +
    +
    + + {/* --- Conclusion --- */} +
    +
    + +
    +

    Aceite e Validade

    +

    + Ao contratar qualquer serviço, você declara estar de acordo com todos os termos acima, reconhecendo a validade legal deste documento eletrônico. +

    +
    +
    ); -} +} \ No newline at end of file diff --git a/src/components/dashboard/BillingContent.tsx b/src/components/dashboard/BillingContent.tsx index cd95242..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) { diff --git a/src/components/dashboard/DashboardNavbar.tsx b/src/components/dashboard/DashboardNavbar.tsx index f2fb00b..c865b3e 100644 --- a/src/components/dashboard/DashboardNavbar.tsx +++ b/src/components/dashboard/DashboardNavbar.tsx @@ -55,9 +55,8 @@ export default function DashboardNavbar({ return index >= 0 ? index : 0; }; - // Função para calcular a posição exata do indicador ativo const getIndicatorPosition = (index: number) => { - return index * 115; // Usando 115px como largura exata do botão + return index * 115; }; return ( @@ -78,7 +77,7 @@ export default function DashboardNavbar({ className="w-10 h-10 object-contain" />
    - + FireHosting 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/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 22274c0..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"); 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 e2026e1..de38cfa 100644 --- a/src/components/dashboard/minecraft/MinecraftServerManager.tsx +++ b/src/components/dashboard/minecraft/MinecraftServerManager.tsx @@ -162,7 +162,6 @@ function MinecraftServerManagerContent() { useEffect(() => { if (!accessKey || !identifier || loading || error) return; - // Limpar qualquer estado de inicialização anterior wsInitializedRef.current = false; let isMounted = true; @@ -171,10 +170,8 @@ function MinecraftServerManagerContent() { let isConnecting = false; const credentialProvider = async () => { - console.log("Solicitando credenciais WebSocket para:", identifier); try { const result = await getServerConsoleCredentials(identifier); - console.log("Resposta de credenciais:", result); if (result.success && result.socket && result.key && result.expiresAt) { return { socket: result.socket, @@ -192,9 +189,7 @@ function MinecraftServerManagerContent() { const connectWebSocket = () => { if (isConnecting || !isMounted) return; - // Limpar qualquer conexão anterior if (wsRef.current) { - console.log("Desconectando WebSocket anterior"); wsRef.current.disconnect(); wsRef.current = null; } @@ -206,7 +201,6 @@ function MinecraftServerManagerContent() { connectionStatus: "Conectando..." })); - console.log("Iniciando nova conexão WebSocket para console do Minecraft"); const ws = new ServerConsoleWebSocket(credentialProvider); wsRef.current = ws; 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