Compare commits

..

No commits in common. "37ab070a8cc66f57dcfd7f7857cb05af73655747" and "92e9abdb84cc121e473a93887a2bfbdd54eef11e" have entirely different histories.

11 changed files with 27 additions and 100 deletions

View File

@ -98,27 +98,6 @@ services:
timeout: 10s
retries: 3
# ── Scheduled Square catalog refresh ─────────────────────────────────────────
# Hits estore's cache-refresh endpoint on a timer so catalog/pricing edits made
# in Square show up without a redeploy or manual admin-panel refresh. Runs as
# part of the stack itself, so it works on any host this compose file is deployed to.
estore-cache-refresh:
image: alpine:3.20
container_name: bpb-estore-cache-refresh
env_file: ./estore/.env
depends_on:
estore:
condition: service_healthy
entrypoint: >
sh -c "
apk add --no-cache curl >/dev/null 2>&1;
while true; do
curl -sf -X POST http://estore:3000/shop/api/cache/refresh -H \"Authorization: Bearer $$CACHE_REFRESH_SECRET\" || echo 'cache refresh failed';
sleep 21600;
done
"
restart: unless-stopped
# ── OSRM download (runs once, exits) ─────────────────────────────────────────
# Downloads the PBF map file into the shared volume if not already present.
osrm-download:

View File

