Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s

Shared web hosting extracted from the platform monolith, with CI deploy
to /var/www/ladill-hosting matching Bird/Domains/Email.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-06 16:24:20 +00:00
co-authored by Cursor
commit e251a4cf60
367 changed files with 66268 additions and 0 deletions
@@ -0,0 +1,52 @@
@extends('layouts.email')
@section('title', 'Billing — Ladill Email')
@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 mailboxes across every Ladill app.</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">
<p class="text-xs font-medium uppercase tracking-wide text-indigo-100">Wallet balance</p>
<p class="mt-1 text-2xl font-semibold">{{ $fmt($balanceMinor) }}</p>
<a href="{{ $topupUrl }}" class="mt-3 inline-block rounded-lg bg-white/15 px-3.5 py-1.5 text-xs font-semibold text-white backdrop-blur transition hover:bg-white/25">Add funds</a>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-xs font-medium uppercase tracking-wide text-slate-400">Spent on email</p>
<p class="mt-1.5 text-2xl font-semibold text-slate-900">{{ $fmt($spentMinor) }}</p>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-xs font-medium uppercase tracking-wide text-slate-400">Refunded / credited</p>
<p class="mt-1.5 text-2xl font-semibold text-slate-900">{{ $fmt($creditedMinor) }}</p>
</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">Mailbox subscriptions</h2>
<span class="text-xs text-slate-400">priced by storage plan</span>
</div>
@if(empty($paidMailboxes))
<p class="px-5 py-8 text-center text-sm text-slate-400">No paid mailboxes yet your free allowance covers you.</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">Mailboxes beyond your free allowance renew monthly from your wallet. Keep it funded to avoid interruption.</p>
</div>
@endsection
@@ -0,0 +1,70 @@
@extends('layouts.email')
@section('title', 'Developers — Ladill Email')
@section('content')
<div class="mx-auto max-w-3xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Developers</h1>
<p class="mt-0.5 text-sm text-slate-500">API tokens to manage your mailboxes programmatically.</p>
@if($newToken)
<div class="mt-6 rounded-2xl border border-emerald-200 bg-emerald-50 p-5">
<p class="text-sm font-semibold text-emerald-900">Your new token copy it now</p>
<p class="mt-1 text-xs text-emerald-700">This is the only time it will be shown.</p>
<div class="mt-3 flex items-center gap-2" x-data="{ copied: false }">
<code class="flex-1 truncate rounded-lg bg-white px-3 py-2 font-mono text-xs text-slate-800 ring-1 ring-emerald-200">{{ $newToken }}</code>
<button @click="navigator.clipboard.writeText('{{ $newToken }}'); copied = true; setTimeout(() => copied = false, 1500)"
class="rounded-lg bg-emerald-600 px-3 py-2 text-xs font-semibold text-white hover:bg-emerald-700">
<span x-show="!copied">Copy</span><span x-show="copied" x-cloak>Copied </span>
</button>
</div>
</div>
@endif
{{-- Create --}}
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5">
<h2 class="text-sm font-semibold text-slate-900">Create a token</h2>
<form method="POST" action="{{ route('account.developers.store') }}" class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-end">
@csrf
<div class="flex-1">
<label class="block text-[11px] font-medium text-slate-500">Token name</label>
<input type="text" name="name" required placeholder="e.g. CI server"
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
@error('name')<p class="mt-1 text-xs text-rose-600">{{ $message }}</p>@enderror
</div>
<button class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700">Generate token</button>
</form>
</div>
{{-- Tokens --}}
<div class="mt-6 rounded-2xl border border-slate-200 bg-white">
<div class="border-b border-slate-100 px-5 py-3.5"><h2 class="text-sm font-semibold text-slate-900">Your tokens</h2></div>
@forelse($tokens as $token)
<div class="flex items-center justify-between px-5 py-3.5 {{ ! $loop->last ? 'border-b border-slate-50' : '' }}">
<div>
<p class="text-sm font-medium text-slate-900">{{ $token->name }}</p>
<p class="text-xs text-slate-400">
Created {{ $token->created_at->diffForHumans() }} ·
{{ $token->last_used_at ? 'last used '.$token->last_used_at->diffForHumans() : 'never used' }}
</p>
</div>
<form method="POST" action="{{ route('account.developers.destroy', $token->id) }}" onsubmit="return confirm('Revoke {{ $token->name }}?')">
@csrf @method('DELETE')
<button class="text-xs font-medium text-rose-600 hover:underline">Revoke</button>
</form>
</div>
@empty
<p class="px-5 py-8 text-center text-sm text-slate-400">No tokens yet.</p>
@endforelse
</div>
{{-- Docs --}}
<div class="mt-6 rounded-2xl border border-slate-200 bg-slate-900 p-5 text-slate-200">
<h2 class="text-sm font-semibold text-white">Quick start</h2>
<p class="mt-1 text-xs text-slate-400">Authenticate with a Bearer token. Base URL:</p>
<code class="mt-2 block rounded-lg bg-black/40 px-3 py-2 font-mono text-[11px] text-emerald-300">{{ $apiBase }}</code>
<pre class="mt-3 overflow-x-auto rounded-lg bg-black/40 px-3 py-3 font-mono text-[11px] leading-relaxed text-slate-300"><code>curl {{ $apiBase }}/mailboxes \
-H "Authorization: Bearer &lt;your-token&gt;" \
-H "Accept: application/json"</code></pre>
<p class="mt-3 text-[11px] text-slate-400">Endpoints: <span class="font-mono text-slate-300">GET /me</span>, <span class="font-mono text-slate-300">GET /mailboxes</span>. More coming soon.</p>
</div>
</div>
@endsection
@@ -0,0 +1,101 @@
@extends('layouts.email')
@section('title', 'Settings — Ladill Email')
@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">Defaults, 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="{{ route('email.domains.index') }}" class="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-indigo-700 hover:text-indigo-800">
Go to domains
<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="{{ route('email.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
<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">Mailbox defaults</h2>
<div class="mt-4 grid gap-3 sm:grid-cols-2">
<div>
<label class="block text-[11px] font-medium text-slate-500">Default mailbox quota</label>
<select name="default_quota_mb" class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none">
@php $cur = (int) ($settings->default_quota_mb ?? config('email.default_quota_mb', 10240)); @endphp
@foreach([1024 => '1 GB', 5120 => '5 GB', 10240 => '10 GB', 25600 => '25 GB', 51200 => '50 GB'] as $mb => $label)
<option value="{{ $mb }}" @selected($cur === $mb)>{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<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) }}"
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
</div>
</div>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<label class="flex items-center justify-between">
<span>
<span class="block text-sm font-medium text-slate-800">Product updates</span>
<span class="block text-xs text-slate-400">Occasional emails about new Ladill Email features.</span>
</span>
<input type="checkbox" name="product_updates" value="1" @checked($settings->product_updates ?? true) class="h-5 w-5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
</label>
</div>
<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>
@endsection
@@ -0,0 +1,82 @@
@extends('layouts.email')
@section('title', 'Team — Ladill Email')
@section('content')
<div class="mx-auto max-w-3xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Team</h1>
<p class="mt-0.5 text-sm text-slate-500">Invite people to help manage this accounts mailboxes & domains.</p>
@if($canManage)
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5">
<h2 class="text-sm font-semibold text-slate-900">Invite a teammate</h2>
<form method="POST" action="{{ route('account.team.store') }}" class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-end">
@csrf
<div class="flex-1">
<label class="block text-[11px] font-medium text-slate-500">Email</label>
<input type="email" name="email" required placeholder="teammate@example.com"
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
@error('email')<p class="mt-1 text-xs text-rose-600">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-[11px] font-medium text-slate-500">Role</label>
<select name="role" class="mt-1 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none">
<option value="member">Member</option>
<option value="admin">Admin</option>
</select>
</div>
<button class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700">Send invite</button>
</form>
<p class="mt-2 text-[11px] text-slate-400">Admins can manage mailboxes and the team. Members can manage mailboxes. Invitees join by signing in with that email.</p>
</div>
@endif
<div class="mt-6 rounded-2xl border border-slate-200 bg-white">
<div class="border-b border-slate-100 px-5 py-3.5"><h2 class="text-sm font-semibold text-slate-900">Members</h2></div>
<ul class="divide-y divide-slate-50">
<li class="flex items-center justify-between px-5 py-3.5">
<div class="flex items-center gap-3">
<span class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-slate-900 text-xs font-semibold text-white">{{ strtoupper(substr($account->name ?? $account->email, 0, 1)) }}</span>
<div>
<p class="text-sm font-medium text-slate-900">{{ $account->name ?? $account->email }} <span class="text-xs font-normal text-slate-400">(you)</span></p>
<p class="text-xs text-slate-400">{{ $account->email }}</p>
</div>
</div>
<span class="rounded-full bg-indigo-50 px-2.5 py-1 text-[11px] font-medium text-indigo-700">Owner</span>
</li>
@forelse($members as $member)
<li class="flex items-center justify-between px-5 py-3.5">
<div class="flex items-center gap-3">
<span class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-xs font-semibold text-slate-600">{{ strtoupper(substr($member->email, 0, 1)) }}</span>
<div>
<p class="text-sm font-medium text-slate-900">{{ $member->member->name ?? $member->email }}</p>
<p class="text-xs text-slate-400">{{ $member->email }}</p>
</div>
</div>
<div class="flex items-center gap-3">
@if($member->status === 'invited')
<span class="rounded-full bg-amber-50 px-2.5 py-1 text-[11px] font-medium text-amber-700">Invited</span>
@endif
@if($canManage)
<form method="POST" action="{{ route('account.team.role', $member) }}">
@csrf @method('PATCH')
<select name="role" onchange="this.form.submit()" class="rounded-lg border border-slate-200 bg-white px-2 py-1 text-xs focus:outline-none">
<option value="member" @selected($member->role === 'member')>Member</option>
<option value="admin" @selected($member->role === 'admin')>Admin</option>
</select>
</form>
<form method="POST" action="{{ route('account.team.destroy', $member) }}" onsubmit="return confirm('Remove {{ $member->email }}?')">
@csrf @method('DELETE')
<button class="text-xs font-medium text-rose-600 hover:underline">Remove</button>
</form>
@else
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-medium capitalize text-slate-600">{{ $member->role }}</span>
@endif
</div>
</li>
@empty
<li class="px-5 py-8 text-center text-sm text-slate-400">No teammates yet.</li>
@endforelse
</ul>
</div>
</div>
@endsection
@@ -0,0 +1,27 @@
@extends('layouts.email')
@section('title', 'Wallet — Ladill Email')
@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>
<p class="mt-0.5 text-sm text-slate-500">Your Ladill wallet funds mailboxes across every Ladill app.</p>
<div class="mt-6 rounded-2xl bg-gradient-to-br from-indigo-700 via-indigo-600 to-blue-500 p-6 text-white shadow-sm">
<p class="text-xs font-medium uppercase tracking-wide text-indigo-100">Balance</p>
<p class="mt-1 text-3xl font-semibold">{{ $fmt($balanceMinor) }}</p>
<a href="{{ $topupUrl }}" class="mt-4 inline-block rounded-lg bg-white/15 px-4 py-2 text-sm font-semibold text-white backdrop-blur transition hover:bg-white/25">Add funds</a>
</div>
<div class="mt-4 grid gap-4 sm:grid-cols-2">
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-xs font-medium uppercase tracking-wide text-slate-400">Spent on email</p>
<p class="mt-1.5 text-2xl font-semibold text-slate-900">{{ $fmt($spentMinor) }}</p>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-xs font-medium uppercase tracking-wide text-slate-400">Refunded / credited</p>
<p class="mt-1.5 text-2xl font-semibold text-slate-900">{{ $fmt($creditedMinor) }}</p>
</div>
</div>
<p class="mt-4 text-xs text-slate-400">Mailboxes beyond your free allowance are billed monthly from this wallet.</p>
</div>
@endsection
+47
View File
@@ -0,0 +1,47 @@
@extends('layouts.email')
@section('title', 'Overview — Ladill Email')
@section('content')
<div class="mx-auto max-w-5xl">
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Overview</h1>
<p class="mt-0.5 text-sm text-slate-500">Your mailboxes at a glance.</p>
</div>
<a href="{{ route('email.mailboxes.create') }}" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-indigo-700">New mailbox</a>
</div>
@if($error)<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">{{ $error }}</div>@endif
<div class="mt-6 grid gap-4 sm:grid-cols-3">
@foreach([['Mailboxes', $mailboxCount, route('email.mailboxes.index')], ['Domains', $domainCount, route('email.domains.index')], ['Verified domains', $verifiedDomainCount, route('email.domains.index')]] as [$label, $value, $link])
<a href="{{ $link }}" class="rounded-2xl border border-slate-200 bg-white p-5 transition hover:border-indigo-200">
<p class="text-xs font-medium uppercase tracking-wide text-slate-400">{{ $label }}</p>
<p class="mt-2 text-2xl font-semibold text-slate-900">{{ $value }}</p>
</a>
@endforeach
</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">Recent mailboxes</h2>
<a href="{{ route('email.mailboxes.index') }}" class="text-xs font-medium text-indigo-600 hover:underline">View all</a>
</div>
@if(empty($recent))
<div class="px-5 py-12 text-center">
<p class="text-sm font-semibold text-slate-700">No mailboxes yet</p>
<p class="mx-auto mt-1 max-w-sm text-xs text-slate-400">Add a domain, verify it, then create your first mailbox.</p>
<a href="{{ route('email.domains.index') }}" class="mt-4 inline-block rounded-lg border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Set up a domain</a>
</div>
@else
<div class="divide-y divide-slate-50">
@foreach($recent as $m)
<a href="{{ route('email.mailboxes.show', $m['id']) }}" class="flex items-center justify-between px-5 py-3.5 transition hover:bg-slate-50">
<span class="text-sm font-medium text-slate-900">{{ $m['address'] }}</span>
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-medium capitalize text-slate-600">{{ $m['status'] }}</span>
</a>
@endforeach
</div>
@endif
</div>
</div>
@endsection
@@ -0,0 +1,30 @@
@extends('layouts.email')
@section('title', 'Domains — Ladill Email')
@section('content')
<div class="mx-auto max-w-3xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Email domains</h1>
<p class="mt-0.5 text-sm text-slate-500">Add a domain and verify it to create mailboxes on it.</p>
@if($error)<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">{{ $error }}</div>@endif
<form method="POST" action="{{ route('email.domains.store') }}" class="mt-5 flex gap-2 rounded-2xl border border-slate-200 bg-white p-2">
@csrf
<input name="domain" required placeholder="yourdomain.com" class="flex-1 rounded-xl border-0 bg-transparent px-3 py-2 text-sm focus:outline-none focus:ring-0">
<button class="rounded-xl bg-indigo-600 px-5 py-2 text-sm font-semibold text-white hover:bg-indigo-700">Add domain</button>
</form>
@if(!empty($domains))
<div class="mt-6 divide-y divide-slate-100 overflow-hidden rounded-2xl border border-slate-200 bg-white">
@foreach($domains as $d)
<a href="{{ route('email.domains.show', $d['id']) }}" class="flex items-center justify-between px-5 py-4 transition hover:bg-slate-50">
<span class="text-sm font-semibold text-slate-900">{{ $d['domain'] }}</span>
@if($d['active'] ?? false)
<span class="rounded-full bg-emerald-50 px-2.5 py-1 text-[11px] font-medium text-emerald-700">Verified</span>
@else
<span class="rounded-full bg-amber-50 px-2.5 py-1 text-[11px] font-medium text-amber-700">Pending verification</span>
@endif
</a>
@endforeach
</div>
@endif
</div>
@endsection
@@ -0,0 +1,62 @@
@extends('layouts.email')
@section('title', $domain['domain'].' — Ladill Email')
@section('content')
<div class="mx-auto max-w-3xl">
<div class="flex items-center gap-2 text-sm text-slate-400">
<a href="{{ route('email.domains.index') }}" class="hover:text-slate-600">Domains</a><span>/</span>
<span class="text-slate-600">{{ $domain['domain'] }}</span>
</div>
<div class="mt-2 flex items-center justify-between">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">{{ $domain['domain'] }}</h1>
@if($domain['active'] ?? false)
<span class="rounded-full bg-emerald-50 px-2.5 py-1 text-[11px] font-medium text-emerald-700">Verified</span>
@else
<span class="rounded-full bg-amber-50 px-2.5 py-1 text-[11px] font-medium text-amber-700">Pending</span>
@endif
</div>
@unless($domain['active'] ?? false)
<div class="mt-6 rounded-2xl border border-slate-200 bg-white">
<div class="border-b border-slate-100 px-5 py-3.5"><h2 class="text-sm font-semibold text-slate-900">Publish these DNS records</h2></div>
@php $records = $domain['dns_records'] ?? []; @endphp
@if(empty($records))
<p class="px-5 py-6 text-sm text-slate-400">No DNS records returned. Try refreshing.</p>
@else
<table class="w-full text-sm">
<thead class="text-left text-[11px] uppercase tracking-wide text-slate-400">
<tr><th class="px-5 py-2 font-medium">Type</th><th class="px-2 py-2 font-medium">Name</th><th class="px-2 py-2 font-medium">Value</th></tr>
</thead>
<tbody class="divide-y divide-slate-50">
@foreach($records as $r)
<tr>
<td class="px-5 py-2.5 font-medium text-slate-700">{{ $r['type'] ?? '' }}</td>
<td class="px-2 py-2.5 font-mono text-xs text-slate-600">{{ $r['name'] ?? ($r['host'] ?? '@') }}</td>
<td class="px-2 py-2.5 max-w-xs truncate font-mono text-xs text-slate-600" title="{{ $r['value'] ?? '' }}">{{ $r['value'] ?? '' }}</td>
</tr>
@endforeach
</tbody>
</table>
@endif
</div>
<form method="POST" action="{{ route('email.domains.verify', $domain['id']) }}" class="mt-4">
@csrf
<button class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-indigo-700">Verify domain</button>
<span class="ml-2 text-xs text-slate-400">DNS changes can take time to propagate.</span>
</form>
@else
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-sm text-slate-600">This domain is verified. <a href="{{ route('email.mailboxes.create') }}" class="font-semibold text-indigo-600 hover:underline">Create a mailbox</a> on it.</p>
<dl class="mt-3 flex gap-4 text-xs text-slate-500">
<div>SPF <span class="{{ ($domain['spf'] ?? false) ? 'text-emerald-600' : 'text-rose-600' }}">{{ ($domain['spf'] ?? false) ? '✓' : '✗' }}</span></div>
<div>DKIM <span class="{{ ($domain['dkim'] ?? false) ? 'text-emerald-600' : 'text-rose-600' }}">{{ ($domain['dkim'] ?? false) ? '✓' : '✗' }}</span></div>
<div>DMARC <span class="{{ ($domain['dmarc'] ?? false) ? 'text-emerald-600' : 'text-rose-600' }}">{{ ($domain['dmarc'] ?? false) ? '✓' : '✗' }}</span></div>
</dl>
</div>
@endunless
<form method="POST" action="{{ route('email.domains.destroy', $domain['id']) }}" class="mt-4" onsubmit="return confirm('Remove {{ $domain['domain'] }}?')">
@csrf @method('DELETE')
<button class="text-xs font-medium text-rose-600 hover:underline">Remove domain</button>
</form>
</div>
@endsection
@@ -0,0 +1,95 @@
@extends('layouts.email')
@section('title', 'New mailbox — Ladill Email')
@section('content')
<div class="mx-auto max-w-lg">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Create a mailbox</h1>
@if(empty($domains))
<div class="mt-6 rounded-2xl border border-amber-200 bg-amber-50 p-6 text-sm text-amber-800">
You need a <strong>verified domain</strong> first. <a href="{{ route('email.domains.index') }}" class="font-semibold underline">Set up a domain</a>.
</div>
@else
@php $fmt = fn ($m) => $quota['currency'].' '.number_format($m / 100, 2); @endphp
<div x-data="{
tiers: @js($quota['tiers']),
sel: {{ $quota['default_mb'] }},
balance: {{ $walletBalanceMinor }},
get tier() { return this.tiers.find(t => t.mb == this.sel) || this.tiers[0]; },
get price() { return this.tier.price_minor; },
get free() { return this.price === 0; },
fmt(m) { return '{{ $quota['currency'] }} ' + (m / 100).toFixed(2); },
}">
{{-- Pricing banner (reacts to the selected storage plan) --}}
<template x-if="!free">
<div class="mt-5 rounded-2xl border border-indigo-200 bg-indigo-50 px-4 py-3 text-sm text-indigo-800">
This plan is <strong><span x-text="fmt(price)"></span>/month</strong>, charged from your Ladill wallet
(balance: <span x-text="fmt(balance)"></span>).
<a href="{{ $topupUrl }}" class="font-semibold underline" x-show="balance < price" x-cloak>Top up</a>
</div>
</template>
<template x-if="free">
<div class="mt-5 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">
The <strong>1 GB</strong> plan is <strong>free, forever</strong>. You can upgrade for more storage anytime.
</div>
</template>
<form method="POST" action="{{ route('email.mailboxes.store') }}" class="mt-4 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
@csrf
<div>
<label class="block text-xs font-medium text-slate-600">Domain</label>
<select name="email_domain_id" class="mt-1 w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none">
@foreach($domains as $d)<option value="{{ $d['id'] }}">{{ $d['domain'] }}</option>@endforeach
</select>
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Mailbox name</label>
<input name="local_part" required value="{{ old('local_part') }}" placeholder="you"
class="mt-1 w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none">
@error('local_part')<p class="mt-1 text-xs text-rose-600">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Display name</label>
<input name="display_name" required value="{{ old('display_name') }}" placeholder="Your Name"
class="mt-1 w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none">
</div>
{{-- Storage plan (sets quota + price) --}}
<div>
<label class="block text-xs font-medium text-slate-600">Storage plan</label>
<div class="mt-1.5 grid grid-cols-1 gap-2">
@foreach($quota['tiers'] as $t)
<label class="flex cursor-pointer items-center justify-between rounded-xl border bg-slate-50 px-3.5 py-2.5 text-sm transition"
:class="sel == {{ $t['mb'] }} ? 'border-indigo-400 bg-indigo-50 ring-1 ring-indigo-200' : 'border-slate-200 hover:bg-slate-100'">
<span class="flex items-center gap-2.5">
<input type="radio" name="quota_mb" value="{{ $t['mb'] }}" x-model.number="sel" class="text-indigo-600 focus:ring-indigo-500">
<span class="font-medium text-slate-800">{{ $t['label'] }}</span>
</span>
@if($t['price_minor'] === 0)
<span class="font-semibold text-emerald-600">Free</span>
@else
<span class="text-slate-500">{{ $fmt($t['price_minor']) }}<span class="text-xs">/mo</span></span>
@endif
</label>
@endforeach
</div>
@error('quota_mb')<p class="mt-1 text-xs text-rose-600">{{ $message }}</p>@enderror
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-slate-600">Password</label>
<input type="password" name="password" required class="mt-1 w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none">
</div>
<div>
<label class="block text-xs font-medium text-slate-600">Confirm</label>
<input type="password" name="password_confirmation" required class="mt-1 w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none">
</div>
</div>
@error('password')<p class="text-xs text-rose-600">{{ $message }}</p>@enderror
<button class="w-full rounded-xl bg-indigo-600 px-4 py-3 text-sm font-semibold text-white hover:bg-indigo-700"
x-text="free ? 'Create free mailbox' : ('Create mailbox — ' + fmt(price) + '/mo')">Create mailbox</button>
</form>
</div>
@endif
</div>
@endsection
@@ -0,0 +1,32 @@
@extends('layouts.email')
@section('title', 'Mailboxes — Ladill Email')
@section('content')
<div class="mx-auto max-w-4xl">
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Mailboxes</h1>
<a href="{{ route('email.mailboxes.create') }}" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700">New mailbox</a>
</div>
@if($error)<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">{{ $error }}</div>@endif
@if(empty($mailboxes))
<div class="mt-6 rounded-2xl border border-dashed border-slate-300 bg-white p-12 text-center">
<p class="text-sm font-semibold text-slate-700">No mailboxes yet</p>
<p class="mx-auto mt-1 max-w-sm text-xs text-slate-400">Verify a domain, then create a mailbox like you@yourdomain.com.</p>
<a href="{{ route('email.mailboxes.create') }}" class="mt-4 inline-block rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700">Create mailbox</a>
</div>
@else
<div class="mt-6 divide-y divide-slate-100 overflow-hidden rounded-2xl border border-slate-200 bg-white">
@foreach($mailboxes as $m)
<a href="{{ route('email.mailboxes.show', $m['id']) }}" class="flex items-center justify-between gap-3 px-5 py-4 transition hover:bg-slate-50">
<div class="min-w-0">
<p class="truncate text-sm font-semibold text-slate-900">{{ $m['address'] }}</p>
<p class="mt-0.5 text-xs text-slate-400">{{ $m['display_name'] ?? '' }} · {{ number_format(($m['quota_mb'] ?? 0)/1024, 0) }} GB</p>
</div>
@php $st = $m['status'] ?? 'pending'; $badge = $st === 'active' ? 'bg-emerald-50 text-emerald-700' : ($st === 'failed' ? 'bg-rose-50 text-rose-700' : 'bg-slate-100 text-slate-600'); @endphp
<span class="shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium capitalize {{ $badge }}">{{ $st }}</span>
</a>
@endforeach
</div>
@endif
</div>
@endsection
@@ -0,0 +1,83 @@
@extends('layouts.email')
@section('title', $mailbox['address'].' — Ladill Email')
@section('content')
@php $host = 'mail.'.config('app.platform_domain'); @endphp
<div class="mx-auto max-w-2xl">
<div class="flex items-center gap-2 text-sm text-slate-400">
<a href="{{ route('email.mailboxes.index') }}" class="hover:text-slate-600">Mailboxes</a><span>/</span>
<span class="text-slate-600">{{ $mailbox['address'] }}</span>
</div>
<div class="mt-2 flex items-center justify-between">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">{{ $mailbox['address'] }}</h1>
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-medium capitalize text-slate-600">{{ $mailbox['status'] ?? '' }}</span>
</div>
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-5">
<h2 class="text-sm font-semibold text-slate-900">Connection settings</h2>
<p class="mt-0.5 text-xs text-slate-400">Use these in any mail client, or just open webmail.</p>
<dl class="mt-4 space-y-2 text-sm">
<div class="flex justify-between"><dt class="text-slate-500">Username</dt><dd class="font-mono text-slate-800">{{ $mailbox['address'] }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-500">IMAP</dt><dd class="font-mono text-slate-800">{{ $host }}:993 (SSL)</dd></div>
<div class="flex justify-between"><dt class="text-slate-500">SMTP</dt><dd class="font-mono text-slate-800">{{ $host }}:587 (STARTTLS)</dd></div>
</dl>
<a href="https://{{ $host }}" class="mt-4 inline-block rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700">Open Webmail </a>
</div>
@php
$quotaMb = (int) ($mailbox['quota_mb'] ?? 0);
$usedBytes = (int) ($mailbox['used_bytes'] ?? 0);
$price = \App\Support\MailboxPricing::priceMinorFor($quotaMb);
$tiers = \App\Support\MailboxPricing::tiers();
$maxMb = collect($tiers)->max('mb');
$pct = $quotaMb > 0 ? min(100, (int) round($usedBytes / ($quotaMb * 1048576) * 100)) : 0;
$fmt = fn ($m) => config('email.currency', 'GHS').' '.number_format($m / 100, 2);
@endphp
<div class="mt-4 rounded-2xl border border-slate-200 bg-white p-5">
<div class="flex items-start justify-between">
<div>
<h2 class="text-sm font-semibold text-slate-900">Storage plan</h2>
<p class="mt-0.5 text-sm text-slate-600">
<span class="font-semibold">{{ \App\Support\MailboxPricing::label($quotaMb) }}</span>
@if($price === 0)
<span class="ml-1 rounded-full bg-emerald-50 px-2 py-0.5 text-[11px] font-medium text-emerald-700">Free</span>
@else
<span class="ml-1 text-xs text-slate-400">{{ $fmt($price) }}/month</span>
@endif
</p>
</div>
@if($quotaMb < $maxMb)
<a href="{{ route('email.mailboxes.upgrade', $mailbox['id']) }}" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700">Upgrade</a>
@else
<span class="rounded-lg bg-slate-100 px-3 py-2 text-xs font-medium text-slate-500">Top plan</span>
@endif
</div>
@if($quotaMb > 0)
<div class="mt-4">
<div class="h-2 w-full overflow-hidden rounded-full bg-slate-100">
<div class="h-full rounded-full {{ $pct >= 90 ? 'bg-rose-500' : 'bg-indigo-500' }}" style="width: {{ max(2, $pct) }}%"></div>
</div>
<p class="mt-1.5 text-xs text-slate-400">{{ number_format($usedBytes / 1048576, 0) }} MB of {{ \App\Support\MailboxPricing::label($quotaMb) }} used ({{ $pct }}%)</p>
</div>
@endif
</div>
<div class="mt-4 rounded-2xl border border-slate-200 bg-white p-5">
<h2 class="text-sm font-semibold text-slate-900">Reset password</h2>
<form method="POST" action="{{ route('email.mailboxes.password', $mailbox['id']) }}" class="mt-3 grid grid-cols-2 gap-3">
@csrf @method('PATCH')
<input type="password" name="password" required placeholder="New password" class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none">
<input type="password" name="password_confirmation" required placeholder="Confirm" class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none">
<button class="col-span-2 rounded-lg border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Update password</button>
</form>
</div>
<div class="mt-4 rounded-2xl border border-rose-200 bg-white p-5">
<h2 class="text-sm font-semibold text-rose-700">Delete mailbox</h2>
<p class="mt-0.5 text-xs text-slate-400">This permanently removes the mailbox and its email.</p>
<form method="POST" action="{{ route('email.mailboxes.destroy', $mailbox['id']) }}" class="mt-3" onsubmit="return confirm('Delete {{ $mailbox['address'] }}? This cannot be undone.')">
@csrf @method('DELETE')
<button class="rounded-lg bg-rose-600 px-4 py-2 text-sm font-semibold text-white hover:bg-rose-700">Delete mailbox</button>
</form>
</div>
</div>
@endsection
@@ -0,0 +1,52 @@
@extends('layouts.email')
@section('title', 'Upgrade storage — Ladill Email')
@section('content')
@php $fmt = fn ($m) => $currency.' '.number_format($m / 100, 2); @endphp
<div class="mx-auto max-w-lg">
<div class="flex items-center gap-2 text-sm text-slate-400">
<a href="{{ route('email.mailboxes.show', $mailbox['id']) }}" class="hover:text-slate-600">{{ $mailbox['address'] }}</a><span>/</span>
<span class="text-slate-600">Upgrade</span>
</div>
<h1 class="mt-2 text-xl font-semibold tracking-tight text-slate-900">Upgrade storage</h1>
<p class="mt-0.5 text-sm text-slate-500">
Currently on <strong>{{ \App\Support\MailboxPricing::label($currentMb) }}</strong>. Pick a larger plan billed monthly from your wallet.
</p>
<div x-data="{
tiers: @js($tiers),
sel: {{ $defaultMb }},
balance: {{ $walletBalanceMinor }},
get tier() { return this.tiers.find(t => t.mb == this.sel) || this.tiers[0]; },
get price() { return this.tier.price_minor; },
fmt(m) { return '{{ $currency }} ' + (m / 100).toFixed(2); },
}">
<div class="mt-5 rounded-2xl border border-indigo-200 bg-indigo-50 px-4 py-3 text-sm text-indigo-800">
New plan: <strong><span x-text="fmt(price)"></span>/month</strong>, charged now from your wallet
(balance: <span x-text="fmt(balance)"></span>).
<a href="{{ $topupUrl }}" class="font-semibold underline" x-show="balance < price" x-cloak>Top up</a>
</div>
<form method="POST" action="{{ route('email.mailboxes.upgrade.store', $mailbox['id']) }}" class="mt-4 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
@csrf @method('PATCH')
<div>
<label class="block text-xs font-medium text-slate-600">Choose a plan</label>
<div class="mt-1.5 grid grid-cols-1 gap-2">
@foreach($tiers as $t)
<label class="flex cursor-pointer items-center justify-between rounded-xl border bg-slate-50 px-3.5 py-2.5 text-sm transition"
:class="sel == {{ $t['mb'] }} ? 'border-indigo-400 bg-indigo-50 ring-1 ring-indigo-200' : 'border-slate-200 hover:bg-slate-100'">
<span class="flex items-center gap-2.5">
<input type="radio" name="quota_mb" value="{{ $t['mb'] }}" x-model.number="sel" class="text-indigo-600 focus:ring-indigo-500">
<span class="font-medium text-slate-800">{{ $t['label'] }}</span>
</span>
<span class="text-slate-500">{{ $fmt($t['price_minor']) }}<span class="text-xs">/mo</span></span>
</label>
@endforeach
</div>
@error('quota_mb')<p class="mt-1 text-xs text-rose-600">{{ $message }}</p>@enderror
</div>
<button class="w-full rounded-xl bg-indigo-600 px-4 py-3 text-sm font-semibold text-white hover:bg-indigo-700"
x-text="'Upgrade — ' + fmt(price) + '/mo'">Upgrade</button>
</form>
</div>
</div>
@endsection
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Signed out Ladill Email</title>
@include('partials.favicon')
@vite(['resources/css/app.css'])
<meta http-equiv="refresh" content="2;url={{ 'https://'.config('app.platform_domain') }}">
</head>
<body class="flex h-full items-center justify-center bg-slate-100 font-sans">
<div class="text-center">
<img src="{{ asset('images/logo/ladillemail-logo.svg') }}" alt="Ladill Email" class="mx-auto h-7 w-auto">
<p class="mt-6 text-sm text-slate-600">Youve been signed out. Redirecting…</p>
<a href="{{ 'https://'.config('app.platform_domain') }}" class="mt-2 inline-block text-sm font-medium text-indigo-600 hover:underline">Go to ladill.com</a>
</div>
</body>
</html>