200 lines
12 KiB
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>
|