perf+fix: lazy images, API caching, iOS scroll lock, color name wrapping
Performance: - Add loading="lazy" decoding="async" to product card images - Preconnect to Square S3 image CDN and fonts.googleapis.com in layout - Cache-Control headers on catalog (20s), inventory (10s), occasions/categories (5min) Scroll lock: - Update useLockBodyScroll to use position:fixed + scroll-restore for iOS Safari - Apply same fix to CartDrawer's inline scroll lock Color names: - Remove word-break:break-word so single words never split across lines; multi-word names still wrap at spaces Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0d95cf93b3
commit
ec748c75a9
@ -70,11 +70,15 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
|
||||
})
|
||||
}
|
||||
|
||||
const CACHE = 'public, max-age=20, stale-while-revalidate=40'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const { items: rawItems } = await getCatalog()
|
||||
const items = applyOverrides(rawItems)
|
||||
return NextResponse.json({ items, source: 'square' })
|
||||
return NextResponse.json({ items, source: 'square' }, {
|
||||
headers: { 'Cache-Control': CACHE },
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[catalog] error:', err)
|
||||
return NextResponse.json({ items: [], source: 'error' }, { status: 500 })
|
||||
|
||||
@ -2,5 +2,7 @@ import { NextResponse } from 'next/server'
|
||||
import { getCategoryDisplayConfig } from '@/lib/categories-display'
|
||||
|
||||
export function GET() {
|
||||
return NextResponse.json(getCategoryDisplayConfig())
|
||||
return NextResponse.json(getCategoryDisplayConfig(), {
|
||||
headers: { 'Cache-Control': 'public, max-age=300, stale-while-revalidate=600' },
|
||||
})
|
||||
}
|
||||
|
||||
@ -21,7 +21,9 @@ export async function GET() {
|
||||
const counts: Record<string, number> = {}
|
||||
countsMap.forEach((qty, id) => { counts[id] = qty })
|
||||
|
||||
return NextResponse.json({ counts })
|
||||
return NextResponse.json({ counts }, {
|
||||
headers: { 'Cache-Control': 'public, max-age=10, stale-while-revalidate=20' },
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
console.error('[inventory] error:', msg)
|
||||
|
||||
@ -8,5 +8,7 @@ export async function GET() {
|
||||
const payload = active.map(({ key, label, emoji, blurb, squareCategorySlug }) => ({
|
||||
key, label, emoji, blurb, squareCategorySlug,
|
||||
}))
|
||||
return NextResponse.json({ occasions: payload })
|
||||
return NextResponse.json({ occasions: payload }, {
|
||||
headers: { 'Cache-Control': 'public, max-age=300, stale-while-revalidate=600' },
|
||||
})
|
||||
}
|
||||
|
||||
@ -191,7 +191,8 @@
|
||||
color: #334854;
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow-wrap: normal;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.color-family-heading {
|
||||
|
||||
@ -24,7 +24,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
{/* Preconnect to Square product image CDN */}
|
||||
<link rel="preconnect" href="https://items-images-production.s3.us-west-2.amazonaws.com" crossOrigin="anonymous" />
|
||||
{/* Google Fonts — same as beachpartyballoons.com */}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Autour+One&display=swap"
|
||||
|
||||
@ -56,9 +56,22 @@ export default function CartDrawer() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerOpen) return
|
||||
const prev = document.body.style.overflow
|
||||
const scrollY = window.scrollY
|
||||
const prevOverflow = document.body.style.overflow
|
||||
const prevPosition = document.body.style.position
|
||||
const prevTop = document.body.style.top
|
||||
const prevWidth = document.body.style.width
|
||||
document.body.style.overflow = 'hidden'
|
||||
return () => { document.body.style.overflow = prev }
|
||||
document.body.style.position = 'fixed'
|
||||
document.body.style.top = `-${scrollY}px`
|
||||
document.body.style.width = '100%'
|
||||
return () => {
|
||||
document.body.style.overflow = prevOverflow
|
||||
document.body.style.position = prevPosition
|
||||
document.body.style.top = prevTop
|
||||
document.body.style.width = prevWidth
|
||||
window.scrollTo(0, scrollY)
|
||||
}
|
||||
}, [drawerOpen])
|
||||
|
||||
const [editingEntry, setEditingEntry] = useState<CartEntry | null>(null)
|
||||
|
||||
@ -51,7 +51,7 @@ export default function ProductCard({ item }: Props) {
|
||||
<div className="card-image" style={{ position: 'relative' }}>
|
||||
{item.imageUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={item.imageUrl} alt={item.name} />
|
||||
<img src={item.imageUrl} alt={item.name} loading="lazy" decoding="async" />
|
||||
) : (
|
||||
<div className="no-image">🎈</div>
|
||||
)}
|
||||
|
||||
@ -1,10 +1,27 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/** Locks body scroll while the calling component is mounted. */
|
||||
/** Locks body scroll while the calling component is mounted.
|
||||
* Uses position:fixed + scroll-position restore so iOS Safari
|
||||
* rubber-band scrolling is also prevented. */
|
||||
export function useLockBodyScroll() {
|
||||
useEffect(() => {
|
||||
const prev = document.body.style.overflow
|
||||
const scrollY = window.scrollY
|
||||
const prevOverflow = document.body.style.overflow
|
||||
const prevPosition = document.body.style.position
|
||||
const prevTop = document.body.style.top
|
||||
const prevWidth = document.body.style.width
|
||||
|
||||
document.body.style.overflow = 'hidden'
|
||||
return () => { document.body.style.overflow = prev }
|
||||
document.body.style.position = 'fixed'
|
||||
document.body.style.top = `-${scrollY}px`
|
||||
document.body.style.width = '100%'
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = prevOverflow
|
||||
document.body.style.position = prevPosition
|
||||
document.body.style.top = prevTop
|
||||
document.body.style.width = prevWidth
|
||||
window.scrollTo(0, scrollY)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user