feat: implement header/footer visibility controls for checkout and thankyou pages
- Created LayoutWrapper component to conditionally render header/footer based on route - Created MinimalHeader component (logo only) - Created MinimalFooter component (trust badges + policy links) - Created usePageVisibility hook to get visibility settings per page - Wrapped ClassicLayout with LayoutWrapper for conditional rendering - Header/footer visibility now controlled directly in React SPA - Settings: show/minimal/hide for both header and footer - Background color support for checkout and thankyou pages
This commit is contained in:
278
admin-spa/src/routes/Appearance/Product.tsx
Normal file
278
admin-spa/src/routes/Appearance/Product.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
|
||||
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
|
||||
import { SettingsSection } from '@/routes/Settings/components/SettingsSection';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
export default function AppearanceProduct() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [imagePosition, setImagePosition] = useState('left');
|
||||
const [galleryStyle, setGalleryStyle] = useState('thumbnails');
|
||||
const [stickyAddToCart, setStickyAddToCart] = useState(false);
|
||||
|
||||
const [elements, setElements] = useState({
|
||||
breadcrumbs: true,
|
||||
related_products: true,
|
||||
reviews: true,
|
||||
share_buttons: false,
|
||||
product_meta: true,
|
||||
});
|
||||
|
||||
const [reviewSettings, setReviewSettings] = useState({
|
||||
placement: 'product_page',
|
||||
hide_if_empty: true,
|
||||
});
|
||||
|
||||
const [relatedProductsTitle, setRelatedProductsTitle] = useState('You May Also Like');
|
||||
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const response = await api.get('/appearance/settings');
|
||||
const product = response.data?.pages?.product;
|
||||
|
||||
if (product) {
|
||||
if (product.layout) {
|
||||
if (product.layout.image_position) setImagePosition(product.layout.image_position);
|
||||
if (product.layout.gallery_style) setGalleryStyle(product.layout.gallery_style);
|
||||
if (product.layout.sticky_add_to_cart !== undefined) setStickyAddToCart(product.layout.sticky_add_to_cart);
|
||||
}
|
||||
if (product.elements) {
|
||||
setElements({
|
||||
breadcrumbs: product.elements.breadcrumbs ?? true,
|
||||
related_products: product.elements.related_products ?? true,
|
||||
reviews: product.elements.reviews ?? true,
|
||||
share_buttons: product.elements.share_buttons ?? false,
|
||||
product_meta: product.elements.product_meta ?? true,
|
||||
});
|
||||
}
|
||||
if (product.related_products) {
|
||||
setRelatedProductsTitle(product.related_products.title ?? 'You May Also Like');
|
||||
}
|
||||
if (product.reviews) {
|
||||
setReviewSettings({
|
||||
placement: product.reviews.placement ?? 'product_page',
|
||||
hide_if_empty: product.reviews.hide_if_empty ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const toggleElement = (key: keyof typeof elements) => {
|
||||
setElements({ ...elements, [key]: !elements[key] });
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await api.post('/appearance/pages/product', {
|
||||
layout: {
|
||||
image_position: imagePosition,
|
||||
gallery_style: galleryStyle,
|
||||
sticky_add_to_cart: stickyAddToCart
|
||||
},
|
||||
elements,
|
||||
related_products: {
|
||||
title: relatedProductsTitle,
|
||||
},
|
||||
reviews: reviewSettings,
|
||||
});
|
||||
toast.success('Product page settings saved successfully');
|
||||
} catch (error) {
|
||||
console.error('Save error:', error);
|
||||
toast.error('Failed to save settings');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsLayout
|
||||
title="Product Page Settings"
|
||||
onSave={handleSave}
|
||||
isLoading={loading}
|
||||
>
|
||||
{/* Layout */}
|
||||
<SettingsCard
|
||||
title="Layout"
|
||||
description="Configure product page layout and gallery"
|
||||
>
|
||||
<SettingsSection label="Image Position" htmlFor="image-position">
|
||||
<Select value={imagePosition} onValueChange={setImagePosition}>
|
||||
<SelectTrigger id="image-position">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="left">Left</SelectItem>
|
||||
<SelectItem value="right">Right</SelectItem>
|
||||
<SelectItem value="top">Top</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection label="Gallery Style" htmlFor="gallery-style">
|
||||
<Select value={galleryStyle} onValueChange={setGalleryStyle}>
|
||||
<SelectTrigger id="gallery-style">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="thumbnails">Thumbnails</SelectItem>
|
||||
<SelectItem value="dots">Dots</SelectItem>
|
||||
<SelectItem value="slider">Slider</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingsSection>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="sticky-cart">Sticky Add to Cart</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Keep add to cart button visible when scrolling
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="sticky-cart"
|
||||
checked={stickyAddToCart}
|
||||
onCheckedChange={setStickyAddToCart}
|
||||
/>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Elements */}
|
||||
<SettingsCard
|
||||
title="Elements"
|
||||
description="Choose which elements to display on the product page"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="element-breadcrumbs" className="cursor-pointer">
|
||||
Show breadcrumbs
|
||||
</Label>
|
||||
<Switch
|
||||
id="element-breadcrumbs"
|
||||
checked={elements.breadcrumbs}
|
||||
onCheckedChange={() => toggleElement('breadcrumbs')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="element-related-products" className="cursor-pointer">
|
||||
Show related products
|
||||
</Label>
|
||||
<Switch
|
||||
id="element-related-products"
|
||||
checked={elements.related_products}
|
||||
onCheckedChange={() => toggleElement('related_products')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="element-reviews" className="cursor-pointer">
|
||||
Show reviews
|
||||
</Label>
|
||||
<Switch
|
||||
id="element-reviews"
|
||||
checked={elements.reviews}
|
||||
onCheckedChange={() => toggleElement('reviews')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="element-share-buttons" className="cursor-pointer">
|
||||
Show share buttons
|
||||
</Label>
|
||||
<Switch
|
||||
id="element-share-buttons"
|
||||
checked={elements.share_buttons}
|
||||
onCheckedChange={() => toggleElement('share_buttons')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="element-product-meta" className="cursor-pointer">
|
||||
Show product meta
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
SKU, categories, tags
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="element-product-meta"
|
||||
checked={elements.product_meta}
|
||||
onCheckedChange={() => toggleElement('product_meta')}
|
||||
/>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Related Products Settings */}
|
||||
<SettingsCard
|
||||
title="Related Products"
|
||||
description="Configure related products section"
|
||||
>
|
||||
<SettingsSection label="Section Title" htmlFor="related-products-title">
|
||||
<input
|
||||
id="related-products-title"
|
||||
type="text"
|
||||
value={relatedProductsTitle}
|
||||
onChange={(e) => setRelatedProductsTitle(e.target.value)}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder="You May Also Like"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
This heading appears above the related products grid
|
||||
</p>
|
||||
</SettingsSection>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Review Settings */}
|
||||
<SettingsCard
|
||||
title="Review Settings"
|
||||
description="Configure how and where reviews are displayed"
|
||||
>
|
||||
<SettingsSection label="Review Placement" htmlFor="review-placement">
|
||||
<Select value={reviewSettings.placement} onValueChange={(value) => setReviewSettings({ ...reviewSettings, placement: value })}>
|
||||
<SelectTrigger id="review-placement">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="product_page">Product Page (Traditional)</SelectItem>
|
||||
<SelectItem value="order_details">Order Details Only (Marketplace Style)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{reviewSettings.placement === 'product_page'
|
||||
? 'Reviews appear on product page. Users can submit reviews directly on the product.'
|
||||
: 'Reviews only appear in order details after purchase. Ensures verified purchases only.'}
|
||||
</p>
|
||||
</SettingsSection>
|
||||
|
||||
{reviewSettings.placement === 'product_page' && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="hide-if-empty" className="cursor-pointer">
|
||||
Hide reviews section if empty
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Only show reviews section when product has at least one review
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="hide-if-empty"
|
||||
checked={reviewSettings.hide_if_empty}
|
||||
onCheckedChange={(checked) => setReviewSettings({ ...reviewSettings, hide_if_empty: checked })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SettingsCard>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user