152 lines
9.6 KiB
PHP
152 lines
9.6 KiB
PHP
{{--
|
|
Buying a package.
|
|
|
|
The first version put five cards in a four-column grid, so the fifth wrapped
|
|
and sat alone; the consent box was at the BOTTOM, which left every button
|
|
disabled on arrival — a page that looks broken before you have done anything
|
|
wrong. And the prices ran across the card in one block, so nothing stood out
|
|
to compare.
|
|
|
|
Now: the consent comes first, because it is the condition for everything under
|
|
it. Three columns, so five plans read as 3 + 2 rather than 4 + 1. Each card is
|
|
one column of the same four rows — price, delivery, what you get, the button —
|
|
so the eye can run down a row and compare like with like.
|
|
--}}
|
|
<div class="mx-auto max-w-[1120px] space-y-6">
|
|
<div class="animate-rise">
|
|
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('order.title') }}</h1>
|
|
<p class="mt-1 max-w-[68ch] text-sm leading-relaxed text-muted">{{ __('order.subtitle') }}</p>
|
|
</div>
|
|
|
|
@if ($contract)
|
|
{{-- Not an error and not a dead end: they have a contract, and the way to
|
|
a bigger package is a plan change, which prorates and leaves their
|
|
data where it is. A second checkout would build a second, empty cloud
|
|
and bill for both. --}}
|
|
<x-ui.alert variant="info">
|
|
{{ __('order.already_customer') }}
|
|
<a href="{{ route('billing') }}" class="font-semibold underline">{{ __('order.to_billing') }}</a>
|
|
</x-ui.alert>
|
|
@elseif ($plans === [])
|
|
<x-ui.alert variant="warning">{{ __('order.no_catalogue') }}</x-ui.alert>
|
|
@else
|
|
@error('plan')<x-ui.alert variant="error">{{ $message }}</x-ui.alert>@enderror
|
|
|
|
<div x-data="{ immediateStart: false }" class="space-y-6">
|
|
{{-- ── The consent, first ────────────────────────────────────────
|
|
It was at the bottom, and every buy button was disabled until it
|
|
was ticked — so the page arrived looking broken. Read in the
|
|
order it applies: this is the condition, the packages are the
|
|
choice.
|
|
|
|
The server rule is still the real gate (CheckoutController
|
|
validates `accepted`), so with JavaScript off the field arrives
|
|
empty and the checkout is refused with the sentence explaining
|
|
why. Ticking a box in a browser was never the record: the record
|
|
is the timestamp taken when the payment lands. --}}
|
|
<label class="flex cursor-pointer items-start gap-3 rounded-lg border p-5 shadow-xs transition-colors animate-rise [animation-delay:40ms]"
|
|
x-bind:class="immediateStart ? 'border-success-border bg-success-bg' : 'border-accent-border bg-accent-subtle'">
|
|
<input type="checkbox" x-model="immediateStart"
|
|
class="mt-0.5 size-4 shrink-0 rounded border-line-strong text-accent-active focus:ring-accent-active" />
|
|
<span class="min-w-0">
|
|
<span class="block text-sm font-semibold text-ink">{{ __('order.consent_title') }}</span>
|
|
<span class="mt-1 block text-xs leading-relaxed text-body">{{ __('checkout.immediate_start') }}</span>
|
|
{{-- What happens if it is left unticked, said here rather than
|
|
only in the refusal afterwards. The box reads like a
|
|
choice between "now" and "in fourteen days", and there is
|
|
no second option: nothing here delivers later, so an
|
|
unticked box means no order at all. --}}
|
|
<span class="mt-2 block text-xs leading-relaxed text-muted">{{ __('order.consent_why') }}</span>
|
|
</span>
|
|
</label>
|
|
|
|
@error('immediate_start')
|
|
<x-ui.alert variant="error">{{ $message }}</x-ui.alert>
|
|
@enderror
|
|
|
|
{{-- ── The packages ──────────────────────────────────────────────
|
|
Three columns: five plans read as 3 + 2, and a fifth card alone
|
|
on a row of four reads as a mistake. items-start, because the
|
|
recommended one is allowed to be taller without dragging its
|
|
neighbours' buttons down with it. --}}
|
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 lg:items-start">
|
|
@foreach ($plans as $i => $plan)
|
|
<div @class([
|
|
'flex flex-col rounded-xl border bg-surface p-6 shadow-xs animate-rise',
|
|
'border-ink shadow-md ring-1 ring-ink' => $plan['recommended'],
|
|
'border-line' => ! $plan['recommended'],
|
|
]) style="animation-delay: {{ 60 + $i * 40 }}ms">
|
|
{{-- Name, who it is for, and the one badge. --}}
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ $plan['name'] }}</h2>
|
|
@if ($plan['recommended'])
|
|
<span class="rounded-pill bg-accent-active px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-on-accent">{{ __('order.recommended') }}</span>
|
|
@endif
|
|
</div>
|
|
@if ($plan['audience'])
|
|
<p class="mt-1 text-xs leading-relaxed text-muted">{{ $plan['audience'] }}</p>
|
|
@endif
|
|
|
|
{{-- The price, gross and large. A customer quoted 58,80 €
|
|
on the website must not meet 49 € on the page with
|
|
the button on it; the net figure is underneath for
|
|
the books. --}}
|
|
<p class="mt-5 flex items-baseline gap-1.5">
|
|
<span class="text-[2rem] font-bold leading-none tabular-nums tracking-[-0.03em] text-ink">{{ Number::currency($plan['gross'] / 100, in: $plan['currency'], locale: app()->getLocale()) }}</span>
|
|
<span class="text-xs text-muted">{{ __('order.per_month') }}</span>
|
|
</p>
|
|
<p class="mt-1.5 text-xs text-muted">
|
|
{{ __('order.incl_vat', ['rate' => $vat]) }} ·
|
|
{{ __('order.net') }} {{ Number::currency($plan['net'] / 100, in: $plan['currency'], locale: app()->getLocale()) }}
|
|
</p>
|
|
@if ($setup > 0)
|
|
<p class="mt-1 text-xs text-muted">
|
|
{{ __('order.setup', ['amount' => Number::currency($setup / 100, in: $plan['currency'], locale: app()->getLocale())]) }}
|
|
</p>
|
|
@endif
|
|
|
|
{{-- Read from the estate, not written down: where a host
|
|
has room this rolls out on its own. --}}
|
|
<x-ui.delivery :state="$plan['delivery']" class="mt-4" />
|
|
|
|
<dl class="mt-4 flex-1 space-y-2 border-t border-line pt-4 text-sm">
|
|
@foreach ([
|
|
__('order.storage') => $plan['quota_gb'] >= 1000 ? ($plan['quota_gb'] / 1000).' TB' : $plan['quota_gb'].' GB',
|
|
__('order.traffic') => $plan['traffic_gb'] >= 1000 ? ($plan['traffic_gb'] / 1000).' TB' : $plan['traffic_gb'].' GB',
|
|
__('order.seats') => __('order.up_to', ['count' => $plan['seats']]),
|
|
] as $term => $value)
|
|
<div class="flex items-baseline justify-between gap-3">
|
|
<dt class="text-muted">{{ $term }}</dt>
|
|
<dd class="font-mono tabular-nums text-ink">{{ $value }}</dd>
|
|
</div>
|
|
@endforeach
|
|
</dl>
|
|
|
|
{{-- A plain form post, not a Livewire action: the response
|
|
is a redirect to another domain, and Livewire's
|
|
XHR-and-swap cannot leave the site. --}}
|
|
<form method="POST" action="{{ route('checkout.start') }}" class="mt-6">
|
|
@csrf
|
|
<input type="hidden" name="plan" value="{{ $plan['key'] }}" />
|
|
<input type="hidden" name="immediate_start" x-bind:value="immediateStart ? '1' : ''" />
|
|
|
|
{{-- Enabled, with the reason it will not go through
|
|
said next to it. A disabled button explains
|
|
nothing; this one is refused by the server with a
|
|
sentence, and the line below says so first. --}}
|
|
<x-ui.button type="submit" :variant="$plan['recommended'] ? 'primary' : 'secondary'" class="w-full">
|
|
{{ __('order.buy') }}
|
|
</x-ui.button>
|
|
<p class="mt-2 text-center text-[11px] leading-snug text-muted" x-show="!immediateStart" x-cloak>
|
|
{{ __('order.consent_first') }}
|
|
</p>
|
|
</form>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
|
|
<p class="max-w-[80ch] text-xs leading-relaxed text-muted">{{ __('order.terms') }}</p>
|
|
@endif
|
|
</div>
|