account pages: login, order history, order details, account details, wishlist

This commit is contained in:
elvira
2026-09-24 17:27:58 +03:00
parent 580adac33a
commit cf3681260b
42 changed files with 1972 additions and 15 deletions
@@ -0,0 +1,65 @@
@extends('layouts.account')
@section('title', __('storefront.auth.enter_code'))
@section('account')
<div class="flex max-w-2xl flex-col gap-12">
<div class="flex flex-col gap-6">
<h1 class="font-display text-h3 font-black tracking-tight sm:text-h2">
{{ __('storefront.auth.enter_code') }}
</h1>
<p>
{{ __('storefront.auth.code_sent_to') }} <strong class="break-all">{{ $email }}</strong>
</p>
<x-ui.status />
</div>
<form method="POST" action="{{ route('account.email.verify') }}" class="flex flex-col gap-8">
@csrf
<x-ui.field
:label="__('storefront.auth.code')"
for="account-email-code"
:required="true"
:error="$errors->first('code')"
>
<x-ui.input
id="account-email-code"
name="code"
inputmode="numeric"
autocomplete="one-time-code"
pattern="[0-9]{6}"
maxlength="6"
:required="true"
autofocus
class="text-2xl font-bold tracking-[0.5em]"
:aria-invalid="$errors->has('code') ? 'true' : null"
:aria-describedby="$errors->has('code') ? 'account-email-code-error' : null"
/>
</x-ui.field>
<div>
<x-ui.button type="submit">{{ __('storefront.account.email_confirm') }}</x-ui.button>
</div>
</form>
<div class="flex flex-wrap items-center gap-x-8 gap-y-4 text-sm">
<form method="POST" action="{{ route('account.email.resend') }}">
@csrf
<button type="submit" class="cursor-pointer underline hover:no-underline">
{{ __('storefront.auth.resend_code') }}
</button>
</form>
<a href="{{ route('account.email.edit') }}" class="underline hover:no-underline">
{{ __('storefront.auth.change_email') }}
</a>
</div>
</div>
@endsection
+53
View File
@@ -0,0 +1,53 @@
@extends('layouts.account')
@section('title', __('storefront.account.email_change_heading'))
@section('account')
<div class="flex max-w-2xl flex-col gap-12">
<div class="flex flex-col gap-6">
<h1 class="font-display text-h3 font-black tracking-tight sm:text-h2">
{{ __('storefront.account.email_change_heading') }}
</h1>
<p>
{{ __('storefront.account.email_current') }} <strong class="break-all">{{ $user->email }}</strong>
</p>
</div>
<form method="POST" action="{{ route('account.email.send') }}" class="flex flex-col gap-8">
@csrf
<x-ui.field
:label="__('storefront.account.email_new')"
for="account-new-email"
:required="true"
:description="__('storefront.account.email_new_hint')"
:error="$errors->first('email')"
>
<x-ui.input
id="account-new-email"
name="email"
type="email"
autocomplete="email"
:value="old('email')"
:required="true"
autofocus
:aria-invalid="$errors->has('email') ? 'true' : null"
:aria-describedby="$errors->has('email') ? 'account-new-email-error' : 'account-new-email-description'"
/>
</x-ui.field>
<div class="flex flex-wrap items-center gap-8">
<x-ui.button type="submit">{{ __('storefront.auth.send_code') }}</x-ui.button>
<a href="{{ route('account') }}" class="text-sm underline hover:no-underline">
{{ __('storefront.account.delete_cancel') }}
</a>
</div>
</form>
</div>
@endsection
@@ -0,0 +1,67 @@
@extends('layouts.account')
@section('title', __('storefront.account.nav_orders'))
@section('account')
<div class="flex max-w-4xl flex-col gap-12">
<h1 class="font-display text-h3 font-black tracking-tight sm:text-h2">
{{ __('storefront.account.nav_orders') }}
</h1>
@if ($orders->isEmpty())
<div class="flex flex-col items-start gap-8">
<p>{{ __('storefront.orders.empty') }}</p>
<x-ui.button :href="route('products')" size="md">{{ __('storefront.orders.shop_now') }}</x-ui.button>
</div>
@else
{{-- A list, not a table, so each order can stack on mobile. The column
headings are visual only; each cell carries its own sr-only label. --}}
<div>
<div class="hidden grid-cols-[1fr_1fr_1.5fr_1fr_auto] gap-6 border-b border-black pb-3 text-sm uppercase font-display font-extrabold sm:grid" aria-hidden="true">
<span>{{ __('storefront.orders.date') }}</span>
<span>{{ __('storefront.orders.number') }}</span>
<span>{{ __('storefront.orders.status') }}</span>
<span>{{ __('storefront.orders.total') }}</span>
<span class="w-24"></span>
</div>
<ul>
@foreach ($orders as $order)
<li class="grid grid-cols-2 gap-x-6 gap-y-2 border-b border-black py-5 sm:grid-cols-[1fr_1fr_1.5fr_1fr_auto] sm:items-center">
<span>
<span class="sr-only">{{ __('storefront.orders.date') }}:</span>
{{ $order->placed_at->format('d/m/Y') }}
</span>
<span class="text-right font-bold sm:text-left">
<span class="sr-only">{{ __('storefront.orders.number') }}:</span>
#{{ $order->reference }}
</span>
<span>
<span class="sr-only">{{ __('storefront.orders.status') }}:</span>
<x-order-status :status="$order->status" />
</span>
<span class="text-right sm:text-left">
<span class="sr-only">{{ __('storefront.orders.total') }}:</span>
{{ $order->total?->formatted() }}
</span>
<a
href="{{ route('account.orders.show', ['orderId' => $order->id]) }}"
class="col-span-2 mt-2 inline-flex w-24 items-center gap-2 font-bold underline hover:no-underline sm:col-span-1 sm:mt-0"
aria-label="{{ __('storefront.orders.view_order', ['number' => $order->reference]) }}"
>
{{ __('storefront.orders.view') }}
<x-ui.icon name="arrow-right" :size="16" />
</a>
</li>
@endforeach
</ul>
</div>
<x-ui.pagination :paginator="$orders" />
@endif
</div>
@endsection
@@ -0,0 +1,151 @@
@extends('layouts.account')
@section('title', __('storefront.orders.order_title', ['number' => $order->reference]))
@php
$productLines = $order->lines->where('type', '!=', 'shipping');
$shippingLine = $order->lines->firstWhere('type', 'shipping');
@endphp
@section('account')
<div class="flex max-w-4xl flex-col gap-12">
<div class="flex flex-col gap-6">
<a href="{{ route('account.orders') }}" class="inline-flex items-center gap-2 self-start text-sm underline hover:no-underline">
<x-ui.icon name="arrow-left" :size="16" />
{{ __('storefront.orders.back') }}
</a>
<h1 class="font-display text-h3 font-black tracking-tight sm:text-h2">
{{ __('storefront.orders.order_title', ['number' => $order->reference]) }}
</h1>
</div>
{{-- Summary --}}
<dl class="grid grid-cols-1 gap-6 border-y border-black py-6 sm:grid-cols-2 lg:grid-cols-4">
<div class="flex flex-col gap-1">
<dt class="text-sm text-neutral-500">{{ __('storefront.orders.date') }}</dt>
<dd>{{ $order->placed_at->format('d/m/Y') }}</dd>
</div>
<div class="flex flex-col gap-1">
<dt class="text-sm text-neutral-500">{{ __('storefront.orders.status') }}</dt>
<dd class="font-bold"><x-order-status :status="$order->status" /></dd>
</div>
@if ($paymentMethodName)
<div class="flex flex-col gap-1">
<dt class="text-sm text-neutral-500">{{ __('storefront.orders.payment') }}</dt>
<dd>{{ $paymentMethodName }}</dd>
</div>
@endif
@if ($shippingLine)
<div class="flex flex-col gap-1">
<dt class="text-sm text-neutral-500">{{ __('storefront.orders.shipping_method') }}</dt>
<dd>{{ $shippingLine->description }}</dd>
</div>
@endif
@foreach ($shipments as $shipment)
<div class="flex flex-col gap-1">
<dt class="text-sm text-neutral-500">{{ __('storefront.orders.tracking') }}</dt>
<dd class="break-all">{{ $shipment->tracking_reference }}</dd>
</div>
@endforeach
</dl>
{{-- Items + totals --}}
<section class="flex flex-col gap-6" aria-labelledby="order-items-heading">
<h2 id="order-items-heading" class="font-display text-h4 font-extrabold">{{ __('storefront.orders.items') }}</h2>
<ul>
@foreach ($productLines as $line)
<li class="flex gap-4 border-b border-black py-5 sm:gap-6">
<div class="size-18 shrink-0 bg-neutral-300 sm:size-24">
@if ($thumb = $line->purchasable?->getThumbnailImage())
<img src="{{ $thumb }}" alt="" aria-hidden="true" width="96" height="96" loading="lazy" class="size-full object-cover">
@endif
</div>
<div class="flex min-w-0 flex-1 flex-col gap-1">
<p class="font-bold">
{{ $line->description }}
<span class="font-normal">&times; {{ $line->quantity }}</span>
</p>
@if ($line->option)
<p class="text-sm text-neutral-500">{{ $line->option }}</p>
@endif
@include('checkout::partials.line-custom-fields', ['line' => $line])
</div>
<p class="shrink-0 font-bold">{{ $line->sub_total?->formatted() }}</p>
</li>
@endforeach
</ul>
<dl class="ml-auto flex w-full max-w-sm flex-col gap-2">
<div class="flex justify-between gap-6">
<dt>{{ __('storefront.orders.subtotal') }}</dt>
<dd>{{ $order->sub_total?->formatted() }}</dd>
</div>
@if ($order->discount_total?->value > 0)
<div class="flex justify-between gap-6">
<dt>{{ __('storefront.orders.discount') }}</dt>
<dd>&minus;{{ $order->discount_total->formatted() }}</dd>
</div>
@endif
<div class="flex justify-between gap-6">
<dt>{{ __('storefront.orders.shipping') }}</dt>
<dd>{{ $order->shipping_total?->formatted() }}</dd>
</div>
@if ($order->tax_total?->value > 0)
<div class="flex justify-between gap-6 text-sm text-neutral-500">
<dt>{{ __('storefront.orders.tax') }}</dt>
<dd>{{ $order->tax_total->formatted() }}</dd>
</div>
@endif
<div class="flex justify-between gap-6 border-t border-black pt-3 font-display text-h4 font-extrabold">
<dt>{{ __('storefront.orders.total') }}</dt>
<dd>{{ $order->total?->formatted() }}</dd>
</div>
</dl>
</section>
{{-- Addresses --}}
<div class="grid grid-cols-1 gap-12 sm:grid-cols-2">
@foreach ([
'shipping_to' => $order->shippingAddress,
'billing' => $order->billingAddress,
] as $heading => $address)
@if ($address)
<section class="flex flex-col gap-4" aria-labelledby="order-{{ $heading }}-heading">
<h2 id="order-{{ $heading }}-heading" class="font-display text-h4 font-extrabold">{{ __("storefront.orders.{$heading}") }}</h2>
<address class="flex flex-col not-italic">
<span>{{ trim($address->first_name.' '.$address->last_name) }}</span>
@if ($address->company_name)<span>{{ $address->company_name }}</span>@endif
@if ($address->tax_identifier)<span>{{ __('storefront.account.tax_identifier') }}: {{ $address->tax_identifier }}</span>@endif
<span>{{ $address->line_one }}</span>
@if ($address->line_two)<span>{{ $address->line_two }}</span>@endif
<span>{{ trim($address->postcode.' '.$address->city) }}</span>
@if ($address->state)
<span>{{ Lang::has("core::states.{$address->state}") ? __("core::states.{$address->state}") : $address->state }}</span>
@endif
@if ($address->contact_phone)<span>{{ $address->contact_phone }}</span>@endif
</address>
</section>
@endif
@endforeach
</div>
</div>
@endsection
+188
View File
@@ -0,0 +1,188 @@
@extends('layouts.account')
@section('title', __('storefront.account.nav_profile'))
@php
$wantsInvoice = (bool) old('invoice', filled($customer?->company_name) || filled($customer?->tax_identifier));
$regionOptions = $regions->map(fn ($region) => [
'value' => $region->name,
'label' => Lang::has("core::states.{$region->name}") ? __("core::states.{$region->name}") : $region->name,
])->all();
@endphp
@section('account')
<div class="flex max-w-2xl flex-col gap-16">
<div class="flex flex-col gap-6">
<h1 class="font-display text-h3 font-black tracking-tight sm:text-h2">
{{ __('storefront.account.nav_profile') }}
</h1>
<x-ui.status />
</div>
{{-- Email: the login itself, so it's changed through its own verified flow --}}
<section class="flex flex-col gap-6" aria-labelledby="account-email-heading">
<h2 id="account-email-heading" class="font-display text-h4 font-extrabold">{{ __('storefront.account.email_heading') }}</h2>
<div class="flex flex-wrap items-center justify-between gap-x-8 gap-y-2 border-b border-black py-2">
<span class="break-all">{{ $user->email }}</span>
<a href="{{ route('account.email.edit') }}" class="text-sm underline hover:no-underline">
{{ __('storefront.account.email_change') }}
</a>
</div>
</section>
<form method="POST" action="{{ route('account.update') }}" class="flex flex-col gap-16">
@csrf
@method('PUT')
{{-- Details --}}
<section class="flex flex-col gap-8" aria-labelledby="account-details-heading">
<h2 id="account-details-heading" class="font-display text-h4 font-extrabold">{{ __('storefront.account.details_heading') }}</h2>
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2">
<x-ui.field :label="__('storefront.account.first_name')" for="account-first-name" :error="$errors->first('first_name')">
<x-ui.input id="account-first-name" name="first_name" autocomplete="given-name"
:value="old('first_name', $customer?->first_name)"
:aria-invalid="$errors->has('first_name') ? 'true' : null" :aria-describedby="$errors->has('first_name') ? 'account-first-name-error' : null" />
</x-ui.field>
<x-ui.field :label="__('storefront.account.last_name')" for="account-last-name" :error="$errors->first('last_name')">
<x-ui.input id="account-last-name" name="last_name" autocomplete="family-name"
:value="old('last_name', $customer?->last_name)"
:aria-invalid="$errors->has('last_name') ? 'true' : null" :aria-describedby="$errors->has('last_name') ? 'account-last-name-error' : null" />
</x-ui.field>
</div>
{{-- Invoice details, revealed by the checkbox (CSS :has(), no JS). Not
`required` in HTML: a hidden required field would block submit, so
the server requires them only when the box is ticked. --}}
<div class="group/invoice flex flex-col gap-8">
<input type="hidden" name="invoice" value="0">
<x-ui.checkbox
id="account-invoice"
name="invoice"
:checked="$wantsInvoice"
:label="__('storefront.account.invoice')"
aria-controls="account-invoice-fields"
/>
<div id="account-invoice-fields" class="hidden grid-cols-1 gap-8 sm:grid-cols-2 group-has-[#account-invoice:checked]/invoice:grid">
<x-ui.field :label="__('storefront.account.company_name')" for="account-company" :error="$errors->first('company_name')">
<x-ui.input id="account-company" name="company_name" autocomplete="organization"
:value="old('company_name', $customer?->company_name)"
:aria-invalid="$errors->has('company_name') ? 'true' : null" :aria-describedby="$errors->has('company_name') ? 'account-company-error' : null" />
</x-ui.field>
<x-ui.field :label="__('storefront.account.tax_identifier')" for="account-tax-id" :error="$errors->first('tax_identifier')">
<x-ui.input id="account-tax-id" name="tax_identifier" inputmode="numeric" maxlength="9" pattern="[0-9]{9}"
:value="old('tax_identifier', $customer?->tax_identifier)"
:aria-invalid="$errors->has('tax_identifier') ? 'true' : null" :aria-describedby="$errors->has('tax_identifier') ? 'account-tax-id-error' : null" />
</x-ui.field>
</div>
</div>
</section>
{{-- Address: one, used as both shipping and billing default --}}
<section class="flex flex-col gap-8" aria-labelledby="account-address-heading">
<h2 id="account-address-heading" class="font-display text-h4 font-extrabold">{{ __('storefront.account.address_heading') }}</h2>
<x-ui.field :label="__('storefront.account.line_one')" for="account-line-one" :error="$errors->first('line_one')">
<x-ui.input id="account-line-one" name="line_one" autocomplete="address-line1"
:value="old('line_one', $address?->line_one)"
:aria-invalid="$errors->has('line_one') ? 'true' : null" :aria-describedby="$errors->has('line_one') ? 'account-line-one-error' : null" />
</x-ui.field>
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2">
<x-ui.field :label="__('storefront.account.city')" for="account-city" :error="$errors->first('city')">
<x-ui.input id="account-city" name="city" autocomplete="address-level2"
:value="old('city', $address?->city)"
:aria-invalid="$errors->has('city') ? 'true' : null" :aria-describedby="$errors->has('city') ? 'account-city-error' : null" />
</x-ui.field>
<x-ui.field :label="__('storefront.account.postcode')" for="account-postcode" :error="$errors->first('postcode')">
<x-ui.input id="account-postcode" name="postcode" autocomplete="postal-code" inputmode="numeric"
:value="old('postcode', $address?->postcode)"
:aria-invalid="$errors->has('postcode') ? 'true' : null" :aria-describedby="$errors->has('postcode') ? 'account-postcode-error' : null" />
</x-ui.field>
</div>
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2">
<x-ui.field :label="__('storefront.account.state')" for="account-state" :error="$errors->first('state')">
<x-ui.select
id="account-state"
name="state"
:block="true"
:options="$regionOptions"
:value="old('state', $address?->state)"
:placeholder="__('storefront.account.state_placeholder')"
:aria-invalid="$errors->has('state') ? 'true' : null"
:aria-describedby="$errors->has('state') ? 'account-state-error' : null"
/>
</x-ui.field>
<x-ui.field :label="__('storefront.account.phone')" for="account-phone" :error="$errors->first('contact_phone')">
<x-ui.input id="account-phone" name="contact_phone" type="tel" autocomplete="tel"
:value="old('contact_phone', $address?->contact_phone)"
:aria-invalid="$errors->has('contact_phone') ? 'true' : null" :aria-describedby="$errors->has('contact_phone') ? 'account-phone-error' : null" />
</x-ui.field>
</div>
</section>
<div>
<x-ui.button type="submit">{{ __('storefront.account.save') }}</x-ui.button>
</div>
</form>
{{-- Delete account --}}
<section class="flex flex-col gap-6 border-t border-black pt-12" aria-labelledby="account-delete-heading">
<h2 id="account-delete-heading" class="font-display text-h4 font-extrabold">{{ __('storefront.account.delete_heading') }}</h2>
<p>{{ __('storefront.account.delete_text') }}</p>
<div>
<x-ui.button
variant="secondary"
size="md"
popovertarget="account-delete-dialog"
aria-haspopup="dialog"
>{{ __('storefront.account.delete') }}</x-ui.button>
</div>
<div
id="account-delete-dialog"
popover
role="dialog"
aria-modal="true"
aria-labelledby="account-delete-dialog-heading"
class="m-auto w-[calc(100%-2rem)] max-w-lg border border-black bg-neutral-200 p-8 backdrop:bg-black/50"
>
<h3 id="account-delete-dialog-heading" class="mb-4 font-display text-h4 font-extrabold">
{{ __('storefront.account.delete_confirm_heading') }}
</h3>
<p class="mb-8">{{ __('storefront.account.delete_confirm_text') }}</p>
<form method="POST" action="{{ route('account.destroy') }}" class="flex flex-wrap gap-4">
@csrf
@method('DELETE')
<x-ui.button type="submit" size="md">{{ __('storefront.account.delete_confirm') }}</x-ui.button>
<x-ui.button
variant="secondary"
size="md"
popovertarget="account-delete-dialog"
popovertargetaction="hide"
>{{ __('storefront.account.delete_cancel') }}</x-ui.button>
</form>
</div>
</section>
</div>
@endsection
@@ -0,0 +1,17 @@
@extends('layouts.account')
@section('title', __('storefront.account.nav_wishlist'))
@section('account')
<div class="flex flex-col gap-12">
<h1 class="font-display text-h3 font-black tracking-tight sm:text-h2">
{{ __('storefront.account.nav_wishlist') }}
</h1>
@include('wishlist.list')
</div>
@endsection