PHP Rebuild
CI / ci (push) Canceled after 0s

Initial PHP rebuild of the app
This commit is contained in:
Stefan Lazic committed 2026-09-05 08:21:33 +01:00
1 parent 8a2b211069
commit 2ef3654841
690 files changed
+33944 -12046

No files matched your search

@@ -0,0 +1 @@
<x-layouts.public :business="$business"><main class="mx-auto max-w-2xl px-5 pb-16 pt-8 lg:px-10"><a href="{{ route('customer.appointments') }}" class="text-xs font-bold uppercase tracking-widest text-muted">← Appointments</a><div class="mt-12"><p class="text-[10px] font-bold uppercase tracking-[0.3em] text-accent-foreground">Appointment details</p><h1 class="mt-3 text-4xl font-black tracking-tight">{{ $appointment->startAt->setTimezone($business->timezone)->format('l, j F') }}</h1><p class="mt-3 text-sm text-muted">{{ $appointment->startAt->setTimezone($business->timezone)->format('g:i A') }} · with {{ $appointment->staff->user?->name ?: 'your barber' }}</p></div><x-ui.card class="mt-8 p-6"><p class="text-sm font-bold">{{ $appointment->services->pluck('service.name')->join(', ') }}</p><p class="mt-3 text-sm text-muted">Status: {{ str_replace('_', ' ', ucfirst(strtolower($appointment->status->value))) }}</p><p class="mt-5 border-t border-line pt-5 text-sm font-bold">Total £{{ number_format((float) $appointment->services->sum('priceAtBooking') - (float) ($appointment->discountAmount ?? 0), 2) }}</p></x-ui.card>@if (in_array($appointment->status->value, ['PENDING', 'CONFIRMED']))<form method="POST" action="{{ route('customer.appointments.cancel', $appointment->id) }}" class="mt-6">@csrf<button class="text-xs font-bold uppercase tracking-widest text-danger">Cancel appointment</button></form>@endif</main></x-layouts.public>
@@ -0,0 +1,13 @@
<x-layouts.public :business="$business">
@php($upcoming = $appointments->filter(fn ($appointment) => $appointment->startAt->isFuture() && ! in_array($appointment->status->value, ['CANCELLED', 'COMPLETED']))->values())
@php($past = $appointments->reject(fn ($appointment) => $upcoming->contains('id', $appointment->id))->values())
<div class="mx-auto min-h-screen max-w-md bg-stone pb-28 text-ink">
<header class="sticky top-0 z-40 border-b border-line bg-white px-6 py-6"><h1 class="mb-5 text-2xl font-bold tracking-tight">Appointments</h1><div class="flex gap-2 rounded-full bg-stone p-1"><button id="tab-upcoming" onclick="switchTab('upcoming')" class="flex-1 rounded-full bg-ink py-2.5 text-xs font-bold uppercase tracking-widest text-white">Upcoming</button><button id="tab-past" onclick="switchTab('past')" class="flex-1 rounded-full py-2.5 text-xs font-bold uppercase tracking-widest text-muted">Past</button></div></header>
<main class="mt-6 space-y-4 px-6">
<div id="content-upcoming" class="space-y-4">@forelse ($upcoming as $appointment)<article class="rounded-jumbo border border-line bg-white p-5 shadow-sm"><div class="mb-4 flex items-start justify-between"><span class="rounded-full bg-accent/10 px-3 py-1 text-[10px] font-bold uppercase tracking-widest text-accent">{{ str_replace('_', ' ', strtolower($appointment->status->value)) }}</span><div class="text-right"><p class="text-sm font-bold">{{ $appointment->startAt->setTimezone($business->timezone)->format('D, M j') }}</p><p class="text-xs text-muted">{{ $appointment->startAt->setTimezone($business->timezone)->format('g:i A') }}</p></div></div><div class="mb-4 flex items-center gap-3"><div class="flex h-10 w-10 items-center justify-center rounded-full bg-stone"><i class="fa-solid fa-scissors"></i></div><div><h3 class="text-sm font-bold">{{ $appointment->services->pluck('service.name')->join(', ') }}</h3><p class="text-xs text-muted">with {{ $appointment->staff->user?->name ?: 'your barber' }}</p></div></div><div class="flex gap-2 border-t border-line pt-4"><a href="{{ route('customer.appointments.show', $appointment->id) }}" class="flex-1 rounded-lg bg-stone py-2.5 text-center text-xs font-bold uppercase tracking-widest">Details</a>@if (in_array($appointment->status->value, ['PENDING', 'CONFIRMED']))<form class="flex-1" method="POST" action="{{ route('customer.appointments.cancel', $appointment->id) }}">@csrf<button class="w-full rounded-lg border border-red-200 py-2.5 text-xs font-bold uppercase tracking-widest text-red-500">Cancel</button></form>@endif</div></article>@empty<div class="flex flex-col items-center px-6 py-16 text-center"><div class="mb-5 flex h-16 w-16 items-center justify-center rounded-full border border-line bg-white text-2xl text-muted"><i class="fa-regular fa-calendar"></i></div><h3 class="mb-1 text-base font-bold">No upcoming appointments</h3><p class="mb-6 text-sm text-muted">Book your next visit in a few taps.</p><a href="{{ route('booking.services') }}" class="rounded-full bg-ink px-8 py-3.5 text-xs font-bold uppercase tracking-widest text-white">Book now</a></div>@endforelse</div>
<div id="content-past" class="hidden space-y-4">@forelse ($past as $appointment)<article class="flex rounded-jumbo border border-line bg-white p-4"><div class="flex-1 pr-4"><span class="mb-3 inline-block rounded-full bg-stone px-3 py-1 text-[10px] font-bold uppercase tracking-widest text-muted">{{ str_replace('_', ' ', strtolower($appointment->status->value)) }}</span><h3 class="mb-1 text-sm font-bold">{{ $appointment->services->pluck('service.name')->join(', ') }}</h3><p class="mb-3 text-xs text-muted">{{ $appointment->startAt->setTimezone($business->timezone)->format('D, M j · g:i A') }}</p><a href="{{ route('booking.services') }}" class="inline-block rounded-lg bg-ink px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white">Book again</a></div><div class="flex w-16 flex-col items-center justify-center border-l border-line pl-4 text-center"><span class="text-[10px] uppercase text-muted">{{ $appointment->startAt->setTimezone($business->timezone)->format('M') }}</span><span class="text-2xl font-light">{{ $appointment->startAt->setTimezone($business->timezone)->format('j') }}</span></div></article>@empty<p class="py-16 text-center text-sm text-muted">No past appointments yet.</p>@endforelse</div>
</main>
<nav class="fixed bottom-0 left-0 right-0 z-50 mx-auto flex max-w-md items-center justify-between border-t border-line bg-white px-5 py-2"><a href="{{ route('customer.dashboard') }}" class="flex flex-col items-center gap-1 text-muted"><i class="fa-solid fa-house text-xl"></i><span class="text-[10px] font-bold uppercase tracking-widest">Home</span></a><a href="{{ route('customer.appointments') }}" class="flex flex-col items-center gap-1 text-ink"><i class="fa-solid fa-calendar-check text-xl"></i><span class="text-[10px] font-bold uppercase tracking-widest">Bookings</span></a><a href="{{ route('customer.profile') }}" class="flex flex-col items-center gap-1 text-muted"><i class="fa-solid fa-user text-xl"></i><span class="text-[10px] font-bold uppercase tracking-widest">Settings</span></a></nav>
</div>
<script>function switchTab(tab){const upcoming=document.getElementById('content-upcoming'),past=document.getElementById('content-past'),upBtn=document.getElementById('tab-upcoming'),pastBtn=document.getElementById('tab-past');const isUpcoming=tab==='upcoming';upcoming.classList.toggle('hidden',!isUpcoming);past.classList.toggle('hidden',isUpcoming);upBtn.classList.toggle('bg-ink',isUpcoming);upBtn.classList.toggle('text-white',isUpcoming);upBtn.classList.toggle('text-muted',!isUpcoming);pastBtn.classList.toggle('bg-ink',!isUpcoming);pastBtn.classList.toggle('text-white',!isUpcoming);pastBtn.classList.toggle('text-muted',isUpcoming)}</script>
</x-layouts.public>
@@ -0,0 +1,22 @@
<x-layouts.public :business="$business">
<div class="mx-auto min-h-screen max-w-md bg-stone pb-28 text-ink">
<header class="flex items-center justify-between px-6 pb-4 pt-10">
<div><p class="text-xs font-medium text-muted">{{ now($business->timezone)->format('H') < 12 ? 'Good morning' : (now($business->timezone)->format('H') < 18 ? 'Good afternoon' : 'Good evening') }}</p><h1 class="text-2xl font-bold tracking-tight">{{ $profile?->firstName ?: $user->name }}</h1><span class="sr-only">Good to see you, {{ $profile?->firstName ?: $user->name }}</span></div>
<div class="flex items-center gap-3"><a href="{{ route('customer.notifications') }}" class="relative flex h-11 w-11 items-center justify-center rounded-full border border-line bg-white"><i class="fa-regular fa-bell"></i></a><a href="{{ route('customer.profile') }}" class="flex h-11 w-11 items-center justify-center rounded-full border border-line bg-ink text-white"><i class="fa-solid fa-user"></i></a></div>
</header>
<main class="space-y-6 px-6">
@if ($appointments->first())
@php($next = $appointments->first())
<a href="{{ route('customer.appointments.show', $next->id) }}" class="relative block overflow-hidden rounded-jumbo bg-ink p-6 text-white shadow-xl"><div class="relative z-10"><div class="mb-6 flex items-center justify-between"><span class="text-[10px] font-bold uppercase tracking-widest text-white/50">Your next visit</span><span class="rounded-full bg-accent/20 px-3 py-1 text-[10px] font-bold uppercase tracking-widest text-accent">{{ ucfirst(strtolower($next->status->value)) }}</span></div><div class="mb-6 flex items-center gap-4"><div class="flex h-14 w-14 items-center justify-center rounded-full border-2 border-white/20 bg-white/10"><i class="fa-solid fa-scissors"></i></div><div><h2 class="text-lg font-bold">{{ $next->services->pluck('service.name')->join(', ') }}</h2><p class="text-sm text-white/60">with {{ $next->staff->user?->name ?: 'your barber' }}</p></div></div><div class="flex items-center justify-between border-t border-white/10 pt-5"><div><p class="text-sm font-bold">{{ $next->startAt->setTimezone($business->timezone)->format('D, M j · g:i A') }}</p><p class="mt-0.5 text-[10px] uppercase tracking-widest text-white/50">{{ $business->name }}</p></div><span class="flex items-center gap-1 text-xs font-bold uppercase tracking-widest text-white/60">Details <i class="fa-solid fa-chevron-right text-[10px]"></i></span></div></div><div class="absolute -bottom-10 -right-10 h-40 w-40 rounded-full bg-accent/10 blur-3xl"></div></a>
@endif
<div class="grid grid-cols-2 gap-3"><a href="{{ route('booking.services') }}" class="flex flex-col gap-3 rounded-jumbo border border-line bg-white p-5"><div class="flex h-10 w-10 items-center justify-center rounded-xl bg-stone"><i class="fa-solid fa-rotate-right"></i></div><span class="text-sm font-bold">Book again</span></a><a href="{{ route('booking.services') }}" class="flex flex-col gap-3 rounded-jumbo border border-line bg-white p-5"><div class="flex h-10 w-10 items-center justify-center rounded-xl bg-stone"><i class="fa-solid fa-plus"></i></div><span class="text-sm font-bold">Book new</span></a></div>
@if ($loyaltySummary)
@php($threshold = max(1, $loyaltySummary['threshold']))
<a href="{{ route('customer.loyalty') }}" class="block rounded-jumbo border border-line bg-white p-5"><div class="mb-4 flex items-center justify-between"><div class="flex items-center gap-3"><div class="flex h-10 w-10 items-center justify-center rounded-xl bg-amber-50 text-amber-500"><i class="fa-solid fa-award"></i></div><div><p class="text-sm font-bold">Loyalty card</p><p class="text-[11px] text-muted">{{ max(0, $threshold - $loyaltySummary['currentStamps']) }} more visits until your reward</p></div></div><i class="fa-solid fa-chevron-right text-[10px] text-muted"></i></div><div class="h-2 w-full overflow-hidden rounded-full bg-stone"><div class="h-full rounded-full bg-amber-500" style="width: {{ min(100, ($loyaltySummary['currentStamps'] / $threshold) * 100) }}%"></div></div></a>
@endif
<section><div class="mb-4 flex items-center justify-between px-1"><h3 class="text-[10px] font-bold uppercase tracking-widest text-muted">Your barbers</h3><a href="{{ route('public.home') }}#details" class="border-b border-ink text-[10px] font-bold uppercase tracking-widest">See all</a></div><div class="flex gap-5 overflow-x-auto no-scrollbar">@forelse ($business->staff->where('status.value', 'ACTIVE')->take(4) as $member)<div class="w-20 shrink-0 text-center"><div class="mb-2 flex h-20 w-20 items-center justify-center rounded-full border border-line bg-white text-xl"><i class="fa-solid fa-user"></i></div><p class="text-xs font-medium">{{ Str::before($member->user?->name ?: 'Barber', ' ') }}</p></div>@empty<p class="text-sm text-muted">Your shop team will appear here.</p>@endforelse</div></section>
<section><div class="mb-4 flex items-center justify-between px-1"><h3 class="text-[10px] font-bold uppercase tracking-widest text-muted">Popular services</h3><a href="{{ route('public.home') }}#services" class="border-b border-ink text-[10px] font-bold uppercase tracking-widest">See all</a></div><div class="divide-y divide-line overflow-hidden rounded-jumbo border border-line bg-white">@foreach ($business->serviceCategories->flatMap->services->where('isActive', true)->take(3) as $service)<div class="flex items-center justify-between px-5 py-4"><div><p class="text-sm font-bold">{{ $service->name }}</p><p class="mt-0.5 text-xs text-muted">{{ $service->durationMinutes }}m</p></div><span class="text-sm font-bold">£{{ number_format((float) $service->price, 2) }}</span></div>@endforeach</div></section>
</main>
<nav class="fixed bottom-0 left-0 right-0 z-50 mx-auto flex max-w-md items-center justify-between border-t border-line bg-white px-5 py-2"><a href="{{ route('customer.dashboard') }}" class="flex flex-col items-center gap-1 text-ink"><i class="fa-solid fa-house text-xl"></i><span class="text-[10px] font-bold uppercase tracking-widest">Home</span></a><a href="{{ route('customer.appointments') }}" class="flex flex-col items-center gap-1 text-muted"><i class="fa-solid fa-calendar-check text-xl"></i><span class="text-[10px] font-bold uppercase tracking-widest">Bookings</span></a><a href="{{ route('customer.profile') }}" class="flex flex-col items-center gap-1 text-muted"><i class="fa-solid fa-user text-xl"></i><span class="text-[10px] font-bold uppercase tracking-widest">Settings</span></a></nav>
</div>
</x-layouts.public>
@@ -0,0 +1,13 @@
<x-layouts.public :business="$business">
<div class="mx-auto min-h-screen max-w-md bg-stone pb-28 text-ink">
<header class="flex items-center justify-between px-6 pb-4 pt-10"><a href="{{ route('customer.dashboard') }}" class="flex h-11 w-11 items-center justify-center rounded-full border border-line bg-white"><i class="fa-solid fa-arrow-left text-sm"></i></a><h1 class="text-xl font-bold tracking-tight">Loyalty Rewards</h1><div class="w-11"></div></header>
<main class="px-5 pt-2">
@if (! $summary)<div class="rounded-jumbo border border-line bg-white p-6 text-sm text-muted">{{ $business->shortName ?: $business->name }} hasn’t enabled a loyalty programme yet.</div>@else
@php($threshold = max(1, $summary['threshold']))
<div class="relative mb-10 aspect-[1.65/1] overflow-hidden rounded-2xl border-[3px] border-amber-500 bg-[#141414] p-4 text-white shadow-2xl"><div class="flex items-center justify-between"><h2 class="text-xl font-black italic tracking-tight">{{ $summary['programme']->cardTitleText ?: 'Loyalty Card' }}</h2><span class="rounded border border-amber-500/30 px-2 py-0.5 text-[8px] font-bold uppercase tracking-widest text-amber-400">Member</span></div><div class="mt-2 h-0.5 w-10 bg-amber-500"></div><div class="mt-4 grid grid-cols-5 gap-2">@for ($i = 1; $i <= $threshold; $i++)<div class="flex aspect-square items-center justify-center rounded-full {{ $i <= $summary['currentStamps'] ? 'bg-white text-ink' : 'border-2 border-dashed border-white/20 bg-[#2a2a2a]' }}">@if ($i <= $summary['currentStamps'])<i class="fa-solid fa-mustache text-xs"></i>@endif</div>@endfor</div><p class="absolute bottom-3 left-0 right-0 text-center text-[7px] font-bold uppercase tracking-[0.18em] text-white/40">{{ $summary['programme']->termsLine ?: 'Earn stamps with every eligible visit' }}</p></div>
<div class="space-y-5"><div class="rounded-3xl border border-line bg-white p-6 text-center shadow-sm"><h3 class="mb-1 text-lg font-bold">Almost there{{ $customer->firstName ? ', '.$customer->firstName : '' }}!</h3><p class="text-sm text-muted">Complete {{ max(0, $threshold - $summary['currentStamps']) }} more visits to unlock your reward.</p><div class="mt-5 h-2 w-full overflow-hidden rounded-full bg-stone"><div class="h-full rounded-full bg-amber-500" style="width: {{ min(100, ($summary['currentStamps'] / $threshold) * 100) }}%"></div></div><p class="mt-2 text-xs text-muted">{{ $summary['currentStamps'] }} of {{ $threshold }} visits completed</p></div><div class="rounded-3xl border border-line bg-white p-6 shadow-sm"><h4 class="mb-5 text-xs font-bold uppercase tracking-widest text-muted">How it works</h4><div class="space-y-4">@foreach (['Book your appointment through the shop.', 'Show this digital card to your barber after your visit.', 'Your barber adds a stamp to your card instantly.'] as $index => $step)<div class="flex items-start gap-4"><div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-stone text-xs font-bold">{{ $index + 1 }}</div><p class="pt-1 text-xs leading-relaxed text-muted">{{ $step }}</p></div>@endforeach</div></div></div>
@endif
</main>
<nav class="fixed bottom-0 left-0 right-0 z-50 mx-auto flex max-w-md items-center justify-between border-t border-line bg-white px-5 py-2"><a href="{{ route('customer.dashboard') }}" class="flex flex-col items-center gap-1 text-muted"><i class="fa-solid fa-house text-xl"></i><span class="text-[10px] font-bold uppercase tracking-widest">Home</span></a><a href="{{ route('customer.appointments') }}" class="flex flex-col items-center gap-1 text-muted"><i class="fa-solid fa-calendar-check text-xl"></i><span class="text-[10px] font-bold uppercase tracking-widest">Bookings</span></a><a href="{{ route('customer.loyalty') }}" class="flex flex-col items-center gap-1 text-ink"><i class="fa-solid fa-user text-xl"></i><span class="text-[10px] font-bold uppercase tracking-widest">Settings</span></a></nav>
</div>
</x-layouts.public>
@@ -0,0 +1,4 @@
<x-layouts.public :business="$business">
<header class="mx-auto flex max-w-6xl items-center justify-between px-5 py-5 lg:px-10"><a href="{{ route('customer.dashboard') }}" class="text-xs font-bold uppercase tracking-widest text-muted">← Account</a><a href="{{ route('customer.loyalty') }}" class="text-xs font-bold uppercase tracking-widest text-muted">Loyalty</a></header>
<main class="mx-auto max-w-3xl px-5 pb-16 pt-10 lg:px-10 lg:pt-16"><p class="text-[10px] font-bold uppercase tracking-[0.3em] text-accent-foreground">Updates</p><h1 class="mt-3 text-4xl font-black tracking-tight">Notifications</h1><div class="mt-8 space-y-3">@forelse ($notifications as $notification)<x-ui.card class="p-5 {{ $notification->readAt ? 'opacity-60' : '' }}"><div class="flex items-start justify-between gap-5"><div><p class="text-sm font-bold">{{ $notification->subject ?: 'Update from '.$business->shortName }}</p><p class="mt-2 text-sm leading-6 text-muted">{{ $notification->bodyText }}</p><p class="mt-3 text-xs text-muted">{{ $notification->createdAt->diffForHumans() }}</p></div>@if (! $notification->readAt)<form method="POST" action="{{ route('customer.notifications.read', $notification->id) }}">@csrf<button class="text-[10px] font-bold uppercase tracking-widest text-accent-foreground">Mark read</button></form>@endif</div></x-ui.card>@empty<x-ui.card class="p-6"><p class="text-sm text-muted">You’re all caught up.</p></x-ui.card>@endforelse</div></main>
</x-layouts.public>
@@ -0,0 +1 @@
<x-layouts.public :business="$business"><main class="mx-auto max-w-2xl px-5 pb-16 pt-8 lg:px-10"><a href="{{ route('customer.dashboard') }}" class="text-xs font-bold uppercase tracking-widest text-muted">← Account</a><p class="mt-12 text-[10px] font-bold uppercase tracking-[0.3em] text-accent-foreground">Your details</p><h1 class="mt-3 text-4xl font-black tracking-tight">Profile</h1>@if (session('status'))<div class="mt-6 rounded-tile bg-accent p-4 text-sm font-bold text-ink">{{ session('status') }}</div>@endif<form method="POST" action="{{ route('customer.profile.update') }}" class="mt-8 space-y-5 rounded-jumbo bg-white p-6">@csrf @method('PUT')<div class="grid gap-5 sm:grid-cols-2"><label class="text-sm font-bold">First name<input name="firstName" value="{{ $profile->firstName }}" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label><label class="text-sm font-bold">Last name<input name="lastName" value="{{ $profile->lastName }}" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label></div><label class="block text-sm font-bold">Email address<input value="{{ $profile->email }}" disabled class="mt-2 w-full rounded-tile border border-line bg-stone px-4 py-3 text-muted"></label><label class="block text-sm font-bold">Phone<input name="phone" value="{{ $profile->phone }}" class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label><x-ui.button type="submit">Save profile</x-ui.button></form></main></x-layouts.public>