@ -10,7 +10,6 @@ export default function AdminLoginPage() {
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const [showPassword, setShowPassword] = useState(false)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
@ -35,30 +34,16 @@ export default function AdminLoginPage() {
<h1 className="title is-4" style={{ marginBottom: '1.5rem' }}>Admin Login</h1>
<form onSubmit={handleSubmit}>
<div className="field">
<div className="control has-icons-right">
<div className="control">
<input
className="input"
type={showPassword ? 'text' : 'password'}
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoFocus
required
/>
<button
type="button"
className="icon is-right"
style={{ pointerEvents: 'auto', cursor: 'pointer', background: 'none', border: 'none' }}
onClick={() => setShowPassword((v) => !v)}
aria-label={showPassword ? 'Hide password' : 'Show password'}
tabIndex={-1}
>
{showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 20c-7 0-11-8-11-8a18.5 18.5 0 0 1 5.06-5.94M9.9 4.24A10.94 10.94 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8Z"/><circle cx="12" cy="12" r="3"/></svg>
)}
</button>
</div>
</div>
{error && <p className="help is-danger">{error}</p>}

View File

@ -8,35 +8,12 @@ import { CartProvider } from '@/context/CartContext'
import ScrollToTop from '@/components/ScrollToTop'
export const metadata: Metadata = {
metadataBase: new URL('https://beachpartyballoons.com'),
title: {
default: 'Shop | Beach Party Balloons',
template: '%s | Beach Party Balloons',
},
description:
"Milford CT's premier balloon studio. Custom organic arrangements, arches, centerpieces, and event installations. 4.9★ Google rating. Walk-ins welcome.",
openGraph: {
title: 'Shop | Beach Party Balloons',
description: "Milford CT's premier balloon studio. Custom organic arrangements, arches, centerpieces, and event installations.",
url: 'https://beachpartyballoons.com/shop',
siteName: 'Beach Party Balloons',
images: [
{
url: 'https://beachpartyballoons.com/assets/pics/classic/hero.webp',
width: 1200,
height: 630,
alt: 'Beach Party Balloons',
},
],
locale: 'en_US',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Shop | Beach Party Balloons',
description: "Milford CT's premier balloon studio. Custom organic arrangements, arches, centerpieces, and event installations.",
images: ['https://beachpartyballoons.com/assets/pics/classic/hero.webp'],
},
icons: {
icon: '/favicon/favicon.ico',
apple: '/favicon/apple-touch-icon.png',

View File

@ -2,9 +2,9 @@ import type { Metadata } from 'next'
import FeaturedProducts from '@/components/FeaturedProducts'
export const metadata: Metadata = {
title: 'Balloon Shop & Delivery in Milford CT',
title: 'Shop',
description:
'Browse all balloon arrangements, arches, centerpieces, and installations by Beach Party Balloons. Order online for delivery or pickup in Milford, CT.',
'Browse all balloon arrangements, arches, centerpieces, and installations by Beach Party Balloons.',
}
export default function ShopPage() {

View File

@ -20,13 +20,13 @@ export default function CartFab() {
color: '#fff',
border: 'none',
borderRadius: '999px',
padding: '0.55rem 1rem',
padding: '0.75rem 1.25rem',
display: 'flex',
alignItems: 'center',
gap: '0.45rem',
gap: '0.6rem',
boxShadow: '0 6px 20px rgba(0,0,0,0.22)',
cursor: 'pointer',
fontSize: '0.85rem',
fontSize: '0.95rem',
fontWeight: 'bold',
fontFamily: 'inherit',
transition: 'transform 0.15s, box-shadow 0.15s',
@ -42,6 +42,7 @@ export default function CartFab() {
>
<i className="fa-solid fa-bag-shopping" />
<span>{totalItems} {totalItems === 1 ? 'item' : 'items'}</span>
<span style={{ opacity: 0.75, fontWeight: 'normal', fontSize: '0.8rem' }}>· View order</span>
</button>
)
}

View File

@ -1,7 +1,7 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useLockBodyScroll, scrollLockedBodyTo } from '@/lib/useLockBodyScroll'
import { useLockBodyScroll } from '@/lib/useLockBodyScroll'
interface TourStep {
target: string | null // CSS selector, or null = centered modal
@ -54,9 +54,8 @@ const STEPS: TourStep[] = [
},
]
const PAD = 10 // px padding around spotlight
const TIP_WIDTH = 300 // tooltip width in px
const TIP_MAX_HEIGHT = 260 // conservative estimate, used to keep the tooltip on-screen
const PAD = 10 // px padding around spotlight
const TIP_WIDTH = 300 // tooltip width in px
interface Props {
onDone: () => void
@ -104,11 +103,7 @@ export default function GuidedTour({ onDone, onStart }: Props) {
const elRect = el.getBoundingClientRect()
scrollParent.scrollTop = elRect.top - parentRect.top + scrollParent.scrollTop - 20
} else {
// Page scroll is locked (useLockBodyScroll), so scrollIntoView has nothing
// to scroll — shift the frozen body's offset instead so the target actually moves into view.
const lockedY = Math.abs(parseFloat(document.body.style.top || '0')) || 0
const elRect = el.getBoundingClientRect()
scrollLockedBodyTo(lockedY + elRect.top - 100)
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
// Measure after layout settles (double-rAF ensures paint is done)
requestAnimationFrame(() => {
@ -184,7 +179,7 @@ export default function GuidedTour({ onDone, onStart }: Props) {
)
tooltipStyle = {
position: 'fixed',
top: Math.max(8, Math.min(spot.top + spot.height + 12, vh - TIP_MAX_HEIGHT - 8)),
top: spot.top + spot.height + 12,
left,
width: Math.min(TIP_WIDTH, vw - 16),
zIndex: 10002,
@ -196,7 +191,7 @@ export default function GuidedTour({ onDone, onStart }: Props) {
)
tooltipStyle = {
position: 'fixed',
bottom: Math.max(8, Math.min(vh - spot.top + 12, vh - TIP_MAX_HEIGHT - 8)),
bottom: vh - spot.top + 12,
left,
width: Math.min(TIP_WIDTH, vw - 16),
zIndex: 10002,
@ -205,7 +200,7 @@ export default function GuidedTour({ onDone, onStart }: Props) {
// right
tooltipStyle = {
position: 'fixed',
top: Math.max(8, Math.min(spot.top + spot.height / 2 - 80, vh - TIP_MAX_HEIGHT - 8)),
top: Math.max(8, spot.top + spot.height / 2 - 80),
left: Math.min(spot.left + spot.width + 12, vw - TIP_WIDTH - 8),
width: Math.min(TIP_WIDTH, vw - 16),
zIndex: 10002,

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
import { useCart } from '@/context/CartContext'
export default function ScrollToTop() {
const { drawerOpen, totalItems } = useCart()
const { drawerOpen } = useCart()
const [scrolled, setScrolled] = useState(false)
useEffect(() => {
@ -24,10 +24,9 @@ export default function ScrollToTop() {
onClick={() => window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })}
style={{
position: 'fixed',
bottom: totalItems > 0 ? '76px' : '12px',
bottom: '12px',
right: '10px',
zIndex: 98,
transition: 'bottom 0.15s',
border: '1px solid #363636',
outline: 'none',
background: '#94d601',

View File

@ -25,9 +25,3 @@ export function useLockBodyScroll() {
}
}, [])
}
/** While the page is frozen by useLockBodyScroll, there's no real scroll to move
* "scrolling" means shifting the fixed body's negative `top` offset instead. */
export function scrollLockedBodyTo(y: number) {
document.body.style.top = `-${Math.max(0, y)}px`
}

View File

@ -28,11 +28,11 @@ async function deriveSessionToken(password: string): Promise<string> {
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname === '/admin/login' || pathname === '/api/admin/login') {
if (pathname === '/shop/admin/login' || pathname === '/api/admin/login') {
return NextResponse.next()
}
if (pathname.startsWith('/admin') || pathname.startsWith('/api/admin')) {
if (pathname.startsWith('/shop/admin') || pathname.startsWith('/api/admin')) {
const token = request.cookies.get(COOKIE)?.value
const password = process.env.ADMIN_PASSWORD

File diff suppressed because one or more lines are too long

View File

@ -81,26 +81,23 @@
<div class="container is-justify-content-center padding">
<figure class="image">
<img src="assets/pics/classic/hero.webp" alt="Beach Party Balloons Milford CT storefront" >
<img class="is-overlay" src="assets/logo/BeachPartyBalloons-logo.webp" alt="Beach Party Balloons Logo" style="width: 50%; height: auto; margin: auto;">
<img src="assets/pics/classic/hero.webp" alt="Image 1" >
<img class="is-overlay" src="assets/logo/BeachPartyBalloons-logo.webp" alt="Image 2" style="width: 50%; height: auto; margin: auto;">
</figure>
</div>
<div class="is-flex-direction-column is-dark">
<h1 class="is-size-3" style="text-align: center;">Visit our store</h1>
<h2 class="is-size-4" style="text-align: center;"> <a target="_blank" href="https://maps.app.goo.gl/gRk6NztgMRUsSVJf9">554 Boston Post Road, Milford, CT 06460</a> </h2>
<h2 class="is-size-4" style="text-align: center;" ><a href="tel:203.283.5626">203.283.5626</a> </h2>
<div class="py-2 has-text-centered buttons is-centered">
<a href="/shop">
<button class="button is-info">Shop Now</button>
</a>
<div class="py-2 has-text-centered">
<a href="#contact-form">
<button class="button is-light">Contact Us</button>
<button class="button is-info">Contact Us</button>
</a>
</div>
</div>
<div class="container is-justify-content-center padding">
<img src="assets/pics/organic/img1.webp" alt="Organic balloon arch decoration" >
<img src="assets/pics/organic/img1.webp" alt="Image 1" >
</div>
<p class="is-size-5 has-text-centered">Walk-ins welcome!</p>
@ -110,13 +107,13 @@
<p class="is-size-5 has-text-centered">...Or consult with one of our designers about your balloon decorating needs.</p>
<div class="container is-justify-content-center padding">
<img src="assets/pics/organic/img2.webp" alt="Custom organic balloon garland" >
<img src="assets/pics/organic/img2.webp" alt="Image 1" >
</div>
<p class="is-size-5 has-text-centered">Delivery available!</p>
<p class="is-size-5 has-text-centered"> Have your balloons delivered! Our delivery staff will make sure your order arrives safe and sound!</p>
<div class="container is-justify-content-center padding">
<img src="assets/pics/helium/img1.webp" alt="Helium balloon bouquet arrangement" >
<img src="assets/pics/helium/img1.webp" alt="Image 1" >
</div>
<p class="has-text-centered is-size-1">Shop Hours</p>