Files
isaaccladandCursor 460edb8719
Deploy Ladill Hosting / deploy (push) Successful in 1m9s
Add first-class hosting subdomain create/attach in Domains panel.
Customers can create label-based subdomains under primary/addon sites with a separate max_domains×5 quota, nginx/docroot/SSL provisioning, and managed DNS A records when Ladill controls the parent zone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 12:08:41 +00:00

258 lines
17 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<x-hosting-panel-layout :account="$account">
<x-slot name="title">Domains - {{ $account->username }}</x-slot>
<x-slot name="header">Domains</x-slot>
<div class="space-y-6">
@if(session('success'))
<div class="rounded-lg bg-emerald-50 border border-emerald-200 p-4">
<p class="text-sm text-emerald-800">{{ session('success') }}</p>
</div>
@endif
@if(session('error'))
<div class="rounded-lg bg-red-50 border border-red-200 p-4">
<p class="text-sm text-red-800">{{ session('error') }}</p>
</div>
@endif
@php
$errors = $errors ?? new \Illuminate\Support\ViewErrorBag;
$ownedDomainHosts = collect($ownedDomains)->map(fn ($d) => (string) $d)->all();
$oldDomain = trim((string) old('domain', ''));
$initialDomainSource = empty($ownedDomainHosts)
? 'external'
: ((string) old('is_owned_domain', '1') === '0' ? 'external' : 'ladill');
$selectedOwnedDomain = in_array($oldDomain, $ownedDomainHosts, true) ? $oldDomain : '';
$externalFallbackDomain = ! in_array($oldDomain, $ownedDomainHosts, true) ? $oldDomain : '';
$parentOptions = collect($parentSites ?? [])->values();
$defaultParentId = (int) old('parent_site_id', $parentOptions->first()?->id ?? 0);
$defaultParentHost = optional($parentOptions->firstWhere('id', $defaultParentId))->domain
?? optional($parentOptions->first())->domain
?? '';
@endphp
<div class="rounded-xl border border-slate-200 bg-white p-4 sm:p-5">
<div class="flex flex-wrap gap-4 text-sm text-slate-600">
<p>
@if ($maxDomains === -1)
<span class="font-semibold text-slate-900">{{ $domainSitesCount }}</span> domain {{ $domainSitesCount === 1 ? 'slot' : 'slots' }} in use (unlimited)
@else
<span class="font-semibold text-slate-900">{{ $domainSitesCount }} of {{ $maxDomains }}</span> domain {{ $maxDomains === 1 ? 'slot' : 'slots' }} in use
@endif
</p>
<p class="text-slate-300">|</p>
<p>
@if ($maxSubdomains === -1)
<span class="font-semibold text-slate-900">{{ $subdomainSitesCount }}</span> subdomain {{ $subdomainSitesCount === 1 ? 'slot' : 'slots' }} in use (unlimited)
@else
<span class="font-semibold text-slate-900">{{ $subdomainSitesCount }} of {{ $maxSubdomains }}</span> subdomain {{ $maxSubdomains === 1 ? 'slot' : 'slots' }} in use
@endif
</p>
</div>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-6">
<h3 class="text-base font-semibold text-slate-900 mb-4">Add Domain</h3>
@if (! $canAddDomain)
<p class="text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2 mb-4">Domain slot limit reached. Upgrade your plan or remove an unused domain.</p>
@endif
<form action="{{ route('hosting.panel.domains.add', $account) }}" method="POST" class="space-y-5"
x-data="ladillDomainSourceForm({
domainSource: @js($initialDomainSource),
selectedOwnedDomain: @js($selectedOwnedDomain),
externalDomain: @js($externalFallbackDomain),
ownedDomains: @js($ownedDomainHosts),
ownedUrl: @js(route('hosting.domains.owned', ['exclude' => $account->sites->pluck('domain')->filter()->all()])),
onboardingMode: @js(old('onboarding_mode', 'ns_auto')),
})">
@csrf
<x-domain-source-fields
:owned-domains="$ownedDomainHosts"
owned-url="{{ route('hosting.domains.owned') }}"
show-connection-method
show-document-root
/>
<button type="submit" class="btn-primary" @disabled(! $canAddDomain)>
<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="M12 4.5v15m7.5-7.5h-15"/></svg>
Add Domain
</button>
</form>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-6">
<h3 class="text-base font-semibold text-slate-900 mb-1">Create subdomain</h3>
<p class="text-sm text-slate-500 mb-4">Create a hostname under one of your attached domains (up to 5× your domain slot limit).</p>
@if ($parentOptions->isEmpty())
<p class="text-sm text-slate-500">Add a primary or addon domain before creating subdomains.</p>
@else
@if (! $canAddSubdomain)
<p class="text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2 mb-4">Subdomain slot limit reached.</p>
@endif
<form action="{{ route('hosting.panel.domains.subdomains.add', $account) }}" method="POST" class="space-y-4"
x-data="{
parentId: @js((string) $defaultParentId),
parents: @js($parentOptions->mapWithKeys(fn ($s) => [(string) $s->id => $s->domain])->all()),
label: @js(old('subdomain', '')),
get preview() {
const parent = this.parents[this.parentId] || '';
const label = (this.label || '').toLowerCase().trim();
return label && parent ? (label + '.' + parent) : parent;
}
}">
@csrf
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label for="parent_site_id" class="mb-1 block text-xs font-medium text-slate-600">Parent domain</label>
<select id="parent_site_id" name="parent_site_id" x-model="parentId" required
class="w-full rounded-lg border-slate-300 text-sm focus:border-indigo-500 focus:ring-indigo-500">
@foreach ($parentOptions as $parent)
<option value="{{ $parent->id }}" @selected((int) $defaultParentId === (int) $parent->id)>
{{ $parent->domain }} ({{ $parent->type }})
</option>
@endforeach
</select>
@error('parent_site_id') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label for="subdomain" class="mb-1 block text-xs font-medium text-slate-600">Subdomain label</label>
<input id="subdomain" type="text" name="subdomain" x-model="label"
value="{{ old('subdomain') }}"
placeholder="blog"
pattern="[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?"
maxlength="63"
required
class="w-full rounded-lg border-slate-300 text-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('subdomain') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-slate-600">Hostname preview</label>
<p class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm font-mono text-slate-800" x-text="preview || '—'"></p>
</div>
<div>
<label for="subdomain_document_root" class="mb-1 block text-xs font-medium text-slate-600">Document root (optional)</label>
<input id="subdomain_document_root" type="text" name="document_root"
value="{{ old('document_root') }}"
placeholder="public_html/blog.example.com"
class="w-full rounded-lg border-slate-300 text-sm focus:border-indigo-500 focus:ring-indigo-500">
<p class="mt-1 text-xs text-slate-500">Relative to /home/{{ $account->username }}/. Leave blank to use public_html/&lt;hostname&gt;.</p>
@error('document_root') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
<button type="submit" class="btn-primary" @disabled(! $canAddSubdomain)>
Create subdomain
</button>
</form>
@endif
</div>
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden">
<div class="px-6 py-4 border-b border-slate-200">
<h3 class="text-base font-semibold text-slate-900">Your Domains</h3>
</div>
@if($account->sites->count())
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Domain</th>
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Document Root</th>
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Status</th>
<th class="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200">
@foreach ($account->sites->sortBy(fn ($site) => [match ($site->type) { 'primary' => 0, 'addon' => 1, default => 2 }, $site->domain]) as $site)
<tr class="hover:bg-slate-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-slate-900">{{ $site->domain }}</span>
<a href="http://{{ $site->domain }}" target="_blank" class="text-slate-400 hover:text-indigo-600">
<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 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
</a>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@php
$typeStyles = [
'primary' => 'bg-indigo-100 text-indigo-800',
'addon' => 'bg-slate-100 text-slate-800',
'subdomain' => 'bg-sky-100 text-sky-800',
];
@endphp
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {{ $typeStyles[$site->type] ?? 'bg-slate-100 text-slate-800' }}">
{{ ucfirst($site->type) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm font-mono text-slate-600">{{ $site->document_root }}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {{ $site->status === 'active' ? 'bg-emerald-100 text-emerald-800' : 'bg-slate-100 text-slate-800' }}">
{{ ucfirst($site->status) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
@if((int) $account->user_id === (int) auth()->id())
<x-confirm-dialog
:name="'remove-domain-'.$site->id"
title="Remove domain?"
message="Remove this domain? The files will not be deleted."
:action="route('hosting.panel.domains.remove', [$account, $site])"
method="DELETE"
confirm-label="Remove"
>
<x-slot:trigger>
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
Remove
</button>
</x-slot:trigger>
</x-confirm-dialog>
@else
<span class="text-xs text-slate-400">Owner only</span>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="px-6 py-12 text-center">
@include('components.icons.domain-globe', ['class' => 'mx-auto h-12 w-12 text-slate-300'])
<p class="mt-4 text-sm text-slate-500">No domains yet. Add one above.</p>
</div>
@endif
</div>
@php $serverIp = $account->node?->ip_address ?? '161.97.138.149'; @endphp
<div class="rounded-xl border border-slate-200 bg-white p-6">
<h3 class="text-base font-semibold text-slate-900 mb-2">DNS Quick Reference</h3>
<p class="text-sm text-slate-600 mb-4">Use one of these methods to connect an external domain:</p>
<div class="grid gap-4 md:grid-cols-2">
<div class="rounded-lg border border-emerald-200 bg-emerald-50/50 p-4">
<h4 class="text-sm font-semibold text-slate-900 mb-2">Nameservers</h4>
<div class="space-y-1.5">
<div class="flex items-center justify-between rounded bg-white border border-emerald-200 px-3 py-1.5">
<span class="text-sm font-mono text-slate-900">ns1.ladill.com</span>
<button type="button" onclick="navigator.clipboard.writeText('ns1.ladill.com')" class="text-xs text-indigo-600 hover:text-indigo-800">Copy</button>
</div>
<div class="flex items-center justify-between rounded bg-white border border-emerald-200 px-3 py-1.5">
<span class="text-sm font-mono text-slate-900">ns2.ladill.com</span>
<button type="button" onclick="navigator.clipboard.writeText('ns2.ladill.com')" class="text-xs text-indigo-600 hover:text-indigo-800">Copy</button>
</div>
</div>
</div>
<div class="rounded-lg border border-slate-200 bg-slate-50/50 p-4">
<h4 class="text-sm font-semibold text-slate-900 mb-2">A Records</h4>
<div class="space-y-1 rounded bg-white border border-slate-200 p-3 text-xs font-mono">
<div>A @ {{ $serverIp }}</div>
<div>A www {{ $serverIp }}</div>
</div>
</div>
</div>
</div>
</div>
</x-hosting-panel-layout>