Layout Changes
Small tweaks to the layout
This commit is contained in:
7 files changed
+81
-37
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
import { Scissors } from "lucide-react";
|
|
||||||
import { getBranding } from "@/lib/business/branding";
|
import { getBranding } from "@/lib/business/branding";
|
||||||
|
import { AuthHeader } from "@/features/auth/components/auth-header";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every auth page (sign-in, register, forgot-password, verify) shares
|
* Every auth page (sign-in, register, forgot-password, verify) shares
|
||||||
@@ -24,14 +24,7 @@ export default async function AuthLayout({ children }: { children: React.ReactNo
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-stone p-6">
|
<div className="flex min-h-screen flex-col bg-stone p-6">
|
||||||
<div id="main-content" className="mx-auto flex w-full max-w-md flex-1 flex-col justify-center">
|
<div id="main-content" className="mx-auto flex w-full max-w-md flex-1 flex-col justify-center">
|
||||||
<div className="mb-8 flex h-16 w-16 items-center justify-center rounded-[1.25rem] bg-ink text-3xl text-white shadow-xl shadow-ink/20">
|
<AuthHeader logoUrl={branding.logoUrl} />
|
||||||
{branding.logoUrl ? (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element -- external, business-supplied URL.
|
|
||||||
<img src={branding.logoUrl} alt="" className="h-full w-full rounded-[1.25rem] object-cover" />
|
|
||||||
) : (
|
|
||||||
<Scissors className="h-7 w-7" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default async function RegisterPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="mb-2 text-3xl font-bold tracking-tight">Join {branding.name}</h1>
|
<h1 className="mb-2 text-3xl font-bold tracking-tight">Join {branding.name}</h1>
|
||||||
<p className="mb-10 text-muted">Create an account to track your appointments and loyalty.</p>
|
<p className="mb-5 text-sm text-muted">Create an account to track your appointments & loyalty.</p>
|
||||||
{/* useSearchParams in RegisterForm requires a Suspense boundary. */}
|
{/* useSearchParams in RegisterForm requires a Suspense boundary. */}
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<RegisterForm />
|
<RegisterForm />
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export default async function SignInPage() {
|
|||||||
const branding = await getBranding();
|
const branding = await getBranding();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="mb-2 text-4xl font-bold tracking-tight">Welcome back to {branding.name}</h1>
|
<h1 className="mb-2 text-2xl font-bold tracking-tight">Welcome back to {branding.name}</h1>
|
||||||
<p className="mb-12 leading-relaxed text-muted">
|
<p className="mb-12 text-sm leading-relaxed text-muted">
|
||||||
Please enter your credentials to access your appointments.
|
Please enter your credentials to access your appointments.
|
||||||
</p>
|
</p>
|
||||||
{/* useSearchParams in SignInForm requires a Suspense boundary. */}
|
{/* useSearchParams in SignInForm requires a Suspense boundary. */}
|
||||||
|
|||||||
@@ -143,12 +143,12 @@ export default async function CustomerHomePage() {
|
|||||||
<div>
|
<div>
|
||||||
{/* Hero — an owner-managed gallery (see /owner/settings/gallery), matching the content width rather than bleeding edge-to-edge. */}
|
{/* Hero — an owner-managed gallery (see /owner/settings/gallery), matching the content width rather than bleeding edge-to-edge. */}
|
||||||
{heroGalleryImages.length > 0 && (
|
{heroGalleryImages.length > 0 && (
|
||||||
<div className="mx-auto max-w-2xl px-6 pt-4 bg-white border-line border-b">
|
<div className="mx-auto max-w-2xl pb-1 bg-white border-line border-b">
|
||||||
<ImageGallery images={heroGalleryImages} />
|
<ImageGallery images={heroGalleryImages} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<main className="mx-auto max-w-2xl bg-white px-6 py-6">
|
<main className="mx-auto max-w-2xl bg-white px-6 pt-3 pb-6">
|
||||||
{/* {session?.user && (
|
{/* {session?.user && (
|
||||||
<p className="text-muted mb-1 text-sm">
|
<p className="text-muted mb-1 text-sm">
|
||||||
Good to see you, {session.user.name?.split(" ")[0] ?? "there"}
|
Good to see you, {session.user.name?.split(" ")[0] ?? "there"}
|
||||||
|
|||||||
+28
-19
@@ -33,41 +33,50 @@ export default async function WelcomePage() {
|
|||||||
const [branding, business] = await Promise.all([getBranding(), getPrimaryBusiness()]);
|
const [branding, business] = await Promise.all([getBranding(), getPrimaryBusiness()]);
|
||||||
const guestBookingEnabled = business.bookingRule?.allowGuestBooking ?? false;
|
const guestBookingEnabled = business.bookingRule?.allowGuestBooking ?? false;
|
||||||
|
|
||||||
const hasBackground = !!(branding.welcomeBackgroundDesktopUrl || branding.welcomeBackgroundMobileUrl);
|
// The hero card's photo — the welcome-specific background the owner
|
||||||
|
// uploaded for this exact screen takes priority (it was purpose-built for
|
||||||
|
// it, see BrandingInfo.welcomeBackgroundMobileUrl/DesktopUrl); the
|
||||||
|
// general-purpose cover shot is only a fallback for a business that
|
||||||
|
// configured a cover image but never set a dedicated welcome background.
|
||||||
|
// Desktop and mobile keep their own independent images, same as before.
|
||||||
|
const heroImageMobileUrl = branding.welcomeBackgroundMobileUrl ?? branding.welcomeBackgroundDesktopUrl ?? branding.coverImageUrl;
|
||||||
|
const heroImageDesktopUrl = branding.welcomeBackgroundDesktopUrl ?? branding.welcomeBackgroundMobileUrl ?? branding.coverImageUrl;
|
||||||
|
|
||||||
|
const hasHeroImage = !!(heroImageMobileUrl || heroImageDesktopUrl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="main-content" className="relative flex min-h-screen flex-col justify-end overflow-hidden bg-ink px-8 pb-16 text-white">
|
<div id="main-content" className="relative flex min-h-screen flex-col justify-end overflow-hidden bg-ink px-8 pb-16 text-white">
|
||||||
{/* Desktop/mobile are independent images (not one image cropped two ways) — see BrandingInfo.welcomeBackgroundDesktopUrl/MobileUrl. Falls back to the plain bg-ink above when neither is set. */}
|
{/* Full-bleed background — desktop/mobile are independent images, not one crop stretched two ways. */}
|
||||||
{branding.welcomeBackgroundDesktopUrl && (
|
{heroImageMobileUrl && (
|
||||||
<div
|
|
||||||
className="absolute inset-0 hidden bg-cover bg-center md:block"
|
|
||||||
style={{ backgroundImage: `url(${branding.welcomeBackgroundDesktopUrl})` }}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{branding.welcomeBackgroundMobileUrl && (
|
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 block bg-cover bg-center md:hidden"
|
className="absolute inset-0 block bg-cover bg-center md:hidden"
|
||||||
style={{ backgroundImage: `url(${branding.welcomeBackgroundMobileUrl})` }}
|
style={{ backgroundImage: `url(${heroImageMobileUrl})` }}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasBackground && (
|
{heroImageDesktopUrl && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 hidden bg-cover bg-center md:block"
|
||||||
|
style={{ backgroundImage: `url(${heroImageDesktopUrl})` }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{hasHeroImage && (
|
||||||
<div className="from-ink via-ink/60 absolute inset-0 bg-gradient-to-t to-transparent" aria-hidden="true" />
|
<div className="from-ink via-ink/60 absolute inset-0 bg-gradient-to-t to-transparent" aria-hidden="true" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-1 flex-col justify-end">
|
<div className="relative z-10 mx-auto flex w-full max-w-sm flex-1 flex-col justify-end">
|
||||||
<div className="mb-8 flex h-16 w-16 items-center justify-center rounded-2xl bg-white text-ink">
|
<div className="mx-auto mb-8 flex items-center justify-center overflow-hidden text-ink">
|
||||||
{branding.logoUrl ? (
|
{branding.logoUrl ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element -- external, business-supplied URL; not a build-time-known local asset for next/image.
|
// eslint-disable-next-line @next/next/no-img-element -- external, business-supplied URL; not a build-time-known local asset for next/image.
|
||||||
<img src={branding.logoUrl} alt="" className="h-full w-full rounded-2xl object-cover" />
|
<img src={branding.logoUrl} alt="" className="h-48 w-48 object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<Scissors className="h-7 w-7" aria-hidden="true" />
|
<Scissors className="h-8 w-8" aria-hidden="true" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mb-4 text-5xl font-bold tracking-tight">{branding.name}</h1>
|
<h1 className="mb-4 text-3xl font-bold tracking-tight">{branding.name}</h1>
|
||||||
<p className="mb-12 max-w-xs text-lg text-white/70">
|
<p className="mb-8 max-w-xs text-lg text-white/85">
|
||||||
Book your next visit in seconds — services, staff and loyalty rewards in one place.
|
Find, book & manage your appointments with ease.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function ImageGallery({ images, className }: ImageGalleryProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("w-full", className)}>
|
<div className={cn("w-full", className)}>
|
||||||
<div className="bg-stone relative aspect-[16/9] w-full overflow-hidden rounded-md">
|
<div className="bg-stone relative aspect-[16/9] w-full overflow-hidden">
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element -- local/user-supplied URL, same pattern used app-wide */}
|
{/* eslint-disable-next-line @next/next/no-img-element -- local/user-supplied URL, same pattern used app-wide */}
|
||||||
<img src={active.url} alt={active.caption ?? ""} className="h-full w-full object-cover" />
|
<img src={active.url} alt={active.caption ?? ""} className="h-full w-full object-cover" />
|
||||||
{images.length > 1 && (
|
{images.length > 1 && (
|
||||||
@@ -36,7 +36,7 @@ export function ImageGallery({ images, className }: ImageGalleryProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => goTo(activeIndex - 1)}
|
onClick={() => goTo(activeIndex - 1)}
|
||||||
aria-label="Previous image"
|
aria-label="Previous image"
|
||||||
className="bg-ink/60 hover:bg-ink/80 absolute top-1/2 left-3 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-md text-white"
|
className="bg-ink/60 hover:bg-ink/80 absolute top-1/2 left-3 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-sm text-white"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
|
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -53,7 +53,7 @@ export function ImageGallery({ images, className }: ImageGalleryProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{images.length > 1 && (
|
{images.length > 1 && (
|
||||||
<div className="mt-2 flex gap-2 overflow-x-auto pb-1">
|
<div className="px-1 mt-1 flex gap-1 overflow-x-auto">
|
||||||
{images.map((image, index) => (
|
{images.map((image, index) => (
|
||||||
<button
|
<button
|
||||||
key={image.id}
|
key={image.id}
|
||||||
@@ -62,7 +62,7 @@ export function ImageGallery({ images, className }: ImageGalleryProps) {
|
|||||||
aria-label={`Show image ${index + 1}`}
|
aria-label={`Show image ${index + 1}`}
|
||||||
aria-current={index === activeIndex}
|
aria-current={index === activeIndex}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-14 w-20 shrink-0 overflow-hidden rounded-lg border-2",
|
"h-14 w-20 shrink-0 overflow-hidden border-2",
|
||||||
index === activeIndex ? "border-ink" : "border-transparent opacity-70",
|
index === activeIndex ? "border-ink" : "border-transparent opacity-70",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ArrowLeft, Scissors } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shared (auth) layout's top-left slot — the business logo by default,
|
||||||
|
* or a back button to the welcome screen on Sign In and Register, the two
|
||||||
|
* auth pages reached directly from Welcome's own buttons (so "back" has an
|
||||||
|
* obvious, single destination there). forgot-password/verify are reached
|
||||||
|
* from elsewhere in the auth flow instead, where a logo reads better than
|
||||||
|
* an ambiguous "back". A small Client Component rather than making the
|
||||||
|
* whole layout client-side, since `usePathname()` is the only piece that
|
||||||
|
* needs the browser.
|
||||||
|
*/
|
||||||
|
export function AuthHeader({ logoUrl }: { logoUrl: string | null }) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
if (pathname === "/register" || pathname === "/sign-in") {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
aria-label="Back to welcome"
|
||||||
|
className="mb-8 flex h-12 w-12 items-center justify-center rounded-full border border-line text-ink hover:bg-white"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto mb-8 flex items-center justify-center text-3xl text-white">
|
||||||
|
{logoUrl ? (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element -- external, business-supplied URL.
|
||||||
|
<img src={logoUrl} alt="" className="h-40 w-40 object-cover" />
|
||||||
|
) : (
|
||||||
|
<Scissors className="h-7 w-7" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user