Extract Ladill QR Plus as standalone app at qr.ladill.com.
Deploy Ladill QR Plus / deploy (push) Failing after 1s
Deploy Ladill QR Plus / deploy (push) Failing after 1s
Utility QR types only (URL, WiFi, link list, business, app download) with SSO, Billing API integration, public /q resolver, and qr-plus:import for platform migration. vCard and commerce types stay on the platform. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user