- Button: add min-h-11 (44px) for mobile touch targets - NavBar: add safe-area-inset-top for notched phones - NavBar mobile menu: increase link padding from py-2 to py-3 - Footer: increase text size from text-xs to text-sm (16px minimum) - Footer: add px-4 for better mobile spacing - DatePicker: increase calendar cell size on mobile to 44px - Checkbox: add p-3 -m-3 on mobile for 44px touch target (desktop unchanged)
204 lines
6.6 KiB
Svelte
204 lines
6.6 KiB
Svelte
<script lang="ts">
|
|
import '../app.css';
|
|
import favicon from '$lib/assets/favicon.svg';
|
|
import NavBar from '$lib/components/layout/NavBar.svelte';
|
|
import { Toaster } from '$lib/components/ui/sonner/index.js';
|
|
import { toast } from 'svelte-sonner';
|
|
import { authStore } from '$lib/stores/auth.svelte';
|
|
import { resendEmailVerification, verifyEmailCode } from '$lib/square/square';
|
|
import { extractErrorMessage } from '$lib/utils/toast-safe';
|
|
import { resetZIndexStack } from '$lib/components/ui/dialog/zindex.js';
|
|
import { onMount } from 'svelte';
|
|
import { page } from '$app/stores';
|
|
import { resolve } from '$app/paths';
|
|
|
|
const { children } = $props();
|
|
|
|
let hideFooter = $derived(
|
|
$page.url.pathname === '/admin/schedule' ||
|
|
$page.url.pathname === '/account' ||
|
|
$page.url.searchParams.get('format') === 'pdf'
|
|
);
|
|
|
|
// Default to 'top-center' (mobile-first approach)
|
|
let toasterPosition = $state<'top-center' | 'bottom-center'>('top-center');
|
|
|
|
function updateToasterPosition() {
|
|
// Use 640px (Tailwind's 'sm' breakpoint) to differentiate desktop and mobile
|
|
if (typeof window !== 'undefined' && window.innerWidth >= 768) {
|
|
toasterPosition = 'bottom-center';
|
|
} else {
|
|
toasterPosition = 'top-center';
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
// Start the dialog z-index stack fresh per full page load so hot reloads
|
|
// / repeated runs don't let the counter climb forever (see zindex.ts).
|
|
resetZIndexStack();
|
|
updateToasterPosition();
|
|
window.addEventListener('resize', updateToasterPosition);
|
|
return () => {
|
|
window.removeEventListener('resize', updateToasterPosition);
|
|
};
|
|
});
|
|
|
|
// Re-send the email verification code via POST /api/verify/generate (the
|
|
// backend only exposes generate/check — there is no /api/verify-email).
|
|
async function verify_email() {
|
|
const loadingToast = toast.loading('Sending verification code...');
|
|
if (!authStore.currentUser?.email) {
|
|
toast.error('Unable to resend the verification code — no email on file', {
|
|
id: loadingToast
|
|
});
|
|
return;
|
|
}
|
|
try {
|
|
const response = await resendEmailVerification(authStore.currentUser.email);
|
|
|
|
if (response.ok) {
|
|
toast.success('If the email exists, a verification code has been sent', {
|
|
id: loadingToast
|
|
});
|
|
window.location.reload();
|
|
} else {
|
|
toast.error('Failed to send the verification code', { id: loadingToast });
|
|
}
|
|
} catch {
|
|
toast.error('Network error sending the verification code — please try again', {
|
|
id: loadingToast
|
|
});
|
|
}
|
|
}
|
|
|
|
// The banner has no submit surface on its own — the code lands out-of-band
|
|
// (email/SMS, or the dev [VERIFY] server log), so this inline input submits
|
|
// the received code to POST /api/verify/check. A success escalates the
|
|
// account to verified_email; the reload clears the banner.
|
|
let verificationCode = $state('');
|
|
let isVerifyingCode = $state(false);
|
|
|
|
async function submitVerificationCode() {
|
|
if (isVerifyingCode) return;
|
|
if (!authStore.currentUser?.email) {
|
|
toast.error('Unable to verify — no email on file');
|
|
return;
|
|
}
|
|
const code = verificationCode.trim();
|
|
if (!code) {
|
|
toast.error('Please enter the verification code');
|
|
return;
|
|
}
|
|
isVerifyingCode = true;
|
|
const loadingToast = toast.loading('Verifying code...');
|
|
try {
|
|
const response = await verifyEmailCode(authStore.currentUser.email, code);
|
|
if (response.ok) {
|
|
const data = (await response.json().catch(() => null)) as {
|
|
message?: unknown;
|
|
} | null;
|
|
toast.success(
|
|
typeof data?.message === 'string' ? data.message : 'Email verified successfully',
|
|
{ id: loadingToast }
|
|
);
|
|
window.location.reload();
|
|
} else {
|
|
const errData = await response.text().catch(() => '');
|
|
toast.error(
|
|
extractErrorMessage(errData) ||
|
|
'Verification failed — please check the code and try again',
|
|
{ id: loadingToast }
|
|
);
|
|
}
|
|
} catch {
|
|
toast.error('Network error verifying your code — please try again', { id: loadingToast });
|
|
} finally {
|
|
isVerifyingCode = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<link rel="icon" href={favicon} />
|
|
<link rel="stylesheet" href="/fonts/playfair-display.css" />
|
|
</svelte:head>
|
|
|
|
<div class="flex min-h-screen flex-col supports-[height:100dvh]:min-h-dvh">
|
|
<NavBar />
|
|
<Toaster position={toasterPosition} />
|
|
|
|
<main class="flex-1 pt-16">
|
|
{#if authStore.currentUser?.role === 'unverified_email'}
|
|
<div class="w-full bg-red-600 py-2 pr-8 pl-8 text-center text-sm font-medium text-white">
|
|
<p>
|
|
Please verify your email address to continue. Didn't recieve the email? Check your spam
|
|
folder, or
|
|
<button
|
|
type="button"
|
|
onclick={verify_email}
|
|
class="cursor-pointer border-0 bg-transparent p-0 text-white underline"
|
|
>
|
|
click here
|
|
</button>
|
|
to resend it.
|
|
</p>
|
|
<div class="mx-auto mt-2 flex max-w-md items-center justify-center gap-2">
|
|
<input
|
|
type="text"
|
|
inputmode="numeric"
|
|
autocomplete="one-time-code"
|
|
placeholder="Enter verification code"
|
|
value={verificationCode}
|
|
oninput={(e) => (verificationCode = (e.target as HTMLInputElement).value)}
|
|
class="w-44 rounded-md border border-white/50 bg-white px-3 py-1.5 text-sm text-gray-900 placeholder:text-gray-400 focus:border-white focus:outline-none"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onclick={submitVerificationCode}
|
|
disabled={isVerifyingCode}
|
|
class="cursor-pointer rounded-md bg-white px-4 py-1.5 text-sm font-semibold text-red-700 transition-colors hover:bg-red-50 disabled:opacity-50"
|
|
>
|
|
Verify
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{@render children?.()}
|
|
</main>
|
|
|
|
{#if !hideFooter}
|
|
<footer class="border-t py-4 text-center text-sm text-gray-500 md:py-6">
|
|
<div class="mx-auto flex max-w-3xl flex-wrap items-center justify-center gap-x-4 gap-y-2 px-4">
|
|
<a href={resolve('/privacy-policy')} class="hover:text-gray-700 hover:underline"
|
|
>Privacy Policy</a
|
|
>
|
|
<a href={resolve('/terms')} class="hover:text-gray-700 hover:underline"
|
|
>Terms & Conditions</a
|
|
>
|
|
<a href={resolve('/cancellation-policy')} class="hover:text-gray-700 hover:underline"
|
|
>Booking, Deposit & Cancellation Policy</a
|
|
>
|
|
<a href={resolve('/gift-card-terms')} class="hover:text-gray-700 hover:underline"
|
|
>Gift Card Terms</a
|
|
>
|
|
<a href={resolve('/gdpr')} class="hover:text-gray-700 hover:underline">Your Data</a>
|
|
</div>
|
|
<div class="mt-2">© {new Date().getFullYear()} Crussell Nails. All rights reserved.</div>
|
|
</footer>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
@media print {
|
|
:global(nav) {
|
|
display: none !important;
|
|
}
|
|
:global(main > div.bg-red-600) {
|
|
display: none !important;
|
|
}
|
|
:global(main) {
|
|
padding-top: 0 !important;
|
|
}
|
|
}
|
|
</style>
|