WFDASH-WEBAPP/src/app/layout.tsx
Dave 18397bc9be feat(counter): add standalone counter order PWA
This commit adds a full unauthenticated PWA for in-store retail order taking:
- Dedicated `/new-order` route with complete order form, customer/company picker
- Public `/api/counter/*` endpoints for search, order creation, and photo uploads
- Photo capture and gallery selection with 10-photo per order limit
- Offline caching via dedicated service worker and app manifest
- Shortcut redirect at `/counter` for quick installation
- Comprehensive setup and usage documentation in `docs/counter-pwa.md`
- Minor cleanup to root layout metadata and service worker registration
- Small layout fix for collection pool row text truncation

Orders submitted via the PWA appear immediately in the main CRM's New tab, and the app is designed for trusted office kiosk phones without requiring user login.
2026-08-13 09:27:20 +02:00

194 lines
5.7 KiB
TypeScript

import type { Metadata, Viewport } from "next";
import {
Geist,
Geist_Mono,
Inter,
Space_Grotesk,
DM_Sans,
Playfair_Display,
Lato,
Nunito,
Outfit,
JetBrains_Mono,
} from "next/font/google";
import "leaflet/dist/leaflet.css";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { AuthProvider } from "@/components/auth-provider";
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { ServiceWorkerRegistration } from "@/components/service-worker-registration";
import { ThemeBootstrapScript } from "@/components/theme-bootstrap";
// Geist (default app fonts)
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
// Theme-bound fonts (registered as CSS vars for the theme picker to swap).
// Each font is restricted to the two weights actually used in the app
// (400 regular + 700 bold). Without these, next/font/google preloads
// every available weight and the browser logs warnings about unused
// preload tags (and the page wastes bandwidth fetching woff2 files
// that will never render).
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
weight: ["400", "700"],
display: "swap",
});
const spaceGrotesk = Space_Grotesk({
variable: "--font-space-grotesk",
subsets: ["latin"],
weight: ["400", "700"],
display: "swap",
});
const dmSans = DM_Sans({
variable: "--font-dm-sans",
subsets: ["latin"],
weight: ["400", "700"],
display: "swap",
});
const playfair = Playfair_Display({
variable: "--font-playfair",
subsets: ["latin"],
weight: ["400", "700"],
display: "swap",
});
const lato = Lato({
variable: "--font-lato",
subsets: ["latin"],
weight: ["400", "700"],
display: "swap",
});
const nunito = Nunito({
variable: "--font-nunito",
subsets: ["latin"],
weight: ["400", "700"],
display: "swap",
});
const outfit = Outfit({
variable: "--font-outfit",
subsets: ["latin"],
weight: ["400", "700"],
display: "swap",
});
const jetbrainsMono = JetBrains_Mono({
variable: "--font-jetbrains-mono",
subsets: ["latin"],
weight: ["400", "700"],
display: "swap",
});
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
viewportFit: "cover",
themeColor: "#4C96D7",
};
// `metadataBase` is required for OpenGraph / Twitter absolute URL resolution,
// otherwise Next falls back to `http://localhost:4001` in dev and emits a
// warning on every page render. We re-use the public PocketBase URL since
// the app and PB sit on the same host in dev, and in prod the same env var
// points at the deployed origin (`NEXT_PUBLIC_*` is also bundle-visible).
const METADATA_BASE =
process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") ||
(typeof process.env.NEXT_PUBLIC_POCKETBASE_URL === "string"
? process.env.NEXT_PUBLIC_POCKETBASE_URL.replace(/\/+$/, "")
: "http://localhost:4001");
export const metadata: Metadata = {
metadataBase: new URL(METADATA_BASE),
title: "WFDASH - Business CRM",
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
manifest: "/manifest.json",
// Counter PWA manifest — iOS/Android pick the most-specific match
// for the installed app icon + start_url when this is the only
// <link rel="manifest"> they recognise. The main `/manifest.json`
// stays the default for the main app.
icons: {
icon: [
{ url: "/favicon.ico", sizes: "any" },
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
{ url: "/favicon-96x96.png", sizes: "96x96", type: "image/png" },
{ url: "/android-chrome-192x192.png", sizes: "192x192", type: "image/png" },
{ url: "/android-chrome-512x512.png", sizes: "512x512", type: "image/png" },
],
apple: "/apple-touch-icon.png",
},
authors: [{ name: "WFDASH" }],
keywords: ["CRM", "Business", "Quotes", "Orders", "Customers", "Suppliers"],
appleWebApp: {
capable: true,
statusBarStyle: "default",
title: "WFDASH",
},
openGraph: {
type: "website",
title: "WFDASH - Business CRM",
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
siteName: "WFDASH",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "WFDASH - Business CRM",
},
],
},
twitter: {
card: "summary_large_image",
title: "WFDASH - Business CRM",
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
images: ["/twitter-image.png"],
},
};
// All theme-bound CSS vars must be present on <html> so themes can swap them
const FONT_VARS = [
geistSans.variable,
geistMono.variable,
inter.variable,
spaceGrotesk.variable,
dmSans.variable,
playfair.variable,
lato.variable,
nunito.variable,
outfit.variable,
jetbrainsMono.variable,
].join(" ");
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html
lang="en"
className={`${FONT_VARS} h-full antialiased`}
data-theme="wfdash"
suppressHydrationWarning
>
<head>
{/* Apply persisted theme before hydration to prevent flash of wrong theme */}
<ThemeBootstrapScript />
</head>
<body className="min-h-full flex flex-col">
<ServiceWorkerRegistration />
<ThemeProvider>
<TooltipProvider>
<AuthProvider>
{children}
</AuthProvider>
</TooltipProvider>
<Toaster richColors />
</ThemeProvider>
</body>
</html>
);
}