From 0e6f74554d3e94cbfa82657fd18869085fb7b450 Mon Sep 17 00:00:00 2001 From: AJ Frio Date: Wed, 1 Oct 2025 13:43:36 -0600 Subject: [PATCH 1/2] feat: add storefront hydration overlay --- src/components/storefront/Footer.jsx | 30 +++++--- src/components/storefront/Hero.jsx | 28 +++++-- .../storefront/HydrationOverlay.jsx | 67 ++++++++++++++++ src/components/storefront/Navbar.jsx | 76 ++++++++++++++----- src/pages/storefront/Storefront.jsx | 64 +++++++++++----- 5 files changed, 211 insertions(+), 54 deletions(-) create mode 100644 src/components/storefront/HydrationOverlay.jsx diff --git a/src/components/storefront/Footer.jsx b/src/components/storefront/Footer.jsx index c51986f..c4cf106 100644 --- a/src/components/storefront/Footer.jsx +++ b/src/components/storefront/Footer.jsx @@ -1,26 +1,36 @@ import { useState, useEffect } from 'react' -import { Button } from '../ui/button' -export function Footer() { - const [contactEmail, setContactEmail] = useState('contact@example.com') - const [loading, setLoading] = useState(true) +const fallbackEmail = 'contact@example.com' + +export function Footer({ contactEmail: initialContactEmail = null }) { + const [contactEmail, setContactEmail] = useState(initialContactEmail || fallbackEmail) + const [loading, setLoading] = useState(!initialContactEmail) + + useEffect(() => { + if (!initialContactEmail) { + fetchContactEmail() + } + }, [initialContactEmail]) useEffect(() => { - fetchContactEmail() - }, []) + if (initialContactEmail) { + setContactEmail(initialContactEmail) + setLoading(false) + } + }, [initialContactEmail]) const fetchContactEmail = async () => { try { const response = await fetch('/api/contact-email') if (response.ok) { const data = await response.json() - setContactEmail(data.email || 'contact@example.com') + setContactEmail(data.email || fallbackEmail) } else { - setContactEmail('contact@example.com') + setContactEmail(fallbackEmail) } } catch (error) { console.error('Error fetching contact email:', error) - setContactEmail('contact@example.com') + setContactEmail(fallbackEmail) } finally { setLoading(false) } @@ -37,7 +47,7 @@ export function Footer() { Have questions about our products or need support? We'd love to hear from you.

{loading ? 'Loading...' : 'Contact Us'} diff --git a/src/components/storefront/Hero.jsx b/src/components/storefront/Hero.jsx index a6ae950..10c37f5 100644 --- a/src/components/storefront/Hero.jsx +++ b/src/components/storefront/Hero.jsx @@ -2,11 +2,16 @@ import { useEffect, useState } from 'react' import { normalizeImageUrl } from '../../lib/utils' import { Button } from '../ui/button' -export function Hero() { +const defaultHeroSettings = { + heroImageUrl: '', + heroTitle: 'Welcome to OpenShop', + heroSubtitle: 'Discover amazing products at unbeatable prices. Built on Cloudflare for lightning-fast performance.' +} + +export function Hero({ initialSettings = null }) { const [settings, setSettings] = useState({ - heroImageUrl: '', - heroTitle: 'Welcome to OpenShop', - heroSubtitle: 'Discover amazing products at unbeatable prices. Built on Cloudflare for lightning-fast performance.' + ...defaultHeroSettings, + ...(initialSettings || {}) }) useEffect(() => { @@ -27,9 +32,20 @@ export function Hero() { console.error('Failed to load store settings', e) } } - fetchSettings() + if (!initialSettings) { + fetchSettings() + } return () => { isMounted = false } - }, []) + }, [initialSettings]) + + useEffect(() => { + if (initialSettings) { + setSettings(prev => ({ + ...prev, + ...initialSettings + })) + } + }, [initialSettings]) return (
diff --git a/src/components/storefront/HydrationOverlay.jsx b/src/components/storefront/HydrationOverlay.jsx new file mode 100644 index 0000000..048bf68 --- /dev/null +++ b/src/components/storefront/HydrationOverlay.jsx @@ -0,0 +1,67 @@ +const shimmer = 'animate-pulse bg-gradient-to-r from-gray-200 via-gray-100 to-gray-200' + +export function HydrationOverlay() { + return ( +
+
+
+ + ) +} diff --git a/src/components/storefront/Navbar.jsx b/src/components/storefront/Navbar.jsx index a24cea1..d109c96 100644 --- a/src/components/storefront/Navbar.jsx +++ b/src/components/storefront/Navbar.jsx @@ -5,22 +5,66 @@ import { Button } from '../ui/button' import { useCart } from '../../contexts/CartContext' import { ShoppingCart } from 'lucide-react' -export function Navbar() { - const [collections, setCollections] = useState([]) +const defaultStoreSettings = { + logoType: 'text', + logoText: 'OpenShop', + logoImageUrl: '' +} + +export function Navbar({ + initialCollections = [], + initialProducts = [], + initialStoreSettings = null +}) { + const [collections, setCollections] = useState(initialCollections) + const [products, setProducts] = useState(initialProducts) const [collectionsWithProducts, setCollectionsWithProducts] = useState([]) const [storeSettings, setStoreSettings] = useState({ - logoType: 'text', - logoText: 'OpenShop', - logoImageUrl: '' + ...defaultStoreSettings, + ...(initialStoreSettings || {}) }) const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const { itemCount, toggleCart } = useCart() + const hasInitialCollections = initialCollections.length > 0 + const hasInitialProducts = initialProducts.length > 0 + const hasInitialSettings = Boolean(initialStoreSettings) + + useEffect(() => { + setCollections(initialCollections) + }, [initialCollections]) + + useEffect(() => { + setProducts(initialProducts) + }, [initialProducts]) useEffect(() => { - // Fetch collections, products, and store settings for navigation - fetchCollectionsAndProducts() - fetchStoreSettings() - }, []) + if (initialStoreSettings) { + setStoreSettings(prev => ({ + ...prev, + ...initialStoreSettings + })) + } + }, [initialStoreSettings]) + + useEffect(() => { + if (!hasInitialCollections || !hasInitialProducts) { + fetchCollectionsAndProducts() + } + }, [hasInitialCollections, hasInitialProducts]) + + useEffect(() => { + if (!hasInitialSettings) { + fetchStoreSettings() + } + }, [hasInitialSettings]) + + useEffect(() => { + const grouped = collections.map(collection => ({ + ...collection, + products: products.filter(product => product.collectionId === collection.id) + })) + setCollectionsWithProducts(grouped) + }, [collections, products]) const fetchCollectionsAndProducts = async () => { try { @@ -34,14 +78,7 @@ export function Navbar() { const productsData = await productsResponse.json() setCollections(collectionsData) - - // Group products by collection - const collectionsWithProducts = collectionsData.map(collection => ({ - ...collection, - products: productsData.filter(product => product.collectionId === collection.id) - })) - - setCollectionsWithProducts(collectionsWithProducts) + setProducts(productsData) } } catch (error) { console.error('Error fetching collections and products:', error) @@ -53,7 +90,10 @@ export function Navbar() { const response = await fetch('/api/store-settings') if (response.ok) { const data = await response.json() - setStoreSettings(data) + setStoreSettings(prev => ({ + ...prev, + ...data + })) } } catch (error) { console.error('Error fetching store settings:', error) diff --git a/src/pages/storefront/Storefront.jsx b/src/pages/storefront/Storefront.jsx index 8a4b313..7d07776 100644 --- a/src/pages/storefront/Storefront.jsx +++ b/src/pages/storefront/Storefront.jsx @@ -4,12 +4,22 @@ import { Hero } from '../../components/storefront/Hero' import { Carousel } from '../../components/storefront/Carousel' import { ProductCard } from '../../components/storefront/ProductCard' import { Footer } from '../../components/storefront/Footer' +import { HydrationOverlay } from '../../components/storefront/HydrationOverlay' import { Button } from '../../components/ui/button' export function Storefront() { const [products, setProducts] = useState([]) const [collections, setCollections] = useState([]) const [selectedCollection, setSelectedCollection] = useState(null) + const [storeSettings, setStoreSettings] = useState({ + logoType: 'text', + logoText: 'OpenShop', + logoImageUrl: '', + heroImageUrl: '', + heroTitle: 'Welcome to OpenShop', + heroSubtitle: 'Discover amazing products at unbeatable prices. Built on Cloudflare for lightning-fast performance.' + }) + const [contactEmail, setContactEmail] = useState('contact@example.com') const [loading, setLoading] = useState(true) useEffect(() => { @@ -21,20 +31,40 @@ export function Storefront() { setLoading(true) // Fetch products and collections - const [productsResponse, collectionsResponse] = await Promise.all([ - fetch('/api/products'), - fetch('/api/collections') + const [ + productsResponse, + collectionsResponse, + settingsResponse, + contactResponse + ] = await Promise.all([ + fetch('/api/products').catch(() => null), + fetch('/api/collections').catch(() => null), + fetch('/api/store-settings').catch(() => null), + fetch('/api/contact-email').catch(() => null) ]) - if (productsResponse.ok) { + if (productsResponse?.ok) { const productsData = await productsResponse.json() setProducts(productsData) } - if (collectionsResponse.ok) { + if (collectionsResponse?.ok) { const collectionsData = await collectionsResponse.json() setCollections(collectionsData) } + + if (settingsResponse?.ok) { + const settingsData = await settingsResponse.json() + setStoreSettings(prev => ({ + ...prev, + ...settingsData + })) + } + + if (contactResponse?.ok) { + const contactData = await contactResponse.json() + setContactEmail(contactData.email || 'contact@example.com') + } } catch (error) { console.error('Error fetching data:', error) } finally { @@ -49,25 +79,19 @@ export function Storefront() { const featuredProducts = products.slice(0, 3) // Show first 3 products in carousel if (loading) { - return ( -
- -
-
-
-

Loading products...

-
-
-
- ) + return } return (
- - + + {/* Hero Section */} - + {/* Featured Products Carousel */} {featuredProducts.length > 0 && ( @@ -132,7 +156,7 @@ export function Storefront() { {/* Footer */} -
+
) } From d27e7d63b2dfabfbf91ca95a03e8db5a0d68319c Mon Sep 17 00:00:00 2001 From: AJ Frio Date: Wed, 1 Oct 2025 14:02:59 -0600 Subject: [PATCH 2/2] Improve storefront hydration caching and navigation --- src/components/storefront/Navbar.jsx | 33 ++++++++-- src/lib/storefrontCache.js | 26 ++++++++ src/pages/storefront/Storefront.jsx | 92 ++++++++++++++++++++++------ 3 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 src/lib/storefrontCache.js diff --git a/src/components/storefront/Navbar.jsx b/src/components/storefront/Navbar.jsx index d109c96..0a9d2c0 100644 --- a/src/components/storefront/Navbar.jsx +++ b/src/components/storefront/Navbar.jsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useCallback } from 'react' import { normalizeImageUrl } from '../../lib/utils' -import { Link } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import { Button } from '../ui/button' import { useCart } from '../../contexts/CartContext' import { ShoppingCart } from 'lucide-react' @@ -25,10 +25,32 @@ export function Navbar({ }) const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const { itemCount, toggleCart } = useCart() + const navigate = useNavigate() const hasInitialCollections = initialCollections.length > 0 const hasInitialProducts = initialProducts.length > 0 const hasInitialSettings = Boolean(initialStoreSettings) + const handleLogoClick = useCallback((event) => { + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.altKey || + event.ctrlKey || + event.shiftKey + ) { + return + } + + event.preventDefault() + setMobileMenuOpen(false) + navigate('/') + + if (typeof window !== 'undefined') { + window.scrollTo({ top: 0, behavior: 'auto' }) + } + }, [navigate]) + useEffect(() => { setCollections(initialCollections) }, [initialCollections]) @@ -105,7 +127,7 @@ export function Navbar({
{/* Logo */} - + {storeSettings.logoType === 'image' && storeSettings.logoImageUrl ? ( Home @@ -243,7 +266,7 @@ export function Navbar({ {/* Mobile menu button */}
-