Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions src/components/storefront/Footer.jsx
Original file line number Diff line number Diff line change
@@ -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)
}
Expand All @@ -37,7 +47,7 @@ export function Footer() {
Have questions about our products or need support? We'd love to hear from you.
</p>
<a
href={`mailto:${contactEmail && contactEmail !== 'contact@example.com' ? contactEmail : 'contact@example.com'}`}
href={`mailto:${contactEmail && contactEmail !== fallbackEmail ? contactEmail : fallbackEmail}`}
className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 hover:border-purple-300 hover:text-purple-600 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
>
{loading ? 'Loading...' : 'Contact Us'}
Expand Down
28 changes: 22 additions & 6 deletions src/components/storefront/Hero.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -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 (
<div className="relative text-white">
Expand Down
67 changes: 67 additions & 0 deletions src/components/storefront/HydrationOverlay.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-gray-50 flex flex-col">
<div className="bg-white border-b shadow-sm">
<div className="max-w-8xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div className={`h-8 w-36 rounded-md ${shimmer}`} aria-hidden="true" />
<div className="hidden md:flex items-center space-x-6">
{[1, 2, 3].map((item) => (
<div key={item} className={`h-5 w-16 rounded-md ${shimmer}`} aria-hidden="true" />
))}
</div>
<div className={`h-9 w-9 rounded-full ${shimmer}`} aria-hidden="true" />
</div>
</div>

<main className="flex-1">
<section className="relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-purple-600 to-blue-600 opacity-80" aria-hidden="true" />
<div className="relative max-w-5xl mx-auto px-6 lg:px-8 py-24">
<div className="space-y-4 text-center text-white">
<div className={`h-10 w-3/4 mx-auto rounded-md ${shimmer}`} aria-hidden="true" />
<div className={`h-6 w-2/3 mx-auto rounded-md ${shimmer}`} aria-hidden="true" />
<div className="flex flex-col sm:flex-row justify-center gap-4 pt-6">
<div className={`h-12 w-32 rounded-full ${shimmer}`} aria-hidden="true" />
<div className={`h-12 w-32 rounded-full ${shimmer}`} aria-hidden="true" />
</div>
</div>
</div>
</section>

<section className="max-w-8xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className={`h-8 w-48 mx-auto rounded-md ${shimmer}`} aria-hidden="true" />
<div className="mt-10 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, index) => (
<div
key={index}
className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4 space-y-4"
style={{ animationDelay: `${index * 60}ms` }}
aria-hidden="true"
>
<div className={`h-40 rounded-xl ${shimmer}`} />
<div className={`h-4 w-3/4 rounded-md ${shimmer}`} />
<div className={`h-4 w-1/2 rounded-md ${shimmer}`} />
<div className="flex items-center justify-between pt-4">
<div className={`h-6 w-16 rounded-md ${shimmer}`} />
<div className={`h-10 w-24 rounded-full ${shimmer}`} />
</div>
</div>
))}
</div>
</section>
</main>

<footer className="bg-white border-t">
<div className="max-w-8xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className={`h-14 rounded-lg ${shimmer}`} aria-hidden="true" />
<div className={`h-14 rounded-lg ${shimmer}`} aria-hidden="true" />
</div>
<div className={`mt-6 h-4 w-1/3 mx-auto rounded-md ${shimmer}`} aria-hidden="true" />
</div>
</footer>
</div>
)
}
109 changes: 86 additions & 23 deletions src/components/storefront/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,92 @@
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'

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 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(() => {
// Fetch collections, products, and store settings for navigation
fetchCollectionsAndProducts()
fetchStoreSettings()
}, [])
setCollections(initialCollections)
}, [initialCollections])

useEffect(() => {
setProducts(initialProducts)
}, [initialProducts])

useEffect(() => {
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 {
Expand All @@ -34,14 +100,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)
Expand All @@ -53,7 +112,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)
Expand All @@ -65,7 +127,7 @@ export function Navbar() {
<div className="max-w-8xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-3 items-center h-16">
{/* Logo */}
<Link to="/" className="flex-shrink-0">
<Link to="/" className="flex-shrink-0" onClick={handleLogoClick}>
{storeSettings.logoType === 'image' && storeSettings.logoImageUrl ? (
<img
src={storeSettings.logoImageUrl}
Expand All @@ -92,6 +154,7 @@ export function Navbar() {
<div className="flex items-baseline space-x-6 justify-center">
<Link
to="/"
onClick={handleLogoClick}
className="text-gray-900 hover:text-purple-600 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
>
Home
Expand Down Expand Up @@ -203,7 +266,7 @@ export function Navbar() {

{/* Mobile menu button */}
<div className="md:hidden">
<button
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="text-gray-900 hover:text-gray-600 transition-colors duration-200"
>
Expand All @@ -228,7 +291,7 @@ export function Navbar() {
<Link
to="/"
className="block px-3 py-2 text-base font-medium text-gray-900 hover:text-purple-600 hover:bg-gray-50 rounded-md transition-colors duration-200"
onClick={() => setMobileMenuOpen(false)}
onClick={handleLogoClick}
>
Home
</Link>
Expand Down
26 changes: 26 additions & 0 deletions src/lib/storefrontCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const CACHE_TTL_MS = 1000 * 60 * 5 // 5 minutes

let cacheEntry = null

export function getStorefrontCache() {
if (!cacheEntry) return null

const isExpired = Date.now() - cacheEntry.timestamp > CACHE_TTL_MS
if (isExpired) {
cacheEntry = null
return null
}

return cacheEntry.data
}

export function setStorefrontCache(data) {
cacheEntry = {
data,
timestamp: Date.now()
}
}

export function clearStorefrontCache() {
cacheEntry = null
}
Loading