Redesign kiosk welcome screen with branding and clear entry flow.
Deploy Ladill Frontdesk / deploy (push) Successful in 23s

Show Frontdesk icon, company welcome message, and a prominent Tap to begin step before visitor type selection.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-27 21:38:06 +00:00
co-authored by Cursor
parent e7f1cb1e5c
commit 0e28b59b10
2 changed files with 140 additions and 94 deletions
+137 -93
View File
@@ -1,3 +1,7 @@
@php
$fdIcon = 'images/launcher-icons/frontdesk.svg';
$fdLogo = 'images/logo/ladillfrontdesk-logo.svg';
@endphp
<!DOCTYPE html>
<html lang="en">
<head>
@@ -5,113 +9,153 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Visitor Check-in · {{ $organization->name }}</title>
@include('partials.favicon')
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
@keyframes kiosk-tap-pulse {
0%, 100% { transform: scale(1); box-shadow: 0 20px 40px -12px rgb(79 70 229 / 0.45); }
50% { transform: scale(1.02); box-shadow: 0 24px 48px -10px rgb(79 70 229 / 0.55); }
}
.kiosk-tap-btn { animation: kiosk-tap-pulse 2.4s ease-in-out infinite; }
</style>
</head>
<body class="min-h-screen bg-gradient-to-br from-teal-600 to-cyan-700 text-white" x-data="kioskFlow()" x-init="startTimer()">
<div class="mx-auto flex min-h-screen max-w-3xl flex-col p-6">
<header class="mb-8 text-center">
<h1 class="text-3xl font-bold">{{ $organization->name }}</h1>
<p class="mt-2 text-teal-100">Welcome please check in</p>
</header>
<body class="min-h-screen bg-slate-100 font-sans text-slate-900 antialiased" x-data="kioskFlow()" x-init="startTimer()">
<div class="pointer-events-none fixed inset-0 overflow-hidden" aria-hidden="true">
<div class="absolute -left-24 top-0 h-72 w-72 rounded-full bg-indigo-200/40 blur-3xl"></div>
<div class="absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-violet-200/40 blur-3xl"></div>
</div>
<main class="flex-1">
<template x-if="step === 'welcome'">
<div class="text-center">
<button @click="step = 'type'; resetTimer()" class="mx-auto block w-full max-w-md rounded-3xl bg-white px-8 py-16 text-2xl font-bold text-teal-700 shadow-xl transition hover:scale-[1.02]">
Tap to begin
</button>
<div class="relative mx-auto flex min-h-screen max-w-3xl flex-col">
<template x-if="step === 'welcome'">
<div class="flex min-h-screen flex-col items-center justify-center px-6 py-12 text-center">
<div class="fixed inset-x-0 top-0 h-1.5 bg-gradient-to-r from-indigo-600 to-violet-600"></div>
<img src="{{ asset($fdIcon) }}?v={{ @filemtime(public_path($fdIcon)) ?: '1' }}"
alt=""
class="h-24 w-24 drop-shadow-sm">
<img src="{{ asset($fdLogo) }}?v={{ @filemtime(public_path($fdLogo)) ?: '1' }}"
alt="Ladill Frontdesk"
class="mt-5 h-7 w-auto">
<div class="mt-14 max-w-xl">
<p class="text-sm font-semibold uppercase tracking-wider text-indigo-600">Visitor check-in</p>
<h1 class="mt-3 text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl">
Welcome to {{ $organization->name }}
</h1>
</div>
</template>
<template x-if="step === 'type'">
<div class="grid gap-4 sm:grid-cols-2">
@foreach ($visitorTypes as $key => $label)
<button @click="selectType('{{ $key }}')"
class="rounded-2xl bg-white/95 px-6 py-8 text-lg font-semibold text-slate-800 shadow-lg hover:bg-white">
{{ $label }}
</button>
@endforeach
</div>
</template>
<button type="button"
@click="step = 'type'; resetTimer()"
class="kiosk-tap-btn btn-primary btn-primary-lg mt-16 w-full max-w-lg py-8 text-2xl font-bold">
Tap to begin
</button>
</div>
</template>
<template x-if="step === 'details'">
<form @submit.prevent="submitCheckIn" class="space-y-4 rounded-3xl bg-white p-6 text-slate-800 shadow-xl">
<p class="text-sm font-medium text-teal-700" x-text="typeConfigs[form.visitor_type]?.label"></p>
<template x-if="step !== 'welcome'">
<div class="flex min-h-screen flex-col px-6 py-6">
<header class="mb-6 text-center">
<img src="{{ asset($fdIcon) }}?v={{ @filemtime(public_path($fdIcon)) ?: '1' }}"
alt=""
class="mx-auto h-10 w-10">
<p class="mt-3 text-sm font-medium text-slate-500">{{ $organization->name }}</p>
</header>
<input type="text" x-model="form.full_name" required placeholder="Full name" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<input type="text" x-model="form.company" placeholder="Company" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<input type="tel" x-model="form.phone" placeholder="Phone" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<input type="email" x-model="form.email" placeholder="Email" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<select x-model="form.host_id" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<option value="">Who are you visiting?</option>
@foreach ($hosts as $host)
<option value="{{ $host->id }}">{{ $host->name }}</option>
@endforeach
</select>
<input type="text" x-model="form.purpose" placeholder="Purpose of visit" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<div class="rounded-xl border border-slate-200 p-4">
<p class="mb-2 text-sm font-medium text-slate-600">Visitor photo (optional)</p>
<video x-ref="camera" x-show="cameraActive" autoplay playsinline class="mb-2 w-full max-h-48 rounded-lg bg-slate-900 object-cover"></video>
<img x-show="form.photo_data" :src="form.photo_data" alt="Captured photo preview" class="mb-2 max-h-48 rounded-lg">
<div class="flex flex-wrap gap-2">
<button type="button" @click="startCamera()" class="rounded-lg bg-slate-100 px-3 py-2 text-sm">Open camera</button>
<button type="button" @click="capturePhoto()" class="rounded-lg bg-slate-100 px-3 py-2 text-sm">Capture</button>
<button type="button" @click="clearPhoto()" class="rounded-lg bg-slate-100 px-3 py-2 text-sm">Clear</button>
</div>
</div>
<template x-for="field in typeConfigs[form.visitor_type]?.fields || []" :key="field.name">
<main class="flex-1">
<template x-if="step === 'type'">
<div>
<label class="mb-1 block text-sm font-medium text-slate-600" x-text="field.label"></label>
<template x-if="field.type === 'textarea'">
<textarea x-model="form.type_fields[field.name]" :required="field.required" rows="2" class="w-full rounded-xl border-slate-200 px-4 py-3 text-lg"></textarea>
</template>
<template x-if="field.type === 'checkbox'">
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" :checked="form.type_fields[field.name]" @change="form.type_fields[field.name] = $event.target.checked" :required="field.required" class="h-5 w-5 rounded">
Yes
</label>
</template>
<template x-if="field.type === 'date' || field.type === 'datetime-local' || field.type === 'text'">
<input :type="field.type" x-model="form.type_fields[field.name]" :required="field.required" class="w-full rounded-xl border-slate-200 px-4 py-3 text-lg">
</template>
<template x-if="field.type === 'signature'">
<canvas :x-ref="'sig_' + field.name" width="400" height="120" class="w-full rounded-xl border border-slate-200 bg-slate-50"
@mousedown="startSign($event, field.name)" @mousemove="drawSign($event, field.name)" @mouseup="endSign(field.name)"
@touchstart.prevent="startSign($event, field.name)" @touchmove.prevent="drawSign($event, field.name)" @touchend.prevent="endSign(field.name)"></canvas>
<button type="button" @click="clearSign(field.name)" class="mt-1 text-xs text-slate-500">Clear signature</button>
</template>
<h2 class="mb-6 text-center text-2xl font-semibold text-slate-900">What brings you here?</h2>
<div class="grid gap-4 sm:grid-cols-2">
@foreach ($visitorTypes as $key => $label)
<button type="button" @click="selectType('{{ $key }}')"
class="rounded-2xl border border-slate-200 bg-white px-6 py-8 text-lg font-semibold text-slate-800 shadow-sm transition hover:border-indigo-200 hover:shadow-md active:scale-[0.98]">
{{ $label }}
</button>
@endforeach
</div>
</div>
</template>
<template x-if="typeConfigs[form.visitor_type]?.requires_approval">
<p class="rounded-xl bg-amber-50 px-4 py-3 text-sm text-amber-800">Reception will review your visit before issuing a badge.</p>
<template x-if="step === 'details'">
<form @submit.prevent="submitCheckIn" class="space-y-4 rounded-3xl border border-slate-200 bg-white p-6 shadow-sm">
<p class="text-sm font-medium text-indigo-700" x-text="typeConfigs[form.visitor_type]?.label"></p>
<input type="text" x-model="form.full_name" required placeholder="Full name" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<input type="text" x-model="form.company" placeholder="Company" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<input type="tel" x-model="form.phone" placeholder="Phone" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<input type="email" x-model="form.email" placeholder="Email" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<select x-model="form.host_id" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<option value="">Who are you visiting?</option>
@foreach ($hosts as $host)
<option value="{{ $host->id }}">{{ $host->name }}</option>
@endforeach
</select>
<input type="text" x-model="form.purpose" placeholder="Purpose of visit" class="w-full rounded-xl border-slate-200 px-4 py-4 text-lg">
<div class="rounded-xl border border-slate-200 p-4">
<p class="mb-2 text-sm font-medium text-slate-600">Visitor photo (optional)</p>
<video x-ref="camera" x-show="cameraActive" autoplay playsinline class="mb-2 w-full max-h-48 rounded-lg bg-slate-900 object-cover"></video>
<img x-show="form.photo_data" :src="form.photo_data" alt="Captured photo preview" class="mb-2 max-h-48 rounded-lg">
<div class="flex flex-wrap gap-2">
<button type="button" @click="startCamera()" class="rounded-lg bg-slate-100 px-3 py-2 text-sm">Open camera</button>
<button type="button" @click="capturePhoto()" class="rounded-lg bg-slate-100 px-3 py-2 text-sm">Capture</button>
<button type="button" @click="clearPhoto()" class="rounded-lg bg-slate-100 px-3 py-2 text-sm">Clear</button>
</div>
</div>
<template x-for="field in typeConfigs[form.visitor_type]?.fields || []" :key="field.name">
<div>
<label class="mb-1 block text-sm font-medium text-slate-600" x-text="field.label"></label>
<template x-if="field.type === 'textarea'">
<textarea x-model="form.type_fields[field.name]" :required="field.required" rows="2" class="w-full rounded-xl border-slate-200 px-4 py-3 text-lg"></textarea>
</template>
<template x-if="field.type === 'checkbox'">
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" :checked="form.type_fields[field.name]" @change="form.type_fields[field.name] = $event.target.checked" :required="field.required" class="h-5 w-5 rounded">
Yes
</label>
</template>
<template x-if="field.type === 'date' || field.type === 'datetime-local' || field.type === 'text'">
<input :type="field.type" x-model="form.type_fields[field.name]" :required="field.required" class="w-full rounded-xl border-slate-200 px-4 py-3 text-lg">
</template>
<template x-if="field.type === 'signature'">
<canvas :x-ref="'sig_' + field.name" width="400" height="120" class="w-full rounded-xl border border-slate-200 bg-slate-50"
@mousedown="startSign($event, field.name)" @mousemove="drawSign($event, field.name)" @mouseup="endSign(field.name)"
@touchstart.prevent="startSign($event, field.name)" @touchmove.prevent="drawSign($event, field.name)" @touchend.prevent="endSign(field.name)"></canvas>
<button type="button" @click="clearSign(field.name)" class="mt-1 text-xs text-slate-500">Clear signature</button>
</template>
</div>
</template>
<template x-if="typeConfigs[form.visitor_type]?.requires_approval">
<p class="rounded-xl bg-amber-50 px-4 py-3 text-sm text-amber-800">Reception will review your visit before issuing a badge.</p>
</template>
<label class="flex items-center gap-3 text-sm">
<input type="checkbox" x-model="form.policies_accepted" required class="h-5 w-5 rounded">
I have read and accept the visitor policies
</label>
<button type="submit" :disabled="loading" class="btn-primary btn-primary-lg w-full disabled:opacity-50">
<span x-text="loading ? 'Submitting…' : 'Complete check-in'"></span>
</button>
</form>
</template>
<label class="flex items-center gap-3 text-sm">
<input type="checkbox" x-model="form.policies_accepted" required class="h-5 w-5 rounded">
I have read and accept the visitor policies
</label>
<button type="submit" :disabled="loading" class="btn-primary btn-primary-lg w-full disabled:opacity-50">
<span x-text="loading ? 'Submitting…' : 'Complete check-in'"></span>
</button>
</form>
</template>
<template x-if="step === 'done'">
<div class="rounded-3xl bg-white p-8 text-center text-slate-800 shadow-xl">
<div class="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-indigo-100 text-4xl"></div>
<h2 class="text-2xl font-bold" x-text="result?.awaiting_approval ? 'Submitted for approval' : 'You\'re checked in!'"></h2>
<p class="mt-2 text-slate-600" x-text="result?.visitor_name"></p>
<template x-if="!result?.awaiting_approval">
<p class="mt-1 text-sm text-slate-500">Badge code: <span class="font-mono font-bold" x-text="result?.badge_code"></span></p>
<template x-if="step === 'done'">
<div class="rounded-3xl border border-slate-200 bg-white p-8 text-center shadow-sm">
<div class="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-indigo-100 text-4xl"></div>
<h2 class="text-2xl font-bold" x-text="result?.awaiting_approval ? 'Submitted for approval' : 'You\'re checked in!'"></h2>
<p class="mt-2 text-slate-600" x-text="result?.visitor_name"></p>
<template x-if="!result?.awaiting_approval">
<p class="mt-1 text-sm text-slate-500">Badge code: <span class="font-mono font-bold" x-text="result?.badge_code"></span></p>
</template>
<p class="mt-4 text-sm text-slate-500" x-text="result?.awaiting_approval ? 'Please wait at reception.' : 'Your host has been notified.'"></p>
<button type="button" @click="reset()" class="btn-primary btn-primary-lg mt-8">Done</button>
</div>
</template>
<p class="mt-4 text-sm text-slate-500" x-text="result?.awaiting_approval ? 'Please wait at reception.' : 'Your host has been notified.'"></p>
<button @click="reset()" class="btn-primary btn-primary-lg mt-8">Done</button>
</div>
</template>
</main>
</main>
</div>
</template>
</div>
<script>
+3 -1
View File
@@ -86,7 +86,9 @@ class FrontdeskPhase7Test extends TestCase
$this->get(route('frontdesk.kiosk.device', $device->device_token))
->assertOk()
->assertSee('Visitor Check-in');
->assertSee('Visitor Check-in')
->assertSee('Welcome to')
->assertSee('Tap to begin');
$this->assertTrue($device->fresh()->isOnline());
}