Phase 8
This commit is contained in:
82 files changed
+4903
-287
No files matched your search
@@ -0,0 +1,111 @@
|
||||
import { cache } from "react";
|
||||
import { prisma } from "@/lib/db/prisma";
|
||||
|
||||
/**
|
||||
* The one place every customer/staff/owner-visible surface gets the
|
||||
* business's white-label identity from — see docs/phase-8-summary.md.
|
||||
* "NextChair" is the internal product name only (repo, Docker project,
|
||||
* package name, technical docs/logs); it must never be the fallback a
|
||||
* real person sees. Fallback order, per the brief:
|
||||
* 1. Configured business name + logo
|
||||
* 2. Configured business name + the app's own neutral icon (handled
|
||||
* naturally: leaving `logoUrl`/`appIconUrl`/`faviconUrl` null falls
|
||||
* through to the static /icon.png / /apple-icon.png files Next
|
||||
* already serves via its file-convention icons — no separate
|
||||
* "neutral icon" asset needed)
|
||||
* 3. Neutral wording with no visible product name at all — only
|
||||
* reachable before installation completes, since every other route
|
||||
* is gated behind a completed install (see
|
||||
* src/lib/installation/installation-state.ts).
|
||||
*
|
||||
* Deliberately a separate, lighter query from getPrimaryBusiness() (which
|
||||
* `findFirstOrThrow`s and eager-loads locations/hours/settings/
|
||||
* bookingRule) — this is called from `generateMetadata` and the root
|
||||
* layout on *every* request, including pre-install ones where no
|
||||
* Business row exists yet, so it must never throw.
|
||||
*/
|
||||
|
||||
export interface BrandingInfo {
|
||||
configured: boolean;
|
||||
name: string;
|
||||
shortName: string;
|
||||
description: string | null;
|
||||
logoUrl: string | null;
|
||||
coverImageUrl: string | null;
|
||||
faviconUrl: string | null;
|
||||
appIconUrl: string | null;
|
||||
themeColor: string;
|
||||
backgroundColor: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
}
|
||||
|
||||
const DEFAULT_THEME_COLOR = "#111b15";
|
||||
const DEFAULT_BACKGROUND_COLOR = "#f4f6f4";
|
||||
|
||||
/** Neutral wording only — never the internal product name. */
|
||||
const UNCONFIGURED_BRANDING: BrandingInfo = {
|
||||
configured: false,
|
||||
name: "Book an Appointment",
|
||||
shortName: "Booking",
|
||||
description: null,
|
||||
logoUrl: null,
|
||||
coverImageUrl: null,
|
||||
faviconUrl: null,
|
||||
appIconUrl: null,
|
||||
themeColor: DEFAULT_THEME_COLOR,
|
||||
backgroundColor: DEFAULT_BACKGROUND_COLOR,
|
||||
email: null,
|
||||
phone: null,
|
||||
};
|
||||
|
||||
export const getBranding = cache(async (): Promise<BrandingInfo> => {
|
||||
// Reached from the root layout's generateMetadata, which Next may
|
||||
// invoke while statically prerendering a route at *build* time (e.g.
|
||||
// /_not-found) — no real database is reachable then (see the
|
||||
// Dockerfile's placeholder build-time DATABASE_URL). A transient/
|
||||
// unreachable database is treated exactly like "not installed yet":
|
||||
// neutral fallback wording, never a crashed build or a 500 at runtime.
|
||||
let business;
|
||||
try {
|
||||
business = await prisma.business.findFirst({
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: {
|
||||
name: true,
|
||||
shortName: true,
|
||||
description: true,
|
||||
logoUrl: true,
|
||||
coverImageUrl: true,
|
||||
faviconUrl: true,
|
||||
appIconUrl: true,
|
||||
themeColor: true,
|
||||
backgroundColor: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return UNCONFIGURED_BRANDING;
|
||||
}
|
||||
if (!business) return UNCONFIGURED_BRANDING;
|
||||
|
||||
return {
|
||||
configured: true,
|
||||
name: business.name,
|
||||
shortName: business.shortName?.trim() || business.name,
|
||||
description: business.description,
|
||||
logoUrl: business.logoUrl,
|
||||
coverImageUrl: business.coverImageUrl,
|
||||
faviconUrl: business.faviconUrl,
|
||||
appIconUrl: business.appIconUrl,
|
||||
themeColor: business.themeColor || DEFAULT_THEME_COLOR,
|
||||
backgroundColor: business.backgroundColor || DEFAULT_BACKGROUND_COLOR,
|
||||
email: business.email,
|
||||
phone: business.phone,
|
||||
};
|
||||
});
|
||||
|
||||
/** "Book an Appointment | Business Name" — the shared page-title format used in generateMetadata across the app. */
|
||||
export function brandedTitle(pageTitle: string, branding: BrandingInfo): string {
|
||||
return `${pageTitle} | ${branding.name}`;
|
||||
}
|
||||
Reference in new issue
Block a user