Fix empty server order modal by restoring serverOrderForm JS.
Deploy Ladill Servers / deploy (push) Successful in 20s

The modal Alpine component was never copied during extraction, so step content never rendered.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-06 20:08:56 +00:00
co-authored by Cursor
parent 608e6947bf
commit b019ed02ef
2 changed files with 361 additions and 1 deletions
+360
View File
@@ -180,5 +180,365 @@ Alpine.data('topbarSearch', (config = {}) => ({
},
}));
window.serverOrderForm = (products = {}, billingCycleLabels = {}, initial = {}) => ({
products,
billingCycleLabels,
steps: [{ label: 'Select Plan' }, { label: 'Configure' }, { label: 'Review' }],
step: Number(initial.step ?? 1),
selectedProductId: initial.selectedProductId ? String(initial.selectedProductId) : '',
selectedProduct: null,
serverName: String(initial.serverName ?? ''),
billingCycle: String(initial.billingCycle ?? 'monthly'),
selections: {
region: '',
image: '',
license: '',
application: '',
default_user: '',
additional_ip: '',
private_networking: '',
storage_type: '',
object_storage: '',
},
serverPassword: '',
serverPasswordConfirmation: '',
submitting: false,
error: '',
clone(value) {
return JSON.parse(JSON.stringify(value ?? null));
},
init() {
if (this.selectedProductId) {
this.applyProduct(this.selectedProductId, true);
}
},
applyProduct(productId, preserveState = false) {
const product = this.products[String(productId)] || null;
if (!product) {
return;
}
this.selectedProductId = String(productId);
this.selectedProduct = this.clone(product);
const defaults = this.clone(product.defaults || {});
const initialSelections = preserveState ? this.clone(initial.selections || {}) : {};
this.selections = {
...defaults,
...initialSelections,
};
if (!preserveState) {
this.serverName = '';
this.billingCycle = 'monthly';
this.serverPassword = '';
this.serverPasswordConfirmation = '';
}
if (!Object.prototype.hasOwnProperty.call(this.billingCycleLabels, this.billingCycle)) {
this.billingCycle = 'monthly';
}
this.syncDefaultUser();
this.syncLicenseSelection();
this.error = '';
},
selectProduct(productId) {
this.applyProduct(productId, false);
},
nextStep() {
if (this.step === 1 && !this.selectedProductId) {
return;
}
if (this.step === 2 && !this.serverName.trim()) {
return;
}
if (this.step < this.steps.length) {
this.step += 1;
}
},
catalog(key) {
return this.selectedProduct?.catalog?.[key] || [];
},
findOption(options, value) {
return (options || []).find((option) => option.value === value) || null;
},
optionAmount(option) {
return Number(option?.prices?.[this.billingCycle] || 0);
},
optionLabel(option) {
if (!option) {
return '';
}
const amount = this.optionAmount(option);
return amount > 0
? `${option.label} (+${this.formatMoney(amount)})`
: option.label;
},
selectionLabel(catalogKey, value) {
const option = this.findOption(this.catalog(catalogKey), value);
return option?.label || value || '-';
},
currentImage() {
return this.findOption(this.catalog('images'), this.selections.image);
},
imageSupportsManagedStack(image = null) {
const currentImage = image || this.currentImage();
const osFamily = String(currentImage?.os_family || this.selectedProduct?.catalog?.default_image_os_family || 'linux').toLowerCase();
const imageLabel = String(currentImage?.label || '').toLowerCase();
const installLater = Boolean(currentImage?.is_install_later);
if (installLater || osFamily.startsWith('win')) {
return false;
}
if (['cpanel', 'plesk', 'whm'].some((needle) => imageLabel.includes(needle))) {
return false;
}
return ['ubuntu 22.04', 'ubuntu 24.04', 'debian 12', 'bookworm', 'almalinux', 'alma linux', 'alma', 'rocky linux', 'rocky', 'fedora', 'opensuse leap', 'open suse leap', 'opensuse', 'leap', 'centos']
.some((needle) => imageLabel.includes(needle));
},
bundledPanelFromImage(image = null) {
const imageLabel = String((image || this.currentImage())?.label || '').toLowerCase();
if (imageLabel.includes('cpanel') || imageLabel.includes('whm')) {
return 'cpanel';
}
if (imageLabel.includes('plesk')) {
return 'plesk';
}
return null;
},
availableLicenseOptions() {
const image = this.currentImage();
const installLater = Boolean(image?.is_install_later);
const bundledPanel = this.bundledPanelFromImage(image);
return this.catalog('licenses').filter((option) => {
if (installLater) {
return option.value === 'none';
}
if (bundledPanel && option.value === 'none') {
return false;
}
const optionLicense = String(option.license || '').toLowerCase();
if (bundledPanel === 'cpanel' && optionLicense.startsWith('plesk')) {
return false;
}
if (bundledPanel === 'plesk' && optionLicense.startsWith('cpanel')) {
return false;
}
if (option.panel !== 'ladill') {
return true;
}
return this.imageSupportsManagedStack(image);
});
},
currentLicenseOption() {
return this.findOption(this.availableLicenseOptions(), this.selections.license)
|| this.findOption(this.catalog('licenses'), this.selections.license);
},
currentPanelSnapshot() {
const license = this.currentLicenseOption();
const image = this.currentImage();
const managedCandidate = this.imageSupportsManagedStack(image);
if (license?.panel === 'ladill') {
return {
label: license.label || 'Ladill Server Manager',
primary: managedCandidate
? 'Ladill Server Manager will provision this server with Ladill-managed controls on the selected supported Linux image.'
: 'Ladill Server Manager is not available for the selected image.',
secondary: managedCandidate
? 'After provisioning finishes, Server Manager can handle power, status, files, databases, domains, PHP, SSL, cron, and logs.'
: 'Choose a plain supported Linux image without cPanel or Plesk to use Ladill-managed hosting features.',
futureCapabilities: managedCandidate ? ['Files', 'Databases', 'Domains', 'PHP', 'SSL', 'Cron', 'Logs'] : [],
toneClasses: managedCandidate
? 'border-amber-200 bg-amber-50 text-amber-900'
: 'border-slate-200 bg-slate-50 text-slate-700',
};
}
if (license?.license) {
return {
label: license.label || 'Server Panel',
primary: `${license.label} will run inside the server as the primary control panel.`,
secondary: 'Ladill will keep the order and infrastructure lifecycle, but site and service management will happen in the installed panel.',
futureCapabilities: [],
toneClasses: 'border-blue-200 bg-blue-50 text-blue-900',
};
}
return {
label: license?.label || 'Remote Login Only',
primary: 'This selection provisions the server without a Ladill-managed or commercial control panel.',
secondary: 'You will manage the machine directly over SSH or RDP until a panel or managed runtime is installed separately.',
futureCapabilities: [],
toneClasses: 'border-gray-200 bg-gray-50 text-gray-700',
};
},
defaultUserOptions() {
const defaultFamily = this.selectedProduct?.catalog?.default_image_os_family || 'linux';
const osFamily = this.currentImage()?.os_family || defaultFamily;
return osFamily.startsWith('win')
? this.catalog('windows_default_users')
: this.catalog('linux_default_users');
},
syncDefaultUser() {
const options = this.defaultUserOptions();
if (!options.some((option) => option.value === this.selections.default_user)) {
this.selections.default_user = options[0]?.value || '';
}
},
syncLicenseSelection() {
const options = this.availableLicenseOptions();
if (!options.some((option) => option.value === this.selections.license)) {
this.selections.license = options[0]?.value || 'none';
}
},
handleImageChange() {
this.syncDefaultUser();
this.syncLicenseSelection();
},
basePrice() {
return Number(this.selectedProduct?.prices?.[this.billingCycle] || 0);
},
setupFee() {
return Number(this.selectedProduct?.setup_fees?.[this.billingCycle] || 0);
},
addOnLineItems() {
const items = [];
const setupFee = this.setupFee();
if (setupFee > 0) {
items.push({
key: 'setup_fee',
label: 'One-time setup fee',
amount: setupFee,
});
}
const keys = ['image', 'region', 'license', 'application', 'additional_ip', 'private_networking', 'storage_type', 'object_storage'];
const catalogKeys = {
image: 'images',
region: 'regions',
license: 'licenses',
application: 'applications',
additional_ip: 'additional_ip',
private_networking: 'private_networking',
storage_type: 'storage_types',
object_storage: 'object_storage',
};
keys.forEach((key) => {
const option = this.findOption(this.catalog(catalogKeys[key]), this.selections[key]);
const amount = this.optionAmount(option);
if (amount > 0) {
items.push({
key,
label: option.label,
amount,
});
}
});
return items;
},
currentTotal() {
return this.basePrice() + this.addOnLineItems().reduce((sum, item) => sum + item.amount, 0);
},
cycleMonths() {
return {
monthly: 1,
quarterly: 3,
semiannual: 6,
yearly: 12,
}[this.billingCycle] || 1;
},
monthlyEquivalent() {
return this.currentTotal() / this.cycleMonths();
},
formatMoney(amount, currency = null) {
if (amount === null || amount === undefined) {
return '-';
}
const code = currency || this.selectedProduct?.currency || 'GHS';
return `${code} ${Number(amount).toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
},
canSubmit() {
return Boolean(
this.selectedProductId
&& this.serverName.trim()
&& this.serverPassword
&& this.serverPassword === this.serverPasswordConfirmation
&& this.serverPassword.length >= 8
);
},
submitOrder() {
if (!this.canSubmit()) {
this.error = 'Complete the required fields and make sure the passwords match.';
return;
}
this.submitting = true;
this.error = '';
this.$refs.form.submit();
},
});
window.Alpine = Alpine;
Alpine.start();
@@ -23,7 +23,7 @@
@click.stop>
<form x-ref="form"
method="POST"
action="{{ route('hosting.servers.store') }}"
action="{{ route('servers.orders.store') }}"
x-data="serverOrderForm(@js($serverOrderPayloads), @js($billingCycles), @js($initialServerOrderState))"
@submit.prevent="submitOrder"
class="max-h-[90vh] overflow-y-auto">