Compare commits
6 Commits
92e9abdb84
...
37ab070a8c
| Author | SHA1 | Date | |
|---|---|---|---|
| 37ab070a8c | |||
| 5b9524e1d9 | |||
| 219256be55 | |||
| f6960fd3ec | |||
| 517e00aeb1 | |||
| ec2ebca836 |
@ -98,6 +98,27 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
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) ─────────────────────────────────────────
|
# ── OSRM download (runs once, exits) ─────────────────────────────────────────
|
||||||
# Downloads the PBF map file into the shared volume if not already present.
|
# Downloads the PBF map file into the shared volume if not already present.
|
||||||
osrm-download:
|
osrm-download:
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export default function AdminLoginPage() {
|
|||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -34,16 +35,30 @@ export default function AdminLoginPage() {
|
|||||||
<h1 className="title is-4" style={{ marginBottom: '1.5rem' }}>Admin Login</h1>
|
<h1 className="title is-4" style={{ marginBottom: '1.5rem' }}>Admin Login</h1>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="control">
|
<div className="control has-icons-right">
|
||||||
<input
|
<input
|
||||||
className="input"
|
className="input"
|
||||||
type="password"
|
type={showPassword ? 'text' : 'password'}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
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>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="help is-danger">{error}</p>}
|
{error && <p className="help is-danger">{error}</p>}
|
||||||
|
|||||||
@ -8,12 +8,35 @@ import { CartProvider } from '@/context/CartContext'
|
|||||||
import ScrollToTop from '@/components/ScrollToTop'
|
import ScrollToTop from '@/components/ScrollToTop'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
metadataBase: new URL('https://beachpartyballoons.com'),
|
||||||
title: {
|
title: {
|
||||||
default: 'Shop | Beach Party Balloons',
|
default: 'Shop | Beach Party Balloons',
|
||||||
template: '%s | Beach Party Balloons',
|
template: '%s | Beach Party Balloons',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
"Milford CT's premier balloon studio. Custom organic arrangements, arches, centerpieces, and event installations. 4.9★ Google rating. Walk-ins welcome.",
|
"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: {
|
icons: {
|
||||||
icon: '/favicon/favicon.ico',
|
icon: '/favicon/favicon.ico',
|
||||||
apple: '/favicon/apple-touch-icon.png',
|
apple: '/favicon/apple-touch-icon.png',
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import type { Metadata } from 'next'
|
|||||||
import FeaturedProducts from '@/components/FeaturedProducts'
|
import FeaturedProducts from '@/components/FeaturedProducts'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Shop',
|
title: 'Balloon Shop & Delivery in Milford CT',
|
||||||
description:
|
description:
|
||||||
'Browse all balloon arrangements, arches, centerpieces, and installations by Beach Party Balloons.',
|
'Browse all balloon arrangements, arches, centerpieces, and installations by Beach Party Balloons. Order online for delivery or pickup in Milford, CT.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ShopPage() {
|
export default function ShopPage() {
|
||||||
|
|||||||
@ -20,13 +20,13 @@ export default function CartFab() {
|
|||||||
color: '#fff',
|
color: '#fff',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '999px',
|
borderRadius: '999px',
|
||||||
padding: '0.75rem 1.25rem',
|
padding: '0.55rem 1rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '0.6rem',
|
gap: '0.45rem',
|
||||||
boxShadow: '0 6px 20px rgba(0,0,0,0.22)',
|
boxShadow: '0 6px 20px rgba(0,0,0,0.22)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: '0.95rem',
|
fontSize: '0.85rem',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
transition: 'transform 0.15s, box-shadow 0.15s',
|
transition: 'transform 0.15s, box-shadow 0.15s',
|
||||||
@ -42,7 +42,6 @@ export default function CartFab() {
|
|||||||
>
|
>
|
||||||
<i className="fa-solid fa-bag-shopping" />
|
<i className="fa-solid fa-bag-shopping" />
|
||||||
<span>{totalItems} {totalItems === 1 ? 'item' : 'items'}</span>
|
<span>{totalItems} {totalItems === 1 ? 'item' : 'items'}</span>
|
||||||
<span style={{ opacity: 0.75, fontWeight: 'normal', fontSize: '0.8rem' }}>· View order</span>
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { useLockBodyScroll } from '@/lib/useLockBodyScroll'
|
import { useLockBodyScroll, scrollLockedBodyTo } from '@/lib/useLockBodyScroll'
|
||||||
|
|
||||||
interface TourStep {
|
interface TourStep {
|
||||||
target: string | null // CSS selector, or null = centered modal
|
target: string | null // CSS selector, or null = centered modal
|
||||||
@ -56,6 +56,7 @@ const STEPS: TourStep[] = [
|
|||||||
|
|
||||||
const PAD = 10 // px padding around spotlight
|
const PAD = 10 // px padding around spotlight
|
||||||
const TIP_WIDTH = 300 // tooltip width in px
|
const TIP_WIDTH = 300 // tooltip width in px
|
||||||
|
const TIP_MAX_HEIGHT = 260 // conservative estimate, used to keep the tooltip on-screen
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onDone: () => void
|
onDone: () => void
|
||||||
@ -103,7 +104,11 @@ export default function GuidedTour({ onDone, onStart }: Props) {
|
|||||||
const elRect = el.getBoundingClientRect()
|
const elRect = el.getBoundingClientRect()
|
||||||
scrollParent.scrollTop = elRect.top - parentRect.top + scrollParent.scrollTop - 20
|
scrollParent.scrollTop = elRect.top - parentRect.top + scrollParent.scrollTop - 20
|
||||||
} else {
|
} else {
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
// 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)
|
||||||
}
|
}
|
||||||
// Measure after layout settles (double-rAF ensures paint is done)
|
// Measure after layout settles (double-rAF ensures paint is done)
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@ -179,7 +184,7 @@ export default function GuidedTour({ onDone, onStart }: Props) {
|
|||||||
)
|
)
|
||||||
tooltipStyle = {
|
tooltipStyle = {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: spot.top + spot.height + 12,
|
top: Math.max(8, Math.min(spot.top + spot.height + 12, vh - TIP_MAX_HEIGHT - 8)),
|
||||||
left,
|
left,
|
||||||
width: Math.min(TIP_WIDTH, vw - 16),
|
width: Math.min(TIP_WIDTH, vw - 16),
|
||||||
zIndex: 10002,
|
zIndex: 10002,
|
||||||
@ -191,7 +196,7 @@ export default function GuidedTour({ onDone, onStart }: Props) {
|
|||||||
)
|
)
|
||||||
tooltipStyle = {
|
tooltipStyle = {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: vh - spot.top + 12,
|
bottom: Math.max(8, Math.min(vh - spot.top + 12, vh - TIP_MAX_HEIGHT - 8)),
|
||||||
left,
|
left,
|
||||||
width: Math.min(TIP_WIDTH, vw - 16),
|
width: Math.min(TIP_WIDTH, vw - 16),
|
||||||
zIndex: 10002,
|
zIndex: 10002,
|
||||||
@ -200,7 +205,7 @@ export default function GuidedTour({ onDone, onStart }: Props) {
|
|||||||
// right
|
// right
|
||||||
tooltipStyle = {
|
tooltipStyle = {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: Math.max(8, spot.top + spot.height / 2 - 80),
|
top: Math.max(8, Math.min(spot.top + spot.height / 2 - 80, vh - TIP_MAX_HEIGHT - 8)),
|
||||||
left: Math.min(spot.left + spot.width + 12, vw - TIP_WIDTH - 8),
|
left: Math.min(spot.left + spot.width + 12, vw - TIP_WIDTH - 8),
|
||||||
width: Math.min(TIP_WIDTH, vw - 16),
|
width: Math.min(TIP_WIDTH, vw - 16),
|
||||||
zIndex: 10002,
|
zIndex: 10002,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { useCart } from '@/context/CartContext'
|
import { useCart } from '@/context/CartContext'
|
||||||
|
|
||||||
export default function ScrollToTop() {
|
export default function ScrollToTop() {
|
||||||
const { drawerOpen } = useCart()
|
const { drawerOpen, totalItems } = useCart()
|
||||||
const [scrolled, setScrolled] = useState(false)
|
const [scrolled, setScrolled] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -24,9 +24,10 @@ export default function ScrollToTop() {
|
|||||||
onClick={() => window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })}
|
onClick={() => window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: '12px',
|
bottom: totalItems > 0 ? '76px' : '12px',
|
||||||
right: '10px',
|
right: '10px',
|
||||||
zIndex: 98,
|
zIndex: 98,
|
||||||
|
transition: 'bottom 0.15s',
|
||||||
border: '1px solid #363636',
|
border: '1px solid #363636',
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
background: '#94d601',
|
background: '#94d601',
|
||||||
|
|||||||
@ -25,3 +25,9 @@ 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`
|
||||||
|
}
|
||||||
|
|||||||
@ -28,11 +28,11 @@ async function deriveSessionToken(password: string): Promise<string> {
|
|||||||
export async function middleware(request: NextRequest) {
|
export async function middleware(request: NextRequest) {
|
||||||
const { pathname } = request.nextUrl
|
const { pathname } = request.nextUrl
|
||||||
|
|
||||||
if (pathname === '/shop/admin/login' || pathname === '/api/admin/login') {
|
if (pathname === '/admin/login' || pathname === '/api/admin/login') {
|
||||||
return NextResponse.next()
|
return NextResponse.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/shop/admin') || pathname.startsWith('/api/admin')) {
|
if (pathname.startsWith('/admin') || pathname.startsWith('/api/admin')) {
|
||||||
const token = request.cookies.get(COOKIE)?.value
|
const token = request.cookies.get(COOKIE)?.value
|
||||||
const password = process.env.ADMIN_PASSWORD
|
const password = process.env.ADMIN_PASSWORD
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -81,23 +81,26 @@
|
|||||||
|
|
||||||
<div class="container is-justify-content-center padding">
|
<div class="container is-justify-content-center padding">
|
||||||
<figure class="image">
|
<figure class="image">
|
||||||
<img src="assets/pics/classic/hero.webp" alt="Image 1" >
|
<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="Image 2" style="width: 50%; height: auto; margin: auto;">
|
<img class="is-overlay" src="assets/logo/BeachPartyBalloons-logo.webp" alt="Beach Party Balloons Logo" style="width: 50%; height: auto; margin: auto;">
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
<div class="is-flex-direction-column is-dark">
|
<div class="is-flex-direction-column is-dark">
|
||||||
<h1 class="is-size-3" style="text-align: center;">Visit our store</h1>
|
<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 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>
|
<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">
|
<div class="py-2 has-text-centered buttons is-centered">
|
||||||
|
<a href="/shop">
|
||||||
|
<button class="button is-info">Shop Now</button>
|
||||||
|
</a>
|
||||||
<a href="#contact-form">
|
<a href="#contact-form">
|
||||||
<button class="button is-info">Contact Us</button>
|
<button class="button is-light">Contact Us</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container is-justify-content-center padding">
|
<div class="container is-justify-content-center padding">
|
||||||
<img src="assets/pics/organic/img1.webp" alt="Image 1" >
|
<img src="assets/pics/organic/img1.webp" alt="Organic balloon arch decoration" >
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="is-size-5 has-text-centered">Walk-ins welcome!</p>
|
<p class="is-size-5 has-text-centered">Walk-ins welcome!</p>
|
||||||
@ -107,13 +110,13 @@
|
|||||||
<p class="is-size-5 has-text-centered">...Or consult with one of our designers about your balloon decorating needs.</p>
|
<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">
|
<div class="container is-justify-content-center padding">
|
||||||
<img src="assets/pics/organic/img2.webp" alt="Image 1" >
|
<img src="assets/pics/organic/img2.webp" alt="Custom organic balloon garland" >
|
||||||
</div>
|
</div>
|
||||||
<p class="is-size-5 has-text-centered">Delivery available!</p>
|
<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>
|
<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">
|
<div class="container is-justify-content-center padding">
|
||||||
<img src="assets/pics/helium/img1.webp" alt="Image 1" >
|
<img src="assets/pics/helium/img1.webp" alt="Helium balloon bouquet arrangement" >
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="has-text-centered is-size-1">Shop Hours</p>
|
<p class="has-text-centered is-size-1">Shop Hours</p>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user