Files
ladill-hosting/resources/views/hosting/partials/server-order-modal.blade.php
T
isaaccladandCursor a2985005ff
Deploy Ladill Hosting / deploy (push) Successful in 20s
Replace monolith user.* routes with cross-app URLs.
Hosting type pages and panel views referenced platform routes that do not exist in the extracted app; link to domains, account, and servers apps instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 16:46:09 +00:00

449 lines
34 KiB
PHP

<template x-teleport="body">
<div x-show="showOrderModal" x-cloak
class="fixed inset-0 z-50 overflow-y-auto sm:flex sm:items-start sm:justify-center sm:p-4 sm:pt-16"
@keydown.escape.window="showOrderModal = false">
<div x-show="showOrderModal"
x-transition:enter="ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-gray-900/50 backdrop-blur-sm hidden sm:block"
@click="showOrderModal = false"></div>
<div x-show="showOrderModal"
x-transition:enter="ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="relative min-h-full sm:min-h-0 w-full sm:max-w-6xl overflow-hidden sm:rounded-3xl sm:border sm:border-gray-200 bg-white shadow-2xl"
@click.stop>
<form x-ref="form"
method="POST"
action="{{ ladill_servers_url('/hosting/orders') }}"
x-data="serverOrderForm(@js($serverOrderPayloads), @js($billingCycles), @js($initialServerOrderState))"
@submit.prevent="submitOrder"
class="max-h-[90vh] overflow-y-auto">
@csrf
<div class="flex items-center justify-between border-b border-gray-100 px-6 py-5">
<div>
<h2 class="text-lg font-semibold text-gray-900">New {{ $title }}</h2>
<p class="text-sm text-gray-500">Configure the server, review pricing, and add it to your cart.</p>
</div>
<button type="button"
@click="showOrderModal = false"
class="rounded-lg p-1.5 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600">
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="border-b border-gray-100 bg-gray-50 px-6 py-4">
<div class="flex flex-wrap items-center justify-center gap-3 sm:gap-4">
<template x-for="(stepInfo, index) in steps" :key="stepInfo.label">
<div class="flex items-center gap-3">
<div :class="step > index + 1 ? 'bg-emerald-500 text-white' : (step === index + 1 ? 'bg-lime-300 text-slate-900' : 'bg-gray-200 text-gray-500')"
class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold transition-colors">
<template x-if="step > index + 1">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>
</template>
<template x-if="step <= index + 1">
<span x-text="index + 1"></span>
</template>
</div>
<span :class="step === index + 1 ? 'text-gray-900' : 'text-gray-500'" class="hidden text-sm font-medium sm:block" x-text="stepInfo.label"></span>
<template x-if="index < steps.length - 1">
<div class="hidden h-px w-10 bg-gray-300 sm:block"></div>
</template>
</div>
</template>
</div>
</div>
<input type="hidden" name="product_id" :value="selectedProductId">
<div class="p-6">
<div x-show="step === 1" x-transition>
<div class="space-y-6">
<div>
<h3 class="text-lg font-semibold text-gray-900">Select Package</h3>
<p class="mt-1 text-sm text-gray-500">Choose the server plan that fits your workload.</p>
</div>
@error('product_id')
<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{{ $message }}</div>
@enderror
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@foreach ($serverOrderPayloads as $productId => $payload)
@php
$packageSummary = collect($nativeForm['packages'] ?? [])->firstWhere('value', (string) $productId)['summary'] ?? null;
@endphp
<button type="button"
@click="selectProduct('{{ $productId }}')"
:class="selectedProductId === '{{ $productId }}' ? 'border-lime-300 ring-2 ring-lime-300/50 bg-lime-50/40' : 'border-gray-200 hover:border-gray-300 hover:shadow-md'"
class="group relative flex flex-col rounded-2xl border bg-white p-5 text-left shadow-sm transition-all duration-200">
<div class="absolute right-4 top-4">
<div :class="selectedProductId === '{{ $productId }}' ? 'border-lime-300 bg-lime-300' : 'border-gray-300'"
class="flex h-5 w-5 items-center justify-center rounded-full border-2 transition-colors">
<svg x-show="selectedProductId === '{{ $productId }}'" class="h-3 w-3 text-slate-900" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>
</div>
</div>
<h4 class="pr-8 text-lg font-bold text-gray-900">{{ $payload['name'] }}</h4>
@if ($packageSummary)
<p class="mt-1 text-sm text-gray-500">{{ $packageSummary }}</p>
@endif
<div class="mt-4 flex items-baseline gap-1">
<span class="text-2xl font-bold text-gray-900">{{ strtoupper($payload['currency']) }} {{ number_format($payload['prices']['monthly'] ?? 0, 2) }}</span>
<span class="text-sm text-gray-500">/mo</span>
</div>
</button>
@endforeach
</div>
<div class="flex justify-end">
<button type="button"
@click="nextStep()"
:disabled="!selectedProductId"
class="inline-flex items-center gap-2 rounded-xl bg-gray-900 px-5 py-3 text-sm font-semibold text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50">
Continue
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/></svg>
</button>
</div>
</div>
</div>
<div x-show="step === 2" x-transition>
<div class="grid gap-6 xl:grid-cols-[1fr_340px]">
<div class="space-y-6">
<div class="rounded-2xl border border-gray-200 bg-white p-5">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-900">Server Configuration</h3>
<p class="mt-1 text-sm text-gray-500" x-text="selectedProduct?.name || 'Select a package first'"></p>
</div>
<button type="button" @click="step = 1" class="text-sm font-medium text-gray-500 transition hover:text-gray-700">Change plan</button>
</div>
<div class="mt-5 grid gap-4 md:grid-cols-2">
<div class="md:col-span-2">
<label class="mb-1.5 block text-sm font-medium text-gray-700">Server Name</label>
<input x-model="serverName" name="server_name" type="text" placeholder="my-server"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
@error('server_name')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Billing Term</label>
<select x-model="billingCycle" name="billing_cycle"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
<template x-for="(label, value) in billingCycleLabels" :key="value">
<option :value="value" x-text="label"></option>
</template>
</select>
@error('billing_cycle')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Region</label>
<select x-model="selections.region" name="region"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
<template x-for="option in catalog('regions')" :key="option.value">
<option :value="option.value" x-text="optionLabel(option)"></option>
</template>
</select>
@error('region')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
</div>
<div class="rounded-2xl border border-gray-200 bg-white p-5">
<h4 class="text-base font-semibold text-gray-900">Image and Access</h4>
<div class="mt-4 grid gap-4 md:grid-cols-2">
<div class="md:col-span-2">
<label class="mb-1.5 block text-sm font-medium text-gray-700">Operating System</label>
<select x-model="selections.image" name="image" @change="handleImageChange()"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
<template x-for="option in catalog('images')" :key="option.value">
<option :value="option.value" x-text="optionLabel(option)"></option>
</template>
</select>
@error('image')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Default Login User</label>
<select x-model="selections.default_user" name="default_user"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
<template x-for="option in defaultUserOptions()" :key="option.value">
<option :value="option.value" x-text="option.label"></option>
</template>
</select>
@error('default_user')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Control Panel</label>
<select x-model="selections.license" name="license"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
<template x-for="option in availableLicenseOptions()" :key="option.value">
<option :value="option.value" x-text="optionLabel(option)"></option>
</template>
</select>
@error('license')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="md:col-span-2 rounded-xl border px-4 py-3 text-sm"
:class="currentPanelSnapshot().toneClasses">
<p class="font-medium text-current" x-text="currentPanelSnapshot().label"></p>
<p class="mt-1 text-current/90" x-text="currentPanelSnapshot().primary"></p>
<p class="mt-1 text-xs text-current/80" x-text="currentPanelSnapshot().secondary"></p>
<template x-if="currentPanelSnapshot().futureCapabilities.length">
<p class="mt-2 text-xs text-current/80">
Managed features after provisioning:
<span x-text="currentPanelSnapshot().futureCapabilities.join(', ')"></span>
</p>
</template>
</div>
<div class="md:col-span-2">
<label class="mb-1.5 block text-sm font-medium text-gray-700">Preinstalled Application</label>
<select x-model="selections.application" name="application"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
<template x-for="option in catalog('applications')" :key="option.value">
<option :value="option.value" x-text="optionLabel(option)"></option>
</template>
</select>
@error('application')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
</div>
<div class="rounded-2xl border border-gray-200 bg-white p-5">
<h4 class="text-base font-semibold text-gray-900">Add-Ons</h4>
<div class="mt-4 grid gap-4 md:grid-cols-2">
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Additional IP</label>
<select x-model="selections.additional_ip" name="additional_ip"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
<template x-for="option in catalog('additional_ip')" :key="option.value">
<option :value="option.value" x-text="optionLabel(option)"></option>
</template>
</select>
@error('additional_ip')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Private Networking</label>
<select x-model="selections.private_networking" name="private_networking"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
<template x-for="option in catalog('private_networking')" :key="option.value">
<option :value="option.value" x-text="optionLabel(option)"></option>
</template>
</select>
@error('private_networking')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Storage Type</label>
<select x-model="selections.storage_type" name="storage_type"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
<template x-for="option in catalog('storage_types')" :key="option.value">
<option :value="option.value" x-text="optionLabel(option)"></option>
</template>
</select>
@error('storage_type')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Object Storage</label>
<select x-model="selections.object_storage" name="object_storage"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
<template x-for="option in catalog('object_storage')" :key="option.value">
<option :value="option.value" x-text="optionLabel(option)"></option>
</template>
</select>
@error('object_storage')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
</div>
</div>
<div class="lg:sticky lg:top-6 lg:self-start">
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="border-b border-gray-100 bg-gray-50 px-5 py-4">
<h4 class="font-semibold text-gray-900">Order Summary</h4>
<p class="text-xs text-gray-500" x-text="selectedProduct?.name || 'Select a plan'"></p>
</div>
<div class="space-y-3 p-5">
<div class="flex items-start justify-between gap-3 text-sm">
<span class="text-gray-600">Base Plan</span>
<span class="font-medium text-gray-900" x-text="formatMoney(basePrice())"></span>
</div>
<template x-for="item in addOnLineItems()" :key="item.key">
<div class="flex items-start justify-between gap-3 text-sm">
<span class="text-gray-600" x-text="item.label"></span>
<span class="font-medium text-gray-900" x-text="'+' + formatMoney(item.amount)"></span>
</div>
</template>
<div class="rounded-xl bg-slate-50 p-4">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">Total</span>
<div class="text-right">
<p class="text-2xl font-bold text-gray-900" x-text="formatMoney(currentTotal())"></p>
<p class="text-xs text-gray-500" x-text="`${cycleMonths()} month term`"></p>
</div>
</div>
</div>
<div class="flex gap-3 pt-2">
<button type="button" @click="step = 1" class="flex-1 rounded-xl border border-gray-200 px-4 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-50">Back</button>
<button type="button"
@click="nextStep()"
:disabled="!serverName.trim()"
class="flex-1 rounded-xl bg-gray-900 px-4 py-3 text-sm font-semibold text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50">
Continue
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div x-show="step === 3" x-transition>
<div class="grid gap-6 xl:grid-cols-[1fr_340px]">
<div class="space-y-6">
<div class="rounded-2xl border border-gray-200 bg-white p-5">
<h3 class="text-lg font-semibold text-gray-900">Server Login Credentials</h3>
<p class="mt-1 text-sm text-gray-500">Set a secure password for the initial server access.</p>
<div class="mt-5 grid gap-4 md:grid-cols-2">
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Password</label>
<input x-model="serverPassword" name="server_password" type="password"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
@error('server_password')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Confirm Password</label>
<input x-model="serverPasswordConfirmation" name="server_password_confirmation" type="password"
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-sm shadow-sm focus:border-lime-300 focus:ring-lime-300">
@error('server_password_confirmation')
<p class="mt-1.5 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
<div class="mt-4 rounded-xl bg-amber-50 px-4 py-4 text-sm text-amber-800">
<p class="font-medium">Password requirements</p>
<ul class="mt-2 list-disc space-y-1 pl-5 text-xs text-amber-700">
<li>At least 8 characters</li>
<li>Include uppercase and lowercase letters</li>
<li>Include either 3 digits and 1 symbol, or 1 digit and 2 symbols</li>
</ul>
</div>
</div>
<div class="rounded-2xl border border-gray-200 bg-white p-5">
<h4 class="text-base font-semibold text-gray-900">Configuration Summary</h4>
<div class="mt-4 space-y-2 text-sm">
<div class="flex items-start justify-between gap-3">
<span class="text-gray-500">Server Name</span>
<span class="text-right font-medium text-gray-900" x-text="serverName"></span>
</div>
<div class="flex items-start justify-between gap-3">
<span class="text-gray-500">Image</span>
<span class="text-right font-medium text-gray-900" x-text="selectionLabel('images', selections.image)"></span>
</div>
<div class="flex items-start justify-between gap-3">
<span class="text-gray-500">Region</span>
<span class="text-right font-medium text-gray-900" x-text="selectionLabel('regions', selections.region)"></span>
</div>
<div class="flex items-start justify-between gap-3">
<span class="text-gray-500">Login User</span>
<span class="text-right font-medium text-gray-900" x-text="selections.default_user"></span>
</div>
</div>
</div>
</div>
<div class="lg:sticky lg:top-6 lg:self-start">
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm">
<div class="border-b border-gray-100 bg-gray-50 px-5 py-4">
<h4 class="font-semibold text-gray-900">Complete Order</h4>
<p class="text-xs text-gray-500">Review and add to cart</p>
</div>
<div class="space-y-4 p-5">
<div class="rounded-xl bg-slate-50 p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Total Due</p>
<p class="text-2xl font-bold text-gray-900" x-text="formatMoney(currentTotal())"></p>
</div>
<div class="text-right">
<p class="text-xs text-gray-500" x-text="`${cycleMonths()} month term`"></p>
<p class="text-sm text-gray-500" x-text="`${formatMoney(monthlyEquivalent())}/mo`"></p>
</div>
</div>
</div>
<template x-if="error">
<p class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" x-text="error"></p>
</template>
<div class="space-y-3">
<button type="submit"
:disabled="submitting || !canSubmit()"
class="inline-flex w-full items-center justify-center gap-2 rounded-xl bg-lime-300 px-5 py-4 text-base font-bold text-slate-900 transition hover:bg-lime-200 disabled:cursor-not-allowed disabled:opacity-50">
<template x-if="submitting">
<svg class="h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
</template>
<svg x-show="!submitting" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"/></svg>
Add to Cart
</button>
<button type="button" @click="step = 2" class="w-full rounded-xl border border-gray-200 px-4 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-50">Back to Configuration</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</template>