fix/feat: hex conflict, scroll-to-top, search all, admin error emails

- Fix Chrome Rose Gold hex (#B76E79 → #C17F87) so it no longer
  conflicts with Classic Rose Gold; image still used for display
- ScrollToTop hides when cart drawer is open and uses z-index 98
  (below the drawer); uses drawerOpen from CartContext
- Search now switches to All tab automatically so results span every
  item, not just the active category
- Add sendAdminErrorAlert() to notify.ts; checkout route emails
  admin@beachpartyballoons.com on unexpected server errors and on
  critical calendar-write failures; card decline errors are not
  forwarded (customers can self-resolve those)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chris 2026-04-17 14:19:29 -04:00
parent 01c908e919
commit 6705293e50
5 changed files with 50 additions and 6 deletions

View File

@ -331,6 +331,16 @@ export async function POST(req: NextRequest) {
console.error('[checkout] CRITICAL: calendar write failed — voiding pre-auth to avoid charge without booking:', {
orderId: order.id, paymentId: payment.id, error: calendarWriteError,
})
void (async () => {
try {
const { sendAdminErrorAlert } = await import('@/lib/notify')
await sendAdminErrorAlert({
subject: 'Calendar write failed — order not booked',
message: `Calendar write failed for order ${order.id}. Pre-auth ${payment.id} is being voided. Customer: ${customerName} (${customerEmail}).`,
context: { orderId: order.id, paymentId: payment.id, error: String(calendarWriteError) },
})
} catch { /* best effort */ }
})()
try {
await cancelSquarePayment(payment.id!)
console.log('[checkout] Pre-auth voided successfully:', payment.id)
@ -447,6 +457,20 @@ export async function POST(req: NextRequest) {
const userMessage = CARD_MESSAGES[code]
?? 'Something went wrong with your payment. Please try again or contact us for help.'
// Email admin for unexpected server errors (not card declines the customer can self-resolve)
if (!CARD_MESSAGES[code]) {
void (async () => {
try {
const { sendAdminErrorAlert } = await import('@/lib/notify')
await sendAdminErrorAlert({
subject: 'Checkout error',
message: err instanceof Error ? err.message : String(err),
context: { code: code || '(none)', customerEmail, customerName },
})
} catch { /* best effort */ }
})()
}
return NextResponse.json({ error: userMessage }, { status: 500 })
}
}

View File

@ -189,7 +189,7 @@ export default function FeaturedProducts() {
placeholder="Search…"
value={search}
autoFocus
onChange={(e) => setSearch(e.target.value)}
onChange={(e) => { setSearch(e.target.value); if (e.target.value) setCategory('all') }}
onBlur={() => { if (!search) setSearchOpen(false) }}
onKeyDown={(e) => { if (e.key === 'Escape') { setSearch(''); setSearchOpen(false) } }}
style={{ width: '160px' }}

View File

@ -1,20 +1,22 @@
'use client'
import { useEffect, useState } from 'react'
import { useCart } from '@/context/CartContext'
export default function ScrollToTop() {
const [visible, setVisible] = useState(false)
const { drawerOpen } = useCart()
const [scrolled, setScrolled] = useState(false)
useEffect(() => {
const onScroll = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
setVisible(scrollTop > 130)
setScrolled(scrollTop > 130)
}
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
if (!visible) return null
if (!scrolled || drawerOpen) return null
return (
<button
@ -24,7 +26,7 @@ export default function ScrollToTop() {
position: 'fixed',
bottom: '12px',
right: '10px',
zIndex: 99,
zIndex: 98,
border: '1px solid #363636',
outline: 'none',
background: '#94d601',

View File

@ -368,3 +368,21 @@ export async function sendNewOrderAlert(params: {
text: lines.join('\n'),
})
}
export async function sendAdminErrorAlert(params: {
subject: string
message: string
context?: Record<string, unknown>
}): Promise<void> {
const to = 'admin@beachpartyballoons.com'
const lines = [
params.message,
'',
...(params.context
? Object.entries(params.context).map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : String(v)}`)
: []),
'',
`Time: ${new Date().toISOString()}`,
]
await send({ to, subject: `⚠️ ${params.subject}`, text: lines.join('\n') })
}

View File

@ -181,7 +181,7 @@
"colors": [
{
"name": "Chrome Rose Gold",
"hex": "#B76E79",
"hex": "#C17F87",
"metallic": true,
"chromeType": "rosegold",
"image": "images/chrome-rosegold.webp"