Compare commits
No commits in common. "37ab070a8cc66f57dcfd7f7857cb05af73655747" and "92e9abdb84cc121e473a93887a2bfbdd54eef11e" have entirely different histories.
37ab070a8c
...
92e9abdb84
@ -98,27 +98,6 @@ 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,7 +10,6 @@ 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()
|
||||||
@ -35,30 +34,16 @@ 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 has-icons-right">
|
<div className="control">
|
||||||
<input
|
<input
|
||||||
className="input"
|
className="input"
|
||||||
type={showPassword ? 'text' : 'password'}
|
type="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,35 +8,12 @@ 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: 'Balloon Shop & Delivery in Milford CT',
|
title: 'Shop',
|
||||||
description:
|
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() {
|
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.55rem 1rem',
|
padding: '0.75rem 1.25rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '0.45rem',
|
gap: '0.6rem',
|
||||||
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.85rem',
|
fontSize: '0.95rem',
|
||||||
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,6 +42,7 @@ 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, scrollLockedBodyTo } from '@/lib/useLockBodyScroll'
|
import { useLockBodyScroll } 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,7 +56,6 @@ 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
|
||||||
@ -104,11 +103,7 @@ 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 {
|
||||||
// Page scroll is locked (useLockBodyScroll), so scrollIntoView has nothing
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
// 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(() => {
|
||||||
@ -184,7 +179,7 @@ export default function GuidedTour({ onDone, onStart }: Props) {
|
|||||||
)
|
)
|
||||||
tooltipStyle = {
|
tooltipStyle = {
|
||||||
position: 'fixed',
|
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,
|
left,
|
||||||
width: Math.min(TIP_WIDTH, vw - 16),
|
width: Math.min(TIP_WIDTH, vw - 16),
|
||||||
zIndex: 10002,
|
zIndex: 10002,
|
||||||
@ -196,7 +191,7 @@ export default function GuidedTour({ onDone, onStart }: Props) {
|
|||||||
)
|
)
|
||||||
tooltipStyle = {
|
tooltipStyle = {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: Math.max(8, Math.min(vh - spot.top + 12, vh - TIP_MAX_HEIGHT - 8)),
|
bottom: vh - spot.top + 12,
|
||||||
left,
|
left,
|
||||||
width: Math.min(TIP_WIDTH, vw - 16),
|
width: Math.min(TIP_WIDTH, vw - 16),
|
||||||
zIndex: 10002,
|
zIndex: 10002,
|
||||||
@ -205,7 +200,7 @@ export default function GuidedTour({ onDone, onStart }: Props) {
|
|||||||
// right
|
// right
|
||||||
tooltipStyle = {
|
tooltipStyle = {
|
||||||
position: 'fixed',
|
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),
|
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, totalItems } = useCart()
|
const { drawerOpen } = useCart()
|
||||||
const [scrolled, setScrolled] = useState(false)
|
const [scrolled, setScrolled] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -24,10 +24,9 @@ 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: totalItems > 0 ? '76px' : '12px',
|
bottom: '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,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`
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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 === '/admin/login' || pathname === '/api/admin/login') {
|
if (pathname === '/shop/admin/login' || pathname === '/api/admin/login') {
|
||||||
return NextResponse.next()
|
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 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,26 +81,23 @@
|
|||||||
|
|
||||||
<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="Beach Party Balloons Milford CT storefront" >
|
<img src="assets/pics/classic/hero.webp" alt="Image 1" >
|
||||||
<img class="is-overlay" src="assets/logo/BeachPartyBalloons-logo.webp" alt="Beach Party Balloons Logo" style="width: 50%; height: auto; margin: auto;">
|
<img class="is-overlay" src="assets/logo/BeachPartyBalloons-logo.webp" alt="Image 2" 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 buttons is-centered">
|
<div class="py-2 has-text-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-light">Contact Us</button>
|
<button class="button is-info">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="Organic balloon arch decoration" >
|
<img src="assets/pics/organic/img1.webp" alt="Image 1" >
|
||||||
</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>
|
||||||
@ -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>
|
<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="Custom organic balloon garland" >
|
<img src="assets/pics/organic/img2.webp" alt="Image 1" >
|
||||||
</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="Helium balloon bouquet arrangement" >
|
<img src="assets/pics/helium/img1.webp" alt="Image 1" >
|
||||||
</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