632 lines
29 KiB
React
632 lines
29 KiB
React
import React, { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
Monitor, Gamepad2, Globe, Plus, Coins, Cpu, MemoryStick, HardDrive,
|
|
Check, Zap, Crown, Award, Rocket, Server, Star, MapPin,
|
|
Layers, Box, Settings, Database, RefreshCw
|
|
} from "lucide-react";
|
|
import { colors } from '../../theme/colors';
|
|
import { dashLayout } from '../../theme/dashboardTokens';
|
|
import { DashboardViewShell, PageHeaderBar, ActionButton, Dropdown } from '../../components/dashboardPrimitives';
|
|
import { minecraftVersions } from '../../components/DashboardComponents';
|
|
|
|
import { API_BASE_URL } from '../../config/api';
|
|
|
|
const Slider = ({ value, min, max, step, onChange, label, unit, icon: Icon, color }) => {
|
|
const percentage = ((value - min) / (max - min)) * 100;
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Icon className="w-4 h-4" style={{ color }} />
|
|
<span className="text-sm font-medium" style={{ color: colors.foreground }}>{label}</span>
|
|
</div>
|
|
<div className="px-3 py-1 rounded-lg text-sm font-bold" style={{ backgroundColor: `${color}20`, color }}>
|
|
{value} {unit}
|
|
</div>
|
|
</div>
|
|
<div className="relative h-3 rounded-full cursor-pointer" style={{ backgroundColor: colors.brightBlack }}>
|
|
<div className="absolute h-full rounded-full transition-all duration-150" style={{ width: `${percentage}%`, backgroundColor: color }} />
|
|
<input type="range" min={min} max={max} step={step} value={value} onChange={(e) => onChange(Number(e.target.value))} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
|
|
<div className="absolute top-1/2 -translate-y-1/2 w-5 h-5 rounded-full border-2 transition-all duration-150 shadow-lg" style={{ left: `calc(${percentage}% - 10px)`, backgroundColor: colors.black, borderColor: color }} />
|
|
</div>
|
|
<div className="flex justify-between text-xs" style={{ color: colors.white }}>
|
|
<span>{min} {unit}</span>
|
|
<span>{max} {unit}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const SectionCard = ({ title, icon: Icon, children, color = colors.blue }) => (
|
|
<div className="rounded-2xl p-6 border border-dash-border transition-colors duration-200 bg-white/[0.02] backdrop-blur-sm w-full">
|
|
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-white/5">
|
|
<div className="p-2.5 rounded-xl shadow-lg shadow-black/20" style={{ backgroundColor: `${color}15` }}>
|
|
<Icon className="w-5 h-5" style={{ color }} />
|
|
</div>
|
|
<h3 className="text-lg font-bold tracking-tight" style={{ color: colors.brightWhite }}>
|
|
{title}
|
|
</h3>
|
|
</div>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
export default function ServerCreationForm({ onCreateServer, isCreating: externalIsCreating, userCredits = 5000, selectedCurrency = 'EUR', onCurrencyChange, wsClient, 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 [serverName, setServerName] = useState('');
|
|
const [serverNameError, setServerNameError] = useState('');
|
|
const [serverType, setServerType] = useState('vps');
|
|
const [cpu, setCpu] = useState(2);
|
|
const [ram, setRam] = useState(4);
|
|
const [storage, setStorage] = useState(50);
|
|
const [selectedImage, setSelectedImage] = useState('');
|
|
const [selectedVersion, setSelectedVersion] = useState('');
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
const [capacityResult, setCapacityResult] = useState(null);
|
|
const [demoEligible, setDemoEligible] = useState(null);
|
|
const [checkingDemoEligibility, setCheckingDemoEligibility] = useState(false);
|
|
const [isDemo, setIsDemo] = useState(false);
|
|
const [selectedJavaVersion, setSelectedJavaVersion] = useState('Java 21');
|
|
const [selectedLoader, setSelectedLoader] = useState('Paper');
|
|
|
|
// State for currencies fetched from backend
|
|
const [currencies, setCurrencies] = useState([]);
|
|
|
|
const checkCapacity = useCallback(async () => {
|
|
if (!serverType || !cpu || !ram || !storage) return;
|
|
|
|
// We don't need setCheckingCapacity(true) as it was removed from state
|
|
setCapacityResult({ checking: true });
|
|
|
|
try {
|
|
// Prepare server config for capacity check
|
|
const serverConfig = {
|
|
name: 'capacity-check',
|
|
serverType: serverType,
|
|
cpu: `${cpu} vCPU`,
|
|
ram: `${ram}GB`,
|
|
storage: `${storage}GB`,
|
|
image: selectedImage || 'ubuntu',
|
|
version: selectedVersion || 'latest'
|
|
};
|
|
|
|
// Get token from localStorage or session
|
|
const token = localStorage.getItem('anchorhost_token') || localStorage.getItem('jwtToken') || '';
|
|
|
|
// Make API call to check capacity
|
|
const response = await fetch(`${API_BASE_URL}/api/capacity/check`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify(serverConfig)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
setCapacityResult({
|
|
success: data.capacity_check.success,
|
|
message: data.capacity_check.message,
|
|
suitable_nodes: data.capacity_check.suitable_nodes,
|
|
details: data.capacity_check.details,
|
|
summary: data.capacity_check.summary,
|
|
cluster: data.cluster_capacity
|
|
});
|
|
} else {
|
|
setCapacityResult({
|
|
success: false,
|
|
message: data.error || 'Capacity check failed',
|
|
checking: false
|
|
});
|
|
}
|
|
} else if (response.status === 401) {
|
|
// Handle unauthorized
|
|
setCapacityResult({
|
|
success: false,
|
|
message: 'Authentication required. Please log in again.',
|
|
checking: false
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error('Capacity check error:', err);
|
|
setCapacityResult({
|
|
success: false,
|
|
message: 'Could not connect to capacity API',
|
|
checking: false
|
|
});
|
|
}
|
|
}, [serverType, cpu, ram, storage, selectedImage, selectedVersion]);
|
|
|
|
// Fallback function to set default EUR-based currencies
|
|
const setDefaultEuroBasedCurrencies = useCallback(() => {
|
|
console.log('Setting default EUR-based currencies');
|
|
const defaultCurrencies = [
|
|
{ code: 'EUR', name: 'Euro', symbol: '€', rate: 1.00 },
|
|
{ code: 'USD', name: 'US Dollar', symbol: '$', rate: 1.08 },
|
|
{ code: 'GBP', name: 'British Pound', symbol: '£', rate: 0.85 },
|
|
{ code: 'PLN', name: 'Polish Złoty', symbol: 'zł', rate: 4.32 },
|
|
{ code: 'PHP', name: 'Philippine Peso', symbol: '₱', rate: 60.50 },
|
|
{ code: 'CZK', name: 'Czech Koruna', symbol: 'Kč', rate: 24.65 }
|
|
];
|
|
setCurrencies(defaultCurrencies);
|
|
}, []);
|
|
|
|
// Minecraft compatibility logic
|
|
useEffect(() => {
|
|
if (serverType === 'game' && selectedImage === 'minecraft' && selectedVersion) {
|
|
// Determine major version
|
|
const parts = selectedVersion.split('.');
|
|
if (parts.length >= 1) {
|
|
const major = parseInt(parts[0]);
|
|
const minor = parts.length >= 2 ? parseInt(parts[1]) : 0;
|
|
const patch = parts.length >= 3 ? parseInt(parts[2]) : 0;
|
|
|
|
// Java Version Logic
|
|
if (major >= 26) {
|
|
setSelectedJavaVersion('Java 25');
|
|
} else if (major === 1) {
|
|
if (minor >= 21 || (minor === 20 && patch >= 5)) {
|
|
setSelectedJavaVersion('Java 21');
|
|
} else if (minor >= 18) {
|
|
setSelectedJavaVersion('Java 17');
|
|
} else if (minor >= 17) {
|
|
setSelectedJavaVersion('Java 16');
|
|
} else if (minor >= 16) {
|
|
setSelectedJavaVersion('Java 11');
|
|
} else if (minor >= 12) {
|
|
setSelectedJavaVersion('Java 8');
|
|
} else {
|
|
setSelectedJavaVersion('Java 8');
|
|
}
|
|
}
|
|
|
|
// Loader Logic
|
|
if (selectedLoader === 'Fabric' && minor < 14) {
|
|
setSelectedLoader('Forge');
|
|
}
|
|
}
|
|
}
|
|
}, [selectedVersion, serverType, selectedImage, selectedLoader]);
|
|
|
|
// Enforce demo server specs when isDemo is true
|
|
useEffect(() => {
|
|
if (isDemo) {
|
|
if (cpu !== 2) setCpu(2);
|
|
if (ram !== 3) setRam(3);
|
|
if (storage !== 20) setStorage(20);
|
|
}
|
|
}, [isDemo, cpu, ram, storage]);
|
|
|
|
// Auto-check capacity when resources change
|
|
useEffect(() => {
|
|
if (serverType && cpu && ram && storage) {
|
|
const debounceTimer = setTimeout(() => {
|
|
checkCapacity();
|
|
}, 1000);
|
|
|
|
return () => clearTimeout(debounceTimer);
|
|
}
|
|
}, [serverType, cpu, ram, storage, selectedImage, checkCapacity]);
|
|
|
|
// Fetch exchange rates from backend
|
|
const fetchExchangeRates = useCallback(async () => {
|
|
try {
|
|
console.log('Fetching exchange rates from backend...');
|
|
|
|
const response = await fetch(`${API_BASE_URL}/api/exchange-rates`);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.rates) {
|
|
console.log('✅ Exchange rates fetched successfully:', data.rates);
|
|
|
|
const backendCurrencies = [
|
|
{ code: 'EUR', name: 'Euro', symbol: '€', rate: 1.00 },
|
|
{ code: 'USD', name: 'US Dollar', symbol: '$', rate: data.rates.USD || 0 },
|
|
{ code: 'GBP', name: 'British Pound', symbol: '£', rate: data.rates.GBP || 0 },
|
|
{ code: 'PLN', name: 'Polish Złoty', symbol: 'zł', rate: data.rates.PLN || 0 },
|
|
{ code: 'PHP', name: 'Philippine Peso', symbol: '₱', rate: data.rates.PHP || 0 },
|
|
{ code: 'CZK', name: 'Czech Koruna', symbol: 'Kč', rate: data.rates.CZK || 0 }
|
|
];
|
|
|
|
const filteredCurrencies = backendCurrencies.filter(currency =>
|
|
currency.code === 'EUR' || currency.rate > 0
|
|
);
|
|
|
|
setCurrencies(filteredCurrencies);
|
|
console.log('💱 Currency rates updated from API, base: EUR');
|
|
} else {
|
|
console.warn('Failed to fetch exchange rates:', data.error);
|
|
setDefaultEuroBasedCurrencies();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching exchange rates:', error);
|
|
setDefaultEuroBasedCurrencies();
|
|
}
|
|
}, [setDefaultEuroBasedCurrencies]);
|
|
|
|
// Initialize with default EUR-based currencies on first render
|
|
useEffect(() => {
|
|
if (currencies.length === 0) {
|
|
setDefaultEuroBasedCurrencies();
|
|
}
|
|
}, [currencies.length, setDefaultEuroBasedCurrencies]);
|
|
|
|
// Fetch exchange rates on component mount
|
|
useEffect(() => {
|
|
fetchExchangeRates();
|
|
const interval = setInterval(fetchExchangeRates, 3600000); // 1 hour
|
|
return () => clearInterval(interval);
|
|
}, [fetchExchangeRates]);
|
|
|
|
const checkDemoEligibility = useCallback(() => {
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
return;
|
|
}
|
|
|
|
setCheckingDemoEligibility(true);
|
|
wsClient.send(JSON.stringify({
|
|
type: 'check_demo_eligibility'
|
|
}));
|
|
}, [wsClient]);
|
|
|
|
// Check demo eligibility when WebSocket is available and connected
|
|
useEffect(() => {
|
|
if (!wsClient) return;
|
|
|
|
const checkEligibility = () => {
|
|
if (wsClient.readyState === WebSocket.OPEN && demoEligible === null && !checkingDemoEligibility) {
|
|
checkDemoEligibility();
|
|
}
|
|
};
|
|
|
|
if (wsClient.readyState === WebSocket.OPEN) {
|
|
checkEligibility();
|
|
}
|
|
|
|
wsClient.addEventListener('open', checkEligibility);
|
|
return () => wsClient.removeEventListener('open', checkEligibility);
|
|
}, [wsClient, demoEligible, checkingDemoEligibility, checkDemoEligibility]);
|
|
|
|
// Handle demo eligibility response
|
|
useEffect(() => {
|
|
if (!wsClient) return;
|
|
|
|
const handleMessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'demo_eligibility') {
|
|
setDemoEligible(data.eligible);
|
|
setCheckingDemoEligibility(false);
|
|
}
|
|
} catch (e) {}
|
|
};
|
|
|
|
wsClient.addEventListener('message', handleMessage);
|
|
return () => wsClient.removeEventListener('message', handleMessage);
|
|
}, [wsClient]);
|
|
|
|
// Calculate currency value based on EUR
|
|
const calculateCurrencyValue = (credits) => {
|
|
const selectedCurrencyData = currencies.find(c => c.code === selectedCurrency);
|
|
if (!selectedCurrencyData) return (credits / 100).toFixed(2);
|
|
const eurValue = credits / 100;
|
|
const rate = selectedCurrencyData.rate;
|
|
return (eurValue * rate).toFixed(2);
|
|
};
|
|
|
|
// Get currency symbol
|
|
const getCurrencySymbol = () => {
|
|
const currency = currencies.find(c => c.code === selectedCurrency);
|
|
return currency ? currency.symbol : '€';
|
|
};
|
|
|
|
const serverTypes = [
|
|
{ id: 'vps', name: 'VPS', icon: Monitor, description: 'Virtual Private Server', color: colors.blue },
|
|
{ id: 'game', name: 'Game Server', icon: Gamepad2, description: 'Game Server Hosting', color: colors.brightRed },
|
|
{ id: 'website', name: 'Website', icon: Globe, description: 'Web Hosting', color: colors.brightGreen },
|
|
{ id: 'database', name: 'Database', icon: Database, description: 'Database Hosting', color: colors.magenta }
|
|
];
|
|
|
|
const presets = [
|
|
{ name: "Starter", cpu: 1, ram: 1, storage: 10, icon: Rocket, color: colors.blue, tag: "Basic" },
|
|
{ name: "Standard", cpu: 2, ram: 2, storage: 20, icon: Crown, color: colors.brightGreen, tag: "Popular", popular: true },
|
|
{ name: "Pro", cpu: 3, ram: 4, storage: 40, icon: Award, color: colors.magenta, tag: "Pro" },
|
|
{ name: "Enterprise", cpu: 4, ram: 8, storage: 60, icon: Zap, color: colors.brightYellow, tag: "Max" }
|
|
];
|
|
|
|
const flatMinecraftVersions = minecraftVersions.flatMap(v => v.versions);
|
|
|
|
const images = {
|
|
vps: [
|
|
{ id: 'ubuntu', name: 'Ubuntu', versions: ['24.04', '22.04', '20.04', '18.04'], popular: true },
|
|
{ id: 'debian', name: 'Debian', versions: ['13', '12', '11', '10', '9'] },
|
|
{ id: 'alpine', name: 'Alpine', versions: ['3.22', '3.21', '3.20'] },
|
|
{ id: 'rocky', name: 'Rocky Linux', versions: ['9', '8'] },
|
|
{ id: 'openbsd', name: 'OpenBSD', versions: ['7.7', '7.6'] },
|
|
{ id: 'windows', name: 'Windows Server', versions: ['2022', '2019'] }
|
|
],
|
|
game: [
|
|
{ id: 'minecraft', name: 'Minecraft', versions: flatMinecraftVersions, popular: true },
|
|
{ id: 'fivem', name: 'FiveM', versions: ['latest'] },
|
|
{ id: 'cs2', name: 'Counter-Strike 2', versions: ['latest'] },
|
|
{ id: 'rust', name: 'Rust', versions: ['latest'] },
|
|
{ id: 'hytale', name: 'Hytale', versions: ['latest'] },
|
|
{ id: 'unturned', name: 'Unturned', versions: ['latest'] },
|
|
{ id: 'zomboid', name: 'Project Zomboid', versions: ['latest', 'unstable'] },
|
|
{ id: 'factorio', name: 'Factorio', versions: ['latest', 'stable'] },
|
|
{ id: 'ark', name: 'ARK: Survival Evolved', versions: ['latest'] },
|
|
{ id: 'valheim', name: 'Valheim', versions: ['latest'] }
|
|
],
|
|
website: [
|
|
{ id: 'nginx', name: 'Nginx', versions: ['latest', 'alpine'], popular: true },
|
|
{ id: 'wordpress', name: 'WordPress', versions: ['latest', 'php8.2', 'php8.1'], popular: true, notice: 'Requires a separate MySQL/PostgreSQL database server.' },
|
|
{ id: 'wordpress-sqlite', name: 'WordPress (SQLite)', versions: [
|
|
'6.9.4-php8.5-apache',
|
|
'6.9.4-php8.4-apache',
|
|
'6.9.4-php8.3-apache',
|
|
'6.9.2-php8.5-apache',
|
|
'6.9.1-php8.5-apache',
|
|
'6.9.0-php8.5-apache',
|
|
'6.8.3-php8.5-apache',
|
|
'6.8.2-php8.4-apache',
|
|
'6.7.2-php8.4-apache'
|
|
], popular: true },
|
|
{ id: 'apache', name: 'Apache', versions: ['latest', '2.4'] },
|
|
{ id: 'node', name: 'Node.js', versions: ['22', '20', '18'] }
|
|
],
|
|
database: [
|
|
{ id: 'mysql', name: 'MySQL', versions: ['8.0', '5.7'], popular: true },
|
|
{ id: 'postgresql', name: 'PostgreSQL', versions: ['15', '14', '13'], popular: true },
|
|
{ id: 'mongodb', name: 'MongoDB', versions: ['7.0', '6.0', '5.0'] },
|
|
{ id: 'redis', name: 'Redis', versions: ['7.2', '7.0', '6.2'] }
|
|
]
|
|
};
|
|
|
|
const currentImages = images[serverType] || [];
|
|
const selectedImageData = currentImages.find(i => i.id === selectedImage);
|
|
|
|
const calculateCost = () => {
|
|
const cpuCost = cpu * (pricing.price_cpu || 35);
|
|
const ramCost = ram * (pricing.price_ram || 35);
|
|
const storageCost = storage * (pricing.price_storage || 5);
|
|
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
|
|
}[serverType] || 1.0;
|
|
return Math.round((cpuCost + ramCost + storageCost) * typeMultiplier);
|
|
};
|
|
|
|
const serverCost = calculateCost();
|
|
const canCreate = userCredits >= serverCost && capacityResult?.success;
|
|
const applyPreset = (p) => {
|
|
if (isDemo) return;
|
|
setCpu(p.cpu);
|
|
setRam(p.ram);
|
|
setStorage(p.storage);
|
|
setTimeout(() => checkCapacity(), 100);
|
|
};
|
|
const isPresetActive = (p) => cpu === p.cpu && ram === p.ram && storage === p.storage;
|
|
|
|
const getDefaultPorts = (type, image) => {
|
|
if (type === 'website') return [{ internal: 80, external: 0, protocol: 'TCP' }];
|
|
if (type === 'game') {
|
|
const ports = { minecraft: 25565, fivem: 30120, cs2: 27015, rust: 28015, ark: 7777, valheim: 2456 };
|
|
return [{ internal: ports[image] || 27015, external: 0, protocol: 'TCP' }];
|
|
}
|
|
if (type === 'database') {
|
|
const ports = { mysql: 3306, postgresql: 5432, mongodb: 27017, redis: 6379 };
|
|
return [{ internal: ports[image] || 3306, external: 0, protocol: 'TCP' }];
|
|
}
|
|
return [{ internal: 22, external: 0, protocol: 'TCP' }];
|
|
};
|
|
|
|
const validateServerName = (name) => {
|
|
if (!name || !name.trim()) return 'Server name cannot be empty';
|
|
if (!/^[a-zA-Z\s]+$/.test(name)) return 'Server name can only contain letters and spaces.';
|
|
if (name.length > 50) return 'Server name must be 50 characters or less';
|
|
return '';
|
|
};
|
|
|
|
const handleServerNameChange = (e) => {
|
|
const value = e.target.value;
|
|
setServerName(value);
|
|
setServerNameError(validateServerName(value));
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
const nameError = validateServerName(serverName);
|
|
if (nameError) {
|
|
setServerNameError(nameError);
|
|
return;
|
|
}
|
|
if (!serverName.trim() || (!isDemo && !canCreate) || !selectedImage) return;
|
|
|
|
setIsCreating(true);
|
|
if (onCreateServer) {
|
|
const serverConfig = {
|
|
name: serverName, type: serverType, cpu: `${cpu} vCPU`, ram: `${ram}GB`, storage: `${storage}GB`,
|
|
image: selectedImage, version: selectedVersion, ports: getDefaultPorts(serverType, selectedImage),
|
|
is_demo: isDemo, java_version: selectedJavaVersion, loader: selectedLoader
|
|
};
|
|
try {
|
|
await onCreateServer(serverConfig);
|
|
setTimeout(() => {
|
|
setIsCreating(false); setServerName(''); setServerType('vps'); setCpu(2); setRam(4); setStorage(50);
|
|
setSelectedImage(''); setSelectedVersion(''); setCapacityResult(null);
|
|
}, 2000);
|
|
} catch (error) { setIsCreating(false); }
|
|
}
|
|
};
|
|
|
|
const creating = externalIsCreating || isCreating;
|
|
|
|
return (
|
|
<div className="w-full space-y-6">
|
|
<DashboardViewShell>
|
|
<PageHeaderBar
|
|
accentColor={colors.green}
|
|
icon={Plus}
|
|
title="Create new server"
|
|
description="Deploy your perfect server in seconds"
|
|
actions={
|
|
<div className="flex items-center gap-3">
|
|
{demoEligible && (
|
|
<ActionButton
|
|
type="button"
|
|
onClick={() => setIsDemo((prev) => !prev)}
|
|
variant={isDemo ? 'primary' : 'secondary'}
|
|
className="!px-4 !py-2 !min-h-0"
|
|
>
|
|
{isDemo ? 'Demo Mode ON' : 'Try Free Demo'}
|
|
</ActionButton>
|
|
)}
|
|
<div className="flex items-center gap-3 px-4 py-3 rounded-xl border border-white/10 bg-black/20">
|
|
<Coins className="w-5 h-5 shrink-0" style={{ color: colors.brightYellow }} />
|
|
<p className="font-bold tabular-nums text-brightYellow">{userCredits.toLocaleString()}</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
<div className={dashLayout.bodyPadding}>
|
|
<SectionCard title="Server Type" icon={Server} color={colors.blue}>
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
{serverTypes.map((type) => {
|
|
const Icon = type.icon;
|
|
const isSelected = serverType === type.id;
|
|
return (
|
|
<button key={type.id} type="button" onClick={() => { setServerType(type.id); setSelectedImage(''); setSelectedVersion(''); setCapacityResult(null); }} className="p-4 rounded-xl border-2 transition-colors duration-200 hover:bg-white/[0.03] text-left h-full" style={{ backgroundColor: isSelected ? `${type.color}15` : colors.brightBlack, borderColor: isSelected ? type.color : 'transparent' }}>
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 rounded-lg shrink-0" style={{ backgroundColor: `${type.color}20` }}>
|
|
<Icon className="w-5 h-5 shrink-0" style={{ color: type.color }} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-semibold truncate" style={{ color: colors.brightWhite }}>{type.name}</p>
|
|
<p className="text-xs truncate" style={{ color: colors.foreground }}>{type.description}</p>
|
|
</div>
|
|
{isSelected && <Check className="w-5 h-5 shrink-0" style={{ color: type.color }} />}
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard title="Quick Presets" icon={Settings} color={colors.magenta}>
|
|
{isDemo ? (
|
|
<div className="p-4 rounded-xl border-2" style={{ backgroundColor: colors.brightBlack, borderColor: colors.yellow }}>
|
|
<p className="text-sm" style={{ color: colors.foreground }}>Presets disabled for demo servers (fixed specs).</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{presets.map((preset) => {
|
|
const Icon = preset.icon;
|
|
const isActive = isPresetActive(preset);
|
|
return (
|
|
<button key={preset.name} type="button" onClick={() => applyPreset(preset)} className="relative p-4 rounded-xl border-2 transition-all duration-200 hover:scale-105 h-full" style={{ backgroundColor: isActive ? `${preset.color}15` : colors.brightBlack, borderColor: isActive ? preset.color : 'transparent' }}>
|
|
{preset.popular && <div className="absolute -top-2 -right-2"><Star className="w-5 h-5" style={{ color: colors.brightYellow, fill: colors.brightYellow }} /></div>}
|
|
<Icon className="w-6 h-6 mx-auto mb-2" style={{ color: preset.color }} />
|
|
<p className="font-semibold text-sm text-center" style={{ color: colors.brightWhite }}>{preset.name}</p>
|
|
<p className="text-xs mt-1 text-center" style={{ color: colors.foreground }}>{preset.cpu}C • {preset.ram}GB • {preset.storage}GB</p>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</SectionCard>
|
|
|
|
<SectionCard title="Custom Configuration" icon={Cpu} color={colors.brightCyan}>
|
|
{isDemo ? (
|
|
<div className="p-4 rounded-xl border-2" style={{ backgroundColor: colors.brightBlack, borderColor: colors.yellow }}>
|
|
<p className="text-sm" style={{ color: colors.foreground }}>Custom configuration disabled for demo servers.</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
<Slider value={cpu} min={1} max={4} step={1} onChange={(v) => setCpu(v)} label="vCPU Cores" unit="vCPU" icon={Cpu} color={colors.blue} />
|
|
<Slider value={ram} min={1} max={12} step={1} onChange={(v) => setRam(v)} label="RAM" unit="GB" icon={MemoryStick} color={colors.brightGreen} />
|
|
<Slider value={storage} min={10} max={100} step={5} onChange={(v) => setStorage(v)} label="Storage" unit="GB" icon={HardDrive} color={colors.brightYellow} />
|
|
</div>
|
|
)}
|
|
</SectionCard>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<SectionCard title="Server Image" icon={Layers} color={colors.brightMagenta}>
|
|
<div className="space-y-4">
|
|
<Dropdown value={selectedImage} options={currentImages.map(i => ({ value: i.id, label: i.name }))} onChange={(v) => { setSelectedImage(v); setSelectedVersion(currentImages.find(i => i.id === v)?.versions[0] || ''); }} placeholder="Server Image" icon={Layers} />
|
|
{selectedImage && <Dropdown value={selectedVersion} options={selectedImageData?.versions.map(v => ({ value: v, label: v })) || []} onChange={setSelectedVersion} placeholder="Select Version" icon={Settings} />}
|
|
</div>
|
|
</SectionCard>
|
|
<SectionCard title="Region" icon={MapPin} color={colors.brightCyan}>
|
|
<div className="p-4 rounded-xl border-2 flex items-center gap-3 " style={{ backgroundColor: `${colors.brightCyan}15`, borderColor: colors.brightCyan }}>
|
|
<div className="text-2xl">🇵🇱</div>
|
|
<div className="flex-1"><p className="font-semibold" style={{ color: colors.brightWhite }}>Reda, Poland</p></div>
|
|
<Check className="w-5 h-5 ml-auto" style={{ color: colors.brightCyan }} />
|
|
</div>
|
|
</SectionCard>
|
|
</div>
|
|
|
|
<SectionCard title="Server Name" icon={Server} color={colors.blue}>
|
|
<input type="text" value={serverName} onChange={handleServerNameChange} className="w-full rounded-xl px-4 py-3 text-sm border-2 focus:outline-none" style={{ backgroundColor: colors.brightBlack, borderColor: serverNameError ? colors.red : (serverName ? colors.blue : 'transparent'), color: colors.brightWhite }} placeholder="My Awesome Server" />
|
|
{serverNameError && <p className="text-xs mt-1" style={{ color: colors.red }}>{serverNameError}</p>}
|
|
</SectionCard>
|
|
|
|
<div className="rounded-2xl p-6 border border-dash-border bg-white/[0.02] backdrop-blur-sm">
|
|
<h3 className="text-lg font-bold tracking-tight mb-4 pb-4 border-b border-white/5 flex items-center gap-2" style={{ color: colors.brightWhite }}>
|
|
<div className="p-2 rounded-xl shadow-lg shadow-black/20" style={{ backgroundColor: `${colors.primary}15` }}>
|
|
<Box className="w-5 h-5" style={{ color: colors.primary }} />
|
|
</div>
|
|
Server Summary
|
|
</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
{[
|
|
{ label: 'CPU', value: `${cpu} vCPU`, color: colors.blue },
|
|
{ label: 'RAM', value: `${ram} GB`, color: colors.brightGreen },
|
|
{ label: 'Storage', value: `${storage} GB`, color: colors.brightYellow },
|
|
{ label: 'Region', value: 'Reda, PL', color: colors.brightCyan }
|
|
].map((item) => (
|
|
<div key={item.label} className="p-3 rounded-xl text-center border" style={{ backgroundColor: `${item.color}10`, borderColor: `${item.color}30` }}>
|
|
<p className="text-xs mb-1 font-medium" style={{ color: item.color }}>{item.label}</p>
|
|
<p className="font-bold text-sm" style={{ color: colors.brightWhite }}>{item.value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 pt-4 border-t border-dash-border">
|
|
<div>
|
|
<p className="text-sm" style={{ color: colors.foreground }}>{isDemo ? 'Demo Server' : 'Monthly Price'}</p>
|
|
<p className="text-3xl font-bold" style={{ color: isDemo ? colors.brightYellow : (canCreate ? colors.brightYellow : colors.brightRed) }}>
|
|
{isDemo ? 'FREE' : `${calculateCurrencyValue(serverCost)} ${getCurrencySymbol()}`}
|
|
{!isDemo && <span className="text-sm font-normal">/mo</span>}
|
|
</p>
|
|
</div>
|
|
<ActionButton
|
|
onClick={handleSubmit}
|
|
disabled={creating || !serverName.trim() || serverNameError || !selectedImage || (!isDemo && !canCreate)}
|
|
variant="primary"
|
|
className="!px-4 !py-2 !min-h-0 sm:!min-h-0"
|
|
>
|
|
{creating ? <RefreshCw className="w-4 h-4 animate-spin" /> : <><Plus className="w-4 h-4" />{isDemo ? 'Create Demo Server' : 'Create Server'}</>}
|
|
</ActionButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DashboardViewShell>
|
|
</div>
|
|
);
|
|
}
|