feat(mobile): set data-form-factor body attr from User-Agent in root layout
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import type { Metadata, Viewport } from 'next';
|
import type { Metadata, Viewport } from 'next';
|
||||||
|
import { headers } from 'next/headers';
|
||||||
import { Inter, JetBrains_Mono } from 'next/font/google';
|
import { Inter, JetBrains_Mono } from 'next/font/google';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
|
import { classifyFormFactor } from '@/lib/form-factor';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
@@ -42,10 +44,16 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const headerList = await headers();
|
||||||
|
const formFactor = classifyFormFactor(headerList.get('user-agent'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased`}>
|
<body
|
||||||
|
data-form-factor={formFactor}
|
||||||
|
className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
<Toaster richColors position="top-right" />
|
<Toaster richColors position="top-right" />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
14
src/lib/form-factor.ts
Normal file
14
src/lib/form-factor.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export type FormFactor = 'mobile' | 'desktop';
|
||||||
|
|
||||||
|
const MOBILE_TOKENS = ['Mobile', 'iPhone', 'iPad', 'Android'] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify a User-Agent string as 'mobile' or 'desktop'.
|
||||||
|
* Defaults to 'desktop' when the UA is missing or unrecognized — the CSS
|
||||||
|
* media-query fallback in globals.css handles desktop browsers resized below
|
||||||
|
* the lg breakpoint, so a wrong-but-defaultish classification never breaks UX.
|
||||||
|
*/
|
||||||
|
export function classifyFormFactor(userAgent: string | null | undefined): FormFactor {
|
||||||
|
if (!userAgent) return 'desktop';
|
||||||
|
return MOBILE_TOKENS.some((token) => userAgent.includes(token)) ? 'mobile' : 'desktop';
|
||||||
|
}
|
||||||
50
tests/unit/lib/form-factor.test.ts
Normal file
50
tests/unit/lib/form-factor.test.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { classifyFormFactor } from '@/lib/form-factor';
|
||||||
|
|
||||||
|
describe('classifyFormFactor', () => {
|
||||||
|
it('returns "mobile" for an iPhone UA', () => {
|
||||||
|
expect(
|
||||||
|
classifyFormFactor(
|
||||||
|
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148',
|
||||||
|
),
|
||||||
|
).toBe('mobile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "mobile" for an iPad UA', () => {
|
||||||
|
expect(
|
||||||
|
classifyFormFactor(
|
||||||
|
'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148',
|
||||||
|
),
|
||||||
|
).toBe('mobile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "mobile" for an Android UA', () => {
|
||||||
|
expect(
|
||||||
|
classifyFormFactor(
|
||||||
|
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Mobile Safari/537.36',
|
||||||
|
),
|
||||||
|
).toBe('mobile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "desktop" for a Mac Safari UA', () => {
|
||||||
|
expect(
|
||||||
|
classifyFormFactor(
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 Version/17.0 Safari/605.1.15',
|
||||||
|
),
|
||||||
|
).toBe('desktop');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "desktop" for a Linux Chrome UA', () => {
|
||||||
|
expect(
|
||||||
|
classifyFormFactor(
|
||||||
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36',
|
||||||
|
),
|
||||||
|
).toBe('desktop');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "desktop" for missing UA', () => {
|
||||||
|
expect(classifyFormFactor(null)).toBe('desktop');
|
||||||
|
expect(classifyFormFactor(undefined)).toBe('desktop');
|
||||||
|
expect(classifyFormFactor('')).toBe('desktop');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user