Show linked domains on cards and fix Email leftovers.
Deploy Ladill Hosting / deploy (push) Successful in 24s

Display hosted_sites on account cards, scope Afia and Settings to hosting,
add hosting_settings for notifications, and remove mailbox UI from the
hosting layout and account pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-06 17:30:40 +00:00
co-authored by Cursor
parent c4bd4efbed
commit 5703a00862
19 changed files with 202 additions and 329 deletions
+4 -9
View File
@@ -5,21 +5,16 @@ import collapse from '@alpinejs/collapse';
Alpine.plugin(collapse);
// Afia — Ladill Email's AI assistant (slide-over chat). Opened via the topbar AI button
// which dispatches a window 'afia-open' event.
// Afia — Ladill in-app AI assistant (slide-over chat). Opened via the topbar AI button
// which dispatches a window 'afia-open' event. Greeting/suggestions are passed from Blade.
Alpine.data('afia', (config = {}) => ({
open: false,
input: '',
loading: false,
messages: [
{ role: 'assistant', text: "Hi, I'm Afia 👋 Ask me anything about email — setting up a domain, verifying DNS, creating mailboxes, IMAP/SMTP settings or billing…" },
],
suggestions: [
'How do I set up email for my domain?',
'What DNS records do I need to verify?',
'How do I create a mailbox?',
'What are my IMAP and SMTP settings?',
{ role: 'assistant', text: config.greeting || "Hi, I'm Afia 👋 How can I help?" },
],
suggestions: config.suggestions || [],
init() {
window.addEventListener('afia-open', () => {
this.open = true;
@@ -1,8 +1,10 @@
<x-app-layout title="Billing — Ladill Hosting">
@extends('layouts.hosting')
@section('title', 'Billing — Ladill Hosting')
@section('content')
@php $fmt = fn ($m) => 'GHS '.number_format($m / 100, 2); @endphp
<div class="mx-auto max-w-3xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Billing</h1>
<p class="mt-0.5 text-sm text-slate-500">Your Ladill wallet funds hosting and other Ladill apps.</p>
<p class="mt-0.5 text-sm text-slate-500">Your Ladill wallet funds hosting renewals and upgrades.</p>
<div class="mt-6 grid gap-4 sm:grid-cols-3">
<div class="rounded-2xl bg-gradient-to-br from-indigo-700 via-indigo-600 to-blue-500 p-5 text-white shadow-sm sm:col-span-1">
@@ -20,31 +22,7 @@
</div>
</div>
<div class="mt-6 rounded-2xl border border-slate-200 bg-white">
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-3.5">
<h2 class="text-sm font-semibold text-slate-900">Hosting subscriptions</h2>
<span class="text-xs text-slate-400">billed from wallet</span>
</div>
@if(empty($paidMailboxes))
<p class="px-5 py-8 text-center text-sm text-slate-400">No active hosting subscriptions yet.</p>
@else
<ul class="divide-y divide-slate-50">
@foreach($paidMailboxes as $m)
<li class="flex items-center justify-between px-5 py-3.5">
<div>
<p class="text-sm font-medium text-slate-900">{{ $m['address'] ?? '—' }}</p>
<p class="text-xs text-slate-400">{{ $m['quota_label'] }} · {{ ucfirst($m['status'] ?? 'active') }}</p>
</div>
<span class="text-sm font-medium text-slate-700">{{ $fmt($m['price_minor']) }}<span class="text-xs text-slate-400">/mo</span></span>
</li>
@endforeach
</ul>
<div class="flex items-center justify-between border-t border-slate-100 px-5 py-3">
<span class="text-xs font-medium uppercase tracking-wide text-slate-400">Monthly total</span>
<span class="text-sm font-semibold text-slate-900">{{ $fmt($monthlyTotalMinor) }}</span>
</div>
@endif
</div>
<p class="mt-4 text-xs text-slate-400">Keep your wallet funded to avoid hosting interruption.</p>
<p class="mt-6 text-sm text-slate-600">Hosting plan renewals and upgrades are billed from your Ladill wallet. Manage individual accounts from the dashboard or account overview pages.</p>
<p class="mt-2 text-xs text-slate-400">Keep your wallet funded to avoid hosting interruption.</p>
</div>
</x-app-layout>
@endsection
@@ -1,71 +1,16 @@
<x-app-layout title="Settings — Ladill Hosting">
@php $emailApp = 'https://email.'.config('app.platform_domain'); @endphp
@extends('layouts.hosting')
@section('title', 'Settings — Ladill Hosting')
@section('content')
<div class="mx-auto max-w-2xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Settings</h1>
<p class="mt-0.5 text-sm text-slate-500">Hosting preferences and account mailbox linking.</p>
@if($showMailboxLinkUi ?? (($linkStatus['linked_mailbox'] ?? null) || ($linkStatus['show_reminder'] ?? false)))
<div class="mt-6 rounded-2xl border border-indigo-200 bg-white p-5 shadow-sm">
<div class="flex items-start gap-3">
<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-indigo-50">
@include('partials.ladill-pro-icon')
</span>
<div class="min-w-0 flex-1">
<h2 class="text-sm font-semibold text-slate-900">Link to mailbox</h2>
<p class="mt-1 text-sm leading-relaxed text-slate-500">
Your Ladill account uses <strong class="font-medium text-slate-700">{{ $linkStatus['account_email'] ?? $account->email }}</strong>.
Link a mailbox so <strong class="font-medium text-slate-700">Ladill Mail</strong> opens with your Ladill sign-in.
</p>
@if($linkStatus['linked_mailbox'] ?? null)
<div class="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2.5 text-sm text-emerald-800">
Linked mailbox: <span class="font-semibold">{{ $linkStatus['linked_mailbox'] }}</span>
</div>
<form method="POST" action="{{ route('account.settings.mailbox-unlink') }}" class="mt-3"
onsubmit="return confirm('Unlink this mailbox from your Ladill account? Ladill Mail will no longer open automatically with Ladill sign-in.')">
@csrf @method('DELETE')
<button type="submit" class="rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50">
Unlink mailbox
</button>
</form>
@elseif(($linkStatus['stage'] ?? '') === 'needs_domain')
<p class="mt-3 text-sm text-amber-800">Add and verify an email domain first.</p>
<a href="{{ $emailApp }}/domains" class="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-indigo-700 hover:text-indigo-800">
Go to Ladill Email
<svg class="h-3.5 w-3.5" 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>
</a>
@elseif(($linkStatus['stage'] ?? '') === 'needs_mailbox')
<p class="mt-3 text-sm text-amber-800">Create a mailbox on your verified domain first.</p>
<a href="{{ $emailApp }}/mailboxes/create" class="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-indigo-700 hover:text-indigo-800">
Create mailbox
<svg class="h-3.5 w-3.5" 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>
</a>
@elseif(count($mailboxOptions) > 0)
<form method="POST" action="{{ route('account.settings.mailbox-link') }}" class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-end">
@csrf @method('PUT')
<div class="min-w-0 flex-1">
<label for="mailbox_address" class="block text-[11px] font-medium text-slate-500">Choose mailbox</label>
<select id="mailbox_address" name="mailbox_address" required class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
<option value="" disabled @selected(! old('mailbox_address'))>Select a mailbox</option>
@foreach($mailboxOptions as $mailbox)
<option value="{{ $mailbox['address'] }}" @selected(old('mailbox_address') === $mailbox['address'])>{{ $mailbox['address'] }}</option>
@endforeach
</select>
@error('mailbox_address')<p class="mt-1 text-xs text-rose-600">{{ $message }}</p>@enderror
</div>
<button class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-indigo-700">Link mailbox</button>
</form>
@endif
</div>
</div>
</div>
@endif
<p class="mt-0.5 text-sm text-slate-500">Hosting notifications and preferences.</p>
<form method="POST" action="{{ route('account.settings.update') }}" class="mt-6 space-y-4">
@csrf @method('PUT')
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<h2 class="text-sm font-semibold text-slate-900">Notifications</h2>
<p class="mt-1 text-xs text-slate-500">Where we send hosting renewal reminders, suspension notices, and resource warnings.</p>
<div class="mt-4">
<label class="block text-[11px] font-medium text-slate-500">Notifications email</label>
<input type="email" name="notify_email" value="{{ old('notify_email', $settings->notify_email ?? $account->email) }}"
@@ -86,4 +31,4 @@
<button class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-indigo-700">Save settings</button>
</form>
</div>
</x-app-layout>
@endsection
@@ -1,4 +1,6 @@
<x-app-layout title="Wallet — Ladill Hosting">
@extends('layouts.hosting')
@section('title', 'Wallet — Ladill Hosting')
@section('content')
@php $fmt = fn ($m) => 'GHS '.number_format($m / 100, 2); @endphp
<div class="mx-auto max-w-3xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Wallet</h1>
@@ -22,4 +24,4 @@
</div>
<p class="mt-4 text-xs text-slate-400">Hosting renewals and upgrades are billed from this wallet.</p>
</div>
</x-app-layout>
@endsection
@@ -31,9 +31,11 @@
</div>
<div>
<h3 class="font-semibold text-slate-900 group-hover:text-indigo-600 transition">
{{ $account->primary_domain ?: $account->username }}
{{ $account->linkedDomainLabel() ?? $account->username }}
</h3>
<div class="flex items-center gap-3 mt-1">
<span class="text-sm text-slate-500">{{ $account->username }}</span>
<span class="text-sm text-slate-400">·</span>
<span class="text-sm text-slate-500">{{ $account->product?->name ?? 'Hosting' }}</span>
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium
@if($account->status === 'active') bg-emerald-100 text-emerald-700
+3 -2
View File
@@ -57,9 +57,10 @@
@endphp
<a href="{{ $accountUrl }}" class="flex items-center justify-between px-5 py-3.5 transition hover:bg-slate-50">
<div class="min-w-0">
<p class="truncate text-sm font-medium text-slate-900">{{ $account->primary_domain ?: $account->username }}</p>
<p class="truncate text-sm font-medium text-slate-900">{{ $account->linkedDomainLabel() ?? $account->username }}</p>
<p class="mt-0.5 text-xs text-slate-400">
{{ $account->product?->name ?? 'Hosting' }}
{{ $account->username }}
· {{ $account->product?->name ?? 'Hosting' }}
· {{ $account->sites->count() }} {{ Str::plural('domain', $account->sites->count()) }}
@if ($account->expires_at)
· Expires {{ $account->expires_at->format('d M Y') }}
+1 -1
View File
@@ -124,7 +124,7 @@
<p class="text-xs text-indigo-600">Developer access</p>
@endif
</td>
<td class="px-5 py-3 text-gray-600">{{ filled($account->primary_domain) ? $account->primary_domain : '—' }}</td>
<td class="px-5 py-3 text-gray-600">{{ $account->linkedDomainLabel() ?? '—' }}</td>
<td class="px-5 py-3">
<span class="inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium {{ $accountStatusColors[$account->status] ?? 'bg-gray-100 text-gray-500' }}">
{{ ucfirst($account->status) }}
@@ -21,7 +21,6 @@
@include('partials.topbar')
<main class="flex-1 p-6 pb-24 lg:p-6 lg:pb-6">
@include('partials.flash')
@include('partials.mailbox-link-banner')
@yield('content')
</main>
</div>
+19 -4
View File
@@ -1,5 +1,20 @@
{{-- Afia Ladill Hosting AI assistant slide-over. Opened via $dispatch('afia-open'). --}}
<div x-data="afia({ chatUrl: '{{ route('hosting.afia.chat') }}', csrf: '{{ csrf_token() }}' })">
@php
$afiaProduct = $afiaProduct ?? (request()->routeIs('email.*') ? 'email' : 'hosting');
$afiaGreeting = $afiaProduct === 'email'
? "Hi, I'm Afia 👋 Ask me anything about email — setting up a domain, verifying DNS, creating mailboxes, IMAP/SMTP settings or billing…"
: "Hi, I'm Afia 👋 Ask me anything about hosting — choosing a plan, linking a domain, using the control panel, SSL, or renewals…";
$afiaSuggestions = $afiaProduct === 'email'
? ['How do I set up email for my domain?', 'What DNS records do I need to verify?', 'How do I create a mailbox?', 'What are my IMAP and SMTP settings?']
: ['How do I link a domain to my hosting?', 'How do I open the control panel?', 'How do I renew my hosting plan?', 'How do I install WordPress?'];
$afiaSubtitle = $afiaProduct === 'email' ? 'Email assistant' : 'Hosting assistant';
@endphp
{{-- Afia Ladill AI assistant slide-over. Opened via $dispatch('afia-open'). --}}
<div x-data="afia({
chatUrl: '{{ $afiaProduct === 'email' ? route('email.afia.chat') : route('hosting.afia.chat') }}',
csrf: '{{ csrf_token() }}',
greeting: @js($afiaGreeting),
suggestions: @js($afiaSuggestions),
})">
<div x-show="open" x-cloak @click="close()" class="fixed inset-0 z-50 bg-slate-900/20"></div>
<section x-show="open" x-cloak
@@ -34,7 +49,7 @@
</span>
<div class="leading-tight">
<p class="text-sm font-semibold text-slate-900">Afia</p>
<p class="text-[11px] text-slate-400">Email assistant</p>
<p class="text-[11px] text-slate-400">{{ $afiaSubtitle }}</p>
</div>
</div>
<button @click="close()" class="rounded-lg p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-600">
@@ -62,7 +77,7 @@
</div>
</div>
<div x-show="messages.length <= 1" class="mt-4 space-y-2">
<div x-show="messages.length <= 1 && suggestions.length" class="mt-4 space-y-2">
<template x-for="s in suggestions" :key="s">
<button @click="useSuggestion(s)" :disabled="loading"
class="flex w-full items-center gap-2 rounded-xl border border-slate-200 bg-white px-3.5 py-2.5 text-left text-[13px] font-medium text-slate-700 transition hover:border-indigo-200 hover:bg-indigo-50 disabled:opacity-50">