Files
anchorhost-frontend/src/pages/DashboardPage.jsx
T

4555 lines
207 KiB
React

import React, { useState, useEffect, useRef, useContext, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
// import { Card, CardContent } from "./components/ui/card";
import {
ChevronDown, X,
Server, Terminal, User, Settings, Plus, Trash2, Play, Square, RefreshCw,
Cpu, HardDrive, MemoryStick, Copy, Maximize2, Minimize2, AlertCircle,
ShoppingCart, Coins, Globe, Database, Activity, Menu, LogOut, Wifi, WifiOff,
Home, BookOpen,
BarChart3, HardDriveIcon, NetworkIcon, Clock, Info,
CheckCircle, Gamepad2,
Folder, Edit,
Check, Package,
Shield, MapPin,
ExternalLink,
AlertTriangle,
Tag,
Layers,
Coffee,
MessageCircle, Mail, Lock,
Eye, EyeOff,
ArrowRight,
} from "lucide-react";
import ServerCreationForm from '../features/servers/ServerCreationForm.jsx'
import ServerCard from '../features/servers/ServerCard.jsx'
import StoreSection from '../features/store/StoreSection.jsx'
import TicketSystem from '../features/support/TicketSystem.jsx'
import useWebSocket from '../hooks/useWebSocket.js';
import { AuthContext } from '../App.jsx';
import GameConsole from '../features/servers/GameConsole.jsx';
import {
colors,
parseServerDate,
formatCPU,
formatMemory,
getServerStatusInfo,
UnifiedTooltip,
Dropdown,
InlineDropdown,
Slider,
SectionCard,
ResourceGraph,
minecraftVersions,
MinecraftSettings,
DashboardViewShell,
PageHeaderBar,
StatTile,
DomainManager,
ActionButton,
DashboardEmptyState,
DashboardLoadingSpinner,
SettingsPanel,
} from '../components/DashboardComponents.jsx';
import { dashLayout } from '../theme/dashboardTokens.js';
import FileManager from '../features/servers/FileManager.jsx';
import BackupSection from '../features/servers/BackupSection.jsx';
import PluginStoreEmbedded from '../features/plugins/PluginStoreEmbedded.jsx';
import { usePanelCurrency } from '../hooks/usePanelCurrency.js';
import { API_BASE_URL, WS_BASE_URL as WS_URL } from '../config/api';
import branding from '../branding';
const NODE_IP = '85.237.179.121';
const currencyFlags = {
'USD': '🇺🇸', // US flag
'EUR': '🇪🇺', // European Union flag
'GBP': '🇬🇧', // UK flag
'PLN': '🇵🇱', // Poland flag
'PHP': '🇵🇭', // Philippines flag
'CZK': '🇨🇿', // Czech Republic flag
// Add more as needed
};
// Country list with codes and dialing codes (from your list)
const countries = [
{ code: 'AF', dialCode: '+93', name: 'Afghanistan' },
{ code: 'AL', dialCode: '+355', name: 'Albania' },
{ code: 'DZ', dialCode: '+213', name: 'Algeria' },
{ code: 'AD', dialCode: '+376', name: 'Andorra' },
{ code: 'AO', dialCode: '+244', name: 'Angola' },
{ code: 'AG', dialCode: '+1-268', name: 'Antigua and Barbuda' },
{ code: 'AR', dialCode: '+54', name: 'Argentina' },
{ code: 'AM', dialCode: '+374', name: 'Armenia' },
{ code: 'AU', dialCode: '+61', name: 'Australia' },
{ code: 'AT', dialCode: '+43', name: 'Austria' },
{ code: 'AZ', dialCode: '+994', name: 'Azerbaijan' },
{ code: 'BS', dialCode: '+1-242', name: 'Bahamas' },
{ code: 'BH', dialCode: '+973', name: 'Bahrain' },
{ code: 'BD', dialCode: '+880', name: 'Bangladesh' },
{ code: 'BB', dialCode: '+1-246', name: 'Barbados' },
{ code: 'BY', dialCode: '+375', name: 'Belarus' },
{ code: 'BE', dialCode: '+32', name: 'Belgium' },
{ code: 'BZ', dialCode: '+501', name: 'Belize' },
{ code: 'BJ', dialCode: '+229', name: 'Benin' },
{ code: 'BT', dialCode: '+975', name: 'Bhutan' },
{ code: 'BO', dialCode: '+591', name: 'Bolivia' },
{ code: 'BA', dialCode: '+387', name: 'Bosnia and Herzegovina' },
{ code: 'BW', dialCode: '+267', name: 'Botswana' },
{ code: 'BR', dialCode: '+55', name: 'Brazil' },
{ code: 'BN', dialCode: '+673', name: 'Brunei' },
{ code: 'BG', dialCode: '+359', name: 'Bulgaria' },
{ code: 'BF', dialCode: '+226', name: 'Burkina Faso' },
{ code: 'BI', dialCode: '+257', name: 'Burundi' },
{ code: 'KH', dialCode: '+855', name: 'Cambodia' },
{ code: 'CM', dialCode: '+237', name: 'Cameroon' },
{ code: 'CA', dialCode: '+1', name: 'Canada' },
{ code: 'CV', dialCode: '+238', name: 'Cape Verde' },
{ code: 'CF', dialCode: '+236', name: 'Central African Republic' },
{ code: 'TD', dialCode: '+235', name: 'Chad' },
{ code: 'CL', dialCode: '+56', name: 'Chile' },
{ code: 'CN', dialCode: '+86', name: 'China' },
{ code: 'CO', dialCode: '+57', name: 'Colombia' },
{ code: 'KM', dialCode: '+269', name: 'Comoros' },
{ code: 'CG', dialCode: '+242', name: 'Congo' },
{ code: 'CR', dialCode: '+506', name: 'Costa Rica' },
{ code: 'HR', dialCode: '+385', name: 'Croatia' },
{ code: 'CU', dialCode: '+53', name: 'Cuba' },
{ code: 'CY', dialCode: '+357', name: 'Cyprus' },
{ code: 'CZ', dialCode: '+420', name: 'Czech Republic' },
{ code: 'DK', dialCode: '+45', name: 'Denmark' },
{ code: 'DJ', dialCode: '+253', name: 'Djibouti' },
{ code: 'DM', dialCode: '+1-767', name: 'Dominica' },
{ code: 'DO', dialCode: '+1-809', name: 'Dominican Republic' },
{ code: 'EC', dialCode: '+593', name: 'Ecuador' },
{ code: 'EG', dialCode: '+20', name: 'Egypt' },
{ code: 'SV', dialCode: '+503', name: 'El Salvador' },
{ code: 'GQ', dialCode: '+240', name: 'Equatorial Guinea' },
{ code: 'ER', dialCode: '+291', name: 'Eritrea' },
{ code: 'EE', dialCode: '+372', name: 'Estonia' },
{ code: 'ET', dialCode: '+251', name: 'Ethiopia' },
{ code: 'FJ', dialCode: '+679', name: 'Fiji' },
{ code: 'FI', dialCode: '+358', name: 'Finland' },
{ code: 'FR', dialCode: '+33', name: 'France' },
{ code: 'GA', dialCode: '+241', name: 'Gabon' },
{ code: 'GM', dialCode: '+220', name: 'Gambia' },
{ code: 'GE', dialCode: '+995', name: 'Georgia' },
{ code: 'DE', dialCode: '+49', name: 'Germany' },
{ code: 'GH', dialCode: '+233', name: 'Ghana' },
{ code: 'GR', dialCode: '+30', name: 'Greece' },
{ code: 'GD', dialCode: '+1-473', name: 'Grenada' },
{ code: 'GT', dialCode: '+502', name: 'Guatemala' },
{ code: 'GN', dialCode: '+224', name: 'Guinea' },
{ code: 'GW', dialCode: '+245', name: 'Guinea-Bissau' },
{ code: 'GY', dialCode: '+592', name: 'Guyana' },
{ code: 'HT', dialCode: '+509', name: 'Haiti' },
{ code: 'HN', dialCode: '+504', name: 'Honduras' },
{ code: 'HU', dialCode: '+36', name: 'Hungary' },
{ code: 'IS', dialCode: '+354', name: 'Iceland' },
{ code: 'IN', dialCode: '+91', name: 'India' },
{ code: 'ID', dialCode: '+62', name: 'Indonesia' },
{ code: 'IR', dialCode: '+98', name: 'Iran' },
{ code: 'IQ', dialCode: '+964', name: 'Iraq' },
{ code: 'IE', dialCode: '+353', name: 'Ireland' },
{ code: 'IL', dialCode: '+972', name: 'Israel' },
{ code: 'IT', dialCode: '+39', name: 'Italy' },
{ code: 'JM', dialCode: '+1-876', name: 'Jamaica' },
{ code: 'JP', dialCode: '+81', name: 'Japan' },
{ code: 'JO', dialCode: '+962', name: 'Jordan' },
{ code: 'KZ', dialCode: '+7', name: 'Kazakhstan' },
{ code: 'KE', dialCode: '+254', name: 'Kenya' },
{ code: 'KI', dialCode: '+686', name: 'Kiribati' },
{ code: 'KP', dialCode: '+850', name: 'North Korea' },
{ code: 'KR', dialCode: '+82', name: 'South Korea' },
{ code: 'KW', dialCode: '+965', name: 'Kuwait' },
{ code: 'KG', dialCode: '+996', name: 'Kyrgyzstan' },
{ code: 'LA', dialCode: '+856', name: 'Laos' },
{ code: 'LV', dialCode: '+371', name: 'Latvia' },
{ code: 'LB', dialCode: '+961', name: 'Lebanon' },
{ code: 'LS', dialCode: '+266', name: 'Lesotho' },
{ code: 'LR', dialCode: '+231', name: 'Liberia' },
{ code: 'LY', dialCode: '+218', name: 'Libya' },
{ code: 'LI', dialCode: '+423', name: 'Liechtenstein' },
{ code: 'LT', dialCode: '+370', name: 'Lithuania' },
{ code: 'LU', dialCode: '+352', name: 'Luxembourg' },
{ code: 'MG', dialCode: '+261', name: 'Madagascar' },
{ code: 'MW', dialCode: '+265', name: 'Malawi' },
{ code: 'MY', dialCode: '+60', name: 'Malaysia' },
{ code: 'MV', dialCode: '+960', name: 'Maldives' },
{ code: 'ML', dialCode: '+223', name: 'Mali' },
{ code: 'MT', dialCode: '+356', name: 'Malta' },
{ code: 'MH', dialCode: '+692', name: 'Marshall Islands' },
{ code: 'MR', dialCode: '+222', name: 'Mauritania' },
{ code: 'MU', dialCode: '+230', name: 'Maurice' },
{ code: 'MX', dialCode: '+52', name: 'Mexico' },
{ code: 'FM', dialCode: '+691', name: 'Micronesia' },
{ code: 'MD', dialCode: '+373', name: 'Moldova' },
{ code: 'MC', dialCode: '+377', name: 'Monaco' },
{ code: 'MN', dialCode: '+976', name: 'Mongolia' },
{ code: 'ME', dialCode: '+382', name: 'Montenegro' },
{ code: 'MA', dialCode: '+212', name: 'Morocco' },
{ code: 'MZ', dialCode: '+258', name: 'Mozambique' },
{ code: 'MM', dialCode: '+95', name: 'Myanmar' },
{ code: 'NA', dialCode: '+264', name: 'Namibia' },
{ code: 'NR', dialCode: '+674', name: 'Nauru' },
{ code: 'NP', dialCode: '+977', name: 'Nepal' },
{ code: 'NL', dialCode: '+31', name: 'Netherlands' },
{ code: 'NZ', dialCode: '+64', name: 'New Zealand' },
{ code: 'NI', dialCode: '+505', name: 'Nicaragua' },
{ code: 'NE', dialCode: '+227', name: 'Niger' },
{ code: 'NG', dialCode: '+234', name: 'Nigeria' },
{ code: 'MK', dialCode: '+389', name: 'North Macedonia' },
{ code: 'NO', dialCode: '+47', name: 'Norway' },
{ code: 'OM', dialCode: '+968', name: 'Oman' },
{ code: 'PK', dialCode: '+92', name: 'Pakistan' },
{ code: 'PW', dialCode: '+680', name: 'Palau' },
{ code: 'PA', dialCode: '+507', name: 'Panama' },
{ code: 'PG', dialCode: '+675', name: 'Papua New Guinea' },
{ code: 'PY', dialCode: '+595', name: 'Paraguay' },
{ code: 'PE', dialCode: '+51', name: 'Peru' },
{ code: 'PH', dialCode: '+63', name: 'Philippines' },
{ code: 'PL', dialCode: '+48', name: 'Poland' },
{ code: 'PT', dialCode: '+351', name: 'Portugal' },
{ code: 'QA', dialCode: '+974', name: 'Qatar' },
{ code: 'RO', dialCode: '+40', name: 'Romania' },
{ code: 'RU', dialCode: '+7', name: 'Russia' },
{ code: 'RW', dialCode: '+250', name: 'Rwanda' },
{ code: 'KN', dialCode: '+1-869', name: 'Saint Kitts and Nevis' },
{ code: 'LC', dialCode: '+1-758', name: 'Saint Lucia' },
{ code: 'VC', dialCode: '+1-784', name: 'Saint Vincent and the Grenadines' },
{ code: 'WS', dialCode: '+685', name: 'Samoa' },
{ code: 'SM', dialCode: '+378', name: 'San Marino' },
{ code: 'ST', dialCode: '+239', name: 'Sao Tome and Principe' },
{ code: 'SA', dialCode: '+966', name: 'Saudi Arabia' },
{ code: 'SN', dialCode: '+221', name: 'Senegal' },
{ code: 'RS', dialCode: '+381', name: 'Serbia' },
{ code: 'SC', dialCode: '+248', name: 'Seychelles' },
{ code: 'SL', dialCode: '+232', name: 'Sierra Leone' },
{ code: 'SG', dialCode: '+65', name: 'Singapore' },
{ code: 'SK', dialCode: '+421', name: 'Slovakia' },
{ code: 'SI', dialCode: '+386', name: 'Slovenia' },
{ code: 'SB', dialCode: '+677', name: 'Solomon Islands' },
{ code: 'SO', dialCode: '+252', name: 'Somalia' },
{ code: 'ZA', dialCode: '+27', name: 'South Africa' },
{ code: 'ES', dialCode: '+34', name: 'Spain' },
{ code: 'LK', dialCode: '+94', name: 'Sri Lanka' },
{ code: 'SD', dialCode: '+249', name: 'Sudan' },
{ code: 'SR', dialCode: '+597', name: 'Suriname' },
{ code: 'SE', dialCode: '+46', name: 'Sweden' },
{ code: 'CH', dialCode: '+41', name: 'Switzerland' },
{ code: 'SY', dialCode: '+963', name: 'Syria' },
{ code: 'TW', dialCode: '+886', name: 'Taiwan' },
{ code: 'TJ', dialCode: '+992', name: 'Tajikistan' },
{ code: 'TZ', dialCode: '+255', name: 'Tanzania' },
{ code: 'TH', dialCode: '+66', name: 'Thailand' },
{ code: 'TL', dialCode: '+670', name: 'Timor-Leste' },
{ code: 'TG', dialCode: '+228', name: 'Togo' },
{ code: 'TO', dialCode: '+676', name: 'Tonga' },
{ code: 'TT', dialCode: '+1-868', name: 'Trinidad and Tobago' },
{ code: 'TN', dialCode: '+216', name: 'Tunisia' },
{ code: 'TR', dialCode: '+90', name: 'Turkey' },
{ code: 'TM', dialCode: '+993', name: 'Turkmenistan' },
{ code: 'TV', dialCode: '+688', name: 'Tuvalu' },
{ code: 'UG', dialCode: '+256', name: 'Uganda' },
{ code: 'UA', dialCode: '+380', name: 'Ukraine' },
{ code: 'AE', dialCode: '+971', name: 'United Arab Emirates' },
{ code: 'GB', dialCode: '+44', name: 'United Kingdom' },
{ code: 'US', dialCode: '+1', name: 'United States' },
{ code: 'UY', dialCode: '+598', name: 'Uruguay' },
{ code: 'UZ', dialCode: '+998', name: 'Uzbekistan' },
{ code: 'VU', dialCode: '+678', name: 'Vanuatu' },
{ code: 'VA', dialCode: '+379', name: 'Vatican City' },
{ code: 'VE', dialCode: '+58', name: 'Venezuela' },
{ code: 'VN', dialCode: '+84', name: 'Vietnam' },
{ code: 'YE', dialCode: '+967', name: 'Yemen' },
{ code: 'ZM', dialCode: '+260', name: 'Zambia' },
{ code: 'ZW', dialCode: '+263', name: 'Zimbabwe' }
];
// Helper function to parse server creation date
// const parseServerDate = (dateString) => { ... } - MOVED TO SHARED
// Utility functions for metrics formatting
// const formatCPU = (cpuStr) => { ... } - MOVED TO SHARED
// const formatMemory = (memoryStr, totalMemoryGB = 1) => { ... } - MOVED TO SHARED
// Shared Dropdown Component
// const Dropdown = ({ value, options, onChange, placeholder, icon: Icon }) => { ... } - MOVED TO SHARED
// Unified Tooltip Component
// const UnifiedTooltip = ({ children, content, show = true, placement = 'top' }) => { ... } - MOVED TO SHARED
// File Manager Component with forwardRef
const fileManagerRef = React.createRef();
// ResourceGraph - MOVED TO SHARED
// Slider - MOVED TO SHARED
// SectionCard - MOVED TO SHARED
// Embedded Plugin Store Component for Dashboard
function DashboardPage({ setIsLoginOpen, setIsRegisterOpen, onCurrencyChange, pricing = {
price_cpu: 35.0,
price_ram: 35.0,
price_storage: 5.0,
multiplier_vps: 1.0,
multiplier_game: 0.8,
multiplier_website: 1.5,
multiplier_database: 1.3
} }) {
const navigate = useNavigate();
const { user, logout } = useContext(AuthContext);
const {
selectedCurrency,
setSelectedCurrency: setSelectedCurrencyBase,
currencies,
calculateCurrencyValue,
getCurrencySymbol,
} = usePanelCurrency();
const setSelectedCurrency = useCallback((v) => {
setSelectedCurrencyBase(v);
if (onCurrencyChange && typeof onCurrencyChange === 'function') {
onCurrencyChange(v);
}
}, [setSelectedCurrencyBase, onCurrencyChange]);
const [userData, setUserData] = useState({
...user,
email_verified: user?.email_verified || false
});
const [activeTab, setActiveTab] = useState('servers');
const [newPortProtocol, setNewPortProtocol] = useState('TCP');
const [newPortInternal, setNewPortInternal] = useState('');
const [showPortMappingGuide, setShowPortMappingGuide] = useState(false);
const [servers, setServers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedServer, setSelectedServer] = useState(null);
const [isEditingVersion, setIsEditingVersion] = useState(false);
const [isEditingLoader, setIsEditingLoader] = useState(false);
const [isEditingJava, setIsEditingJava] = useState(false);
const [newVersion, setNewVersion] = useState('');
const [newLoader, setNewLoader] = useState('');
const [newJavaVersion, setNewJavaVersion] = useState('');
const [isEditingResources, setIsEditingResources] = useState(false);
const [resourceValues, setResourceValues] = useState({ cpu: 1, ram: 1, storage: 10 });
const [toggleSFTPPassword, setToggleSFTPPassword] = useState(false);
// Initialize editing states when server is selected
useEffect(() => {
if (selectedServer) {
if (!isEditingVersion) setNewVersion(selectedServer.version || '');
if (!isEditingLoader) setNewLoader(selectedServer.loader || 'Paper');
if (!isEditingJava) setNewJavaVersion(selectedServer.java_version || 'Java 21');
setNewPortInternal('');
setShowPortMappingGuide(false);
}
}, [selectedServer, isEditingVersion, isEditingLoader, isEditingJava]);
// Compatibility logic for DashboardPage
useEffect(() => {
if (isEditingVersion && selectedServer?.image === 'minecraft' && newVersion) {
const parts = newVersion.split('.');
if (parts.length >= 2) {
const major = parseInt(parts[0]);
const minor = parseInt(parts[1]);
const patch = parts.length >= 3 ? parseInt(parts[2]) : 0;
// Auto-select Java version based on MC version ONLY if not manually editing Java
if (!isEditingJava) {
if (major >= 26) {
setNewJavaVersion('Java 25');
} else if (major === 1) {
if (minor >= 21 || (minor === 20 && patch >= 5)) {
setNewJavaVersion('Java 21');
} else if (minor >= 17) {
setNewJavaVersion('Java 17');
} else if (minor >= 16) {
setNewJavaVersion('Java 11');
} else if (minor >= 12) {
setNewJavaVersion('Java 8');
} else if (minor === 8) {
setNewJavaVersion('Java 7');
} else {
setNewJavaVersion('Java 8');
}
}
}
// Auto-correct incompatible loader (Fabric not on < 1.14)
if (newLoader === 'Fabric' && minor < 14) {
setNewLoader('Forge');
}
}
}
}, [newVersion, isEditingVersion, selectedServer?.image, isEditingJava, newLoader]);
const handleUpdateConfig = (type, value) => {
if (!selectedServer) return;
// We'll use the existing sendDashboardMessage logic
if (sendMessageRef.current) {
sendMessageRef.current({
type: 'change_server_version',
serverId: selectedServer.id,
version: type === 'version' ? value : newVersion,
loader: type === 'loader' ? value : newLoader,
java_version: type === 'java' ? value : newJavaVersion
});
}
// Close editing states
setIsEditingVersion(false);
setIsEditingLoader(false);
setIsEditingJava(false);
};
const [serverLogs, setServerLogs] = useState('');
const [isCreatingServer, setIsCreatingServer] = useState(false);
const [terminalOutput, setTerminalOutput] = useState('');
const [terminalInput, setTerminalInput] = useState('');
const [isTerminalConnected, setIsTerminalConnected] = useState(false);
const [isTerminalFullscreen, setIsTerminalFullscreen] = useState(false);
const [serverMetrics, setServerMetrics] = useState({ cpu: '0m', memory: '0Mi', cpu_percentage: 0, memory_percentage: 0 });
const [metricsHistory, setMetricsHistory] = useState({ cpu: [], memory: [] });
const [mcSettings, setMcSettings] = useState(null);
const [isMcSettingsLoading, setIsMcSettingsLoading] = useState(false);
const [userCredits, setUserCredits] = useState(user?.credits || 0);
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
const [activeServerDetailTab, setActiveServerDetailTab] = useState('overview');
// Ref for sendMessage to break circular dependency
const sendMessageRef = useRef(null);
const [displayNameModal, setDisplayNameModal] = useState({
open: false,
currentName: '',
field: '',
title: '',
label: '',
placeholder: ''
});
const [emailModal, setEmailModal] = useState({
open: false,
currentEmail: '',
title: '',
label: '',
placeholder: ''
});
const [passwordModal, setPasswordModal] = useState({
open: false,
title: '',
requireCurrent: true
});
const [alertModal, setAlertModal] = useState({
open: false,
title: '',
message: '',
type: 'info' // 'info', 'success', 'error', 'warning'
});
const [confirmModal, setConfirmModal] = useState({
open: false,
title: '',
message: '',
onConfirm: () => {},
type: 'danger',
confirmLabel: 'Confirm',
cancelLabel: 'Cancel',
});
const showAlert = useCallback((title, message, type = 'info') => {
setAlertModal({
open: true,
title,
message,
type
});
}, []);
const showConfirm = useCallback((title, message, onConfirm, type = 'danger', labels = {}) => {
setConfirmModal({
open: true,
title,
message,
onConfirm,
type,
confirmLabel: labels.confirmLabel || 'Confirm',
cancelLabel: labels.cancelLabel || 'Cancel',
});
}, []);
const [show2FASetup, setShow2FASetup] = useState(false);
const [twoFactorData, setTwoFactorData] = useState(null);
const [deleteAccountModal, setDeleteAccountModal] = useState(false);
const [deleteServerModal, setDeleteServerModal] = useState({ open: false, serverId: null });
const [convertDemoModal, setConvertDemoModal] = useState({ open: false, server: null, cost: 0 });
const [serverCreationProgress, setServerCreationProgress] = useState({
visible: false,
phase: '',
progress: 0,
message: ''
});
const [personalInfo, setPersonalInfo] = useState({
firstName: user?.first_name || '',
lastName: user?.last_name || '',
address: user?.address || '',
city: user?.city || '',
country: user?.country || '',
postalCode: user?.postal_code || '',
phone: user?.phone || ''
});
const [countryOptions, setCountryOptions] = useState([]);
const [selectedCountry, setSelectedCountry] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
const [detectingLocation, setDetectingLocation] = useState(false);
const token = localStorage.getItem('anchorhost_token');
// Format countries for dropdown
useEffect(() => {
const formattedCountries = countries.map(country => ({
value: country.code,
label: `${country.name} (${country.dialCode})`,
dialCode: country.dialCode,
name: country.name
}));
// Sort alphabetically by country name
formattedCountries.sort((a, b) => a.name.localeCompare(b.name));
setCountryOptions(formattedCountries);
// Set initial country from user data if available
if (user?.country) {
const userCountry = formattedCountries.find(c => c.value === user.country || c.name === user.country);
if (userCountry) {
setSelectedCountry(userCountry.value);
}
}
// Set initial phone number
if (user?.phone) {
setPhoneNumber(user.phone);
}
}, [user]);
// Function to detect user location
const detectUserLocation = useCallback(async () => {
setDetectingLocation(true);
try {
const response = await fetch('https://geoapi.info/api/geo');
if (!response.ok) {
throw new Error('Failed to fetch location');
}
const data = await response.json();
console.log('Location data:', data);
if (data.location?.country) {
const countryCode = data.location.country;
const foundCountry = countryOptions.find(c => c.value === countryCode);
if (foundCountry) {
setSelectedCountry(countryCode);
// Update personal info with detected country
setPersonalInfo(prev => ({
...prev,
country: foundCountry.name
}));
}
}
} catch (error) {
console.error('Error detecting location:', error);
setAlertModal({
open: true,
title: "Location Detection Failed",
message: "We couldn't automatically detect your location. Please select your country manually.",
type: "warning"
});
} finally {
setDetectingLocation(false);
}
}, [countryOptions]);
// Auto-detect location when country is not set
useEffect(() => {
if (!selectedCountry && countryOptions.length > 0) {
detectUserLocation();
}
}, [selectedCountry, countryOptions, detectUserLocation]);
// Get selected country info
const getSelectedCountry = () => {
return countryOptions.find(c => c.value === selectedCountry);
};
// Function to update user profile with phone and country
const updateUserProfile = async (profileData) => {
try {
const token = localStorage.getItem('anchorhost_token');
const response = await fetch(`${API_BASE_URL}/api/user/profile`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(profileData)
});
const data = await response.json();
if (data.success) {
return { success: true, message: data.message || 'Profile updated successfully!' };
} else {
return { success: false, message: data.error || 'Failed to update profile' };
}
} catch (error) {
console.error('Error updating profile:', error);
return { success: false, message: 'Error updating profile' };
}
};
const changeUserEmail = async (newEmail) => {
try {
const token = localStorage.getItem('anchorhost_token');
const response = await fetch(`${API_BASE_URL}/api/user/email`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: newEmail })
});
const data = await response.json();
if (data.success) {
return { success: true, message: data.message || 'Email changed successfully! Please check your new email for verification.' };
} else {
return { success: false, message: data.error || 'Failed to change email' };
}
} catch (error) {
console.error('Error changing email:', error);
return { success: false, message: 'Error changing email' };
}
};
const changeUserPassword = async (currentPassword, newPassword) => {
try {
const token = localStorage.getItem('anchorhost_token');
const response = await fetch(`${API_BASE_URL}/api/user/password`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
currentPassword,
newPassword
})
});
const data = await response.json();
if (data.success) {
return { success: true, message: data.message || 'Password changed successfully!' };
} else {
return { success: false, message: data.error || 'Failed to change password' };
}
} catch (error) {
console.error('Error changing password:', error);
return { success: false, message: 'Error changing password' };
}
};
const deleteUserAccount = async (confirmationText) => {
try {
const token = localStorage.getItem('anchorhost_token');
const response = await fetch(`${API_BASE_URL}/api/user/account`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ confirmation: confirmationText })
});
const data = await response.json();
if (data.success) {
localStorage.removeItem('anchorhost_token');
window.location.href = '/';
return { success: true, message: 'Account deleted successfully!' };
} else {
return { success: false, message: data.error || 'Failed to delete account' };
}
} catch (error) {
console.error('Error deleting account:', error);
return { success: false, message: 'Error deleting account' };
}
};
const exportUserData = async () => {
try {
const token = localStorage.getItem('anchorhost_token');
const response = await fetch(`${API_BASE_URL}/api/user/export-data`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.success) {
const blob = new Blob([JSON.stringify(data.data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `anchorhost-data-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} else {
showAlert('Export Failed', data.error || 'Unknown error', 'error');
}
} catch (error) {
showAlert('Export Error', 'An unexpected error occurred while exporting your data.', 'error');
}
};
const loadUserProfile = useCallback(async () => {
try {
const token = localStorage.getItem('anchorhost_token');
if (!token) {
setAlertModal({
open: true,
title: "Authentication Error",
message: "No authentication token found. Please log in again.",
type: "error"
});
return;
}
console.log('Loading user profile...');
const response = await fetch(`${API_BASE_URL}/api/user/profile`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.status === 401 || response.status === 403) {
logout();
return;
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success && data.user) {
// IMPORTANT: Update userData with email_verified
setUserData(prev => ({
...prev,
...data.user,
email_verified: data.user.email_verified || false
}));
setPersonalInfo({
firstName: data.user.first_name || '',
lastName: data.user.last_name || '',
address: data.user.address || '',
city: data.user.city || '',
country: data.user.country || '',
postalCode: data.user.postal_code || '',
phone: data.user.phone || ''
});
// Set country dropdown
if (data.user.country && countryOptions.length > 0) {
const userCountry = countryOptions.find(c =>
c.value === data.user.country || c.name === data.user.country
);
if (userCountry) {
setSelectedCountry(userCountry.value);
}
}
// Set phone number
if (data.user.phone) {
setPhoneNumber(data.user.phone);
}
console.log('✅ Profile loaded successfully:', data.user);
console.log('📧 Email verified status:', data.user.email_verified);
} else {
console.error('Failed to load user profile:', data.error);
setAlertModal({
open: true,
title: "Failed to Load Profile",
message: data.error || 'Unknown error',
type: "error"
});
}
} catch (error) {
console.error('Error loading user profile:', error);
}
}, [countryOptions, logout]);
// Process metrics for human-readable display
const processMetrics = useCallback((metrics) => {
if (!metrics) return { cpu: formatCPU('0m'), memory: formatMemory('0Mi') };
const serverRAM = selectedServer ? parseInt(selectedServer.ram) || 1 : 1;
// Calculate CPU max value for percentage calculation
const cpuStr = selectedServer?.cpu || '1';
let cpuCores = 1;
if (cpuStr.includes('vCPU')) {
cpuCores = parseInt(cpuStr.split()[0]) || 1;
} else if (typeof cpuStr === 'string' && /^\d+$/.test(cpuStr.trim())) {
cpuCores = parseInt(cpuStr.trim()) || 1;
} else {
const match = cpuStr.match(/(\d+)/);
if (match) cpuCores = parseInt(match[1]) || 1;
}
const maxCpuMillicores = cpuCores * 1000;
// Format CPU with correct percentage
const cpuFormatted = formatCPU(metrics.cpu);
// Recalculate percentage based on server's actual CPU allocation
const cpuPercentage = metrics.cpu_percentage !== undefined
? metrics.cpu_percentage
: (cpuFormatted.value / maxCpuMillicores) * 100;
return {
cpu: {
...cpuFormatted,
percentage: Math.min(cpuPercentage, 100)
},
memory: formatMemory(metrics.memory, serverRAM),
cpu_percentage: Math.min(cpuPercentage, 100),
memory_percentage: metrics.memory_percentage || 0,
timestamp: metrics.timestamp || new Date().toISOString()
};
}, [selectedServer]);
// WebSocket message handler
const handleWebSocketMessage = useCallback((data) => {
console.log('Dashboard received message:', data.type, data);
// Forward game console messages
const gameConsoleMessages = [
'game_console_result',
'game_console_output'
];
if (gameConsoleMessages.includes(data.type) && window.__gameConsoleHandler) {
window.__gameConsoleHandler(data);
return;
}
// First, let the file manager handle file-related messages
// For upload_error, also forward if it's a file-related message (even if serverId doesn't match exactly)
const shouldForwardToFileManager = fileManagerRef.current && (
String(data.serverId) === String(selectedServer?.id) ||
(data.type === 'upload_error' && data.filePath) // Forward upload_error even if serverId mismatch
);
if (shouldForwardToFileManager) {
const fileManagerHandlers = [
'files_list', 'file_content', 'file_written', 'file_uploaded',
'file_downloaded', 'directory_created', 'path_deleted',
'upload_started', 'upload_chunk_received', 'upload_error',
'path_copied', 'path_moved', 'zip_packed', 'zip_unpacked', 'zip_error'
];
if (fileManagerHandlers.includes(data.type)) {
fileManagerRef.current.handleWebSocketMessage(data);
return;
}
}
// Handle command results for file listing
if (data.type === 'command_result' && data.serverId === selectedServer?.id) {
// Let file manager handle ls command results when in files tab
if (fileManagerRef.current && activeServerDetailTab === 'files') {
fileManagerRef.current.handleWebSocketMessage(data);
}
// Handle terminal output
if (isTerminalConnected) {
setTerminalOutput(prev => prev + `$ ${data.command}\n${data.result}\n`);
}
// Return early to prevent falling through to default case
return;
}
// Handle domain-related messages - forward to DomainManager
const domainMessageTypes = [
'domains_list',
'custom_domains_list',
'domain_created',
'domain_deleted',
'domain_linked',
'domain_unlinked',
'custom_domain_verified',
'custom_domain_created',
'custom_domain_deleted'
];
if (domainMessageTypes.includes(data.type) && window.__domainManagerHandler) {
window.__domainManagerHandler(data);
return;
}
// Handle other message types
switch (data.type) {
case 'email_verified':
// Update user state after verification
setUserData(prev => ({
...prev,
email_verified: true
}));
// Show success message
setAlertModal({
open: true,
title: "Email Verified! 🎉",
message: data.message || "Your email has been verified successfully!",
type: "success"
});
break;
case 'authenticated':
setUserCredits(data.user?.credits || 0);
// IMPORTANT: Update email_verified during authentication
setUserData(prev => ({
...prev,
...data.user,
email_verified: data.user?.email_verified || false
}));
sendMessageRef.current?.({
type: 'get_servers'
});
// Request server creation progress if we have an active creation
if (serverCreationProgress.visible) {
sendMessageRef.current?.({
type: 'get_server_creation_progress'
});
}
break;
case 'servers_list':
setServers(data.servers || []);
// Update selectedServer if it's currently selected to reflect port changes
if (selectedServer && data.servers) {
const updatedServer = data.servers.find(s => s.id === selectedServer.id);
if (updatedServer) {
setSelectedServer(updatedServer);
}
}
setIsLoading(false);
break;
case 'server_created':
// Hide progress notification when server creation completes
setServerCreationProgress(prev => ({ ...prev, visible: false }));
if (data.server && data.server.id) {
setServers(prev => [...prev, data.server]);
setIsCreatingServer(false);
} else if (data.serverId) {
sendMessageRef.current?.({
type: 'get_servers'
});
setIsCreatingServer(false);
}
if (data.credits !== undefined) {
setUserCredits(data.credits);
}
// Redirect to My Servers tab after successful server creation
setActiveTab('servers');
break;
case 'server_updated':
if (data.server && data.server.id) {
setServers(prev => prev.map(s =>
s.id === data.server.id ? { ...s, ...data.server } : s
));
if (selectedServer && selectedServer.id === data.server.id) {
setSelectedServer(prev => ({ ...prev, ...data.server }));
}
}
break;
case 'sftp_generated':
if (data.serverId === selectedServer?.id) {
setAlertModal({
open: true,
title: "SFTP Provisioned! 🚀",
message: "SFTP credentials have been generated. You can now connect via any SFTP client.",
type: "success"
});
// Refresh servers list to get new credentials
sendMessageRef.current?.({ type: 'get_servers' });
}
break;
case 'server_version_changed':
if (data.serverId && data.version) {
setServers(prev => prev.map(s =>
s.id === data.serverId ? { ...s, version: data.version } : s
));
if (selectedServer && selectedServer.id === data.serverId) {
setSelectedServer(prev => ({ ...prev, version: data.version }));
}
setIsEditingVersion(false);
setNewVersion('');
// Show success message
setAlertModal({
open: true,
title: "Version Changed! ✅",
message: `Server version has been updated to ${data.version}. Billing date remains unchanged.`,
type: "success"
});
}
break;
case 'minecraft_settings':
if (data.serverId === selectedServer?.id) {
setMcSettings(data.settings);
setIsMcSettingsLoading(false);
}
break;
case 'minecraft_settings_updated':
if (data.serverId === selectedServer?.id) {
setAlertModal({
open: true,
title: "Settings Updated!",
message: "Quick settings updated. Restart server to apply.",
type: "success"
});
setIsMcSettingsLoading(false);
}
break;
case 'server_deleted':
if (data.serverId) {
setServers(prev => prev.filter(s => s.id !== data.serverId));
if (selectedServer && selectedServer.id === data.serverId) {
setSelectedServer(null);
setServerLogs('');
disconnectTerminal();
}
}
break;
case 'server_logs':
setServerLogs(data.logs || '');
break;
case 'server_metrics':
if (data.serverId === selectedServer?.id && data.metrics) {
const processedMetrics = processMetrics(data.metrics);
setServerMetrics(processedMetrics);
const now = new Date();
const timeLabel = now.toLocaleTimeString();
setMetricsHistory(prev => {
const newCpuHistory = [...prev.cpu.slice(-59), {
value: processedMetrics.cpu.value,
time: timeLabel,
percentage: processedMetrics.cpu_percentage || processedMetrics.cpu.percentage
}];
const newMemoryHistory = [...prev.memory.slice(-59), {
value: processedMetrics.memory.value,
time: timeLabel,
percentage: processedMetrics.memory_percentage || processedMetrics.memory.percentage
}];
return {
cpu: newCpuHistory,
memory: newMemoryHistory
};
});
}
break;
case 'credits_balance':
setUserCredits(data.credits || 0);
break;
case 'credits_added':
setUserCredits(data.new_balance || userCredits);
break;
case 'demo_eligibility':
// Demo eligibility is handled in ServerCreationForm
break;
case 'demo_converted':
setUserCredits(data.credits || userCredits);
// Refresh servers list
sendMessageRef.current?.({ type: 'get_servers' });
// Show success message in custom themed popup
setAlertModal({
open: true,
title: 'Conversion Successful',
message: `Demo server converted successfully! Charged ${data.credits_charged} credits. New balance: ${data.credits} credits.`,
type: 'success'
});
break;
case 'server_creation_progress':
// Hide progress if no active creation found (after reconnection check)
if (data.phase === 'Unknown' && data.progress === 0 && data.message === 'No active server creation found') {
setServerCreationProgress(prev => ({ ...prev, visible: false }));
break;
}
setServerCreationProgress({
visible: true,
phase: data.phase || '',
progress: data.progress || 0,
message: data.message || 'Creating server...'
});
// Hide notification when progress reaches 100%
if (data.progress >= 100) {
setTimeout(() => {
setServerCreationProgress(prev => ({ ...prev, visible: false }));
}, 2000);
}
break;
case 'port_exposed':
// Refresh servers list to get updated ports
sendMessageRef.current?.({
type: 'get_servers'
});
// Show success message if available
if (data.message) {
setAlertModal({
open: true,
title: "Port Exposed! ✅",
message: data.message,
type: "success"
});
}
break;
case 'port_removed':
// Refresh servers list to get updated ports
sendMessageRef.current?.({
type: 'get_servers'
});
// Show success message if available
if (data.message) {
setAlertModal({
open: true,
title: "Port Removed! ✅",
message: data.message,
type: "success"
});
}
break;
case 'auth_error':
console.error('Authentication error:', data.error);
logout();
break;
case 'error':
console.error('WebSocket error:', data.error);
if (data.error === 'Not authenticated') {
sendMessageRef.current?.({
type: 'authenticate',
token: token
});
} else if (data.error && (data.error.toLowerCase().includes('convert demo') || data.error.toLowerCase().includes('convert_demo') || (data.error.toLowerCase().includes('demo') && data.error.toLowerCase().includes('paid')))) {
// Show custom themed popup for demo conversion errors
setAlertModal({
open: true,
title: 'Conversion Failed',
message: data.error || 'Failed to convert demo server to paid',
type: 'error'
});
} else if (data.error && data.error !== "0" && data.error !== 0) {
// Show popup for other errors
setAlertModal({
open: true,
title: 'Error',
message: data.error || 'An error occurred',
type: 'error'
});
}
break;
case '2fa_setup_details':
setTwoFactorData(data);
setShow2FASetup(true);
break;
case '2fa_enabled':
setShow2FASetup(false);
setAlertModal({ open: true, title: 'Success', message: '2FA enabled successfully!', type: 'success' });
loadUserProfile();
break;
case '2fa_disabled':
setAlertModal({ open: true, title: 'Success', message: '2FA disabled successfully!', type: 'success' });
loadUserProfile();
break;
default:
console.log('Unhandled message type:', data.type);
}
}, [selectedServer, activeServerDetailTab, isTerminalConnected, processMetrics, token, loadUserProfile, serverCreationProgress.visible, userCredits, logout]);
const { ws: dashboardWs, sendMessage: sendDashboardMessage, connectionStatus: dashboardStatus, lastJsonMessage: lastMessage } = useWebSocket(
`${WS_URL}?token=${token}`,
handleWebSocketMessage
);
// Update ref when sendDashboardMessage changes to avoid circular dependency
useEffect(() => {
sendMessageRef.current = sendDashboardMessage;
}, [sendDashboardMessage]);
// Request server creation progress after reconnection if creation is in progress
useEffect(() => {
if (dashboardStatus === 'connected' && serverCreationProgress.visible && sendDashboardMessage) {
// Small delay to ensure authentication is complete
const timeoutId = setTimeout(() => {
sendDashboardMessage({
type: 'get_server_creation_progress'
});
}, 500);
return () => clearTimeout(timeoutId);
}
}, [dashboardStatus, serverCreationProgress.visible, sendDashboardMessage]);
// Request metrics when server is selected and periodically update
useEffect(() => {
if (!selectedServer || !sendDashboardMessage || dashboardStatus !== 'connected') {
// Reset metrics when no server selected
setServerMetrics({ cpu: '0m', memory: '0Mi', cpu_percentage: 0, memory_percentage: 0 });
setMetricsHistory({ cpu: [], memory: [] });
return;
}
// Request metrics immediately when server is selected
const requestMetrics = () => {
sendDashboardMessage({
type: 'get_server_metrics',
serverId: selectedServer.id
});
};
// Request metrics immediately
requestMetrics();
// Set up interval to request metrics every 2 seconds
const metricsInterval = setInterval(requestMetrics, 2000);
return () => {
clearInterval(metricsInterval);
};
}, [selectedServer, sendDashboardMessage, dashboardStatus]);
// Request Quick settings when tab is active
useEffect(() => {
if (activeServerDetailTab === 'minecraft-settings' && selectedServer && dashboardStatus === 'connected' && !mcSettings) {
setIsMcSettingsLoading(true);
sendDashboardMessage({
type: 'get_minecraft_settings',
serverId: selectedServer.id
});
}
}, [activeServerDetailTab, selectedServer, dashboardStatus, sendDashboardMessage, mcSettings]);
// Clear Quick settings when server changes
useEffect(() => {
setMcSettings(null);
}, [selectedServer]);
const terminalRef = useRef(null);
const connectTerminal = () => {
if (selectedServer) {
setIsTerminalConnected(true);
setTerminalOutput("Connected to server terminal. Type commands below.");
}
};
const sendTerminalCommand = (command) => {
if (selectedServer && dashboardStatus === 'connected') {
sendDashboardMessage({
type: 'exec_command',
serverId: selectedServer.id,
command: command
});
setTerminalInput('');
}
};
const handleTerminalKeyDown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
sendTerminalCommand(terminalInput);
}
};
const disconnectTerminal = () => {
setIsTerminalConnected(false);
setTerminalOutput('');
};
const handleCreateServer = async (serverConfig) => {
setIsCreatingServer(true);
sendDashboardMessage({
type: 'create_server',
name: serverConfig.name,
serverType: serverConfig.type,
image: serverConfig.image,
version: serverConfig.version,
cpu: serverConfig.cpu,
ram: serverConfig.ram,
storage: serverConfig.storage,
ports: serverConfig.ports,
is_demo: serverConfig.is_demo || false
});
};
const handleDeleteServer = async (serverId) => {
setDeleteServerModal({ open: true, serverId: serverId });
};
const confirmDeleteServer = async () => {
if (deleteServerModal.serverId) {
sendDashboardMessage({
type: 'delete_server',
serverId: deleteServerModal.serverId
});
setDeleteServerModal({ open: false, serverId: null });
}
};
// Calculate server cost based on specs (matching backend logic)
const calculateServerCost = (server) => {
// Get CPU cores
let cpuCores = 1;
if (server.cpu) {
if (typeof server.cpu === 'string' && server.cpu.includes('vCPU')) {
cpuCores = parseInt(server.cpu.split(' ')[0]);
} else {
cpuCores = parseInt(server.cpu);
}
}
// Get RAM GB
let ramGb = 1;
if (server.ram) {
if (typeof server.ram === 'string' && server.ram.includes('GB')) {
ramGb = parseInt(server.ram.replace('GB', ''));
} else {
ramGb = parseInt(server.ram);
}
}
// Get Storage GB
let storageGb = 10;
if (server.storage) {
if (typeof server.storage === 'string' && server.storage.includes('GB')) {
storageGb = parseInt(server.storage.replace('GB', ''));
}
else {
storageGb = parseInt(server.storage);
}
}
// Type multipliers
const typeMultiplier = {
'vps': pricing.multiplier_vps || 1.0,
'game': pricing.multiplier_game || 0.8,
'website': pricing.multiplier_website || 1.5,
'database': pricing.multiplier_database || 1.3
}[server.type] || 1.0;
// Calculate costs (matching backend: 1 vCPU = 35 credits, 1GB RAM = 35 credits, 1GB Storage = 5 credits)
const cpuCost = cpuCores * (pricing.price_cpu || 35);
const ramCost = ramGb * (pricing.price_ram || 35);
const storageCost = storageGb * (pricing.price_storage || 5);
const baseCost = 0;
// Total cost
const totalCost = Math.round((baseCost + cpuCost + ramCost + storageCost) * typeMultiplier);
return totalCost;
};
const handleConvertDemoToPaid = async (serverId) => {
// Find the server in the servers list
const server = servers.find(s => s.id === serverId);
if (!server) {
setAlertModal({
open: true,
title: 'Error',
message: 'Server not found',
type: 'error'
});
return;
}
// Calculate cost
const cost = calculateServerCost(server);
// Open the custom modal
setConvertDemoModal({
open: true,
server: server,
cost: cost
});
};
const confirmConvertDemoToPaid = () => {
if (!convertDemoModal.server) return;
sendDashboardMessage({
type: 'convert_demo_to_paid',
serverId: convertDemoModal.server.id
});
// Close modal
setConvertDemoModal({ open: false, server: null, cost: 0 });
};
const handleServerAction = async (serverId, action) => {
if (action === 'restart') {
sendDashboardMessage({
type: 'restart_server',
serverId: serverId
});
} else {
sendDashboardMessage({
type: action + '_server',
serverId: serverId
});
}
};
const fetchServerLogs = async (serverId) => {
sendDashboardMessage({
type: 'get_server_logs',
serverId: serverId
});
};
const handleExposePort = async (serverId, internalPort, protocol = 'TCP') => {
sendDashboardMessage({
type: 'expose_port',
serverId: serverId,
internalPort: parseInt(internalPort),
protocol: protocol
});
};
const handleRemovePort = async (serverId, internalPort, protocol = 'TCP') => {
sendDashboardMessage({
type: 'remove_port',
serverId: serverId,
internalPort: parseInt(internalPort),
protocol: protocol
});
};
// Helper function to parse CPU value (e.g., "2 vCPU" -> 2)
const parseCpuValue = (cpuStr) => {
if (!cpuStr) return 1;
const match = cpuStr.match(/(\d+)/);
return match ? parseInt(match[1]) : 1;
};
// Helper function to parse RAM value (e.g., "4GB" -> 4)
const parseRamValue = (ramStr) => {
if (!ramStr) return 1;
const match = ramStr.match(/(\d+)\s*GB/i);
return match ? parseInt(match[1]) : 1;
};
// Helper function to parse Storage value (e.g., "50GB" -> 50)
const parseStorageValue = (storageStr) => {
if (!storageStr) return 10;
const match = storageStr.match(/(\d+)\s*GB/i);
return match ? parseInt(match[1]) : 10;
};
// Initialize resource values when server is selected
useEffect(() => {
if (selectedServer) {
setResourceValues({
cpu: parseCpuValue(selectedServer.cpu),
ram: parseRamValue(selectedServer.ram),
storage: parseStorageValue(selectedServer.storage)
});
}
}, [selectedServer]);
// Function to handle adjusting server parameters
const handleAdjustServerParameters = async () => {
if (!selectedServer) return;
try {
const token = localStorage.getItem('anchorhost_token');
const response = await fetch(`${API_BASE_URL}/api/servers/${selectedServer.id}/adjust-parameters`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
cpu: `${resourceValues.cpu} vCPU`,
ram: `${resourceValues.ram}GB`,
storage: `${resourceValues.storage}GB`
})
});
const data = await response.json();
if (response.ok && data.success) {
let message = 'Server parameters adjusted successfully.';
if (data.is_upgrade && data.credits_charged > 0) {
message = `Server upgraded successfully. Charged ${data.credits_charged} credits (difference between new plan: ${data.new_monthly_cost} credits/month and current plan: ${data.current_monthly_cost} credits/month).`;
} else if (!data.is_upgrade) {
message = `Server downgraded successfully. No credits were charged. Your new monthly cost is ${data.new_monthly_cost} credits (reduced from ${data.current_monthly_cost} credits).`;
} else {
message = `Server parameters adjusted successfully. No additional charge.`;
}
setAlertModal({
open: true,
title: 'Success',
message: message,
type: 'success'
});
setIsEditingResources(false);
// Refresh servers to get updated values
refreshServers();
// Refresh credits
refreshCredits();
} else {
setAlertModal({
open: true,
title: 'Error',
message: data.error || 'Failed to adjust server parameters',
type: 'error'
});
}
} catch (error) {
setAlertModal({
open: true,
title: 'Error',
message: `Failed to adjust server parameters: ${error.message}`,
type: 'error'
});
}
};
const handleUpdateMcSettings = (newSettings) => {
if (!selectedServer) return;
setIsMcSettingsLoading(true);
sendDashboardMessage({
type: 'update_minecraft_settings',
serverId: selectedServer.id,
settings: newSettings
});
};
const refreshCredits = () => {
sendDashboardMessage({
type: 'get_credits'
});
};
const handleAddCredits = (amount) => {
sendDashboardMessage({
type: 'add_credits',
amount: amount
});
};
const refreshServers = () => {
setIsLoading(true);
sendDashboardMessage({
type: 'get_servers'
});
};
// Auto-refresh servers when My Servers tab is active
useEffect(() => {
if (activeTab === 'servers' && sendDashboardMessage) {
// Refresh immediately when tab becomes active
sendDashboardMessage({
type: 'get_servers'
});
// Set up interval to refresh every 10 seconds
const interval = setInterval(() => {
sendDashboardMessage({
type: 'get_servers'
});
}, 10000);
// Cleanup interval when tab changes or component unmounts
return () => clearInterval(interval);
}
}, [activeTab, sendDashboardMessage]);
const handleTabChange = (tab) => {
if (tab === 'admin') {
navigate('/admin');
return;
}
setActiveTab(tab);
setIsMobileSidebarOpen(false);
};
const settingsInputClass =
'w-full rounded-xl px-4 py-3 text-sm border border-dash-border bg-brightBlack/60 text-dash-text placeholder:text-dash-textMuted/45 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:border-primary/35 transition-colors';
// Calculate total resources used
const totalResources = servers.reduce((acc, server) => {
const cpu = parseInt(server.cpu) || 0;
const ram = parseInt(server.ram) || 0;
const storage = parseInt(server.storage) || 0;
return {
cpu: acc.cpu + cpu,
ram: acc.ram + ram,
storage: acc.storage + storage
};
}, { cpu: 0, ram: 0, storage: 0 });
// Check if port is default (cannot be removed)
const isDefaultPort = (server, port) => {
if (!server || !port) return false;
const defaultPorts = {
'website': [{ internal: 80, protocol: 'TCP' }],
'game': {
'minecraft': [{ internal: 25565, protocol: 'TCP' }],
'fivem': [{ internal: 30120, protocol: 'TCP' }],
'cs2': [{ internal: 27015, protocol: 'TCP' }],
'rust': [{ internal: 28015, protocol: 'TCP' }],
'ark': [{ internal: 7777, protocol: 'TCP' }],
'valheim': [{ internal: 2456, protocol: 'TCP' }]
},
'vps': {
'windows': [{ internal: 3389, protocol: 'TCP' }],
'default': [{ internal: 22, protocol: 'TCP' }]
},
'database': {
'mysql': [{ internal: 3306, protocol: 'TCP' }],
'postgresql': [{ internal: 5432, protocol: 'TCP' }],
'mongodb': [{ internal: 27017, protocol: 'TCP' }],
'redis': [{ internal: 6379, protocol: 'TCP' }]
}
};
if (server.type === 'website') {
return defaultPorts.website.some(p => p.internal === port.internal && p.protocol === port.protocol);
} else if (server.type === 'game') {
const gamePorts = defaultPorts.game[server.image] || [];
return gamePorts.some(p => p.internal === port.internal && p.protocol === port.protocol);
} else if (server.type === 'vps') {
const vpsPorts = defaultPorts.vps[server.image] || defaultPorts.vps.default;
return vpsPorts.some(p => p.internal === port.internal && p.protocol === port.protocol);
} else if (server.type === 'database') {
const dbPorts = defaultPorts.database[server.image] || [];
return dbPorts.some(p => p.internal === port.internal && p.protocol === port.protocol);
}
return false;
};
// Get port tooltip information for common ports
const getPortTooltip = (internalPort, externalPort) => {
const portInfo = {
22: {
name: 'SSH (Secure Shell)',
description: 'Used for secure remote access to servers',
connection: `Connect using: ssh root@${selectedServer?.ip || 'SERVER_IP'} -p ${externalPort}`
},
80: {
name: 'HTTP',
description: 'Web server port for unencrypted HTTP traffic',
connection: `Access at: http://${selectedServer?.ip || 'SERVER_IP'}:${externalPort}`
},
443: {
name: 'HTTPS',
description: 'Web server port for encrypted HTTPS traffic',
connection: `Access at: https://${selectedServer?.ip || 'SERVER_IP'}:${externalPort}`
},
3306: {
name: 'MySQL',
description: 'MySQL database server port',
connection: `Connect using: mysql -h ${selectedServer?.ip || 'SERVER_IP'} -P ${externalPort} -u USERNAME -p`
},
5432: {
name: 'PostgreSQL',
description: 'PostgreSQL database server port',
connection: `Connect using: psql -h ${selectedServer?.ip || 'SERVER_IP'} -p ${externalPort} -U USERNAME`
},
27017: {
name: 'MongoDB',
description: 'MongoDB database server port',
connection: `Connect using: mongosh mongodb://${selectedServer?.ip || 'SERVER_IP'}:${externalPort}`
},
6379: {
name: 'Redis',
description: 'Redis in-memory data store port',
connection: `Connect using: redis-cli -h ${selectedServer?.ip || 'SERVER_IP'} -p ${externalPort}`
},
3389: {
name: 'RDP (Remote Desktop)',
description: 'Windows Remote Desktop Protocol port',
connection: `Connect using Remote Desktop Client to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}`
},
25565: {
name: 'Minecraft',
description: 'Minecraft server port',
connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}`
},
30120: {
name: 'FiveM',
description: 'FiveM game server port',
connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}`
},
27015: {
name: 'CS2/Steam',
description: 'Counter-Strike 2 / Steam game server port',
connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}`
},
28015: {
name: 'Rust',
description: 'Rust game server port',
connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}`
},
7777: {
name: 'ARK',
description: 'ARK: Survival Evolved game server port',
connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}`
},
2456: {
name: 'Valheim',
description: 'Valheim game server port',
connection: `Connect in-game to ${selectedServer?.ip || 'SERVER_IP'}:${externalPort}`
}
};
return portInfo[internalPort] || null;
};
const getPortServiceName = (server, port, isDefault) => {
if (port.name) return port.name;
const tooltip = getPortTooltip(parseInt(port.internal, 10), port.external);
if (tooltip) return tooltip.name;
if (isDefault) return 'Main Service';
return 'Custom Service';
};
const getNetworkConnectionHint = (server) => {
switch (server?.type) {
case 'game':
return 'Paste the public address into your game client\'s Direct Connect or Add Server field.';
case 'vps':
return 'Use SSH, RDP, or your remote tool with the public IP and external port below.';
case 'database':
return 'Point your database client at the public IP and external port — not the internal one.';
case 'website':
return 'Open the public address in your browser. HTTP services are mapped automatically.';
default:
return 'Copy the full IP:port address and use it in your application or client.';
}
};
const submitNetworkExposePort = () => {
if (!selectedServer || !newPortInternal.trim()) return;
const val = parseInt(newPortInternal, 10);
if (val >= 1 && val <= 65535) {
handleExposePort(selectedServer.id, newPortInternal, newPortProtocol);
setNewPortInternal('');
} else {
setAlertModal({ open: true, title: 'Invalid Port', message: 'Port must be between 1 and 65535', type: 'error' });
}
};
// Port Tooltip Component with fixed positioning to avoid overflow clipping
const PortTooltip = ({ portTooltip }) => {
const [isVisible, setIsVisible] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const iconRef = useRef(null);
const hideTimeoutRef = useRef(null);
const handleMouseEnter = () => {
// Clear any pending hide timeout
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
if (iconRef.current) {
const rect = iconRef.current.getBoundingClientRect();
// Position tooltip above the icon, centered horizontally
// Position at the top of the icon, then translate up by 100% of tooltip height plus 8px gap
setPosition({
top: rect.top,
left: rect.left + rect.width / 2 // Center horizontally on icon
});
setIsVisible(true);
}
};
const handleMouseLeave = () => {
// Add a small delay before hiding to allow smooth transition to tooltip
hideTimeoutRef.current = setTimeout(() => {
setIsVisible(false);
hideTimeoutRef.current = null;
}, 100);
};
const handleTooltipMouseEnter = () => {
// Clear any pending hide timeout when mouse enters tooltip
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
setIsVisible(true);
};
const handleTooltipMouseLeave = () => {
setIsVisible(false);
};
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
}
};
}, []);
const tooltipContent = isVisible ? (
<div
className="ah-tooltip ah-tooltip-detail fixed w-72 max-w-[calc(100vw-2rem)] p-4 text-sm z-[9999]"
style={{
top: `${position.top}px`,
left: `${position.left}px`,
transform: 'translate(-50%, calc(-100% - 8px))'
}}
onMouseEnter={handleTooltipMouseEnter}
onMouseLeave={handleTooltipMouseLeave}
>
<div className="ah-tooltip-heading">{portTooltip.name}</div>
<div className="ah-tooltip-description">{portTooltip.description}</div>
<div className="ah-tooltip-code">{portTooltip.connection}</div>
</div>
) : null;
return (
<>
<div
ref={iconRef}
className="group relative inline-block"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<Info className="w-3 h-3 cursor-help" style={{ color: colors.blue }} />
</div>
{tooltipContent && createPortal(tooltipContent, document.body)}
</>
);
};
const dashboardSidebarItems = useMemo(
() => [
{ id: 'servers', label: 'My Servers', icon: Server, color: colors.blue },
{ id: 'create', label: 'Create Server', icon: Plus, color: colors.green },
{ id: 'domains', label: 'Domains', icon: Globe, color: colors.cyan },
{ id: 'store', label: 'Store', icon: ShoppingCart, color: colors.yellow },
{ id: 'tickets', label: 'Support', icon: MessageCircle, color: colors.magenta },
{ id: 'settings', label: 'Settings', icon: Settings, color: colors.magenta },
...(user?.role === 'admin'
? [{ id: 'admin', label: 'Admin Panel', icon: Shield, color: colors.red }]
: []),
],
[user?.role]
);
return (
<div
className="dashboard-app flex h-screen overflow-hidden bg-dash-surface font-sans selection:bg-primary/30 text-dash-text antialiased"
>
<aside
className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-all duration-300 ease-in-out md:relative md:translate-x-0 border-r border-dash-border bg-dash-surfaceElevated/50 backdrop-blur-xl pt-[env(safe-area-inset-top,0px)] ${
isMobileSidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex flex-col h-full min-h-0">
<div className="p-6 border-b border-dash-border bg-black/10">
<div className="flex items-center space-x-3 group cursor-pointer" onClick={() => navigate('/')}>
<div className="p-2 rounded-xl bg-primary/10 border border-primary/20 transition-all group-hover:scale-110 shadow-lg shadow-primary/5">
<img src={branding.logoPath} alt="" className="h-7 w-auto shrink-0" />
</div>
<div className="min-w-0">
<h1 className="text-xl font-bold tracking-tight text-dash-text leading-tight">Dashboard</h1>
<p className="text-[10px] font-bold text-dash-textMuted/40 uppercase tracking-[0.2em]">Control Center</p>
</div>
</div>
</div>
<div className="px-6 py-4 flex flex-col gap-2 border-b border-dash-border bg-black/5 shrink-0">
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-wider px-3 py-2.5 rounded-xl justify-center border border-dash-border bg-black/20">
{dashboardStatus === 'connected' ? (
<Wifi className="w-3.5 h-3.5 shrink-0" style={{ color: colors.green }} />
) : (
<WifiOff className="w-3.5 h-3.5 shrink-0" style={{ color: colors.red }} />
)}
<span style={{ color: dashboardStatus === 'connected' ? colors.green : colors.red }}>
{dashboardStatus === 'connected' ? 'Live' : 'Offline'}
</span>
</div>
<UnifiedTooltip content={`${userCredits.toLocaleString()} credits`} placement="bottom">
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-wider px-3 py-2.5 rounded-xl w-full justify-center border border-dash-border bg-black/20">
<Coins className="w-3.5 h-3.5 shrink-0" style={{ color: colors.yellow }} />
<span style={{ color: colors.brightWhite }} className="truncate min-w-0">
{calculateCurrencyValue(userCredits)} {getCurrencySymbol(userCredits)}
</span>
</div>
</UnifiedTooltip>
</div>
<nav className="flex-1 overflow-y-auto py-6 px-4 space-y-8 min-h-0 custom-scrollbar bg-black/5">
<div className="space-y-2">
<div className="space-y-1">
{dashboardSidebarItems.map((tab) => {
const Icon = tab.icon;
const active = activeTab === tab.id;
return (
<button
key={tab.id}
type="button"
onClick={() => handleTabChange(tab.id)}
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-200 group text-sm font-bold border ${
active
? 'bg-primary/10 border-primary/25 text-dash-text shadow-lg shadow-primary/5'
: 'border-transparent text-dash-textMuted/60 hover:bg-white/5 hover:text-dash-text hover:border-dash-border/10'
}`}
>
<div className={`p-2 rounded-lg transition-all duration-300 ${active ? 'bg-primary/20 scale-110' : 'bg-white/5 group-hover:bg-white/10 group-hover:scale-110'}`}>
<Icon className={`w-4 h-4 transition-transform ${active ? 'text-primary' : 'opacity-50'} group-hover:rotate-3`} />
</div>
<span className="text-left truncate">{tab.label}</span>
</button>
);
})}
</div>
</div>
<div className="space-y-4">
<p className="px-4 text-[10px] font-bold text-dash-textMuted/40 uppercase tracking-[0.2em]">Resources</p>
<div className="space-y-1">
<button type="button" onClick={() => navigate('/')} className="w-full flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-200 group text-sm font-bold border border-transparent text-dash-textMuted/60 hover:bg-white/5 hover:text-dash-text hover:border-dash-border/10">
<div className="p-2 rounded-lg bg-white/5 group-hover:bg-white/10 transition-all duration-300 group-hover:scale-110">
<Home className="w-4 h-4 opacity-50 transition-transform group-hover:rotate-3" />
</div>
<span>Home</span>
</button>
<button type="button" onClick={() => navigate('/documentation')} className="w-full flex items-center space-x-3 px-4 py-3 rounded-xl transition-all duration-200 group text-sm font-bold border border-transparent text-dash-textMuted/60 hover:bg-white/5 hover:text-dash-text hover:border-dash-border/10">
<div className="p-2 rounded-lg bg-white/5 group-hover:bg-white/10 transition-all duration-300 group-hover:scale-110">
<BookOpen className="w-4 h-4 opacity-50 transition-transform group-hover:rotate-3" />
</div>
<span>Documentation</span>
</button>
</div>
</div>
</nav>
<div className="p-4 border-t border-dash-border bg-black/20 pb-[calc(1.5rem+env(safe-area-inset-bottom,0px))] shrink-0">
<div className="flex items-center p-3 rounded-2xl border border-dash-border bg-dash-surfaceElevated/50">
<div className="w-10 h-10 rounded-xl bg-primary/10 border border-primary/25 flex items-center justify-center font-bold text-sm text-primary shadow-inner">
{user?.username ? user.username.substring(0, 2).toUpperCase() : '??'}
</div>
<div className="ml-3 flex-1 min-w-0">
<p className="text-sm font-bold truncate text-dash-text">{user?.username}</p>
<p className="text-[10px] uppercase font-bold tracking-[0.15em] text-dash-textMuted/40">{user?.role || 'Account'}</p>
</div>
<button
type="button"
onClick={logout}
className="p-2.5 rounded-xl hover:bg-red-500/10 text-dash-textMuted/40 hover:text-red-500 transition-all group"
title="Logout"
>
<LogOut className="w-4 h-4 group-hover:scale-110 group-hover:-translate-x-0.5 transition-transform" />
</button>
</div>
</div>
</div>
</aside>
<main className="flex-1 flex flex-col min-w-0 overflow-hidden relative">
<header className="md:hidden flex items-center justify-between p-4 border-b border-dash-border bg-dash-surfaceElevated/80 backdrop-blur-md pt-[calc(1rem+env(safe-area-inset-top,0px))]">
<div className="flex items-center space-x-3 min-w-0">
<div className="p-2 rounded-lg bg-primary/10 border border-primary/20 shadow-sm">
<Server className="w-5 h-5 text-primary" />
</div>
<span className="font-bold text-dash-text tracking-tight truncate">Dashboard</span>
</div>
<button type="button" onClick={() => setIsMobileSidebarOpen(!isMobileSidebarOpen)} className="p-2.5 rounded-xl text-dash-textMuted hover:bg-white/10 active:scale-95 transition-all">
<Menu className="w-6 h-6" />
</button>
</header>
<div className="flex-1 overflow-y-auto custom-scrollbar pt-[env(safe-area-inset-top,0px)]">
<div className="p-4 md:p-8 lg:p-12 max-w-[1600px] mx-auto w-full animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="w-full">
{activeTab === 'servers' && (
<div className="w-full space-y-6">
<DashboardViewShell>
<PageHeaderBar
accentColor={colors.blue}
icon={Server}
title="My Servers"
description="Manage and monitor your deployed servers"
actions={
<ActionButton variant="secondary" onClick={refreshServers} aria-label="Refresh server list">
<RefreshCw className="w-4 h-4 shrink-0" aria-hidden />
<span>Refresh</span>
</ActionButton>
}
/>
<div className={dashLayout.bodyPadding}>
{servers.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<StatTile icon={Cpu} label="Total CPU" value={`${totalResources.cpu} vCPU`} accentColor={colors.blue} />
<StatTile icon={MemoryStick} label="Total RAM" value={`${totalResources.ram} GB`} accentColor={colors.green} />
<StatTile icon={HardDrive} label="Total Storage" value={`${totalResources.storage} GB`} accentColor={colors.yellow} />
</div>
)}
{isLoading ? (
<DashboardLoadingSpinner label="Loading servers" />
) : servers.length === 0 ? (
<DashboardEmptyState
icon={Server}
title="No servers yet"
description="Create a server to deploy games, sites, or databases on your infrastructure."
action={
<ActionButton
variant="primary"
onClick={() => setActiveTab('create')}
className="!px-4 !py-2 !min-h-0 sm:!min-h-0"
>
Create your first server
</ActionButton>
}
/>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{servers.map((server) => (
<ServerCard
key={server.id}
server={server}
pricing={pricing}
userRole={user?.role}
calculateCurrencyValue={calculateCurrencyValue}
getCurrencySymbol={getCurrencySymbol}
onConvertDemo={handleConvertDemoToPaid}
onOpenConsole={(targetServer) => {
setSelectedServer(targetServer);
setActiveServerDetailTab('overview');
fetchServerLogs(targetServer.id);
}}
onServerAction={handleServerAction}
/>
))}
</div>
)}
</div>
</DashboardViewShell>
</div>
)}
{activeTab === 'create' && (
<ServerCreationForm
onCreateServer={handleCreateServer}
isCreating={isCreatingServer}
userCredits={userCredits}
selectedCurrency={selectedCurrency}
onCurrencyChange={setSelectedCurrency}
wsClient={dashboardWs}
pricing={pricing}
/>
)}
{activeTab === 'store' && (
<div className="w-full space-y-6">
<StoreSection
userCredits={userCredits}
onAddCredits={handleAddCredits}
onRefreshCredits={refreshCredits}
selectedCurrency={selectedCurrency}
onCurrencyChange={setSelectedCurrency}
/>
</div>
)}
{activeTab === 'domains' && (
<div className="w-full space-y-6">
<DashboardViewShell>
<PageHeaderBar
accentColor={colors.cyan}
icon={Globe}
title="Domain manager"
description="Manage your subdomains and custom domains"
actions={
<ActionButton
variant="secondary"
onClick={() => {
sendDashboardMessage({ type: 'list_domains', includeServerless: true });
sendDashboardMessage({ type: 'list_custom_domains' });
}}
aria-label="Refresh domain list"
>
<RefreshCw className="w-4 h-4 shrink-0" aria-hidden />
<span>Refresh</span>
</ActionButton>
}
/>
<div className={dashLayout.bodyPadding}>
<DomainManager
userId={user?.id}
username={user?.username}
servers={servers}
sendMessage={sendDashboardMessage}
activeTab={activeTab}
showAlert={showAlert}
showConfirm={showConfirm}
/>
</div>
</DashboardViewShell>
</div>
)}
{activeTab === 'tickets' && (
<TicketSystem />
)}
{activeTab === 'settings' && (
<div className="w-full space-y-6">
<DashboardViewShell>
<PageHeaderBar
accentColor={colors.magenta}
icon={Settings}
title="Settings"
description="Manage your account and preferences"
/>
<div className={dashLayout.bodyPadding}>
<SectionCard title="Security" icon={Shield} color={colors.green}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<SettingsPanel
title="Email verification"
description="Confirm your email to secure account recovery and billing notices."
>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-3 min-w-0">
<div
className="p-2.5 rounded-xl shrink-0 border border-dash-border"
style={{ backgroundColor: `${userData?.email_verified ? colors.green : colors.yellow}18` }}
>
{userData?.email_verified ? (
<CheckCircle className="w-5 h-5 text-brightGreen" aria-hidden />
) : (
<AlertCircle className="w-5 h-5 text-brightYellow" aria-hidden />
)}
</div>
<span
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border border-dash-border"
style={{
backgroundColor: userData?.email_verified ? `${colors.green}18` : `${colors.yellow}18`,
color: userData?.email_verified ? colors.brightGreen : colors.brightYellow,
}}
>
{userData?.email_verified ? 'Verified' : 'Pending'}
</span>
</div>
{!userData?.email_verified && (
<ActionButton
variant="secondary"
className="shrink-0 !px-4 !py-2 !min-h-0 sm:!min-h-0"
onClick={async () => {
try {
const token = localStorage.getItem('anchorhost_token');
const response = await fetch(`${API_BASE_URL}/api/user/resend-verification`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (data.success) {
setAlertModal({
open: true,
title: 'Verification email sent',
message:
'A new verification email has been sent. Please check your inbox and spam folder.',
type: 'success',
});
} else {
setAlertModal({
open: true,
title: 'Failed to send verification',
message: `Failed to send verification email: ${data.error || 'Unknown error'}`,
type: 'error',
});
}
} catch (error) {
console.error('Error resending verification:', error);
setAlertModal({
open: true,
title: 'Error',
message:
'An error occurred while sending the verification email. Please try again.',
type: 'error',
});
}
}}
>
<Mail className="w-4 h-4 shrink-0" aria-hidden />
<span>Resend email</span>
</ActionButton>
)}
</div>
</SettingsPanel>
<SettingsPanel
title="Two-factor authentication"
description="Add a second step at sign-in. Recommended for accounts with payment methods."
>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-3">
<div
className="p-2.5 rounded-xl border border-dash-border"
style={{ backgroundColor: `${colors.magenta}18` }}
>
<Lock className="w-5 h-5" style={{ color: colors.magenta }} aria-hidden />
</div>
<p className="text-sm text-dash-textMuted">
{userData.two_factor_enabled ? '2FA is on for this account.' : '2FA is off.'}
</p>
</div>
{userData.two_factor_enabled ? (
<ActionButton
variant="danger"
className="shrink-0 !px-4 !py-2 !min-h-0 sm:!min-h-0"
onClick={() => {
const otp = window.prompt('Enter your authentication code to disable 2FA:');
if (otp) {
sendDashboardMessage({
type: '2fa_disable',
otp_code: otp,
});
}
}}
>
Disable 2FA
</ActionButton>
) : (
<ActionButton
variant="success"
className="shrink-0 !px-4 !py-2 !min-h-0 sm:!min-h-0"
onClick={() => sendDashboardMessage({ type: '2fa_setup' })}
>
Enable 2FA
</ActionButton>
)}
</div>
</SettingsPanel>
</div>
</SectionCard>
<SectionCard title="Sign-in & profile" icon={User} color={colors.blue}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<SettingsPanel
title="Email address"
description="Used for login and important account notifications."
>
<div className="flex flex-col gap-3">
<div className="rounded-lg border border-dash-border bg-brightBlack/40 px-3 py-2.5 text-sm">
<span className="text-dash-textMuted">Current </span>
<span className="text-dash-text font-medium break-all">{user?.email || 'Not set'}</span>
{user?.email_verified && (
<span className="ml-2 inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-green-500/15 text-brightGreen border border-green-500/25">
Verified
</span>
)}
</div>
<div>
<UnifiedTooltip
content={
user?.google_id
? 'This account uses Google sign-in. Email is managed in your Google account.'
: 'Change the email you use to sign in. You may need to verify the new address.'
}
multiline
>
<span className="inline-flex">
<ActionButton
variant="success"
className="!px-4 !py-2 !min-h-0 sm:!min-h-0"
disabled={!!user?.google_id}
onClick={() => {
if (user?.google_id) return;
setEmailModal({
open: true,
currentEmail: user?.email || '',
title: 'Change Email Address',
label: 'New Email Address',
placeholder: 'Enter new email address',
});
}}
>
Change email
</ActionButton>
</span>
</UnifiedTooltip>
</div>
</div>
</SettingsPanel>
<SettingsPanel
title="Password"
description={
user?.password_changed_at
? `Last updated ${new Date(user.password_changed_at).toLocaleString()}`
: 'Use a strong password you do not reuse elsewhere.'
}
>
<div className="flex flex-col gap-3">
<div className="rounded-lg border border-dash-border bg-brightBlack/40 px-3 py-2.5 text-sm">
{user?.google_id ? (
<p className="text-dash-textMuted">
No {branding.name} password — you sign in with Google.
</p>
) : (
<>
<span className="text-dash-textMuted">Current password </span>
<span className="text-dash-text font-medium tracking-[0.2em] select-none" aria-hidden>
••••••••
</span>
<span className="sr-only">Hidden for security</span>
</>
)}
</div>
<div>
<UnifiedTooltip
content={
user?.google_id
? `Google sign-in accounts do not use a ${branding.name} password. Manage security in Google.`
: 'Choose a new password. You will need your current password to confirm.'
}
multiline
>
<span className="inline-flex">
<ActionButton
variant="warning"
className="!px-4 !py-2 !min-h-0 sm:!min-h-0"
disabled={!!user?.google_id}
onClick={() => {
if (user?.google_id) return;
setPasswordModal({
open: true,
title: 'Change Password',
requireCurrent: true,
});
}}
>
Change password
</ActionButton>
</span>
</UnifiedTooltip>
</div>
</div>
</SettingsPanel>
</div>
</SectionCard>
<SectionCard title="Contact & billing" icon={MapPin} color={colors.cyan}>
<form
onSubmit={async (e) => {
e.preventDefault();
try {
const selectedCountryInfo = getSelectedCountry();
const profileData = {
first_name: personalInfo.firstName || '',
last_name: personalInfo.lastName || '',
address: personalInfo.address || '',
city: personalInfo.city || '',
country: selectedCountryInfo?.name || '',
postal_code: personalInfo.postalCode || '',
phone: phoneNumber || '',
};
const result = await updateUserProfile(profileData);
setAlertModal({
open: true,
title: result.success ? 'Profile updated' : 'Update failed',
message: result.message,
type: result.success ? 'success' : 'error',
});
if (result.success) {
loadUserProfile();
}
} catch (error) {
console.error('Error updating profile:', error);
setAlertModal({
open: true,
title: 'Error',
message: 'An error occurred while updating your profile. Please try again.',
type: 'error',
});
}
}}
>
<div className="space-y-6">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div>
<p className="text-sm text-dash-textMuted leading-relaxed max-w-xl">
Used for invoices, fraud checks, and support. Keep this information accurate.
</p>
</div>
<UnifiedTooltip
content="Guess country from your network to pre-fill the country selector. You can still edit fields before saving."
multiline
>
<span className="inline-flex shrink-0">
<ActionButton
type="button"
variant="secondary"
className="!px-4 !py-2.5 !min-h-0 sm:!min-h-0"
onClick={detectUserLocation}
disabled={detectingLocation}
>
{detectingLocation ? (
<RefreshCw className="w-4 h-4 animate-spin shrink-0" aria-hidden />
) : (
<MapPin className="w-4 h-4 shrink-0" aria-hidden />
)}
<span>{detectingLocation ? 'Detecting…' : 'Auto-detect country'}</span>
</ActionButton>
</span>
</UnifiedTooltip>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div>
<label className="block text-sm font-medium mb-2 text-dash-textMuted" htmlFor="settings-first-name">
First name
</label>
<input
id="settings-first-name"
type="text"
value={personalInfo.firstName || ''}
onChange={(e) => setPersonalInfo((prev) => ({ ...prev, firstName: e.target.value }))}
className={settingsInputClass}
placeholder="First name"
autoComplete="given-name"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-dash-textMuted" htmlFor="settings-last-name">
Last name
</label>
<input
id="settings-last-name"
type="text"
value={personalInfo.lastName || ''}
onChange={(e) => setPersonalInfo((prev) => ({ ...prev, lastName: e.target.value }))}
className={settingsInputClass}
placeholder="Last name"
autoComplete="family-name"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-dash-textMuted">Country</label>
<Dropdown
value={selectedCountry}
options={countryOptions}
onChange={(value) => {
setSelectedCountry(value);
const country = countryOptions.find((c) => c.value === value);
if (country) {
setPersonalInfo((prev) => ({ ...prev, country: country.name }));
}
}}
placeholder="Select country"
icon={Globe}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-dash-textMuted" htmlFor="settings-phone">
Phone
</label>
<div className="flex items-center gap-2">
<div
className="w-28 sm:w-32 shrink-0 py-3 px-2 rounded-xl text-sm text-center tabular-nums border border-dash-border bg-brightBlack/60 text-dash-text"
aria-hidden
>
{getSelectedCountry()?.dialCode || '—'}
</div>
<input
id="settings-phone"
type="text"
value={phoneNumber || ''}
onChange={(e) => {
const value = e.target.value.replace(/[^\d+]/g, '');
setPhoneNumber(value);
}}
className={`${settingsInputClass} flex-1 min-w-0`}
placeholder="National number"
autoComplete="tel-national"
/>
</div>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium mb-2 text-dash-textMuted" htmlFor="settings-address">
Street address
</label>
<input
id="settings-address"
type="text"
value={personalInfo.address || ''}
onChange={(e) => setPersonalInfo((prev) => ({ ...prev, address: e.target.value }))}
className={settingsInputClass}
placeholder="Street, building, unit"
autoComplete="street-address"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-dash-textMuted" htmlFor="settings-city">
City
</label>
<input
id="settings-city"
type="text"
value={personalInfo.city || ''}
onChange={(e) => setPersonalInfo((prev) => ({ ...prev, city: e.target.value }))}
className={settingsInputClass}
placeholder="City"
autoComplete="address-level2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-dash-textMuted" htmlFor="settings-postal">
Postal code
</label>
<input
id="settings-postal"
type="text"
value={personalInfo.postalCode || ''}
onChange={(e) => setPersonalInfo((prev) => ({ ...prev, postalCode: e.target.value }))}
className={settingsInputClass}
placeholder="Postal code"
autoComplete="postal-code"
/>
</div>
</div>
<div className="flex justify-end pt-2 border-t border-dash-border">
<ActionButton type="submit" variant="success" className="!px-8 !py-3">
<Check className="w-4 h-4 shrink-0" aria-hidden />
<span>Save contact details</span>
</ActionButton>
</div>
</div>
</form>
</SectionCard>
<SectionCard title="Currency & pricing display" icon={Globe} color={colors.blue}>
<SettingsPanel
title="Preferred currency"
description="Prices in the panel and store convert using this currency where supported."
>
<div className="space-y-4">
<Dropdown
value={selectedCurrency}
options={currencies.map((currency) => ({
value: currency.code,
label: (
<div className="flex items-center gap-2.5">
<span className="text-base" aria-hidden>
{currencyFlags[currency.code] || '💱'}
</span>
<div className="flex flex-col items-start min-w-0">
<span className="text-sm font-medium text-dash-text">{currency.code}</span>
<span className="text-xs text-dash-textMuted truncate max-w-[12rem]">{currency.name}</span>
</div>
</div>
),
}))}
onChange={(value) => {
setSelectedCurrency(value);
if (onCurrencyChange && typeof onCurrencyChange === 'function') {
onCurrencyChange(value);
}
}}
/>
<div className="flex items-center gap-3 pt-1 rounded-lg border border-dash-border bg-brightBlack/30 px-3 py-2.5 w-fit">
<span className="text-lg" aria-hidden>
{currencyFlags[selectedCurrency] || '💱'}
</span>
<div className="flex items-baseline gap-1.5">
<span className="text-sm font-medium tabular-nums text-dash-text">
{currencies.find((c) => c.code === selectedCurrency)?.symbol || '$'}
</span>
<span className="text-xs text-dash-textMuted">{selectedCurrency}</span>
</div>
</div>
</div>
</SettingsPanel>
</SectionCard>
{/* Support & Contact Information */}
{/* <SectionCard title="Support & Contact" icon={Headphones} color={colors.magenta}>
<div className="space-y-6">
<div className="p-6 rounded-xl border text-center" style={{ backgroundColor: colors.brightBlack, borderColor: colors.magenta }}>
<Headphones className="w-12 h-12 mx-auto mb-4" style={{ color: colors.magenta }} />
<h4 className="text-xl font-bold mb-2" style={{ color: colors.brightWhite }}>Need Help?</h4>
<p className="text-sm mb-4" style={{ color: colors.foreground }}>
Our support team is here to help you with any questions or issues you might have.
</p>
<div className="space-y-3 max-w-md mx-auto">
<div className="flex items-center justify-between p-3 rounded-lg" style={{ backgroundColor: colors.black }}>
<span style={{ color: colors.foreground }}>Email Support:</span>
<a
href={`mailto:${branding.supportEmailAddress}`}
className="font-semibold hover:underline transition-all duration-200 hover:scale-105"
style={{ color: colors.magenta }}
>
{branding.supportEmailAddress}
</a>
</div>
<div className="flex items-center justify-between p-3 rounded-lg" style={{ backgroundColor: colors.black }}>
<span style={{ color: colors.foreground }}>Sales Inquiries:</span>
<a
href={`mailto:${branding.salesEmail}`}
className="font-semibold hover:underline transition-all duration-200 hover:scale-105"
style={{ color: colors.magenta }}
>
{branding.salesEmail}
</a>
</div>
<div className="flex items-center justify-between p-3 rounded-lg" style={{ backgroundColor: colors.black }}>
<span style={{ color: colors.foreground }}>Business Contact:</span>
<a
href={`mailto:${branding.supportEmail}`}
className="font-semibold hover:underline transition-all duration-200 hover:scale-105"
style={{ color: colors.magenta }}
>
{branding.supportEmail}
</a>
</div>
</div>
<div className="mt-6 pt-6 border-t" style={{ borderColor: colors.brightBlack }}>
<p className="text-xs" style={{ color: colors.foreground }}>
Typical response time: <span style={{ color: colors.green }}>Within 24 hours</span>
</p>
<p className="text-xs mt-1" style={{ color: colors.foreground }}>
Support available: <span style={{ color: colors.green }}>Monday - Friday, 9:00 - 17:00 CET</span>
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
onClick={() => setActiveTab('tickets')}
className="p-4 rounded-xl border transition-all duration-200 hover:scale-105 text-center"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.blue,
color: colors.brightWhite
}}
>
<MessageCircle className="w-6 h-6 mx-auto mb-2" style={{ color: colors.blue }} />
<span className="font-semibold">Open Support Ticket</span>
</button>
<a
href={branding.documentationUrl}
target="_blank"
rel="noopener noreferrer"
className="p-4 rounded-xl border transition-all duration-200 hover:scale-105 text-center"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.green,
color: colors.brightWhite
}}
>
<BookOpen className="w-6 h-6 mx-auto mb-2" style={{ color: colors.green }} />
<span className="font-semibold">Documentation</span>
</a>
<a
href="https://discord.gg/mthfJRNYJC"
target="_blank"
rel="noopener noreferrer"
className="p-4 rounded-xl border transition-all duration-200 hover:scale-105 text-center"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.yellow,
color: colors.brightWhite
}}
>
<Users className="w-6 h-6 mx-auto mb-2" style={{ color: colors.yellow }} />
<span className="font-semibold">Community Forum</span>
</a>
</div>
</div>
</SectionCard> */}
<SectionCard title="Data & account" icon={AlertTriangle} color={colors.red}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<SettingsPanel
title="Export your data"
description="Download a JSON copy of the personal data we hold for GDPR / portability."
>
<ActionButton variant="warning" className="!px-4 !py-2 !min-h-0 sm:!min-h-0 w-full sm:w-auto" onClick={exportUserData}>
Download JSON export
</ActionButton>
</SettingsPanel>
<SettingsPanel
title="Delete account"
description="This removes your account, servers, and billing profile. This cannot be undone."
>
<ActionButton
variant="danger"
className="!px-4 !py-2 !min-h-0 sm:!min-h-0 w-full sm:w-auto"
onClick={() => setDeleteAccountModal(true)}
>
Delete account…
</ActionButton>
</SettingsPanel>
</div>
</SectionCard>
</div>
</DashboardViewShell>
</div>
)}
</div>
</div>
</div>
</main>
{/* Enhanced Server Details Modal */}
{selectedServer && createPortal(
<div className="fixed inset-0 bg-black/60 backdrop-blur-md flex items-center justify-center z-[9998] p-4 lg:p-8 animate-in fade-in duration-300">
<DashboardViewShell className={`${isTerminalFullscreen ? 'h-full max-w-full' : 'w-full max-w-6xl h-[min(820px,calc(100vh-4rem))]'} flex flex-col min-h-0 overflow-hidden shadow-2xl shadow-black/50 animate-in zoom-in-95 duration-200`}>
<div className="shrink-0">
<PageHeaderBar
accentColor={colors.blue}
icon={Server}
title={selectedServer.name}
description={
<div className="flex items-center gap-2 mt-1">
<span
className="px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-wider border"
style={{
backgroundColor: `${getServerStatusInfo(selectedServer.status).color}15`,
color: getServerStatusInfo(selectedServer.status).color,
borderColor: `${getServerStatusInfo(selectedServer.status).color}30`,
}}
>
{getServerStatusInfo(selectedServer.status).label}
</span>
<span className="text-[10px] font-bold text-dash-textMuted/40 uppercase tracking-widest">• {selectedServer.type} node</span>
</div>
}
actions={
<div className="flex items-center gap-2">
{selectedServer.status === 'Running' ? (
<>
<ActionButton
variant="danger"
onClick={() => {
handleServerAction(selectedServer.id, 'stop');
setSelectedServer({ ...selectedServer, status: 'Stopping' });
}}
className="!px-3 !min-h-[40px]"
>
<Square className="w-4 h-4" />
</ActionButton>
<ActionButton
variant="warning"
onClick={() => {
handleServerAction(selectedServer.id, 'restart');
setSelectedServer({ ...selectedServer, status: 'Restarting' });
}}
className="!px-3 !min-h-[40px]"
>
<RefreshCw className="w-4 h-4" />
</ActionButton>
</>
) : (
<ActionButton
variant="success"
onClick={() => {
handleServerAction(selectedServer.id, 'start');
setSelectedServer({ ...selectedServer, status: 'Starting' });
}}
className="!px-3 !min-h-[40px]"
>
<Play className="w-4 h-4" />
</ActionButton>
)}
<ActionButton
variant="danger"
onClick={() => handleDeleteServer(selectedServer.id)}
className="!px-3 !min-h-[40px]"
>
<Trash2 className="w-4 h-4" />
</ActionButton>
<div className="w-px h-6 mx-1 bg-dash-border"></div>
<ActionButton
variant="secondary"
onClick={() => setIsTerminalFullscreen(!isTerminalFullscreen)}
className="!px-3 !min-h-[40px]"
>
{isTerminalFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</ActionButton>
<ActionButton
variant="secondary"
onClick={() => {
setSelectedServer(null);
setServerLogs('');
disconnectTerminal();
setServerMetrics({ cpu: '0m', memory: '0Mi' });
setMetricsHistory({ cpu: [], memory: [] });
setActiveServerDetailTab('overview');
}}
className="!px-3 !min-h-[40px]"
>
<X className="w-4 h-4" />
</ActionButton>
</div>
}
/>
</div>
<div className="border-b border-dash-border bg-black/5 shrink-0">
<div className="flex space-x-1 px-6 overflow-x-auto custom-scrollbar">
{[
{ id: 'overview', label: 'Overview', icon: Info },
...(selectedServer.type === 'game'
? [
{ id: 'game-console', label: 'Game Console', icon: Gamepad2 },
...(selectedServer.image && selectedServer.image.toLowerCase().includes('minecraft')
? [
{ id: 'plugin-store', label: 'Plugin Store', icon: Package },
{ id: 'minecraft-settings', label: 'Quick Settings', icon: Settings },
]
: []),
]
: []),
...(selectedServer.type !== 'database' && selectedServer.type !== 'vps'
? [{ id: 'files', label: 'Files & SFTP', icon: Folder }]
: []),
{ id: 'network', label: 'Network', icon: NetworkIcon },
{ id: 'backups', label: 'Backups', icon: Database },
].map((tab) => {
const Icon = tab.icon;
const active = activeServerDetailTab === tab.id;
return (
<button
key={tab.id}
onClick={() => {
setActiveServerDetailTab(tab.id);
if (tab.id !== 'network') setShowPortMappingGuide(false);
}}
className={`flex items-center space-x-2 px-5 py-4 text-xs font-bold uppercase tracking-widest border-b-2 transition-all duration-200 ${
active
? 'border-primary text-primary bg-primary/5'
: 'border-transparent text-dash-textMuted hover:text-dash-text hover:bg-white/5'
}`}
>
<Icon className="w-3.5 h-3.5" />
<span>{tab.label}</span>
</button>
);
})}
</div>
</div>
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
<div className={dashLayout.bodyPadding}>
{activeServerDetailTab === 'overview' && (
<div className="max-w-7xl mx-auto space-y-8">
{/* Top section: Overview and Resources */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 w-full">
<SectionCard title="Server Overview" icon={Info} color={colors.blue} className="h-full">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
{[
{ label: 'Status', value: getServerStatusInfo(selectedServer.status).label, color: getServerStatusInfo(selectedServer.status).color },
{ label: 'Type', value: selectedServer.type.charAt(0).toUpperCase() + selectedServer.type.slice(1), color: colors.brightWhite },
{ label: 'Created', value: parseServerDate(selectedServer.created_at), color: colors.brightWhite },
...(selectedServer.image ? [{ label: 'Image', value: selectedServer.image, color: colors.brightWhite }] : []),
].map((item, index) => (
<div key={index} className="flex justify-between items-center p-4 rounded-xl border transition-all duration-200 hover:bg-white/[0.02]" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack }}>
<span style={{ color: colors.foreground }} className="font-medium">{item.label}</span>
<span style={{ color: item.color }} className="font-bold">{item.value}</span>
</div>
))}
</div>
</SectionCard>
<SectionCard title="System Allocation" icon={BarChart3} color={colors.green} className="h-full">
<div className="flex flex-col h-full gap-5 text-sm">
<div className="grid grid-cols-3 gap-3">
{[
{ icon: Cpu, label: 'vCPU', value: selectedServer.cpu, color: colors.blue },
{ icon: MemoryStick, label: 'RAM', value: selectedServer.ram, color: colors.green },
{ icon: HardDriveIcon, label: 'SSD', value: selectedServer.storage, color: colors.yellow },
].map((item, index) => {
const MetricIcon = item.icon;
return (
<div
key={index}
className="flex flex-col items-center justify-center p-4 rounded-2xl border text-center transition-all duration-300 hover:scale-[1.02] group/metric"
style={{ backgroundColor: `${item.color}08`, borderColor: `${item.color}22` }}
>
<MetricIcon className="w-5 h-5 mb-2 opacity-60 group-hover/metric:opacity-100 transition-opacity" style={{ color: item.color }} />
<span className="font-bold text-lg tabular-nums tracking-tight" style={{ color: colors.brightWhite }}>{item.value}</span>
<span className="text-[10px] font-bold uppercase tracking-widest opacity-50 mt-0.5" style={{ color: item.color }}>{item.label}</span>
</div>
);
})}
</div>
{isEditingResources && (
<div
className="space-y-4 p-4 rounded-2xl border"
style={{ backgroundColor: `${colors.green}06`, borderColor: `${colors.green}20` }}
>
{[
{ id: 'cpu', icon: Cpu, label: 'CPU Cores', editValue: resourceValues.cpu, unit: 'vCPU', min: 1, max: 4, step: 1, color: colors.blue },
{ id: 'ram', icon: MemoryStick, label: 'RAM Capacity', editValue: resourceValues.ram, unit: 'GB', min: 1, max: 12, step: 1, color: colors.green },
{ id: 'storage', icon: HardDriveIcon, label: 'SSD Storage', editValue: resourceValues.storage, unit: 'GB', min: 10, max: 100, step: 5, color: colors.yellow },
].map((item) => (
<Slider
key={item.id}
value={item.editValue}
min={item.min}
max={item.max}
step={item.step}
onChange={(value) => setResourceValues({ ...resourceValues, [item.id]: value })}
label={item.label}
unit={item.unit}
icon={item.icon}
color={item.color}
/>
))}
</div>
)}
<div className="mt-auto pt-4 border-t border-white/5">
{!isEditingResources ? (
<button
onClick={() => {
setIsEditingResources(true);
setResourceValues({
cpu: parseCpuValue(selectedServer.cpu),
ram: parseRamValue(selectedServer.ram),
storage: parseStorageValue(selectedServer.storage),
});
}}
disabled={selectedServer.is_demo === true || selectedServer.is_demo === 1}
className="w-full flex items-center justify-center gap-2.5 px-4 py-3.5 rounded-xl border font-bold text-sm transition-all duration-200 hover:brightness-110 active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed group"
style={{ backgroundColor: `${colors.green}12`, borderColor: `${colors.green}35`, color: colors.brightGreen }}
>
<Edit className="w-4 h-4 transition-transform group-hover:rotate-12" />
Adjust Resources
</button>
) : (
<div className="flex gap-3">
<button
onClick={handleAdjustServerParameters}
className="flex-1 py-3.5 rounded-xl font-bold text-sm transition-all duration-200 hover:brightness-110 active:scale-[0.98] flex items-center justify-center gap-2 shadow-lg"
style={{ backgroundColor: colors.green, color: colors.brightWhite }}
>
<CheckCircle className="w-4 h-4" />
Save Changes
</button>
<button
onClick={() => setIsEditingResources(false)}
className="flex-1 py-3.5 rounded-xl font-bold text-sm transition-all duration-200 hover:bg-white/5 active:scale-[0.98] flex items-center justify-center gap-2 border border-white/10"
style={{ color: colors.foreground }}
>
<X className="w-4 h-4" />
Cancel
</button>
</div>
)}
</div>
</div>
</SectionCard>
</div>
{/* Minecraft Software Configuration */}
{((selectedServer.image && selectedServer.image.toLowerCase() === 'minecraft') || (selectedServer.type === 'game' && selectedServer.image && selectedServer.image.toLowerCase().includes('minecraft'))) && (
<SectionCard title="Software & Environment" icon={Settings} color={colors.cyan} className="w-full">
<p className="text-xs text-dash-textMuted -mt-4 mb-6 leading-relaxed">
Minecraft stack — game version, server engine, and JVM runtime.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[
{
key: 'version',
label: 'Version',
hint: 'Game release',
icon: Tag,
accent: colors.cyan,
display: selectedServer.version,
isEditing: isEditingVersion,
onEdit: () => setIsEditingVersion(true),
editContent: (
<InlineDropdown
value={newVersion}
options={minecraftVersions.flatMap((v) => v.versions).map((v) => ({ value: v, label: v }))}
onChange={setNewVersion}
onSave={() => handleUpdateConfig('version', newVersion)}
onCancel={() => setIsEditingVersion(false)}
color={colors.cyan}
/>
),
},
{
key: 'engine',
label: 'Engine',
hint: 'Server software',
icon: Layers,
accent: colors.magenta,
display: selectedServer.loader || 'Paper',
isEditing: isEditingLoader,
onEdit: () => setIsEditingLoader(true),
editContent: (
<InlineDropdown
value={newLoader}
options={[
{ value: 'Paper', label: 'Paper' },
{ value: 'Vanilla', label: 'Vanilla' },
{ value: 'Forge', label: 'Forge' },
{ value: 'Fabric', label: 'Fabric' },
].filter((opt) => {
if (opt.value === 'Fabric') {
const parts = (newVersion || selectedServer.version || '').split('.');
if (parts.length >= 2 && parseInt(parts[1], 10) < 14) return false;
}
return true;
})}
onChange={setNewLoader}
onSave={() => handleUpdateConfig('loader', newLoader)}
onCancel={() => setIsEditingLoader(false)}
color={colors.magenta}
/>
),
},
{
key: 'java',
label: 'Java Version',
hint: 'JVM runtime',
icon: Coffee,
accent: colors.yellow,
display: selectedServer.java_version || 'Java 21',
isEditing: isEditingJava,
onEdit: () => setIsEditingJava(true),
editContent: (
<InlineDropdown
value={newJavaVersion}
options={[
{ value: 'Java 25', label: 'Java 25' },
{ value: 'Java 21', label: 'Java 21' },
{ value: 'Java 17', label: 'Java 17' },
{ value: 'Java 11', label: 'Java 11' },
{ value: 'Java 8', label: 'Java 8' },
{ value: 'Java 7', label: 'Java 7' },
]}
onChange={setNewJavaVersion}
onSave={() => handleUpdateConfig('java', newJavaVersion)}
onCancel={() => setIsEditingJava(false)}
color={colors.yellow}
/>
),
},
].map((tile) => {
const TileIcon = tile.icon;
const isDemo = selectedServer.is_demo === true || selectedServer.is_demo === 1;
const canEdit = !isDemo;
return (
<div
key={tile.key}
className="group relative flex flex-col min-h-[148px] p-5 rounded-2xl border border-solid transition-all duration-300 hover:scale-[1.01] outline-none"
style={{ backgroundColor: `${tile.accent}06`, borderColor: `${tile.accent}28`, borderWidth: '1px' }}
>
<div
className="absolute inset-x-0 top-0 h-px rounded-t-2xl opacity-60"
style={{ background: `linear-gradient(90deg, transparent, ${tile.accent}80, transparent)` }}
/>
<div className="flex items-start justify-between gap-3 mb-4">
<div className="flex items-center gap-3 min-w-0">
<div
className="p-2.5 rounded-xl shrink-0 border border-solid shadow-lg shadow-black/20"
style={{ backgroundColor: `${tile.accent}14`, borderColor: `${tile.accent}35`, borderWidth: '1px' }}
>
<TileIcon className="w-4 h-4" style={{ color: tile.accent }} />
</div>
<div className="min-w-0">
<p className="text-[10px] font-black uppercase tracking-widest" style={{ color: `${tile.accent}CC` }}>
{tile.label}
</p>
<p className="text-[10px] opacity-40 mt-0.5 truncate">{tile.hint}</p>
</div>
</div>
{canEdit && !tile.isEditing && (
<button
type="button"
onClick={tile.onEdit}
className="focus-skip-global p-2 rounded-lg opacity-50 sm:opacity-0 sm:group-hover:opacity-100 hover:bg-black/30 transition-all shrink-0 outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-offset-0"
style={{ color: tile.accent, '--tw-ring-color': tile.accent }}
aria-label={`Edit ${tile.label}`}
>
<Edit className="w-3.5 h-3.5" />
</button>
)}
</div>
<div className="mt-auto">
{tile.isEditing && canEdit ? (
tile.editContent
) : (
<p className="font-bold text-2xl tabular-nums tracking-tight truncate" style={{ color: colors.brightWhite }}>
{tile.display}
</p>
)}
</div>
</div>
);
})}
</div>
</SectionCard>
)}
{/* Bottom section: Real-time Metrics */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 w-full">
<ResourceGraph
data={metricsHistory.cpu}
color={colors.blue}
title="Real-time CPU Load"
unit="m"
maxValue={(() => {
const cpuStr = selectedServer?.cpu || '1';
let cpuCores = 1;
if (cpuStr.includes('vCPU')) cpuCores = parseInt(cpuStr.split()[0]) || 1;
else if (typeof cpuStr === 'string' && /^\d+$/.test(cpuStr.trim())) cpuCores = parseInt(cpuStr.trim()) || 1;
else { const match = cpuStr.match(/(\d+)/); if (match) cpuCores = parseInt(match[1]) || 1; }
return cpuCores * 1000;
})()}
currentValue={serverMetrics.cpu?.display || '0m'}
percentage={Math.round(serverMetrics.cpu_percentage || 0)}
serverStatus={selectedServer?.status}
/>
<ResourceGraph
data={metricsHistory.memory}
color={colors.green}
title="Memory Consumption"
unit="Mi"
maxValue={(parseInt(selectedServer.ram) || 1) * 1024}
currentValue={serverMetrics.memory?.display || '0.0 GB'}
percentage={Math.round(serverMetrics.memory_percentage || 0)}
serverStatus={selectedServer?.status}
/>
</div>
</div>
)}
{activeServerDetailTab === 'game-console' && (
<GameConsole
server={selectedServer}
sendMessage={sendDashboardMessage}
isActive={activeServerDetailTab === 'game-console'}
/>
)}
{activeServerDetailTab === 'plugin-store' && selectedServer.image && selectedServer.image.toLowerCase().includes('minecraft') && (
<div className="h-full">
<PluginStoreEmbedded serverId={selectedServer.id} />
</div>
)}
{activeServerDetailTab === 'minecraft-settings' && selectedServer.image && selectedServer.image.toLowerCase().includes('minecraft') && (
<MinecraftSettings
server={selectedServer}
settings={mcSettings}
onSave={handleUpdateMcSettings}
isLoading={isMcSettingsLoading}
/>
)}
{activeServerDetailTab === 'files' && (
<div className="space-y-6">
{selectedServer.sftp_username ? (
<SectionCard title="SFTP Access" icon={NetworkIcon} color={colors.blue}>
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 flex-1">
{[
{ label: 'SFTP Address', value: selectedServer.ip || NODE_IP },
{ label: 'Port', value: selectedServer.sftp_port || '2022' },
{ label: 'Username', value: selectedServer.sftp_username },
{ label: 'Password', value: selectedServer.sftp_password, isPassword: true }
].map((item, idx) => (
<div key={idx} className="p-4 rounded-xl border bg-white/5" style={{ borderColor: 'rgba(255,255,255,0.05)' }}>
<div className="text-xs opacity-50 mb-1">{item.label}</div>
<div className="flex items-center justify-between">
<div className="font-mono text-sm truncate mr-2" style={{ color: colors.brightWhite }}>
{item.isPassword ? (window[`show_sftp_pass_${selectedServer.id}`] ? item.value : '••••••••••••') : item.value}
</div>
<div className="flex items-center space-x-1">
{item.isPassword && (
<button
onClick={() => {
window[`show_sftp_pass_${selectedServer.id}`] = !window[`show_sftp_pass_${selectedServer.id}`];
// Force a re-render by updating a dummy state or just rely on the next update
// Since we don't have a local state here easily without refactoring,
// we use a trick to trigger re-render if needed, but standard React pattern is better.
// For now, let's use a simple approach.
const btn = document.activeElement;
if (btn) btn.blur();
// We will add a small state to DashboardPage to handle this properly
setToggleSFTPPassword(!toggleSFTPPassword);
}}
className="p-1.5 rounded-lg hover:bg-white/10 transition-colors"
title={window[`show_sftp_pass_${selectedServer.id}`] ? "Hide password" : "Show password"}
>
{window[`show_sftp_pass_${selectedServer.id}`] ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
)}
<button
onClick={() => {
navigator.clipboard.writeText(item.value);
showAlert('Success', `${item.label} copied to clipboard`, 'success');
}}
className="p-1.5 rounded-lg hover:bg-white/10 transition-colors"
>
<Copy className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
))}
</div>
<div className="flex flex-row lg:flex-col gap-2">
<button
onClick={() => {
const sftpUrl = `sftp://${selectedServer.sftp_username}:${selectedServer.sftp_password}@${selectedServer.ip || NODE_IP}:${selectedServer.sftp_port || '2022'}`;
window.open(sftpUrl, '_blank');
}}
className="flex-1 lg:w-full flex items-center justify-center space-x-2 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 hover:scale-105 border"
style={{ backgroundColor: colors.blue, borderColor: colors.blue, color: colors.brightWhite }} >
<ExternalLink className="w-4 h-4" />
<span>Launch SFTP</span>
</button>
<button
onClick={() => {
const cmd = `sftp -P ${selectedServer.sftp_port || '2022'} ${selectedServer.sftp_username}@${selectedServer.ip || NODE_IP}`;
navigator.clipboard.writeText(cmd);
showAlert('Success', 'SFTP command copied to clipboard', 'success');
}}
className="flex-1 lg:w-full flex items-center justify-center space-x-2 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 hover:scale-105 border bg-white/5"
style={{ borderColor: 'rgba(255,255,255,0.1)', color: colors.brightWhite }}
>
<Terminal className="w-4 h-4" />
<span>Copy SSH Cmd</span>
</button>
</div>
</div>
</SectionCard>
) : (
<SectionCard title="SFTP Access" icon={NetworkIcon} color={colors.yellow}>
<div className="flex flex-col items-center justify-center py-8 text-center">
<div className="p-4 rounded-full mb-4" style={{ backgroundColor: `${colors.yellow}20` }}>
<NetworkIcon className="w-8 h-8" style={{ color: colors.yellow }} />
</div>
<h3 className="text-lg font-semibold mb-2" style={{ color: colors.brightWhite }}>SFTP Not Provisioned</h3>
<p className="text-sm opacity-60 max-w-md mb-6" style={{ color: colors.foreground }}>
SFTP access allows you to manage files using external clients like FileZilla.
</p>
<button
onClick={() => sendDashboardMessage({ type: 'generate_sftp', serverId: selectedServer.id })}
className="px-8 py-3 rounded-xl font-bold transition-all duration-200 hover:scale-105 border flex items-center space-x-2"
style={{ backgroundColor: colors.yellow, borderColor: colors.yellow, color: 'black' }}
>
<RefreshCw className="w-4 h-4" />
<span>Provision SFTP Access</span>
</button>
</div>
</SectionCard>
)}
<FileManager
ref={fileManagerRef}
serverId={selectedServer.id}
sendMessage={sendDashboardMessage}
isActive={activeServerDetailTab === 'files'}
connectionStatus={dashboardStatus}
showAlert={showAlert}
/>
</div>
)}
{activeServerDetailTab === 'network' && (() => {
const serverIp = selectedServer.ip || NODE_IP;
const serverPorts = selectedServer.ports || [];
const primaryPort = serverPorts[0];
const mappingCount = serverPorts.length + (selectedServer.sftp_port ? 1 : 0);
const flowExampleInternal = primaryPort?.internal || '8080';
const flowExampleExternal = primaryPort?.external || '31234';
const canExposePorts = ['vps', 'game', 'database'].includes(selectedServer.type);
return (
<div className="max-w-7xl mx-auto space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300">
<div
className="rounded-2xl border transition-all duration-300 bg-brightBlack/20 backdrop-blur-sm w-full overflow-hidden"
style={{ borderColor: `${colors.blue}25`, boxShadow: `0 10px 30px -15px rgba(0,0,0,0.5), 0 0 0 1px ${colors.blue}10` }}
>
<button
type="button"
onClick={() => setShowPortMappingGuide((open) => !open)}
className="w-full flex items-center justify-between gap-4 p-5 sm:p-6 text-left transition-colors hover:bg-white/[0.02]"
>
<div className="flex items-center gap-4 min-w-0">
<div
className="p-3 rounded-xl border shrink-0"
style={{ backgroundColor: `${colors.blue}15`, borderColor: `${colors.blue}30` }}
>
<Info className="w-5 h-5" style={{ color: colors.blue }} />
</div>
<div className="min-w-0">
<h3 className="text-base sm:text-lg font-bold tracking-tight" style={{ color: colors.brightWhite }}>
How Port Mapping Works
</h3>
<p className="text-xs opacity-50 mt-0.5 truncate sm:whitespace-normal">
{showPortMappingGuide ? 'Tap to collapse' : 'Port mapping, how to connect, and tips'}
</p>
</div>
</div>
<ChevronDown
className={`w-5 h-5 shrink-0 opacity-50 transition-transform duration-300 ${showPortMappingGuide ? 'rotate-180' : ''}`}
style={{ color: colors.blue }}
/>
</button>
{showPortMappingGuide && (
<div className="px-5 sm:px-6 pb-6 border-t border-white/5 animate-in slide-in-from-top-2 duration-200">
<p className="text-sm leading-relaxed opacity-60 mb-6 mt-5 max-w-3xl">
Your server listens on an <span className="text-white font-semibold">internal port</span> inside its container.
{branding.name} maps it to a unique <span className="text-white font-semibold">external port</span> on our public IP so people on the internet can reach it — without exposing your server directly.
</p>
<div className="flex flex-col lg:flex-row items-stretch justify-center gap-3 lg:gap-4">
<div className="flex-1 p-5 rounded-2xl border text-center" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack }}>
<p className="text-[10px] font-black uppercase tracking-widest opacity-40 mb-3">Inside your server</p>
<code className="text-sm font-bold font-mono" style={{ color: colors.brightWhite }}>:{flowExampleInternal}</code>
<p className="text-[10px] opacity-40 mt-3 leading-relaxed">What your app or game listens on</p>
</div>
<div className="flex items-center justify-center py-1 lg:py-0">
<ArrowRight className="w-5 h-5 text-primary rotate-90 lg:rotate-0 shrink-0 opacity-60" />
</div>
<div className="flex-1 p-5 rounded-2xl border text-center" style={{ backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}25` }}>
<p className="text-[10px] font-black uppercase tracking-widest text-primary mb-3">{branding.name} gateway</p>
<NetworkIcon className="w-6 h-6 mx-auto text-primary mb-1" />
<p className="text-[10px] opacity-50 mt-2 leading-relaxed">Secure NodePort + firewall</p>
</div>
<div className="flex items-center justify-center py-1 lg:py-0">
<ArrowRight className="w-5 h-5 text-primary rotate-90 lg:rotate-0 shrink-0 opacity-60" />
</div>
<div className="flex-1 p-5 rounded-2xl border text-center" style={{ backgroundColor: `${colors.green}08`, borderColor: `${colors.green}20` }}>
<p className="text-[10px] font-black uppercase tracking-widest mb-3" style={{ color: colors.brightGreen }}>Public address</p>
<code className="text-sm font-bold font-mono text-primary">{serverIp}:{flowExampleExternal}</code>
<p className="text-[10px] opacity-40 mt-3 leading-relaxed">What you give to users & clients</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-8 pt-8 border-t border-white/5">
<div>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg border shrink-0" style={{ backgroundColor: `${colors.blue}15`, borderColor: `${colors.blue}30` }}>
<ExternalLink className="w-4 h-4" style={{ color: colors.blue }} />
</div>
<h4 className="text-sm font-bold tracking-tight" style={{ color: colors.brightWhite }}>How to Connect</h4>
</div>
<p className="text-xs leading-relaxed opacity-50 mb-4">{getNetworkConnectionHint(selectedServer)}</p>
<div className="p-5 rounded-2xl border space-y-4" style={{ backgroundColor: `${colors.primary}05`, borderColor: `${colors.primary}15` }}>
{[
{ step: '01', title: 'Copy your address', body: 'Pick a port from the table below and copy its public IP:port address.' },
{ step: '02', title: 'Open your client', body: selectedServer.type === 'game' ? 'Launch your game and open Multiplayer → Direct Connect.' : 'Open SSH, RDP, a browser, or your database tool.' },
{ step: '03', title: 'Connect', body: `Paste the full address, e.g. ${serverIp}:${flowExampleExternal}` },
].map((item) => (
<div key={item.step} className="flex items-start gap-4">
<div className="w-8 h-8 rounded-xl flex items-center justify-center shrink-0 border" style={{ backgroundColor: `${colors.primary}15`, borderColor: `${colors.primary}30` }}>
<span className="text-xs font-black text-primary">{item.step}</span>
</div>
<div>
<p className="text-sm font-bold mb-0.5" style={{ color: colors.brightWhite }}>{item.title}</p>
<p className="text-xs leading-relaxed opacity-60">{item.body}</p>
</div>
</div>
))}
</div>
</div>
<div>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg border shrink-0" style={{ backgroundColor: `${colors.green}15`, borderColor: `${colors.green}30` }}>
<Shield className="w-4 h-4" style={{ color: colors.brightGreen }} />
</div>
<h4 className="text-sm font-bold tracking-tight" style={{ color: colors.brightWhite }}>Good to Know</h4>
</div>
<div className="space-y-3">
{[
{
icon: Globe,
iconColor: colors.blue,
title: 'External ports are assigned for you',
body: 'For security, public ports are unique and randomly chosen. Your internal port stays whatever your service expects.',
},
{
icon: Shield,
iconColor: colors.brightGreen,
title: 'Firewall opens automatically',
body: `When you expose a port, ${branding.name} opens it on the edge firewall in real time — no manual rules needed.`,
},
{
icon: Lock,
iconColor: colors.yellow,
title: 'System ports cannot be removed',
body: 'Default service ports (game, SSH, SFTP) are managed by the platform and marked as System in the table.',
},
].map((item, idx) => {
const TipIcon = item.icon;
return (
<div key={idx} className="flex items-start gap-4 p-4 rounded-xl border transition-all duration-200 hover:bg-white/[0.02]" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack }}>
<div className="p-2 rounded-lg shrink-0" style={{ backgroundColor: `${item.iconColor}15` }}>
<TipIcon className="w-4 h-4" style={{ color: item.iconColor }} />
</div>
<div>
<p className="text-xs font-bold mb-1" style={{ color: colors.brightWhite }}>{item.title}</p>
<p className="text-[10px] leading-relaxed opacity-45">{item.body}</p>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
)}
</div>
<SectionCard
title="Port Mappings"
icon={NetworkIcon}
color={colors.primary}
className="w-full"
>
<p className="text-xs opacity-50 mb-6 -mt-2">
Internal ports are inside your server. External ports are on our public IP ({serverIp}) — always use the external address when connecting from outside.
</p>
<div className="rounded-2xl border overflow-hidden mb-6" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack }}>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b" style={{ borderColor: `${colors.white}08`, backgroundColor: `${colors.white}03` }}>
<th className="px-5 py-4 text-[10px] font-black uppercase tracking-widest opacity-40">Service</th>
<th className="px-5 py-4 text-[10px] font-black uppercase tracking-widest opacity-40">Internal</th>
<th className="px-5 py-4 text-[10px] font-black uppercase tracking-widest opacity-40">External (public)</th>
<th className="px-5 py-4 text-[10px] font-black uppercase tracking-widest opacity-40">Protocol</th>
<th className="px-5 py-4 text-[10px] font-black uppercase tracking-widest opacity-40 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{selectedServer.sftp_port && (
<tr className="group transition-colors hover:bg-white/[0.02]">
<td className="px-5 py-4">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-blue-400 shrink-0" />
<div>
<span className="text-sm font-bold" style={{ color: colors.brightWhite }}>SFTP Access</span>
<p className="text-[10px] opacity-40 mt-0.5">File transfers via Files tab</p>
</div>
</div>
</td>
<td className="px-5 py-4">
<code className="text-xs px-2 py-1 rounded font-mono border" style={{ backgroundColor: 'rgba(0,0,0,0.4)', borderColor: 'rgba(255,255,255,0.05)', color: colors.foreground }}>22</code>
</td>
<td className="px-5 py-4">
<div className="flex items-center gap-2">
<code className="text-xs px-2.5 py-1.5 rounded font-mono font-bold border text-blue-400" style={{ backgroundColor: `${colors.blue}15`, borderColor: `${colors.blue}25` }}>
{serverIp}:{selectedServer.sftp_port}
</code>
<UnifiedTooltip content="System-managed — cannot be removed">
<Shield className="w-3.5 h-3.5 opacity-40" style={{ color: colors.blue }} />
</UnifiedTooltip>
</div>
</td>
<td className="px-5 py-4">
<span className="text-[10px] font-black uppercase px-2.5 py-1 rounded-lg border opacity-60" style={{ backgroundColor: `${colors.white}05`, borderColor: `${colors.white}08` }}>TCP</span>
</td>
<td className="px-5 py-4 text-right">
<div className="flex items-center justify-end gap-1">
<button
onClick={() => {
navigator.clipboard.writeText(`${serverIp}:${selectedServer.sftp_port}`);
setAlertModal({ open: true, title: 'Copied', message: 'SFTP address copied to clipboard', type: 'success' });
}}
className="p-2 rounded-lg transition-all duration-200 hover:bg-white/10"
style={{ color: colors.primary }}
>
<Copy className="w-4 h-4" style={{ color: 'inherit' }} />
</button>
<div className="flex items-center gap-1.5 px-2 py-1 opacity-30">
<Shield className="w-3.5 h-3.5" />
<span className="text-[9px] font-black uppercase tracking-widest">System</span>
</div>
</div>
</td>
</tr>
)}
{serverPorts.map((port, index) => {
const isDefault = isDefaultPort(selectedServer, port);
const portTooltip = getPortTooltip(parseInt(port.internal, 10), port.external);
const shouldShowTooltip = portTooltip && (selectedServer.type === 'vps' || isDefault);
const serviceName = getPortServiceName(selectedServer, port, isDefault);
return (
<tr key={index} className="group transition-colors hover:bg-white/[0.02]">
<td className="px-5 py-4">
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full shrink-0 ${isDefault ? 'bg-primary' : 'bg-white/30'}`} />
<div>
<span className="text-sm font-bold" style={{ color: colors.brightWhite }}>{serviceName}</span>
{portTooltip && (
<p className="text-[10px] opacity-40 mt-0.5 max-w-[200px] truncate">{portTooltip.description}</p>
)}
</div>
</div>
</td>
<td className="px-5 py-4">
<code className="text-xs px-2 py-1 rounded font-mono border" style={{ backgroundColor: 'rgba(0,0,0,0.4)', borderColor: 'rgba(255,255,255,0.05)', color: colors.foreground }}>{port.internal}</code>
</td>
<td className="px-5 py-4">
<div className="flex items-center gap-2">
<code className="text-xs px-2.5 py-1.5 rounded font-mono font-bold border text-primary" style={{ backgroundColor: `${colors.primary}10`, borderColor: `${colors.primary}20` }}>
{serverIp}:{port.external}
</code>
{shouldShowTooltip && <PortTooltip portTooltip={portTooltip} />}
</div>
</td>
<td className="px-5 py-4">
<span className="text-[10px] font-black uppercase px-2.5 py-1 rounded-lg border opacity-60" style={{ backgroundColor: `${colors.white}05`, borderColor: `${colors.white}08` }}>
{port.protocol || 'TCP'}
</span>
</td>
<td className="px-5 py-4 text-right">
<div className="flex items-center justify-end gap-1">
<button
onClick={() => {
navigator.clipboard.writeText(`${serverIp}:${port.external}`);
setAlertModal({ open: true, title: 'Success', message: 'Address copied to clipboard', type: 'success' });
}}
className="p-2 rounded-lg transition-all duration-200 hover:bg-white/10"
style={{ color: colors.primary }}
>
<UnifiedTooltip content="Copy address">
<Copy className="w-4 h-4" style={{ color: 'inherit' }} />
</UnifiedTooltip>
</button>
{!isDefault ? (
<button
onClick={() => {
showConfirm(
'Remove port mapping?',
`Remove the public mapping for internal port ${port.internal} (${port.protocol || 'TCP'})? Players and clients will no longer reach this service on the external address.`,
() => handleRemovePort(selectedServer.id, port.internal, port.protocol),
'danger',
{ confirmLabel: 'Yes', cancelLabel: 'No' }
);
}}
className="p-2 rounded-lg transition-all duration-200 hover:bg-red-500/20"
style={{ color: colors.red }}
>
<UnifiedTooltip content="Remove mapping">
<Trash2 className="w-4 h-4" style={{ color: colors.brightRed }} />
</UnifiedTooltip>
</button>
) : (
<div className="flex items-center gap-1.5 px-2 py-1 opacity-30">
<Shield className="w-3.5 h-3.5" />
<span className="text-[9px] font-black uppercase tracking-widest">System</span>
</div>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{mappingCount === 0 && (
<div className="py-16 px-6 text-center">
<NetworkIcon className="w-10 h-10 mx-auto mb-4 opacity-15" />
<p className="text-sm font-bold opacity-40 mb-1">No port mappings yet</p>
<p className="text-xs opacity-30 max-w-sm mx-auto leading-relaxed">
{selectedServer.status === 'running'
? 'Ports usually appear when the server starts. If yours is running, try refreshing or expose a custom port below.'
: 'Start your server to provision default ports, or expose a custom port manually.'}
</p>
</div>
)}
</div>
{canExposePorts && (
<div className="p-6 sm:p-8 rounded-2xl border-2 border-dashed flex flex-col items-center justify-center space-y-5 w-full" style={{ borderColor: `${colors.primary}25`, backgroundColor: `${colors.primary}05` }}>
<div className="text-center">
<h5 className="text-sm font-black uppercase tracking-[0.2em] mb-2" style={{ color: colors.primary }}>Expose New Port</h5>
<p className="text-xs opacity-45 font-medium max-w-md">Enter the port your app listens on inside the server. {branding.name} assigns a public external port automatically.</p>
</div>
<form
onSubmit={(e) => { e.preventDefault(); submitNetworkExposePort(); }}
className="flex flex-col sm:flex-row items-end gap-4 w-full max-w-xl"
>
<div className="flex-1 w-full space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest opacity-40 ml-1">Internal port</label>
<input
type="number"
value={newPortInternal}
onChange={(e) => setNewPortInternal(e.target.value)}
placeholder="e.g. 8080"
min="1"
max="65535"
className="w-full rounded-xl px-4 py-3 text-sm font-bold focus:outline-none transition-all duration-200 border bg-black/40 text-center placeholder:text-white/20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
style={{ borderColor: colors.brightBlack, color: colors.brightWhite }}
/>
</div>
<div className="w-full sm:w-36 space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest opacity-40 ml-1">Protocol</label>
<Dropdown
value={newPortProtocol}
options={[{ value: 'TCP', label: 'TCP' }, { value: 'UDP', label: 'UDP' }]}
onChange={setNewPortProtocol}
placeholder="Protocol"
icon={NetworkIcon}
/>
</div>
<ActionButton type="submit" variant="primary" className="w-full sm:w-auto !h-[46px] !px-8 shadow-lg shadow-primary/20" disabled={!newPortInternal.trim()}>
<Plus className="w-4 h-4" />
<span>Expose Port</span>
</ActionButton>
</form>
</div>
)}
</SectionCard>
</div>
);
})()}
{activeServerDetailTab === 'backups' && (
<BackupSection
serverId={selectedServer.id}
sendMessage={sendDashboardMessage}
lastMessage={lastMessage}
/>
)}
{activeServerDetailTab === 'terminal' && (
<div>
<div className="flex justify-between items-center mb-4">
<h4 className="font-semibold flex items-center text-lg" style={{ color: colors.brightWhite }}>
<Terminal className="w-5 h-5 mr-3" />
Terminal
</h4>
<div className="flex items-center space-x-3">
<span className={`text-xs px-4 py-2 rounded-xl border transition-all duration-200 ${
isTerminalConnected ? 'bg-green-500/20 text-green-400 border-green-500/30' : 'bg-red-500/20 text-red-400 border-red-500/30'
}`}>
{isTerminalConnected ? 'Connected' : 'Disconnected'}
</span>
{isTerminalConnected ? (
<button
onClick={disconnectTerminal}
className="px-5 py-3 text-sm rounded-xl transition-all duration-200 hover:scale-105 border"
style={{ backgroundColor: `${colors.red}20`, borderColor: colors.red, color: colors.brightRed }}
>
Disconnect
</button>
) : (
<button
onClick={connectTerminal}
className="px-5 py-3 text-sm rounded-xl transition-all duration-200 hover:scale-105 border"
style={{ backgroundColor: `${colors.green}20`, borderColor: colors.green, color: colors.brightGreen }}
>
Connect
</button>
)}
</div>
</div>
<div className="rounded-xl overflow-hidden border transition-all duration-200" style={{ borderColor: colors.brightBlack }}>
<div
ref={terminalRef}
className="p-6 font-mono text-sm whitespace-pre-wrap overflow-y-auto"
style={{
height: '400px',
backgroundColor: colors.brightBlack,
color: colors.brightWhite
}}
>
{terminalOutput || "Connect to server to use terminal"}
</div>
{isTerminalConnected && (
<div className="flex items-center border-t p-4" style={{ borderColor: colors.brightBlack, backgroundColor: colors.brightBlack }}>
<span className="mr-3" style={{ color: colors.green }}>$</span>
<input
type="text"
value={terminalInput}
onChange={(e) => setTerminalInput(e.target.value)}
onKeyDown={handleTerminalKeyDown}
className="flex-1 bg-transparent outline-none font-mono py-2"
style={{ color: colors.brightWhite }}
placeholder="Type command and press Enter"
autoFocus
/>
</div>
)}
</div>
</div>
)}
{activeServerDetailTab === 'logs' && (
<div>
<div className="flex justify-between items-center mb-4">
<h4 className="font-semibold flex items-center text-lg" style={{ color: colors.brightWhite }}>
<Terminal className="w-5 h-5 mr-3" />
Server Logs
</h4>
<button
onClick={() => fetchServerLogs(selectedServer.id)}
className="px-5 py-3 text-sm rounded-xl flex items-center space-x-2 transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.blue,
borderColor: colors.blue,
color: colors.brightWhite
}}
>
<RefreshCw className="w-4 h-4" />
<span>Refresh</span>
</button>
</div>
<div className="rounded-xl p-6 font-mono text-sm whitespace-pre-wrap overflow-x-auto max-h-96 overflow-y-auto border transition-all duration-200"
style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack, color: colors.brightWhite }}>
{serverLogs || "No logs available"}
</div>
</div>
)}
</div>
</div>
</DashboardViewShell>
</div>,
document.body
)}
{/* ALL MODALS */}
{displayNameModal.open && createPortal(
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[9999]">
<div className="rounded-2xl w-full max-w-md border" style={{ backgroundColor: colors.backgroundSecondary, borderColor: colors.brightBlack }}>
<div className="p-6 border-b flex justify-between items-center" style={{ borderColor: colors.brightBlack }}>
<h3 className="text-lg font-semibold" style={{ color: colors.brightWhite }}>{displayNameModal.title}</h3>
<button
onClick={() => setDisplayNameModal({ open: false, currentName: '', field: '', title: '', label: '', placeholder: '' })}
className="p-2 rounded-lg transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.foreground
}}
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6">
<div className="mb-6">
<label className="block text-sm font-medium mb-3" style={{ color: colors.foreground }}>
{displayNameModal.label}
</label>
<input
type="text"
value={displayNameModal.currentName}
onChange={(e) => setDisplayNameModal(prev => ({ ...prev, currentName: e.target.value }))}
className="w-full rounded-xl px-4 py-3 text-sm focus:outline-none transition-all duration-200 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.brightWhite
}}
placeholder={displayNameModal.placeholder}
autoFocus
/>
</div>
<div className="flex space-x-3">
<button
onClick={() => setDisplayNameModal({ open: false, currentName: '', field: '', title: '', label: '', placeholder: '' })}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.brightWhite
}}
>
Cancel
</button>
<button
onClick={async () => {
if (displayNameModal.currentName.trim()) {
const result = await updateUserProfile({ [displayNameModal.field]: displayNameModal.currentName.trim() });
setAlertModal({
open: true,
title: result.success ? "Success" : "Error",
message: result.message,
type: result.success ? "success" : "error"
});
if (result.success) {
setDisplayNameModal({ open: false, currentName: '', field: '', title: '', label: '', placeholder: '' });
}
}
}}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.blue,
borderColor: colors.blue,
color: colors.brightWhite
}}
>
Save Changes
</button>
</div>
</div>
</div>
</div>,
document.body
)}
{emailModal.open && createPortal(
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[9999]">
<div
className="rounded-2xl w-full max-w-md border border-dash-border bg-dash-surfaceElevated/95"
>
<div className="p-6 border-b border-dash-border flex justify-between items-center">
<h3 className="text-lg font-semibold text-dash-text">{emailModal.title}</h3>
<button
type="button"
onClick={() => setEmailModal({ open: false, currentEmail: '', title: '', label: '', placeholder: '' })}
className="p-2 rounded-lg border-0 bg-brightBlack/80 text-dash-textMuted hover:text-dash-text hover:bg-brightBlack transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/45"
aria-label="Close"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6">
<div className="mb-6">
<label className="block text-sm font-medium mb-3" style={{ color: colors.foreground }}>
{emailModal.label}
</label>
<input
type="email"
value={emailModal.currentEmail}
onChange={(e) => setEmailModal(prev => ({ ...prev, currentEmail: e.target.value }))}
className="w-full rounded-xl px-4 py-3 text-sm border border-dash-border bg-brightBlack/60 text-dash-text placeholder:text-dash-textMuted/45 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/40 focus-visible:border-primary/35 transition-colors"
placeholder={emailModal.placeholder}
autoFocus
/>
</div>
<div className="flex gap-3">
<ActionButton
variant="secondary"
className="flex-1 !py-3 !min-h-0 sm:!min-h-0"
onClick={() => setEmailModal({ open: false, currentEmail: '', title: '', label: '', placeholder: '' })}
>
Cancel
</ActionButton>
<ActionButton
variant="success"
className="flex-1 !py-3 !min-h-0 sm:!min-h-0"
onClick={async () => {
if (emailModal.currentEmail.trim()) {
const result = await changeUserEmail(emailModal.currentEmail.trim());
setAlertModal({
open: true,
title: result.success ? "Success" : "Error",
message: result.message,
type: result.success ? "success" : "error"
});
if (result.success) {
setEmailModal({ open: false, currentEmail: '', title: '', label: '', placeholder: '' });
}
}
}}
>
Change Email
</ActionButton>
</div>
</div>
</div>
</div>,
document.body
)}
{passwordModal.open && createPortal(
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[9999]">
<div className="rounded-2xl w-full max-w-md border border-dash-border bg-dash-surfaceElevated/95">
<div className="p-6 border-b border-dash-border flex justify-between items-center">
<h3 className="text-lg font-semibold text-dash-text">{passwordModal.title}</h3>
<button
type="button"
onClick={() => setPasswordModal({ open: false, title: '', requireCurrent: true })}
className="p-2 rounded-lg border-0 bg-brightBlack/80 text-dash-textMuted hover:text-dash-text hover:bg-brightBlack transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/45"
aria-label="Close"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 space-y-4">
{passwordModal.requireCurrent && (
<div>
<label className="block text-sm font-medium mb-3" style={{ color: colors.foreground }}>Current Password</label>
<input
type="password"
id="currentPassword"
className="w-full rounded-xl px-4 py-3 text-sm border border-dash-border bg-brightBlack/60 text-dash-text placeholder:text-dash-textMuted/45 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/40 focus-visible:border-primary/35 transition-colors"
placeholder="Enter current password"
/>
</div>
)}
<div>
<label className="block text-sm font-medium mb-3" style={{ color: colors.foreground }}>New Password</label>
<input
type="password"
id="newPassword"
className="w-full rounded-xl px-4 py-3 text-sm border border-dash-border bg-brightBlack/60 text-dash-text placeholder:text-dash-textMuted/45 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/40 focus-visible:border-primary/35 transition-colors"
placeholder="Enter new password (min 6 characters)"
/>
</div>
<div>
<label className="block text-sm font-medium mb-3" style={{ color: colors.foreground }}>Confirm New Password</label>
<input
type="password"
id="confirmPassword"
className="w-full rounded-xl px-4 py-3 text-sm border border-dash-border bg-brightBlack/60 text-dash-text placeholder:text-dash-textMuted/45 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/40 focus-visible:border-primary/35 transition-colors"
placeholder="Confirm new password"
/>
</div>
<div className="flex gap-3 mt-6">
<ActionButton
variant="secondary"
className="flex-1 !py-3 !min-h-0 sm:!min-h-0"
onClick={() => setPasswordModal({ open: false, title: '', requireCurrent: true })}
>
Cancel
</ActionButton>
<ActionButton
variant="warning"
className="flex-1 !py-3 !min-h-0 sm:!min-h-0"
onClick={async () => {
const currentPassword = document.getElementById('currentPassword')?.value;
const newPassword = document.getElementById('newPassword')?.value;
const confirmPassword = document.getElementById('confirmPassword')?.value;
if (!newPassword || newPassword.length < 6) {
setAlertModal({
open: true,
title: "Invalid Password",
message: "New password must be at least 6 characters long",
type: "error"
});
return;
}
if (newPassword !== confirmPassword) {
setAlertModal({
open: true,
title: "Passwords Don't Match",
message: "New password and confirmation do not match",
type: "error"
});
return;
}
if (passwordModal.requireCurrent && !currentPassword) {
setAlertModal({
open: true,
title: "Current Password Required",
message: "Please enter your current password",
type: "error"
});
return;
}
const result = await changeUserPassword(currentPassword, newPassword);
setAlertModal({
open: true,
title: result.success ? "Success" : "Error",
message: result.message,
type: result.success ? "success" : "error"
});
if (result.success) {
setPasswordModal({ open: false, title: '', requireCurrent: true });
}
}}
>
Change Password
</ActionButton>
</div>
</div>
</div>
</div>,
document.body
)}
{/* Convert Demo to Paid Server Modal */}
{convertDemoModal.open && convertDemoModal.server && createPortal(
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[9999]">
<div className="rounded-2xl w-full max-w-lg" style={{ backgroundColor: colors.backgroundSecondary }}>
<div className="p-6 border-b flex justify-between items-center" style={{ borderColor: colors.brightYellow }}>
<div className="flex items-center space-x-3">
<Coins className="w-5 h-5" style={{ color: colors.brightYellow }} />
<h3 className="text-lg font-semibold" style={{ color: colors.brightWhite }}>Convert Demo Server to Paid</h3>
</div>
<button
onClick={() => setConvertDemoModal({ open: false, server: null, cost: 0 })}
className="p-2 rounded-lg transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.foreground
}}
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6">
<div className="mb-4">
<p className="text-sm mb-4" style={{ color: colors.foreground }}>
Convert <strong style={{ color: colors.brightWhite }}>{convertDemoModal.server.name}</strong> from a demo server to a paid server. Credits will be charged based on the server specifications.
</p>
{/* Server Specs Display */}
<div className="mb-4 p-4 rounded-xl border" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack }}>
<h4 className="text-sm font-semibold mb-3" style={{ color: colors.brightWhite }}>Server Specifications</h4>
<div className="grid grid-cols-2 gap-3">
<div className="flex items-center space-x-2">
<Cpu className="w-4 h-4" style={{ color: colors.blue }} />
<div>
<span className="text-xs" style={{ color: colors.foreground }}>CPU:</span>
<p className="text-sm font-semibold" style={{ color: colors.brightWhite }}>{convertDemoModal.server.cpu}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<MemoryStick className="w-4 h-4" style={{ color: colors.green }} />
<div>
<span className="text-xs" style={{ color: colors.foreground }}>RAM:</span>
<p className="text-sm font-semibold" style={{ color: colors.brightWhite }}>{convertDemoModal.server.ram}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<HardDrive className="w-4 h-4" style={{ color: colors.yellow }} />
<div>
<span className="text-xs" style={{ color: colors.foreground }}>Storage:</span>
<p className="text-sm font-semibold" style={{ color: colors.brightWhite }}>{convertDemoModal.server.storage}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Server className="w-4 h-4" style={{ color: colors.magenta }} />
<div>
<span className="text-xs" style={{ color: colors.foreground }}>Type:</span>
<p className="text-sm font-semibold" style={{ color: colors.brightWhite }}>{convertDemoModal.server.type?.toUpperCase() || 'VPS'}</p>
</div>
</div>
</div>
</div>
{/* Cost Display */}
<div className="mb-4 p-4 rounded-xl border" style={{ backgroundColor: `${colors.brightYellow}15`, borderColor: colors.brightYellow }}>
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-semibold" style={{ color: colors.brightWhite }}>Cost to Convert:</span>
<span className="text-xl font-bold" style={{ color: colors.brightYellow }}>
{convertDemoModal.cost.toLocaleString()} credits
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs" style={{ color: colors.foreground }}>Your Balance:</span>
<span className="text-sm font-semibold" style={{ color: userCredits >= convertDemoModal.cost ? colors.green : colors.red }}>
{userCredits.toLocaleString()} credits
</span>
</div>
{userCredits < convertDemoModal.cost && (
<div className="mt-2 p-2 rounded border" style={{ backgroundColor: `${colors.red}20`, borderColor: colors.red }}>
<p className="text-xs" style={{ color: colors.red }}>
Insufficient credits. You need {convertDemoModal.cost.toLocaleString()} credits but only have {userCredits.toLocaleString()} credits.
</p>
</div>
)}
<div className="mt-2">
<p className="text-xs" style={{ color: colors.foreground }}>
Rate: 100 credits = {getCurrencySymbol()}{calculateCurrencyValue(100)}
</p>
</div>
</div>
</div>
<div className="flex space-x-3">
<button
onClick={() => setConvertDemoModal({ open: false, server: null, cost: 0 })}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.brightWhite
}}
>
Cancel
</button>
<button
onClick={confirmConvertDemoToPaid}
disabled={userCredits < convertDemoModal.cost}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border disabled:opacity-50 disabled:cursor-not-allowed"
style={{
backgroundColor: userCredits >= convertDemoModal.cost ? colors.brightYellow : colors.brightBlack,
borderColor: userCredits >= convertDemoModal.cost ? colors.brightYellow : colors.brightBlack,
color: userCredits >= convertDemoModal.cost ? colors.black : colors.foreground
}}
>
<div className="flex items-center justify-center space-x-2">
<Coins className="w-4 h-4" />
<span>Convert to Paid</span>
</div>
</button>
</div>
</div>
</div>
</div>,
document.body
)}
{confirmModal.open && createPortal(
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[10000] p-4 backdrop-blur-sm animate-in fade-in duration-300">
<div className="rounded-2xl w-full max-w-md border border-dash-border bg-dash-surfaceElevated shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
<div className={`p-6 border-b flex justify-between items-center ${
confirmModal.type === 'danger' ? 'border-red-500/25' : 'border-dash-border'
}`}>
<div className="flex items-center space-x-3">
{confirmModal.type === 'danger' ? (
<Trash2 className="w-5 h-5 text-brightRed" />
) : (
<Info className="w-5 h-5 text-primary" />
)}
<h3 className="text-lg font-bold text-dash-text">{confirmModal.title}</h3>
</div>
<button
type="button"
onClick={() => setConfirmModal({ ...confirmModal, open: false })}
className="p-2 rounded-lg text-dash-textMuted hover:bg-white/5 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6">
<p className="text-sm leading-relaxed text-dash-textMuted">
{confirmModal.message}
</p>
<div className="flex space-x-3 mt-8">
<button
onClick={() => setConfirmModal({ ...confirmModal, open: false })}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:bg-white/5 border border-dash-border text-dash-text"
>
{confirmModal.cancelLabel}
</button>
<ActionButton
variant={confirmModal.type === 'danger' ? 'danger' : 'primary'}
onClick={() => {
confirmModal.onConfirm();
setConfirmModal({ ...confirmModal, open: false });
}}
className="flex-1"
>
{confirmModal.confirmLabel}
</ActionButton>
</div>
</div>
</div>
</div>,
document.body
)}
{alertModal.open && createPortal(
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[10000]">
<div className="rounded-2xl w-full max-w-md border border-dash-border bg-dash-surfaceElevated/95">
<div
className={`p-6 border-b flex justify-between items-center ${
alertModal.type === 'error'
? 'border-red-500/25'
: alertModal.type === 'success'
? 'border-green-500/25'
: alertModal.type === 'warning'
? 'border-brightYellow/25'
: 'border-dash-border'
}`}
>
<div className="flex items-center space-x-3">
{alertModal.type === 'success' && (
<CheckCircle className="w-5 h-5" style={{ color: colors.green }} />
)}
{alertModal.type === 'error' && (
<AlertCircle className="w-5 h-5" style={{ color: colors.red }} />
)}
{alertModal.type === 'warning' && (
<AlertTriangle className="w-5 h-5" style={{ color: colors.yellow }} />
)}
{alertModal.type === 'info' && (
<Info className="w-5 h-5" style={{ color: colors.blue }} />
)}
<h3 className="text-lg font-semibold text-dash-text">{alertModal.title}</h3>
</div>
<button
type="button"
onClick={() => setAlertModal({ open: false, title: '', message: '', type: 'info' })}
className="p-2 rounded-lg border-0 bg-brightBlack/80 text-dash-textMuted hover:text-dash-text hover:bg-brightBlack transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/45"
aria-label="Close"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6">
<p className="text-sm" style={{ color: colors.foreground }}>{alertModal.message}</p>
<div className="flex justify-end mt-6">
<ActionButton
variant={
alertModal.type === 'error'
? 'danger'
: alertModal.type === 'success'
? 'success'
: alertModal.type === 'warning'
? 'warning'
: 'primary'
}
className="!px-6 !py-3 !min-h-0 sm:!min-h-0"
onClick={() => setAlertModal({ open: false, title: '', message: '', type: 'info' })}
>
OK
</ActionButton>
</div>
</div>
</div>
</div>,
document.body
)}
{deleteAccountModal && createPortal(
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[10000]">
<div className="rounded-2xl w-full max-w-md border border-red-500/35 bg-dash-surfaceElevated/95">
<div className="p-6 border-b border-red-500/25 flex justify-between items-center">
<div className="flex items-center space-x-3">
<AlertTriangle className="w-5 h-5" style={{ color: colors.red }} />
<h3 className="text-lg font-semibold text-dash-text">Delete Account</h3>
</div>
<button
type="button"
onClick={() => setDeleteAccountModal(false)}
className="p-2 rounded-lg border-0 bg-brightBlack/80 text-dash-textMuted hover:text-dash-text hover:bg-brightBlack transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/45"
aria-label="Close"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6">
<div className="mb-4 p-4 rounded-xl border border-red-500/30 bg-red-500/10">
<p className="text-sm text-dash-text">Warning: This action cannot be undone!</p>
<p className="text-xs mt-1" style={{ color: colors.foreground }}>
All your servers, domains, tickets, and account data will be permanently deleted.
</p>
</div>
<div className="mb-6">
<label className="block text-sm font-medium mb-3" style={{ color: colors.foreground }}>
Type <span className="font-bold">DELETE MY ACCOUNT</span> to confirm:
</label>
<input
type="text"
id="deleteConfirmation"
className="w-full rounded-xl px-4 py-3 text-sm border border-dash-border bg-brightBlack/60 text-dash-text placeholder:text-dash-textMuted/45 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/40 focus-visible:border-primary/35 transition-colors"
placeholder="DELETE MY ACCOUNT"
/>
</div>
<div className="flex gap-3">
<ActionButton
variant="secondary"
className="flex-1 !py-3 !min-h-0 sm:!min-h-0"
onClick={() => setDeleteAccountModal(false)}
>
Cancel
</ActionButton>
<ActionButton
variant="danger"
className="flex-1 !py-3 !min-h-0 sm:!min-h-0"
onClick={async () => {
const confirmation = document.getElementById('deleteConfirmation')?.value;
if (confirmation === 'DELETE MY ACCOUNT') {
await deleteUserAccount(confirmation);
setDeleteAccountModal(false);
} else {
setAlertModal({
open: true,
title: "Invalid Confirmation",
message: "Please type exactly: DELETE MY ACCOUNT",
type: "error"
});
}
}}
>
Delete Account
</ActionButton>
</div>
</div>
</div>
</div>,
document.body
)}
{/* Delete Server Confirmation Modal */}
{deleteServerModal.open && createPortal(
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[10000]">
<div className="rounded-2xl w-full max-w-md border" style={{ backgroundColor: colors.backgroundSecondary, borderColor: colors.red }}>
<div className="p-6 border-b flex justify-between items-center" style={{ borderColor: colors.red }}>
<div className="flex items-center space-x-3">
<AlertTriangle className="w-5 h-5" style={{ color: colors.red }} />
<h3 className="text-lg font-semibold" style={{ color: colors.brightWhite }}>Delete Server</h3>
</div>
<button
onClick={() => setDeleteServerModal({ open: false, serverId: null })}
className="p-2 rounded-lg transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.foreground
}}
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6">
<div className="mb-4 p-4 rounded-xl border" style={{ backgroundColor: `${colors.red}10`, borderColor: colors.red }}>
<p className="text-sm" style={{ color: colors.brightWhite }}>Warning: This action cannot be undone!</p>
<p className="text-xs mt-1" style={{ color: colors.foreground }}>
All server data, files, and configurations will be permanently deleted.
</p>
</div>
<div className="flex space-x-3">
<button
onClick={() => setDeleteServerModal({ open: false, serverId: null })}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.brightBlack,
borderColor: colors.brightBlack,
color: colors.brightWhite
}}
>
Cancel
</button>
<button
onClick={confirmDeleteServer}
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: colors.red,
borderColor: colors.red,
color: colors.brightWhite
}}
>
Delete Server
</button>
</div>
</div>
</div>
</div>,
document.body
)}
{/* Server Creation Progress Notifier */}
{serverCreationProgress.visible && createPortal(
<div className="fixed bottom-0 left-0 right-0 z-[9999] p-4 flex justify-center pointer-events-none">
<div
className="w-full max-w-md rounded-xl border shadow-2xl animate-slide-up pointer-events-auto"
style={{
backgroundColor: colors.backgroundSecondary,
borderColor: colors.brightBlack
}}
>
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
<div className="p-2 rounded-lg" style={{ backgroundColor: `${colors.blue}20` }}>
<Activity className="w-5 h-5" style={{ color: colors.blue }} />
</div>
<div>
<h4 className="text-sm font-semibold" style={{ color: colors.brightWhite }}>
Server Creation in Progress
</h4>
<p className="text-xs mt-0.5" style={{ color: colors.foreground }}>
{serverCreationProgress.message}
</p>
</div>
</div>
<button
onClick={() => setServerCreationProgress(prev => ({ ...prev, visible: false }))}
className="p-1 rounded-lg transition-all duration-200 hover:scale-105"
style={{
backgroundColor: colors.brightBlack,
color: colors.foreground
}}
>
<X className="w-4 h-4" />
</button>
</div>
{/* Progress Bar */}
<div className="w-full h-2 rounded-full overflow-hidden" style={{ backgroundColor: colors.brightBlack }}>
<div
className="h-full rounded-full transition-all duration-300 ease-out"
style={{
width: `${serverCreationProgress.progress}%`,
backgroundColor: colors.blue
}}
/>
</div>
{/* Progress Percentage */}
<div className="flex items-center justify-between mt-2">
<span className="text-xs" style={{ color: colors.foreground }}>
{serverCreationProgress.phase}
</span>
<span className="text-xs font-medium" style={{ color: colors.blue }}>
{serverCreationProgress.progress.toFixed(1)}%
</span>
</div>
</div>
</div>
</div>,
document.body
)}
{show2FASetup && createPortal(
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[9999]">
<div className="rounded-2xl w-full max-w-md" style={{ backgroundColor: colors.backgroundSecondary }}>
<div className="p-6 border-b flex justify-between items-center" style={{ borderColor: colors.brightBlack }}>
<h3 className="text-lg font-semibold" style={{ color: colors.brightWhite }}>Enable Two-Factor Authentication</h3>
<button onClick={() => setShow2FASetup(false)} className="p-2 rounded-lg transition-all duration-200 hover:scale-105 border" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack, color: colors.foreground }}>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6">
<p className="text-sm text-center" style={{ color: colors.foreground }}>Scan the QR code with your authenticator app.</p>
<div className="flex justify-center my-4">
<img src={`data:image/png;base64,${twoFactorData.qr_code}`} alt="2FA QR Code" />
</div>
<p className="text-sm text-center" style={{ color: colors.foreground }}>Then enter the code from your app below.</p>
<div className="mt-4">
<input type="text" id="otp-code" className="w-full rounded-xl px-4 py-3 text-sm focus:outline-none transition-all duration-200 border" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack, color: colors.brightWhite }} placeholder="6-digit code" />
</div>
<div className="flex space-x-3 mt-6">
<button onClick={() => setShow2FASetup(false)} className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack, color: colors.brightWhite }}>
Cancel
</button>
<button onClick={() => {
const otpCode = document.getElementById('otp-code').value;
if (otpCode) {
sendDashboardMessage({ type: '2fa_enable', otp_code: otpCode });
}
}} className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border" style={{ backgroundColor: colors.green, borderColor: colors.green, color: colors.brightWhite }}>
Verify & Enable
</button>
</div>
</div>
</div>
</div>,
document.body
)}
</div>
);
}
export default DashboardPage;