feat(receipts): upload guide page + scanner head-tag fix

Adds /invoices/upload-receipts as the dedicated explainer for the
mobile scanner PWA: install instructions for iOS/Android, direct
deep-link button, and a walkthrough of the scan -> verify -> save
flow. Sidebar entry replaces the old "Scan receipt" tab so the
desktop side picks up the install steps before sending users to
the mobile-only surface.

Scanner layout moves PWA manifest + apple-* meta tags from inline
JSX into Next.js's metadata/viewport exports so the App Router
doesn't try to render a second <head>, fixing a hydration error
that surfaced as two console warnings on the scan page.

Scanner shell gains a centered Port Nimara logo header so the
standalone PWA looks branded when launched from the home screen
without the dashboard chrome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-04 22:55:42 +02:00
parent 77ad10ced1
commit 089f4a67a4
4 changed files with 333 additions and 30 deletions

View File

@@ -0,0 +1,16 @@
import type { Metadata } from 'next';
import { UploadReceiptsGuide } from '@/components/invoices/upload-receipts-guide';
export const metadata: Metadata = {
title: 'How to upload receipts',
};
export default async function UploadReceiptsPage({
params,
}: {
params: Promise<{ portSlug: string }>;
}) {
const { portSlug } = await params;
return <UploadReceiptsGuide portSlug={portSlug} />;
}

View File

@@ -1,20 +1,51 @@
import type { Metadata, Viewport } from 'next';
import { redirect } from 'next/navigation';
import { headers } from 'next/headers';
import { eq } from 'drizzle-orm';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { ports as portsTable } from '@/lib/db/schema/ports';
import { QueryProvider } from '@/providers/query-provider';
import { PortProvider } from '@/providers/port-provider';
import { eq } from 'drizzle-orm';
/**
* Minimal layout for the mobile receipt-scanner PWA. No sidebar, no
* topbar the scanner is its own contained surface. Adds the PWA
* manifest link + theme color so iOS/Android pick up "Add to Home
* Screen". Auth check matches the dashboard layout so unauthorized
* users still bounce to /login.
* topbar - the scanner is its own contained surface. PWA manifest +
* iOS web-app meta tags are emitted via Next.js's metadata/viewport
* exports so React doesn't try to render a second `<head>` mid-tree
* (which throws hydration errors in the App Router). Auth check
* matches the dashboard layout so unauthorized users still bounce.
*/
export async function generateMetadata({
params,
}: {
params: Promise<{ portSlug: string }>;
}): Promise<Metadata> {
const { portSlug } = await params;
return {
manifest: `/${portSlug}/scan/manifest.webmanifest`,
appleWebApp: {
capable: true,
title: 'PN Scanner',
statusBarStyle: 'default',
},
other: {
// Android/Chrome equivalent of the apple-* meta. metadata.appleWebApp
// covers iOS only; this preserves the existing PWA hint for Chrome.
'mobile-web-app-capable': 'yes',
},
};
}
export const viewport: Viewport = {
themeColor: '#3a7bc8',
width: 'device-width',
initialScale: 1,
viewportFit: 'cover',
};
export default async function ScannerLayout({
children,
params,
@@ -33,16 +64,7 @@ export default async function ScannerLayout({
return (
<QueryProvider>
<PortProvider ports={port ? [port] : []} defaultPortId={port?.id ?? null}>
<head>
<link rel="manifest" href={`/${portSlug}/scan/manifest.webmanifest`} />
<meta name="theme-color" content="#3a7bc8" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="PN Scanner" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
</head>
<PortProvider ports={[port]} defaultPortId={port.id}>
<div className="min-h-[100dvh] bg-background">{children}</div>
</PortProvider>
</QueryProvider>