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:
parent
01c908e919
commit
6705293e50
@ -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:', {
|
console.error('[checkout] CRITICAL: calendar write failed — voiding pre-auth to avoid charge without booking:', {
|
||||||
orderId: order.id, paymentId: payment.id, error: calendarWriteError,
|
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 {
|
try {
|
||||||
await cancelSquarePayment(payment.id!)
|
await cancelSquarePayment(payment.id!)
|
||||||
console.log('[checkout] Pre-auth voided successfully:', 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]
|
const userMessage = CARD_MESSAGES[code]
|
||||||
?? 'Something went wrong with your payment. Please try again or contact us for help.'
|
?? '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 })
|
return NextResponse.json({ error: userMessage }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -189,7 +189,7 @@ export default function FeaturedProducts() {
|
|||||||
placeholder="Search…"
|
placeholder="Search…"
|
||||||
value={search}
|
value={search}
|
||||||
autoFocus
|
autoFocus
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => { setSearch(e.target.value); if (e.target.value) setCategory('all') }}
|
||||||
onBlur={() => { if (!search) setSearchOpen(false) }}
|
onBlur={() => { if (!search) setSearchOpen(false) }}
|
||||||
onKeyDown={(e) => { if (e.key === 'Escape') { setSearch(''); setSearchOpen(false) } }}
|
onKeyDown={(e) => { if (e.key === 'Escape') { setSearch(''); setSearchOpen(false) } }}
|
||||||
style={{ width: '160px' }}
|
style={{ width: '160px' }}
|
||||||
|
|||||||
@ -1,20 +1,22 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useCart } from '@/context/CartContext'
|
||||||
|
|
||||||
export default function ScrollToTop() {
|
export default function ScrollToTop() {
|
||||||
const [visible, setVisible] = useState(false)
|
const { drawerOpen } = useCart()
|
||||||
|
const [scrolled, setScrolled] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
|
||||||
setVisible(scrollTop > 130)
|
setScrolled(scrollTop > 130)
|
||||||
}
|
}
|
||||||
window.addEventListener('scroll', onScroll, { passive: true })
|
window.addEventListener('scroll', onScroll, { passive: true })
|
||||||
return () => window.removeEventListener('scroll', onScroll)
|
return () => window.removeEventListener('scroll', onScroll)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!visible) return null
|
if (!scrolled || drawerOpen) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -24,7 +26,7 @@ export default function ScrollToTop() {
|
|||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: '12px',
|
bottom: '12px',
|
||||||
right: '10px',
|
right: '10px',
|
||||||
zIndex: 99,
|
zIndex: 98,
|
||||||
border: '1px solid #363636',
|
border: '1px solid #363636',
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
background: '#94d601',
|
background: '#94d601',
|
||||||
|
|||||||
@ -368,3 +368,21 @@ export async function sendNewOrderAlert(params: {
|
|||||||
text: lines.join('\n'),
|
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') })
|
||||||
|
}
|
||||||
|
|||||||
@ -181,7 +181,7 @@
|
|||||||
"colors": [
|
"colors": [
|
||||||
{
|
{
|
||||||
"name": "Chrome Rose Gold",
|
"name": "Chrome Rose Gold",
|
||||||
"hex": "#B76E79",
|
"hex": "#C17F87",
|
||||||
"metallic": true,
|
"metallic": true,
|
||||||
"chromeType": "rosegold",
|
"chromeType": "rosegold",
|
||||||
"image": "images/chrome-rosegold.webp"
|
"image": "images/chrome-rosegold.webp"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user