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

200 lines
12 KiB
PHP

{{--
Choosing a package. Choosing only nothing is agreed to on this page.
## What was wrong with the last one
Everything but the cards was pinned to the left half of the window: a
68-character title, an 80-character footnote, and a switch under them, on a
1240px shell. It read as a column that had lost its second half.
And the terms sat at the TOP as a condition of looking at prices, which is
the wrong order twice over it is a condition of BUYING, and the buttons
below it were dead until it was ticked, so the page arrived looking broken.
The acceptance now lives one step later, on the checkout page, directly above
the button that spends money. This page has no form on it at all.
## The shape
A header band that uses the width name and purpose left, the term switch
right, where a control belongs. Then the cards. Then a closing band of three
columns rather than a paragraph trailing off to the left.
--}}
@php
use Illuminate\Support\Number;
$eur = fn (int $cents, string $currency) => Number::currency($cents / 100, in: $currency, locale: app()->getLocale());
$bestFreeMonths = (int) collect($plans)->max('free_months');
@endphp
<div class="space-y-6" x-data="{ term: 'monthly' }">
{{-- ── Header band ──────────────────────────────────────────────────────
Title left, control right, one rule under both. The switch is a page
control and not a card decoration, so it sits with the heading. --}}
<header class="flex flex-wrap items-end gap-x-6 gap-y-4 border-b border-line pb-5 animate-rise">
<div class="min-w-0 flex-1">
<p class="lbl">{{ __('order.eyebrow') }}</p>
<h1 class="mt-[7px] text-[23px] font-bold leading-[1.12] tracking-[-0.03em] text-ink min-[901px]:text-[30px]">
{{ __('order.title') }}
</h1>
<p class="mt-2 max-w-[76ch] text-sm leading-relaxed text-muted">{{ __('order.subtitle') }}</p>
</div>
@if ($plans !== [] && ! $contract)
<div class="flex flex-col items-start gap-2 sm:items-end">
{{-- One radius scale, nested: the shell is rounded-lg (16px) and
the option inside it rounded (11px), which is the shell's
radius less its own padding. A rounded-md option — Tailwind's
untokenised 6px read as a different component sitting
inside this one. --}}
<div class="inline-flex rounded-lg border border-line bg-surface p-1 shadow-xs" role="group">
@foreach (['monthly', 'yearly'] as $option)
<button type="button" x-on:click="term = '{{ $option }}'"
class="rounded px-5 py-2 text-sm font-semibold transition-colors"
x-bind:class="term === '{{ $option }}' ? 'bg-ink text-bg shadow-xs' : 'text-muted hover:text-ink'">
{{ __('order.term_'.$option) }}
</button>
@endforeach
</div>
@if ($bestFreeMonths > 0)
<span class="text-xs font-semibold text-success" x-show="term !== 'yearly'">
{{ trans_choice('order.free_months_hint', $bestFreeMonths, ['count' => $bestFreeMonths]) }}
</span>
@endif
</div>
@endif
</header>
@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
{{-- ── 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-lg 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">
<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 of the term that is selected — the whole price,
in the unit it is charged in. The yearly view used to
show the monthly EQUIVALENT in the headline, so on a
package with no free months the figure did not move at
all when the switch was thrown, and the page looked like
it had ignored the click. What a year works out to per
month is worth knowing and is said underneath. --}}
<div x-show="term === 'monthly'">
<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">{{ $eur($plan['gross'], $plan['currency']) }}</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') }} {{ $eur($plan['net'], $plan['currency']) }}
</p>
</div>
<div x-show="term === 'yearly'" x-cloak>
<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">{{ $eur($plan['yearly_gross'], $plan['currency']) }}</span>
<span class="text-xs text-muted">{{ __('order.per_year') }}</span>
</p>
<p class="mt-1.5 text-xs text-muted">
{{ __('order.per_month_equivalent', ['amount' => $eur($plan['yearly_per_month'], $plan['currency'])]) }} ·
{{ __('order.incl_vat', ['rate' => $vat]) }} · {{ __('order.net') }} {{ $eur($plan['yearly_net'], $plan['currency']) }}
</p>
@if ($plan['free_months'] > 0)
<p class="mt-1 text-xs font-semibold text-success">
{{ trans_choice('order.free_months_hint', $plan['free_months'], ['count' => $plan['free_months']]) }}
· {{ __('order.instead_of', ['amount' => $eur($plan['twelve_months_gross'], $plan['currency'])]) }}
</p>
@endif
</div>
@if ($setup > 0)
<p class="mt-1 text-xs text-muted">
{{ __('order.setup', ['amount' => $eur($setup, $plan['currency'])]) }}
</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 $label => $value)
<div class="flex items-baseline justify-between gap-3">
<dt class="text-muted">{{ $label }}</dt>
<dd class="font-mono tabular-nums text-ink">{{ $value }}</dd>
</div>
@endforeach
</dl>
{{-- A link, not a form: choosing a package is a navigation,
and what follows is a page where the order is confirmed.
The term travels in the address, so the checkout shows
what was being looked at — and a reload of that page
still shows it. --}}
<a href="{{ route('checkout', ['plan' => $plan['key'], 'term' => 'monthly']) }}"
x-bind:href="'{{ route('checkout', ['plan' => $plan['key']]) }}?term=' + term"
@class([
'mt-6 inline-flex w-full items-center justify-center gap-2 rounded px-4 py-2.5 text-sm font-semibold transition-colors',
'bg-ink text-bg hover:bg-accent-text' => $plan['recommended'],
'border border-line-strong bg-surface text-ink hover:bg-surface-hover' => ! $plan['recommended'],
])>
{{ __('order.choose') }}
<x-ui.icon name="chevron-down" class="size-4 -rotate-90" />
</a>
</div>
@endforeach
</div>
{{-- ── The closing band ──────────────────────────────────────────────
Three columns across the same width as the cards. It was one
paragraph wrapped at 80 characters, which left two thirds of the
page empty under a grid that used all of it. --}}
<div class="grid gap-px overflow-hidden rounded-lg border border-line bg-line shadow-xs sm:grid-cols-3 animate-rise [animation-delay:240ms]">
@foreach ([
['icon' => 'refresh', 'title' => __('order.note_cancel_title'), 'body' => __('order.note_cancel_body')],
['icon' => 'lock', 'title' => __('order.note_payment_title'), 'body' => __('order.note_payment_body')],
['icon' => 'box', 'title' => __('order.note_migration_title'), 'body' => __('order.note_migration_body')],
] as $note)
<div class="bg-surface p-6">
<p class="flex items-center gap-2 text-sm font-semibold text-ink">
<x-ui.icon :name="$note['icon']" class="size-4 text-accent-text" />{{ $note['title'] }}
</p>
<p class="mt-2 text-xs leading-relaxed text-muted">{{ $note['body'] }}</p>
</div>
@endforeach
</div>
@endif
</div>