CluPilotCloud/resources/views/livewire/order.blade.php

168 lines
10 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="{ terms: false }" class="space-y-6">
{{-- ── The terms, first ──────────────────────────────────────────
One declaration, and the terms behind it regulate the sale:
price, VAT, delivery, cancellation, the fourteen-day withdrawal
right and the express request to begin at once. The box used to
carry that last one as a paragraph of its own, which read like a
choice between "now" and "in fourteen days" and there is no
second option.
The label is not the whole contract in small print: it says what
is being accepted, links to it, and summarises underneath what a
reader most needs to know before ticking.
The server rule is the real gate (CheckoutController validates
`accepted`), so with JavaScript off the field arrives empty and
the checkout is refused with the sentence explaining why. --}}
<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="terms ? 'border-success-border bg-success-bg' : 'border-accent-border bg-accent-subtle'">
<input type="checkbox" x-model="terms"
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.terms_title') }}</span>
<span class="mt-1 block text-sm leading-relaxed text-body">
{{-- A new tab: following the link must not lose the page
with the choice on it. --}}
{!! __('order.terms_label', [
'link' => '<a href="'.route('legal.agb').'" target="_blank" rel="noopener"
class="font-semibold text-accent-text underline underline-offset-4">'
.e(__('order.terms_link')).'</a>',
]) !!}
</span>
<span class="mt-2 block text-xs leading-relaxed text-muted">{{ __('order.terms_why') }}</span>
</span>
</label>
@error('terms_accepted')
<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="terms_accepted" x-bind:value="terms ? '1' : ''" />
{{-- No order without the terms so the button is not
usable until they are accepted, and says why
underneath rather than leaving somebody to guess
at a dead button. The refusal on the server
stands regardless: this is the visible half of
one rule, not the rule itself.
x-bind on a plain attribute, not @disabled on the
component R24#3: a Blade directive in a
component tag's attribute list lands in the
attribute bag and takes the view apart. --}}
<x-ui.button type="submit" :variant="$plan['recommended'] ? 'primary' : 'secondary'" class="w-full"
x-bind:disabled="!terms"
x-bind:class="terms ? '' : 'cursor-not-allowed opacity-50'">
{{ __('order.buy') }}
</x-ui.button>
<p class="mt-2 text-center text-[11px] leading-snug text-muted" x-show="!terms" x-cloak>
{{ __('order.terms_first') }}
</p>
</form>
</div>
@endforeach
</div>
</div>
<p class="max-w-[80ch] text-xs leading-relaxed text-muted">{{ __('order.terms') }}</p>
@endif
</div>